Skip to content

Commit 9decc74

Browse files
committed
feat: add @types/ packages in install command
1 parent 14ccc37 commit 9decc74

9 files changed

Lines changed: 259 additions & 103 deletions

File tree

app/components/PackageMetricsBadges.vue

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
11
<script setup lang="ts">
2-
import type { ModuleFormat, TypesStatus } from '#shared/utils/package-analysis'
2+
import { NuxtLink } from '#components'
33
44
const props = defineProps<{
55
packageName: string
66
version?: string
77
}>()
88
9-
interface PackageAnalysisResponse {
10-
package: string
11-
version: string
12-
moduleFormat: ModuleFormat
13-
types: TypesStatus
14-
engines?: {
15-
node?: string
16-
npm?: string
17-
}
18-
}
19-
20-
const { data: analysis, status } = useLazyFetch<PackageAnalysisResponse>(
21-
() => {
22-
const base = `/api/registry/analysis/${props.packageName}`
23-
return props.version ? `${base}/v/${props.version}` : base
24-
},
25-
{
26-
server: false, // Client-side only to avoid blocking initial render
27-
},
9+
const { data: analysis } = usePackageAnalysis(
10+
() => props.packageName,
11+
() => props.version,
2812
)
2913
3014
const moduleFormatLabel = computed(() => {
@@ -86,10 +70,14 @@ const typesHref = computed(() => {
8670
<!-- TypeScript types -->
8771
<li v-if="hasTypes">
8872
<component
89-
:is="typesHref ? 'NuxtLink' : 'span'"
73+
:is="typesHref ? NuxtLink : 'span'"
9074
:to="typesHref"
9175
class="inline-flex items-center px-1.5 py-0.5 font-mono text-xs text-fg-muted bg-bg-muted border border-border rounded transition-colors duration-200"
92-
:class="typesHref ? 'hover:text-fg hover:border-border-hover' : ''"
76+
:class="
77+
typesHref
78+
? 'hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50'
79+
: ''
80+
"
9381
:title="typesTooltip"
9482
>
9583
TS

app/components/SettingsMenu.vue

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ onKeyStroke(',', e => {
4343
<button
4444
ref="triggerRef"
4545
type="button"
46-
class="link-subtle font-mono text-sm inline-flex items-center justify-center gap-2"
46+
class="link-subtle font-mono text-sm inline-flex items-center justify-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
4747
:aria-expanded="isOpen"
4848
aria-haspopup="menu"
4949
aria-label="Settings"
@@ -81,34 +81,52 @@ onKeyStroke(',', e => {
8181

8282
<div class="p-2 space-y-1">
8383
<!-- Relative dates toggle -->
84-
<div
85-
class="flex items-center justify-between gap-3 px-2 py-2 rounded-md hover:bg-bg-muted transition-[background-color] duration-150 cursor-pointer"
84+
<button
85+
type="button"
86+
class="w-full flex items-center justify-between gap-3 px-2 py-2 rounded-md hover:bg-bg-muted transition-[background-color] duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
87+
role="menuitemcheckbox"
88+
:aria-checked="settings.relativeDates"
8689
@click="settings.relativeDates = !settings.relativeDates"
8790
>
88-
<label
89-
:id="`settings-relative-dates-label`"
90-
class="text-sm text-fg cursor-pointer select-none"
91-
>
92-
Relative dates
93-
</label>
94-
<button
95-
type="button"
96-
role="switch"
97-
:aria-checked="settings.relativeDates"
98-
aria-labelledby="settings-relative-dates-label"
99-
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-[background-color] duration-200 ease-in-out motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
91+
<span class="text-sm text-fg select-none">Relative dates</span>
92+
<span
93+
class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent transition-[background-color] duration-200 ease-in-out motion-reduce:transition-none"
10094
:class="settings.relativeDates ? 'bg-fg' : 'bg-bg-subtle'"
101-
@click.stop="settings.relativeDates = !settings.relativeDates"
95+
aria-hidden="true"
10296
>
10397
<span
104-
aria-hidden="true"
10598
class="pointer-events-none inline-block h-4 w-4 rounded-full shadow-sm ring-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none"
10699
:class="
107100
settings.relativeDates ? 'translate-x-4 bg-bg' : 'translate-x-0 bg-fg-muted'
108101
"
109102
/>
110-
</button>
111-
</div>
103+
</span>
104+
</button>
105+
106+
<!-- Include @types in install toggle -->
107+
<button
108+
type="button"
109+
class="w-full flex items-center justify-between gap-3 px-2 py-2 rounded-md hover:bg-bg-muted transition-[background-color] duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
110+
role="menuitemcheckbox"
111+
:aria-checked="settings.includeTypesInInstall"
112+
@click="settings.includeTypesInInstall = !settings.includeTypesInInstall"
113+
>
114+
<span class="text-sm text-fg select-none">Include @types in install</span>
115+
<span
116+
class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent transition-[background-color] duration-200 ease-in-out motion-reduce:transition-none"
117+
:class="settings.includeTypesInInstall ? 'bg-fg' : 'bg-bg-subtle'"
118+
aria-hidden="true"
119+
>
120+
<span
121+
class="pointer-events-none inline-block h-4 w-4 rounded-full shadow-sm ring-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none"
122+
:class="
123+
settings.includeTypesInInstall
124+
? 'translate-x-4 bg-bg'
125+
: 'translate-x-0 bg-fg-muted'
126+
"
127+
/>
128+
</span>
129+
</button>
112130
</div>
113131
</div>
114132
</Transition>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ModuleFormat, TypesStatus } from '#shared/utils/package-analysis'
2+
3+
export interface PackageAnalysisResponse {
4+
package: string
5+
version: string
6+
moduleFormat: ModuleFormat
7+
types: TypesStatus
8+
engines?: {
9+
node?: string
10+
npm?: string
11+
}
12+
}
13+
14+
/**
15+
* Composable for fetching package analysis data (module format, types info, etc.)
16+
*/
17+
export function usePackageAnalysis(
18+
packageName: MaybeRefOrGetter<string>,
19+
version?: MaybeRefOrGetter<string | null | undefined>,
20+
) {
21+
return useLazyFetch<PackageAnalysisResponse>(() => {
22+
const name = toValue(packageName)
23+
const ver = toValue(version)
24+
const base = `/api/registry/analysis/${name}`
25+
return ver ? `${base}/v/${ver}` : base
26+
})
27+
}

app/composables/useSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { useLocalStorage } from '@vueuse/core'
77
export interface AppSettings {
88
/** Display dates as relative (e.g., "3 days ago") instead of absolute */
99
relativeDates: boolean
10+
/** Include @types/* package in install command for packages without built-in types */
11+
includeTypesInInstall: boolean
1012
}
1113

1214
const DEFAULT_SETTINGS: AppSettings = {
1315
relativeDates: false,
16+
includeTypesInInstall: true,
1417
}
1518

1619
const STORAGE_KEY = 'npmx-settings'

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

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,23 @@ function hasProvenance(version: PackumentVersion | null): boolean {
250250
}
251251
252252
const selectedPM = useSelectedPackageManager()
253+
const { settings } = useSettings()
254+
255+
// Fetch package analysis for @types info
256+
const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
257+
258+
// Get @types package name if available (non-deprecated)
259+
const typesPackageName = computed(() => {
260+
if (!packageAnalysis.value) return null
261+
if (packageAnalysis.value.types.kind !== '@types') return null
262+
if (packageAnalysis.value.types.deprecated) return null
263+
return packageAnalysis.value.types.packageName
264+
})
265+
266+
// Check if we should show @types in install command
267+
const showTypesInInstall = computed(() => {
268+
return settings.value.includeTypesInInstall && typesPackageName.value
269+
})
253270
254271
const installCommandParts = computed(() => {
255272
if (!pkg.value) return []
@@ -271,11 +288,48 @@ const installCommand = computed(() => {
271288
})
272289
})
273290
291+
// Get the dev dependency flag for the selected package manager
292+
function getDevFlag(pmId: string): string {
293+
// bun uses lowercase -d, all others use -D
294+
return pmId === 'bun' ? '-d' : '-D'
295+
}
296+
297+
// @types install command parts (for display)
298+
const typesInstallCommandParts = computed(() => {
299+
if (!typesPackageName.value) return []
300+
const pm = packageManagers.find(p => p.id === selectedPM.value)
301+
if (!pm) return []
302+
303+
const devFlag = getDevFlag(selectedPM.value)
304+
const pkgSpec =
305+
selectedPM.value === 'deno' ? `npm:${typesPackageName.value}` : typesPackageName.value
306+
307+
return [pm.label, pm.action, devFlag, pkgSpec]
308+
})
309+
310+
// Full install command including @types (for copying)
311+
const fullInstallCommand = computed(() => {
312+
if (!installCommand.value) return ''
313+
if (!showTypesInInstall.value || !typesPackageName.value) {
314+
return installCommand.value
315+
}
316+
317+
const pm = packageManagers.find(p => p.id === selectedPM.value)
318+
if (!pm) return installCommand.value
319+
320+
const devFlag = getDevFlag(selectedPM.value)
321+
const pkgSpec =
322+
selectedPM.value === 'deno' ? `npm:${typesPackageName.value}` : typesPackageName.value
323+
324+
// Use semicolon to separate commands
325+
return `${installCommand.value}; ${pm.label} ${pm.action} ${devFlag} ${pkgSpec}`
326+
})
327+
274328
// Copy install command
275329
const copied = ref(false)
276330
async function copyInstallCommand() {
277-
if (!installCommand.value) return
278-
await navigator.clipboard.writeText(installCommand.value)
331+
if (!fullInstallCommand.value) return
332+
await navigator.clipboard.writeText(fullInstallCommand.value)
279333
copied.value = true
280334
setTimeout(() => (copied.value = false), 2000)
281335
}
@@ -453,6 +507,7 @@ defineOgImageComponent('Package', {
453507
<button
454508
type="button"
455509
class="font-mono text-xs text-fg-muted hover:text-fg bg-bg px-1 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
510+
aria-label="Show full description"
456511
@click="descriptionExpanded = true"
457512
>
458513
show more
@@ -719,7 +774,7 @@ defineOgImageComponent('Package', {
719774
:key="pm.id"
720775
role="tab"
721776
:aria-selected="selectedPM === pm.id"
722-
class="px-2 py-1 font-mono text-xs rounded transition-all duration-150"
777+
class="px-2 py-1 font-mono text-xs rounded transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
723778
:class="
724779
selectedPM === pm.id
725780
? 'bg-bg-elevated text-fg'
@@ -750,29 +805,54 @@ defineOgImageComponent('Package', {
750805
<span class="w-2.5 h-2.5 rounded-full bg-[#333]" />
751806
<span class="w-2.5 h-2.5 rounded-full bg-[#333]" />
752807
</div>
753-
<div class="flex items-center gap-2 px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4">
754-
<span class="text-fg-subtle font-mono text-sm select-none">$</span>
755-
<code class="font-mono text-sm"
756-
><ClientOnly
808+
<div class="space-y-1 px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4">
809+
<!-- Main package install -->
810+
<div class="flex items-center gap-2">
811+
<span class="text-fg-subtle font-mono text-sm select-none">$</span>
812+
<code class="font-mono text-sm"
813+
><ClientOnly
814+
><span
815+
v-for="(part, i) in installCommandParts"
816+
:key="i"
817+
:class="i === 0 ? 'text-fg' : 'text-fg-muted'"
818+
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
819+
><template #fallback
820+
><span class="text-fg">npm</span
821+
><span class="text-fg-muted"> install {{ pkg.name }}</span></template
822+
></ClientOnly
823+
></code
824+
>
825+
</div>
826+
<!-- @types package install (when enabled) -->
827+
<div v-if="showTypesInInstall" class="flex items-center gap-2">
828+
<span class="text-fg-subtle font-mono text-sm select-none">$</span>
829+
<code class="font-mono text-sm"
757830
><span
758-
v-for="(part, i) in installCommandParts"
831+
v-for="(part, i) in typesInstallCommandParts"
759832
:key="i"
760833
:class="i === 0 ? 'text-fg' : 'text-fg-muted'"
761834
>{{ i > 0 ? ' ' : '' }}{{ part }}</span
762-
><template #fallback
763-
><span class="text-fg">npm</span
764-
><span class="text-fg-muted"> install {{ pkg.name }}</span></template
765-
></ClientOnly
766-
></code
767-
>
835+
></code
836+
>
837+
<NuxtLink
838+
v-if="typesPackageName"
839+
:to="`/${typesPackageName}`"
840+
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"
841+
title="View @types package"
842+
>
843+
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
844+
<span class="sr-only">View {{ typesPackageName }}</span>
845+
</NuxtLink>
846+
</div>
768847
</div>
769848
</div>
770849
<button
771850
type="button"
772851
class="absolute top-3 right-3 px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
852+
aria-label="Copy install command"
773853
@click="copyInstallCommand"
774854
>
775-
{{ copied ? 'copied!' : 'copy' }}
855+
<span aria-live="polite">{{ copied ? 'copied!' : 'copy' }}</span>
776856
</button>
777857
</div>
778858
</section>

server/api/registry/[...pkg].get.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)