forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2import type { JsrPackageInfo } from '#shared/types/jsr'
3import type { DevDependencySuggestion } from '#shared/utils/dev-dependency'
4import type { PackageManagerId } from '~/utils/install-command'
5
6const props = defineProps<{
7 packageName: string
8 requestedVersion?: string | null
9 installVersionOverride?: string | null
10 jsrInfo?: JsrPackageInfo | null
11 devDependencySuggestion?: DevDependencySuggestion | null
12 typesPackageName?: string | null
13 executableInfo?: { hasExecutable: boolean; primaryCommand?: string } | null
14 createPackageInfo?: { packageName: string } | null
15}>()
16
17const { selectedPM, showTypesInInstall, copied, copyInstallCommand } = useInstallCommand(
18 () => props.packageName,
19 () => props.requestedVersion ?? null,
20 () => props.jsrInfo ?? null,
21 () => props.typesPackageName ?? null,
22 () => props.installVersionOverride ?? null,
23)
24
25// Generate install command parts for a specific package manager
26function getInstallPartsForPM(pmId: PackageManagerId) {
27 return getInstallCommandParts({
28 packageName: props.packageName,
29 packageManager: pmId,
30 version: props.installVersionOverride ?? props.requestedVersion,
31 jsrInfo: props.jsrInfo,
32 })
33}
34
35const devDependencySuggestion = computed(
36 () => props.devDependencySuggestion ?? { recommended: false as const },
37)
38
39function getDevInstallPartsForPM(pmId: PackageManagerId) {
40 return getInstallCommandParts({
41 packageName: props.packageName,
42 packageManager: pmId,
43 version: props.requestedVersion,
44 jsrInfo: props.jsrInfo,
45 dev: true,
46 })
47}
48
49// Generate run command parts for a specific package manager
50function getRunPartsForPM(pmId: PackageManagerId, command?: string) {
51 return getRunCommandParts({
52 packageName: props.packageName,
53 packageManager: pmId,
54 jsrInfo: props.jsrInfo,
55 command,
56 isBinaryOnly: false,
57 })
58}
59
60// Generate create command parts for a specific package manager
61function getCreatePartsForPM(pmId: PackageManagerId) {
62 if (!props.createPackageInfo) return []
63 const pm = packageManagers.find(p => p.id === pmId)
64 if (!pm) return []
65
66 const createPkgName = props.createPackageInfo.packageName
67 let shortName: string
68 if (createPkgName.startsWith('@')) {
69 const slashIndex = createPkgName.indexOf('/')
70 const name = createPkgName.slice(slashIndex + 1)
71 shortName = name.startsWith('create-') ? name.slice('create-'.length) : name
72 } else {
73 shortName = createPkgName.startsWith('create-')
74 ? createPkgName.slice('create-'.length)
75 : createPkgName
76 }
77
78 return [...pm.create.split(' '), shortName]
79}
80
81// Generate @types install command parts for a specific package manager
82function getTypesInstallPartsForPM(pmId: PackageManagerId) {
83 if (!props.typesPackageName) return []
84 const pm = packageManagers.find(p => p.id === pmId)
85 if (!pm) return []
86
87 const devFlag = getDevDependencyFlag(pmId)
88 const pkgSpec = pmId === 'deno' ? `npm:${props.typesPackageName}` : props.typesPackageName
89
90 return [pm.label, pm.action, devFlag, pkgSpec]
91}
92
93// Full run command for copying (uses current selected PM)
94function getFullRunCommand(command?: string) {
95 return getRunCommand({
96 packageName: props.packageName,
97 packageManager: selectedPM.value,
98 jsrInfo: props.jsrInfo,
99 command,
100 })
101}
102
103// Full create command for copying (uses current selected PM)
104function getFullCreateCommand() {
105 return getCreatePartsForPM(selectedPM.value).join(' ')
106}
107
108// Copy handlers
109const { copied: runCopied, copy: copyRun } = useClipboard({ copiedDuring: 2000 })
110const copyRunCommand = (command?: string) => copyRun(getFullRunCommand(command))
111
112const { copied: createCopied, copy: copyCreate } = useClipboard({ copiedDuring: 2000 })
113const copyCreateCommand = () => copyCreate(getFullCreateCommand())
114
115const { copied: devInstallCopied, copy: copyDevInstall } = useClipboard({ copiedDuring: 2000 })
116const copyDevInstallCommand = () =>
117 copyDevInstall(
118 getInstallCommand({
119 packageName: props.packageName,
120 packageManager: selectedPM.value,
121 version: props.requestedVersion,
122 jsrInfo: props.jsrInfo,
123 dev: true,
124 }),
125 )
126</script>
127
128<template>
129 <div class="relative group">
130 <!-- Terminal-style install command -->
131 <div class="bg-bg-subtle border border-border rounded-lg overflow-hidden">
132 <div class="flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3">
133 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" />
134 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" />
135 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" />
136 </div>
137 <div class="px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4 space-y-1 overflow-x-auto" dir="ltr">
138 <!-- Install command - render all PM variants, CSS controls visibility -->
139 <div
140 v-for="pm in packageManagers"
141 :key="`install-${pm.id}`"
142 :data-pm-cmd="pm.id"
143 class="flex items-center gap-2 group/installcmd min-w-0"
144 >
145 <span class="self-start text-fg-subtle font-mono text-sm select-none shrink-0">$</span>
146 <code class="font-mono text-sm min-w-0"
147 ><span
148 v-for="(part, i) in getInstallPartsForPM(pm.id)"
149 :key="i"
150 :class="i === 0 ? 'text-fg' : 'text-fg-muted'"
151 >{{ i > 0 ? ' ' : '' }}{{ part }}</span
152 ></code
153 >
154 <button
155 type="button"
156 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/installcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
157 :aria-label="$t('package.get_started.copy_command')"
158 @click.stop="copyInstallCommand"
159 >
160 <span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span>
161 </button>
162 </div>
163
164 <!-- Suggested dev dependency install command -->
165 <template v-if="devDependencySuggestion.recommended">
166 <div class="flex items-center gap-2 pt-1 select-none">
167 <span class="text-fg-subtle font-mono text-sm"
168 ># {{ $t('package.get_started.dev_dependency_hint') }}</span
169 >
170 </div>
171 <div
172 v-for="pm in packageManagers"
173 :key="`install-dev-${pm.id}`"
174 :data-pm-cmd="pm.id"
175 class="flex items-center gap-2 group/devinstallcmd min-w-0"
176 >
177 <span class="text-fg-subtle font-mono text-sm select-none shrink-0">$</span>
178 <code class="font-mono text-sm min-w-0"
179 ><span
180 v-for="(part, i) in getDevInstallPartsForPM(pm.id)"
181 :key="i"
182 :class="i === 0 ? 'text-fg' : 'text-fg-muted'"
183 >{{ i > 0 ? ' ' : '' }}{{ part }}</span
184 ></code
185 >
186 <ButtonBase
187 type="button"
188 size="small"
189 class="text-fg-muted bg-bg-subtle/80 border-border opacity-0 group-hover/devinstallcmd:opacity-100 active:scale-95 focus-visible:opacity-100 select-none"
190 :aria-label="$t('package.get_started.copy_dev_command')"
191 @click.stop="copyDevInstallCommand"
192 >
193 <span aria-live="polite">{{
194 devInstallCopied ? $t('common.copied') : $t('common.copy')
195 }}</span>
196 </ButtonBase>
197 </div>
198 </template>
199
200 <!-- @types package install - render all PM variants when types package exists -->
201 <template v-if="typesPackageName && showTypesInInstall">
202 <div
203 v-for="pm in packageManagers"
204 :key="`types-${pm.id}`"
205 :data-pm-cmd="pm.id"
206 class="flex items-center gap-2 min-w-0"
207 >
208 <span class="self-start text-fg-subtle font-mono text-sm select-none shrink-0">$</span>
209 <code class="font-mono text-sm min-w-0"
210 ><span
211 v-for="(part, i) in getTypesInstallPartsForPM(pm.id)"
212 :key="i"
213 :class="i === 0 ? 'text-fg' : 'text-fg-muted'"
214 >{{ i > 0 ? ' ' : '' }}{{ part }}</span
215 ></code
216 >
217 <NuxtLink
218 :to="packageRoute(typesPackageName!)"
219 class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-accent/70 rounded select-none"
220 :title="$t('package.get_started.view_types', { package: typesPackageName })"
221 >
222 <span class="i-lucide:arrow-right rtl-flip w-3 h-3 align-middle" aria-hidden="true" />
223 <span class="sr-only">View {{ typesPackageName }}</span>
224 </NuxtLink>
225 </div>
226 </template>
227
228 <!-- Run command (only if package has executables) - render all PM variants -->
229 <template v-if="executableInfo?.hasExecutable">
230 <!-- Comment line -->
231 <div class="flex items-center gap-2 pt-1" dir="auto">
232 <span class="text-fg-subtle font-mono text-sm select-none"
233 ># {{ $t('package.run.locally') }}</span
234 >
235 </div>
236
237 <div
238 v-for="pm in packageManagers"
239 :key="`run-${pm.id}`"
240 :data-pm-cmd="pm.id"
241 class="flex items-center gap-2 group/runcmd"
242 >
243 <span class="self-start text-fg-subtle font-mono text-sm select-none">$</span>
244 <code class="font-mono text-sm"
245 ><span
246 v-for="(part, i) in getRunPartsForPM(pm.id, executableInfo?.primaryCommand)"
247 :key="i"
248 :class="i === 0 ? 'text-fg' : 'text-fg-muted'"
249 >{{ i > 0 ? ' ' : '' }}{{ part }}</span
250 ></code
251 >
252 <button
253 type="button"
254 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-accent/70 select-none"
255 @click.stop="copyRunCommand(executableInfo?.primaryCommand)"
256 >
257 {{ runCopied ? $t('common.copied') : $t('common.copy') }}
258 </button>
259 </div>
260 </template>
261
262 <!-- Create command (for packages with associated create-* package) - render all PM variants -->
263 <template v-if="createPackageInfo">
264 <!-- Comment line -->
265 <div class="flex items-center gap-2 pt-1 select-none" dir="auto">
266 <span class="text-fg-subtle font-mono text-sm"># {{ $t('package.create.title') }}</span>
267 <TooltipApp
268 :text="$t('package.create.view', { packageName: createPackageInfo.packageName })"
269 >
270 <NuxtLink
271 :to="packageRoute(createPackageInfo.packageName)"
272 class="inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1 text-fg-muted hover:text-fg text-xs transition-colors focus-visible:outline-2 focus-visible:outline-accent/70 rounded"
273 >
274 <span class="i-lucide:info w-3 h-3" aria-hidden="true" />
275 <span class="sr-only">{{
276 $t('package.create.view', { packageName: createPackageInfo.packageName })
277 }}</span>
278 </NuxtLink>
279 </TooltipApp>
280 </div>
281
282 <div
283 v-for="pm in packageManagers"
284 :key="`create-${pm.id}`"
285 :data-pm-cmd="pm.id"
286 class="flex items-center gap-2 group/createcmd"
287 >
288 <span class="self-start text-fg-subtle font-mono text-sm select-none">$</span>
289 <code class="font-mono text-sm"
290 ><span
291 v-for="(part, i) in getCreatePartsForPM(pm.id)"
292 :key="i"
293 :class="i === 0 ? 'text-fg' : 'text-fg-muted'"
294 >{{ i > 0 ? ' ' : '' }}{{ part }}</span
295 ></code
296 >
297 <button
298 type="button"
299 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-accent/70 select-none"
300 :aria-label="$t('package.create.copy_command')"
301 @click.stop="copyCreateCommand"
302 >
303 <span aria-live="polite">{{
304 createCopied ? $t('common.copied') : $t('common.copy')
305 }}</span>
306 </button>
307 </div>
308 </template>
309 </div>
310 </div>
311 </div>
312</template>
313
314<style>
315/*
316 * Package manager command visibility based on data-pm attribute on <html>.
317 * All variants are rendered; CSS shows only the selected one.
318 */
319
320/* Hide all variants by default when preference is set */
321:root[data-pm] [data-pm-cmd] {
322 display: none;
323}
324
325/* Show only the matching package manager command */
326:root[data-pm='npm'] [data-pm-cmd='npm'],
327:root[data-pm='pnpm'] [data-pm-cmd='pnpm'],
328:root[data-pm='yarn'] [data-pm-cmd='yarn'],
329:root[data-pm='bun'] [data-pm-cmd='bun'],
330:root[data-pm='deno'] [data-pm-cmd='deno'],
331:root[data-pm='vlt'] [data-pm-cmd='vlt'] {
332 display: flex;
333}
334
335/* Fallback: when no data-pm is set (SSR initial), show npm as default */
336:root:not([data-pm]) [data-pm-cmd]:not([data-pm-cmd='npm']) {
337 display: none;
338}
339</style>