Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 527
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?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
2231fd7f9d0df536d03debb37228fb860dc58e112d21590742530ba9d80dd640c221f542648141b38e9e8fa2405ea683a6fe0420cec631d962e8c99a8d922bcb12a0abaf08225cc1b0a75f9c0019c82f35722aaccd707664b9a9006e44c347f4ec8ecaeaebd5457d9c588ddfc14e5cd825485391ddcb21ad353f35b2e3890f56d3cd67ef4aFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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,280 @@ | ||
| <script setup lang="ts"> | ||
| import type { NewOperation } from '~/composables/useConnector' | ||
| import * as v from 'valibot' | ||
| import { PackageDeprecateParamsSchema } from '#shared/schemas/package' | ||
| 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 strings that are already deprecated (computed by parent from pkg.versions). */ | ||
| deprecatedVersions?: string[] | ||
| }>(), | ||
| { version: '', isAlreadyDeprecated: false, deprecatedVersions: () => [] }, | ||
| ) | ||
| 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') | ||
| 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() | ||
Check warning on line 40 in app/components/Package/DeprecatePackageModal.vue
| ||
| if (!v || !props.deprecatedVersions.length) return false | ||
| return props.deprecatedVersions.includes(v) | ||
| }) | ||
| 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 = v.safeParse(PackageDeprecateParamsSchema, params) | ||
| if (!parsed.success) { | ||
| const firstIssue = parsed.issues[0] | ||
| const path = firstIssue?.path?.map(p => p.key).join('.') || '' | ||
| const message = firstIssue?.message || 'Validation failed' | ||
Check warning on line 62 in app/components/Package/DeprecatePackageModal.vue
| ||
| deprecateError.value = path ? `${path}: ${message}` : message | ||
| return | ||
| } | ||
| isDeprecating.value = true | ||
| deprecateError.value = null | ||
| try { | ||
| const escapedMessage = parsed.output.message.replace(/"/g, '\\"') | ||
| const command = parsed.output.version | ||
| ? `npm deprecate ${parsed.output.pkg}@${parsed.output.version} "${escapedMessage}"` | ||
| : `npm deprecate ${parsed.output.pkg} "${escapedMessage}"` | ||
| const operationParams: Record<string, string> = { | ||
| pkg: parsed.output.pkg, | ||
| message: parsed.output.message, | ||
| } | ||
| if (parsed.output.version) { | ||
| operationParams.version = parsed.output.version | ||
| } | ||
| const newOperation: NewOperation = { | ||
| type: 'package:deprecate', | ||
| params: operationParams, | ||
| description: parsed.output.version | ||
| ? `Deprecate ${parsed.output.pkg}@${parsed.output.version}` | ||
| : `Deprecate ${parsed.output.pkg}`, | ||
| command, | ||
| } | ||
| const operation = await addOperation(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.
Uh oh!There was an error while loading. Please reload this page. | ||
| isDeprecating.value = false | ||
| } | ||
| } | ||
| const dialogRef = useTemplateRef('dialogRef') | ||
| 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-accent/70" | ||
| @click="close" | ||
| > | ||
| {{ $t('common.close') }} | ||
| </button> | ||
Comment on lines
+163
to
+169
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-accent/70" | ||
| @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-xs text-fg-subtle uppercase tracking-wider mb-1.5" | ||
| > | ||
| {{ $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 appearance-none bg-bg-subtle border border-border font-mono text-sm leading-none px-3 py-2.5 rounded-lg text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent outline-offset-2 focus:border-accent focus-visible:outline-accent/70 disabled:(opacity-50 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-xs text-fg-subtle uppercase tracking-wider mb-1.5" | ||
| > | ||
| {{ $t('package.deprecation.modal.version') }} | ||
| </label> | ||
| <InputBase | ||
| id="deprecate-version" | ||
| v-model="deprecateVersion" | ||
| type="text" | ||
| name="deprecate-version" | ||
| :disabled="isSelectedVersionDeprecated" | ||
| class="w-full" | ||
| size="md" | ||
| :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 |
|---|---|---|
| @@ -329,6 +329,21 @@ 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) | ||
| }) | ||
| /** Version strings that are already deprecated; passed to DeprecatePackageModal to avoid extra fetch. */ | ||
| const deprecatedVersions = computed(() => { | ||
| if (!pkg.value?.versions) return [] | ||
| return Object.entries(pkg.value.versions) | ||
| .filter(([, metadata]) => !!metadata.deprecated) | ||
| .map(([version]) => version) | ||
| }) | ||
| const deprecationNotice = computed(() => { | ||
| if (!displayVersion.value?.deprecated) return null | ||
| @@ -350,6 +365,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 publishSecurityDowngrade = computed(() => { | ||
| const currentVersion = displayVersion.value?.version | ||
| if (!currentVersion) return null | ||
| @@ -1009,6 +1035,22 @@ const showSkeleton = shallowRef(false) | ||
| <!-- 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="ps-7" | ||
| > | ||
| <ButtonBase | ||
| type="button" | ||
| variant="secondary" | ||
| class="w-full text-red" | ||
| @click="deprecateModal?.open()" | ||
| classicon="i-lucide:triangle-alert text-red" | ||
| > | ||
| {{ $t('package.deprecation.action') }} | ||
| </ButtonBase> | ||
| </div> | ||
eryue0220 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| </div> | ||
| </PackageSidebar> | ||
| @@ -1131,6 +1173,16 @@ const showSkeleton = shallowRef(false) | ||
| $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? ContributorAuthor 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. Then it will deprecate the whole package. | ||
| :is-already-deprecated="isCurrentVersionDeprecated" | ||
| :deprecated-versions="deprecatedVersions" | ||
| /> | ||
| </ClientOnly> | ||
| </main> | ||
| </template> | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.