Skip to content

Commit 38cace3

Browse files
committed
fix: add composable for package analysis and copy, fix create edge cases, add i18n
1 parent dc7e0ca commit 38cace3

9 files changed

Lines changed: 351 additions & 69 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Composable for copying text to clipboard with a "copied" state.
3+
* The copied state automatically resets after a timeout.
4+
*/
5+
export function useCopyToClipboard(timeout = 2000) {
6+
const copied = ref(false)
7+
8+
async function copy(text: string | undefined | null) {
9+
if (!text) return false
10+
await navigator.clipboard.writeText(text)
11+
copied.value = true
12+
setTimeout(() => (copied.value = false), timeout)
13+
return true
14+
}
15+
16+
return { copied, copy }
17+
}

app/composables/usePackageAnalysis.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ModuleFormat, TypesStatus } from '#shared/utils/package-analysis'
1+
import type { ModuleFormat, TypesStatus, CreatePackageInfo } from '#shared/utils/package-analysis'
22

33
export interface PackageAnalysisResponse {
44
package: string
@@ -9,6 +9,7 @@ export interface PackageAnalysisResponse {
99
node?: string
1010
npm?: string
1111
}
12+
createPackage?: CreatePackageInfo
1213
}
1314

1415
/**

app/pages/[...package].vue

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,9 @@ const fullInstallCommand = computed(() => {
334334
return `${installCommand.value}; ${pm.label} ${pm.action} ${devFlag} ${pkgSpec}`
335335
})
336336
337-
// Copy install command
338-
const copied = ref(false)
339-
async function copyInstallCommand() {
340-
if (!fullInstallCommand.value) return
341-
await navigator.clipboard.writeText(fullInstallCommand.value)
342-
copied.value = true
343-
setTimeout(() => (copied.value = false), 2000)
344-
}
337+
// Copy commands
338+
const { copied: installCopied, copy: copyInstall } = useCopyToClipboard()
339+
const copyInstallCommand = () => copyInstall(fullInstallCommand.value)
345340
346341
// Executable detection for run command
347342
const executableInfo = computed(() => {
@@ -404,13 +399,49 @@ const executeCommand = computed(() => {
404399
})
405400
406401
// Copy execute command (for binary-only packages)
407-
const executeCopied = ref(false)
408-
async function copyExecuteCommand() {
409-
if (!executeCommand.value) return
410-
await navigator.clipboard.writeText(executeCommand.value)
411-
executeCopied.value = true
412-
setTimeout(() => (executeCopied.value = false), 2000)
413-
}
402+
const { copied: executeCopied, copy: copyExecute } = useCopyToClipboard()
403+
const copyExecuteCommand = () => copyExecute(executeCommand.value)
404+
405+
// Get associated create-* package info (e.g., vite -> create-vite)
406+
const createPackageInfo = computed(() => {
407+
if (!packageAnalysis.value?.createPackage) return null
408+
// Don't show if deprecated
409+
if (packageAnalysis.value.createPackage.deprecated) return null
410+
return packageAnalysis.value.createPackage
411+
})
412+
413+
// Create command parts for associated create-* package
414+
const createCommandParts = computed(() => {
415+
if (!createPackageInfo.value) return []
416+
const pm = packageManagers.find(p => p.id === selectedPM.value)
417+
if (!pm) return []
418+
419+
// Extract short name: create-vite -> vite
420+
const createPkgName = createPackageInfo.value.packageName
421+
let shortName: string
422+
if (createPkgName.startsWith('@')) {
423+
// @scope/create-foo -> foo
424+
const slashIndex = createPkgName.indexOf('/')
425+
const name = createPkgName.slice(slashIndex + 1)
426+
shortName = name.startsWith('create-') ? name.slice('create-'.length) : name
427+
} else {
428+
// create-vite -> vite
429+
shortName = createPkgName.startsWith('create-')
430+
? createPkgName.slice('create-'.length)
431+
: createPkgName
432+
}
433+
434+
return [...pm.create.split(' '), shortName]
435+
})
436+
437+
// Full create command string for copying
438+
const createCommand = computed(() => {
439+
return createCommandParts.value.join(' ')
440+
})
441+
442+
// Copy create command
443+
const { copied: createCopied, copy: copyCreate } = useCopyToClipboard()
444+
const copyCreateCommand = () => copyCreate(createCommand.value)
414445
415446
// Primary run command parts
416447
const runCommandParts = computed(() => {
@@ -430,19 +461,8 @@ function getFullRunCommand(command?: string) {
430461
}
431462
432463
// Copy run command
433-
const runCopied = ref(false)
434-
const runCopiedCommand = ref<string | null>(null)
435-
async function copyRunCommand(command?: string) {
436-
const cmd = getFullRunCommand(command)
437-
if (!cmd) return
438-
await navigator.clipboard.writeText(cmd)
439-
runCopied.value = true
440-
runCopiedCommand.value = command || null
441-
setTimeout(() => {
442-
runCopied.value = false
443-
runCopiedCommand.value = null
444-
}, 2000)
445-
}
464+
const { copied: runCopied, copy: copyRun } = useCopyToClipboard()
465+
const copyRunCommand = (command?: string) => copyRun(getFullRunCommand(command))
446466
447467
// Expandable description
448468
const descriptionExpanded = ref(false)
@@ -1015,7 +1035,7 @@ defineOgImageComponent('Package', {
10151035
@click.stop="copyInstallCommand"
10161036
>
10171037
<span aria-live="polite">{{
1018-
copied ? t('common.copied') : t('common.copy')
1038+
installCopied ? t('common.copied') : t('common.copy')
10191039
}}</span>
10201040
</button>
10211041
</div>
@@ -1074,14 +1094,58 @@ defineOgImageComponent('Package', {
10741094
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/runcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
10751095
@click.stop="copyRunCommand(executableInfo?.primaryCommand)"
10761096
>
1077-
{{
1078-
runCopied && runCopiedCommand === (executableInfo?.primaryCommand || null)
1079-
? t('common.copied')
1080-
: t('common.copy')
1081-
}}
1097+
{{ runCopied ? t('common.copied') : t('common.copy') }}
10821098
</button>
10831099
</div>
10841100
</template>
1101+
1102+
<!-- Create command (for packages with associated create-* package) -->
1103+
<template v-if="createPackageInfo">
1104+
<!-- Comment line -->
1105+
<div class="flex items-center gap-2 pt-1">
1106+
<span class="text-fg-subtle/50 font-mono text-sm select-none"
1107+
># {{ t('package.create.title') }}</span
1108+
>
1109+
</div>
1110+
1111+
<!-- Create command -->
1112+
<div class="flex items-center gap-2 group/createcmd">
1113+
<span class="text-fg-subtle font-mono text-sm select-none">$</span>
1114+
<code class="font-mono text-sm"
1115+
><ClientOnly
1116+
><span
1117+
v-for="(part, i) in createCommandParts"
1118+
:key="i"
1119+
:class="i === 0 ? 'text-fg' : 'text-fg-muted'"
1120+
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
1121+
><template #fallback
1122+
><span class="text-fg">npm</span
1123+
><span class="text-fg-muted">
1124+
create {{ createPackageInfo.packageName.replace('create-', '') }}</span
1125+
></template
1126+
></ClientOnly
1127+
></code
1128+
>
1129+
<button
1130+
type="button"
1131+
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/createcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
1132+
:aria-label="t('package.create.copy_command')"
1133+
@click.stop="copyCreateCommand"
1134+
>
1135+
<span aria-live="polite">{{
1136+
createCopied ? t('common.copied') : t('common.copy')
1137+
}}</span>
1138+
</button>
1139+
<NuxtLink
1140+
:to="`/${createPackageInfo.packageName}`"
1141+
class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
1142+
:title="`View ${createPackageInfo.packageName}`"
1143+
>
1144+
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
1145+
<span class="sr-only">View {{ createPackageInfo.packageName }}</span>
1146+
</NuxtLink>
1147+
</div>
1148+
</template>
10851149
</div>
10861150
</div>
10871151
</div>

app/utils/install-command.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { JsrPackageInfo } from '#shared/types/jsr'
2+
import { getCreateShortName } from '#shared/utils/package-analysis'
23

34
export const packageManagers = [
45
{
@@ -119,39 +120,13 @@ export function getExecuteCommandParts(options: ExecuteCommandOptions): string[]
119120

120121
// For create-* packages, use the shorthand create command
121122
if (options.isCreatePackage) {
122-
const createName = extractCreateName(options.packageName)
123-
if (createName) {
124-
return [...pm.create.split(' '), createName]
123+
const shortName = getCreateShortName(options.packageName)
124+
if (shortName !== options.packageName) {
125+
return [...pm.create.split(' '), shortName]
125126
}
126127
}
127128

128129
// Choose remote or local execute based on package type
129130
const executeCmd = options.isBinaryOnly ? pm.executeRemote : pm.executeLocal
130131
return [...executeCmd.split(' '), getPackageSpecifier(options)]
131132
}
132-
133-
/**
134-
* Extract the short name from a create-* package.
135-
* e.g., "create-vite" -> "vite", "@vue/create-app" -> "vue"
136-
*/
137-
function extractCreateName(packageName: string): string | null {
138-
// Handle scoped packages: @scope/create-foo -> foo
139-
if (packageName.startsWith('@')) {
140-
const parts = packageName.split('/')
141-
const baseName = parts[1]
142-
if (baseName?.startsWith('create-')) {
143-
return baseName.slice('create-'.length)
144-
}
145-
// Handle @vue/create-app style (scope as the name)
146-
if (baseName?.startsWith('create')) {
147-
return parts[0]!.slice(1) // Remove @ from scope
148-
}
149-
}
150-
151-
// Handle unscoped: create-foo -> foo
152-
if (packageName.startsWith('create-')) {
153-
return packageName.slice('create-'.length)
154-
}
155-
156-
return null
157-
}

i18n/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@
9898
"copy_command": "Copy install command",
9999
"view_types": "View {package}"
100100
},
101+
"create": {
102+
"title": "Create new project",
103+
"copy_command": "Copy create command"
104+
},
105+
"run": {
106+
"title": "Run",
107+
"locally": "Run locally"
108+
},
101109
"readme": {
102110
"title": "Readme",
103111
"no_readme": "No README available.",

i18n/locales/zh-CN.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@
9898
"copy_command": "复制安装命令",
9999
"view_types": "查看 {package}"
100100
},
101+
"create": {
102+
"title": "创建新项目",
103+
"copy_command": "复制创建命令"
104+
},
105+
"run": {
106+
"title": "运行",
107+
"locally": "本地运行"
108+
},
101109
"readme": {
102110
"title": "Readme",
103111
"no_readme": "没有可用的 README。",

0 commit comments

Comments
 (0)