Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2231fd7
feat: add deprecate feature
eryue0220 Feb 4, 2026
f9d0df5
add i18n
eryue0220 Feb 4, 2026
36d03de
Merge branch 'main' into feat/resolve-40
eryue0220 Feb 4, 2026
bb37228
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 4, 2026
fb860dc
Merge branch 'main' into feat/resolve-40
eryue0220 Feb 4, 2026
58e112d
update
eryue0220 Feb 4, 2026
2159074
update
eryue0220 Feb 4, 2026
2530ba9
update
eryue0220 Feb 4, 2026
d80dd64
ut
eryue0220 Feb 4, 2026
0c221f5
Update app/components/Package/DeprecatePackageModal.vue
eryue0220 Feb 4, 2026
4264814
add ut
eryue0220 Feb 4, 2026
1b38e9e
Merge branch 'feat/resolve-40' of github.com:eryue0220/npmx.dev into …
eryue0220 Feb 4, 2026
8fa2405
chore: add lunaria az-AZ locale file
eryue0220 Feb 8, 2026
ea683a6
wip
eryue0220 Feb 8, 2026
fe0420c
cr
eryue0220 Feb 9, 2026
ec631d9
cr
eryue0220 Feb 9, 2026
62e8c99
cr
eryue0220 Feb 9, 2026
a8d922b
ci
eryue0220 Feb 9, 2026
cb12a0a
fix ut
eryue0220 Feb 9, 2026
baf0822
cr
eryue0220 Feb 9, 2026
5cc1b0a
add locale file
eryue0220 Feb 10, 2026
75f9c00
conflict & update cr
eryue0220 Feb 10, 2026
19c82f3
Merge locale file
eryue0220 Feb 12, 2026
5722aac
ci & conflict
eryue0220 Feb 12, 2026
cd70766
Merge remote-tracking branch 'origin/main' into feat/resolve-40
danielroe Feb 26, 2026
4b9a900
fix: use InputBase, update cli mock + avoid extra fetch
danielroe Feb 26, 2026
6e44c34
fix unocss checker
eryue0220 Apr 17, 2026
7f4ec8e
fix ci
eryue0220 Apr 17, 2026
caeaebd
fix ci
eryue0220 Apr 17, 2026
5457d9c
Merge branch 'main' into feat/resolve-40
eryue0220 Apr 20, 2026
588ddfc
Update app/pages/package/[[org]]/[name].vue
eryue0220 Apr 20, 2026
14e5cd8
update request param
eryue0220 Apr 20, 2026
2548539
Merge branch 'feat/resolve-40' of github.com:eryue0220/npmx.dev into …
eryue0220 Apr 20, 2026
1ddcb21
fix ci
eryue0220 Apr 20, 2026
ad353f3
Merge branch 'main' into feat/resolve-40
eryue0220 Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions app/components/Package/DeprecatePackageModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<script setup lang="ts">
import type { NewOperation } from '~/composables/useConnector'

const props = withDefaults(
defineProps<{
packageName: string
version?: string
}>(),
{ version: '' },
)

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}`,
)
Comment thread
eryue0220 marked this conversation as resolved.

async function handleDeprecate() {
const message = deprecateMessage.value.trim()
if (!message || !isConnected.value) return

isDeprecating.value = true
deprecateError.value = null

try {
const params: Record<string, string> = {
pkg: props.packageName,
message,
}
if (deprecateVersion.value.trim()) {
params.version = deprecateVersion.value.trim()
}

const escapedMessage = message.replace(/"/g, '\\"')
const command = params.version
? `npm deprecate ${props.packageName}@${params.version} "${escapedMessage}"`
: `npm deprecate ${props.packageName} "${escapedMessage}"`

const operation = await addOperation({
type: 'package:deprecate',
params,
description: params.version
? `Deprecate ${props.packageName}@${params.version}`
: `Deprecate ${props.packageName}`,
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 {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
isDeprecating.value = false
}
}

const dialogRef = ref<HTMLDialogElement>()

function open() {
deprecateError.value = null
deprecateSuccess.value = false
deprecateMessage.value = ''
deprecateVersion.value = props.version ?? ''
dialogRef.value?.showModal()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

function close() {
dialogRef.value?.close()
}

defineExpose({ open, close })
</script>

<template>
<Modal ref="dialogRef" :modal-title="modalTitle" id="deprecate-package-modal" class="max-w-md">
<!-- Success state -->
<div v-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 (only shown when connected; parent only opens modal when isConnected) -->
<div v-else class="space-y-4">
<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"
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"
:placeholder="$t('package.deprecation.modal.reason_placeholder')"
/>
</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"
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"
: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()"
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>
35 changes: 35 additions & 0 deletions app/pages/package/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,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 &&
Expand Down Expand Up @@ -1228,6 +1239,22 @@ onKeyStroke(
:peer-dependencies-meta="displayVersion.peerDependenciesMeta"
:optional-dependencies="displayVersion.optionalDependencies"
/>

<!-- Deprecation (when connected as package owner) -->
<div v-if="isConnected && resolvedVersion && isPackageOwner" 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" />
{{
deprecationNotice
? $t('package.deprecation.action_change')
: $t('package.deprecation.action')
}}
</button>
</div>
</div>
</div>
</article>
Expand All @@ -1246,6 +1273,14 @@ onKeyStroke(
</p>
<NuxtLink to="/" class="btn">{{ $t('common.go_back_home') }}</NuxtLink>
</div>
<ClientOnly>
<PackageDeprecatePackageModal
v-if="pkg"
ref="deprecateModal"
:package-name="pkg.name"
:version="resolvedVersion ?? ''"
/>
</ClientOnly>
</main>
</template>

Expand Down
27 changes: 27 additions & 0 deletions cli/src/npm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,30 @@ export async function packageInit(
})
}
}

/**
* Deprecate a package or a specific version with a custom message.
* @param pkg Package name (e.g. "vue" or "@nuxt/kit")
* @param reason Deprecation message shown to users
* @param version Optional version to deprecate (e.g. "1.0.0"); if omitted, deprecates the whole package
* @param options.dryRun If true, passes --dry-run to npm (report what would be done without making changes)
* @param options.registry Registry URL (e.g. "https://registry.npmjs.org"); if set, passes --registry
*/
export async function packageDeprecate(
pkg: string,
reason: string,
version?: string,
otp?: string,
options?: { dryRun?: boolean; registry?: string },
): Promise<NpmExecResult> {
validatePackageName(pkg)
const target = version ? `${pkg}@${version}` : pkg
const args = ['deprecate', target, reason]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (options?.dryRun) {
args.push('--dry-run')
}
if (options?.registry?.trim()) {
args.push('--registry', options.registry.trim())
}
return execNpm(args, { otp })
Comment thread
eryue0220 marked this conversation as resolved.
}
16 changes: 16 additions & 0 deletions cli/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const OperationTypeSchema = v.picklist([
'owner:add',
'owner:rm',
'package:init',
'package:deprecate',
])

/**
Expand Down Expand Up @@ -240,6 +241,18 @@ export const PackageInitParamsSchema = v.object({
author: v.optional(UsernameSchema),
})

const PackageDeprecateParamsSchema = v.object({
pkg: PackageNameSchema,
message: v.pipe(
v.string(),
v.nonEmpty('Deprecation message is required'),
v.maxLength(500, 'Message is too long'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested npm deprecate with 1001 chars and it worked... xD

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

),
version: v.optional(v.pipe(v.string(), v.nonEmpty())),
dryRun: v.optional(v.picklist(['true', 'false'], 'dryRun must be "true" or "false"')),
registry: v.optional(v.pipe(v.string(), v.minLength(1, 'Registry URL cannot be empty'))),
})

// ============================================================================
// Helper Functions
// ============================================================================
Expand Down Expand Up @@ -289,6 +302,9 @@ export function validateOperationParams(
case 'package:init':
v.parse(PackageInitParamsSchema, params)
break
case 'package:deprecate':
v.parse(PackageDeprecateParamsSchema, params)
break
}
}

Expand Down
9 changes: 9 additions & 0 deletions cli/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ownerAdd,
ownerRemove,
packageInit,
packageDeprecate,
listUserPackages,
type NpmExecResult,
} from './npm-client.ts'
Expand Down Expand Up @@ -734,6 +735,14 @@ async function executeOperation(op: PendingOperation, otp?: string): Promise<Npm
return ownerRemove(params.user, params.pkg, otp)
case 'package:init':
return packageInit(params.name, params.author, otp)
case 'package:deprecate': {
const dryRun = params.dryRun === 'true'
const registry = params.registry?.trim() ?? undefined
return packageDeprecate(params.pkg, params.message, params.version, otp, {
dryRun: dryRun ?? undefined,
registry,
})
}
default:
return {
stdout: '',
Expand Down
1 change: 1 addition & 0 deletions cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type OperationType =
| 'owner:add'
| 'owner:rm'
| 'package:init'
| 'package:deprecate'

export type OperationStatus =
| 'pending'
Expand Down
15 changes: 14 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,22 @@
"navigation": "Package",
"copy_name": "Copy package name",
"deprecation": {
"action": "Deprecate",
"action_change": "Change deprecation",
"package": "This package has been deprecated.",
"version": "This version has been deprecated.",
"no_reason": "No reason provided"
"no_reason": "No reason provided",
"modal": {
"deprecating": "Deprecating",
"title": "Deprecate",
"title_version": "Deprecate Version",
"reason": "Reason",
"reason_placeholder": "e.g. Use package-x instead. This package is no longer maintained.",
"success": "Package deprecated",
"success_detail": "The package has been deprecated.",
"version": "Version",
"version_placeholder": "Leave empty to deprecate the whole package"
}
},
"replacement": {
"title": "You might not need this dependency.",
Expand Down
15 changes: 14 additions & 1 deletion i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,22 @@
"navigation": "包导航",
"copy_name": "拷贝包名",
"deprecation": {
"action": "废弃",
"action_change": "修改弃用",
"package": "这个包已经被弃用。",
"version": "这个版本已经被弃用。",
"no_reason": "没有提供原因"
"no_reason": "没有提供原因",
"modal": {
"deprecating": "废弃中",
"title": "废弃",
"title_version": "废弃版本",
"reason": "原因",
"reason_placeholder": "例如:使用 package-x 替代。这个包不再维护。",
"success": "包已废弃",
"success_detail": "这个包已废弃。",
"version": "版本",
"version_placeholder": "留空则废弃整个包"
}
},
"replacement": {
"title": "你可能不需要这个依赖。",
Expand Down
Loading
Loading