-
-
Notifications
You must be signed in to change notification settings - Fork 425
feat: add deprecate command with custom reason #921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 16 commits
2231fd7
f9d0df5
36d03de
bb37228
fb860dc
58e112d
2159074
2530ba9
d80dd64
0c221f5
4264814
1b38e9e
8fa2405
ea683a6
fe0420c
ec631d9
62e8c99
a8d922b
cb12a0a
baf0822
5cc1b0a
75f9c00
19c82f3
5722aac
cd70766
4b9a900
6e44c34
7f4ec8e
caeaebd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ test-results | |
|
|
||
| # Test coverage | ||
| coverage/ | ||
| *.junit.xml | ||
|
|
||
| # Playwright | ||
| playwright-report/ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| <script setup lang="ts"> | ||
| import type { NewOperation } from '~/composables/useConnector' | ||
| import type Modal from '~/components/Modal.client.vue' | ||
| import { PackageDeprecateParamsSchema, safeParse } from '~~/cli/src/schemas' | ||
| import { fetchAllPackageVersions } from '~/utils/npm/api' | ||
|
|
||
| const DEPRECATE_MESSAGE_MAX_LENGTH = 500 | ||
|
|
||
| const props = withDefaults( | ||
| defineProps<{ | ||
| packageName: string | ||
| version?: string | ||
| /** When true, the package or version is already deprecated; form is hidden and state cannot be changed. */ | ||
| isAlreadyDeprecated?: boolean | ||
| }>(), | ||
| { version: '', isAlreadyDeprecated: false }, | ||
| ) | ||
|
|
||
| const { t } = useI18n() | ||
| const { isConnected, state, addOperation, approveOperation, executeOperations, refreshState } = | ||
| useConnector() | ||
|
|
||
| const deprecateMessage = ref('') | ||
| const deprecateVersion = ref(props.version) | ||
| const isDeprecating = shallowRef(false) | ||
| const deprecateSuccess = shallowRef(false) | ||
| const deprecateError = shallowRef<string | null>(null) | ||
|
|
||
| const connectorModal = useModal('connector-modal') | ||
|
|
||
| /** Full version list (same as "Other versions"); fetched in modal for deprecated check. */ | ||
| const allPackageVersions = shallowRef<Awaited<ReturnType<typeof fetchAllPackageVersions>> | null>( | ||
| null, | ||
| ) | ||
|
|
||
| /** Deprecated version strings from fetched full list (includes Other versions). */ | ||
| const effectiveDeprecatedVersions = computed(() => { | ||
| const list = allPackageVersions.value | ||
| if (!list) return [] | ||
| return list.filter(v => v.deprecated).map(v => v.version) | ||
| }) | ||
|
|
||
| const modalTitle = computed(() => | ||
| deprecateVersion.value | ||
| ? `${t('package.deprecation.modal.title')} ${props.packageName}@${deprecateVersion.value}` | ||
| : `${t('package.deprecation.modal.title')} ${props.packageName}`, | ||
| ) | ||
|
|
||
| /** True when the user has entered a version in the form that is already deprecated. */ | ||
| const isSelectedVersionDeprecated = computed(() => { | ||
| const v = deprecateVersion.value.trim() | ||
| if (!v || !effectiveDeprecatedVersions.value.length) return false | ||
| return effectiveDeprecatedVersions.value.includes(v) | ||
| }) | ||
|
|
||
| // Load full version list so deprecated check includes "Other versions" | ||
| watch( | ||
| () => props.packageName, | ||
| name => { | ||
| if (!name) return | ||
| fetchAllPackageVersions(name).then(versions => { | ||
| allPackageVersions.value = versions | ||
| }) | ||
| }, | ||
| { immediate: true }, | ||
| ) | ||
|
eryue0220 marked this conversation as resolved.
|
||
|
|
||
| async function handleDeprecate() { | ||
| if (props.isAlreadyDeprecated || isSelectedVersionDeprecated.value) return | ||
| const message = deprecateMessage.value.trim() | ||
| if (!isConnected.value) return | ||
|
|
||
| const params: Record<string, string> = { | ||
| pkg: props.packageName, | ||
| message, | ||
| } | ||
| if (deprecateVersion.value.trim()) { | ||
| params.version = deprecateVersion.value.trim() | ||
| } | ||
|
|
||
| const parsed = safeParse(PackageDeprecateParamsSchema, params) | ||
| if (!parsed.success) { | ||
| deprecateError.value = parsed.error | ||
| return | ||
| } | ||
|
|
||
| isDeprecating.value = true | ||
| deprecateError.value = null | ||
|
|
||
| try { | ||
| const escapedMessage = parsed.data.message.replace(/"/g, '\\"') | ||
| const command = parsed.data.version | ||
| ? `npm deprecate ${parsed.data.pkg}@${parsed.data.version} "${escapedMessage}"` | ||
| : `npm deprecate ${parsed.data.pkg} "${escapedMessage}"` | ||
|
|
||
| const operation = await addOperation({ | ||
| type: 'package:deprecate', | ||
| params: { | ||
| pkg: parsed.data.pkg, | ||
| message: parsed.data.message, | ||
| ...(parsed.data.version && { version: parsed.data.version }), | ||
| }, | ||
| description: parsed.data.version | ||
| ? `Deprecate ${parsed.data.pkg}@${parsed.data.version}` | ||
| : `Deprecate ${parsed.data.pkg}`, | ||
| command, | ||
| } as NewOperation) | ||
|
|
||
| if (!operation) { | ||
| throw new Error('Failed to create operation') | ||
| } | ||
|
|
||
| await approveOperation(operation.id) | ||
| await executeOperations() | ||
| await refreshState() | ||
|
|
||
| const completedOp = state.value.operations.find(op => op.id === operation.id) | ||
| if (completedOp?.status === 'completed') { | ||
| deprecateSuccess.value = true | ||
| } else if (completedOp?.status === 'failed') { | ||
| if (completedOp.result?.requiresOtp) { | ||
| close() | ||
| connectorModal.open() | ||
| } else { | ||
| deprecateError.value = completedOp.result?.stderr || t('common.try_again') | ||
| } | ||
| } else { | ||
| close() | ||
| connectorModal.open() | ||
| } | ||
| } catch (err) { | ||
| deprecateError.value = err instanceof Error ? err.message : t('common.try_again') | ||
| } finally { | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| isDeprecating.value = false | ||
| } | ||
| } | ||
|
|
||
| const dialogRef = ref<InstanceType<typeof Modal> | undefined>() | ||
|
|
||
| function open() { | ||
| deprecateError.value = null | ||
| deprecateSuccess.value = false | ||
| deprecateMessage.value = '' | ||
| deprecateVersion.value = props.version ?? '' | ||
| dialogRef.value?.showModal() | ||
| } | ||
|
|
||
| function close() { | ||
| dialogRef.value?.close() | ||
| } | ||
|
|
||
| defineExpose({ open, close }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <Modal ref="dialogRef" :modal-title="modalTitle" id="deprecate-package-modal" class="max-w-md"> | ||
| <!-- Already deprecated: entire module read-only, hint only, no form / no deprecate button --> | ||
| <div v-if="isAlreadyDeprecated" class="space-y-4" aria-readonly="true"> | ||
| <div | ||
| class="flex items-center gap-3 p-4 bg-amber-500/10 border border-amber-500/20 rounded-lg" | ||
| role="status" | ||
| > | ||
| <span class="i-carbon-warning-alt text-amber-500 w-6 h-6 shrink-0" aria-hidden="true" /> | ||
| <div> | ||
| <p class="font-mono text-sm text-fg"> | ||
| {{ | ||
| deprecateVersion | ||
| ? $t('package.deprecation.modal.already_deprecated_version') | ||
| : $t('package.deprecation.modal.already_deprecated') | ||
| }} | ||
| </p> | ||
| <p class="text-xs text-fg-muted mt-0.5"> | ||
| {{ $t('package.deprecation.modal.already_deprecated_detail') }} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| <button | ||
| type="button" | ||
| class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" | ||
| @click="close" | ||
| > | ||
| {{ $t('common.close') }} | ||
| </button> | ||
|
Comment on lines
+157
to
+163
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove inline focus-visible utilities on buttons. 💡 Suggested fix- class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
+ class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover"- class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
+ class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover"- class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
+ class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed"Also applies to: 199-205, 272-276 |
||
| </div> | ||
|
|
||
| <!-- Success state --> | ||
| <div v-else-if="deprecateSuccess" class="space-y-4"> | ||
| <div | ||
| class="flex items-center gap-3 p-4 bg-green-500/10 border border-green-500/20 rounded-lg" | ||
| > | ||
| <span class="i-carbon-checkmark-filled text-green-500 w-6 h-6" aria-hidden="true" /> | ||
| <div> | ||
| <p class="font-mono text-sm text-fg">{{ $t('package.deprecation.modal.success') }}</p> | ||
| <p class="text-xs text-fg-muted"> | ||
| {{ $t('package.deprecation.modal.success_detail') }} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| <button | ||
| type="button" | ||
| class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" | ||
| @click="close" | ||
| > | ||
| {{ $t('common.close') }} | ||
| </button> | ||
| </div> | ||
|
|
||
| <!-- Form --> | ||
| <div v-else class="space-y-4"> | ||
| <!-- Hint when user-entered version is already deprecated --> | ||
| <div | ||
| v-if="isSelectedVersionDeprecated" | ||
| class="flex items-center gap-3 p-4 bg-amber-500/10 border border-amber-500/20 rounded-lg" | ||
| role="status" | ||
| > | ||
| <span class="i-carbon-warning-alt text-amber-500 w-6 h-6 shrink-0" aria-hidden="true" /> | ||
| <div> | ||
| <p class="font-mono text-sm text-fg"> | ||
| {{ $t('package.deprecation.modal.already_deprecated_version') }} | ||
| </p> | ||
| <p class="text-xs text-fg-muted mt-0.5"> | ||
| {{ $t('package.deprecation.modal.already_deprecated_detail') }} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| <div> | ||
| <label for="deprecate-message" class="block text-sm font-medium text-fg mb-1"> | ||
| {{ $t('package.deprecation.modal.reason') }} | ||
| </label> | ||
| <textarea | ||
| id="deprecate-message" | ||
| v-model="deprecateMessage" | ||
| rows="3" | ||
| :maxlength="DEPRECATE_MESSAGE_MAX_LENGTH" | ||
| :disabled="isSelectedVersionDeprecated" | ||
| class="w-full px-3 py-2 font-mono text-sm bg-bg border border-border rounded-md text-fg placeholder:text-fg-muted focus:outline-none focus:ring-2 focus:ring-fg/50 disabled:opacity-60 disabled:cursor-not-allowed" | ||
| :placeholder="$t('package.deprecation.modal.reason_placeholder')" | ||
| :aria-describedby=" | ||
| deprecateMessage.length >= DEPRECATE_MESSAGE_MAX_LENGTH | ||
| ? 'deprecate-message-hint' | ||
| : undefined | ||
| " | ||
| /> | ||
| <p | ||
| v-if="deprecateMessage.length >= DEPRECATE_MESSAGE_MAX_LENGTH * 0.9" | ||
| id="deprecate-message-hint" | ||
| class="mt-1 text-xs text-fg-muted" | ||
| > | ||
| {{ deprecateMessage.length }} / {{ DEPRECATE_MESSAGE_MAX_LENGTH }} | ||
| </p> | ||
| </div> | ||
| <div> | ||
| <label for="deprecate-version" class="block text-sm font-medium text-fg mb-1"> | ||
| {{ $t('package.deprecation.modal.version') }} | ||
| </label> | ||
| <input | ||
| id="deprecate-version" | ||
| v-model="deprecateVersion" | ||
| type="text" | ||
| :disabled="isSelectedVersionDeprecated" | ||
| class="w-full px-3 py-2 font-mono text-sm bg-bg border border-border rounded-md text-fg placeholder:text-fg-muted focus:outline-none focus:ring-2 focus:ring-fg/50 disabled:opacity-60 disabled:cursor-not-allowed" | ||
| :placeholder="$t('package.deprecation.modal.version_placeholder')" | ||
| /> | ||
| </div> | ||
| <div | ||
| v-if="deprecateError" | ||
| role="alert" | ||
| class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md" | ||
| > | ||
| {{ deprecateError }} | ||
| </div> | ||
| <button | ||
| type="button" | ||
| :disabled="isDeprecating || !deprecateMessage.trim() || isSelectedVersionDeprecated" | ||
| class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" | ||
| @click="handleDeprecate" | ||
| > | ||
| {{ | ||
| isDeprecating | ||
| ? $t('package.deprecation.modal.deprecating') | ||
| : $t('package.deprecation.action') | ||
| }} | ||
| </button> | ||
| </div> | ||
| </Modal> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -204,6 +204,13 @@ const latestVersion = computed(() => { | |
| return pkg.value.versions[latestTag] ?? null | ||
| }) | ||
|
|
||
| /** True when the currently displayed version (or resolved version) is deprecated; used to hide deprecate button. */ | ||
| const isCurrentVersionDeprecated = computed(() => { | ||
| if (displayVersion.value?.deprecated) return true | ||
| const ver = resolvedVersion.value | ||
| return !!(ver && pkg.value?.versions?.[ver]?.deprecated) | ||
| }) | ||
|
|
||
| const deprecationNotice = computed(() => { | ||
| if (!displayVersion.value?.deprecated) return null | ||
|
|
||
|
|
@@ -225,6 +232,17 @@ const deprecationNoticeMessage = useMarkdown(() => ({ | |
| text: deprecationNotice.value?.message ?? '', | ||
| })) | ||
|
|
||
| const { isConnected, npmUser } = useConnector() | ||
| const deprecateModal = useTemplateRef<{ open: () => void }>('deprecateModal') | ||
|
|
||
| const isPackageOwner = computed(() => { | ||
| const maintainers = pkg.value?.maintainers | ||
| const user = npmUser.value | ||
| if (!maintainers?.length || !user) return false | ||
| const userLower = user.toLowerCase() | ||
| return maintainers.some((m: { name?: string }) => (m.name ?? '').toLowerCase() === userLower) | ||
| }) | ||
|
|
||
| const sizeTooltip = computed(() => { | ||
| const chunks = [ | ||
| displayVersion.value && | ||
|
|
@@ -1190,6 +1208,21 @@ onKeyStroke( | |
|
|
||
| <!-- Maintainers (with admin actions when connected) --> | ||
| <PackageMaintainers :package-name="pkg.name" :maintainers="pkg.maintainers" /> | ||
|
|
||
| <!-- Deprecation (when connected as package owner; hidden when current version is already deprecated) --> | ||
| <div | ||
| v-if="isConnected && resolvedVersion && isPackageOwner && !isCurrentVersionDeprecated" | ||
| class="space-y-1" | ||
| > | ||
| <button | ||
| type="button" | ||
| class="flex items-center justify-center w-full px-3 py-1.5 bg-bg-subtle rounded text-sm font-mono text-red-400 hover:text-red-500 transition-colors inline-flex items-center gap-1.5 w-full" | ||
| @click="deprecateModal?.open()" | ||
| > | ||
| <span class="i-carbon-warning-alt w-4 h-4 shrink-0" aria-hidden="true" /> | ||
| {{ $t('package.deprecation.action') }} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </PackageSidebar> | ||
| </article> | ||
|
|
@@ -1210,6 +1243,15 @@ onKeyStroke( | |
| $t('common.go_back_home') | ||
| }}</LinkBase> | ||
| </div> | ||
| <ClientOnly> | ||
| <PackageDeprecatePackageModal | ||
| v-if="pkg" | ||
| ref="deprecateModal" | ||
| :package-name="pkg.name" | ||
| :version="resolvedVersion ?? ''" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if we don't get the version? |
||
| :is-already-deprecated="isCurrentVersionDeprecated" | ||
| /> | ||
| </ClientOnly> | ||
| </main> | ||
| </template> | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.