Skip to content

Commit f04afc4

Browse files
committed
feat: add jsr badge when available on jsr
1 parent 53a7581 commit f04afc4

10 files changed

Lines changed: 197 additions & 6 deletions

File tree

app/components/JsrBadge.vue

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
/** JSR package URL (e.g., "https://jsr.io/@std/fs") */
4+
url: string
5+
/** Whether to show as compact (icon only) or full (with text) */
6+
compact?: boolean
7+
}>()
8+
</script>
9+
10+
<template>
11+
<a
12+
:href="url"
13+
target="_blank"
14+
rel="noopener noreferrer"
15+
class="inline-flex items-center gap-1 text-xs font-mono text-fg-muted hover:text-fg transition-colors duration-200"
16+
title="also available on JSR"
17+
>
18+
<span
19+
class="i-simple-icons-jsr shrink-0"
20+
:class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'"
21+
aria-hidden="true"
22+
/>
23+
<span v-if="!compact" class="sr-only sm:not-sr-only">jsr</span>
24+
</a>
25+
</template>

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { joinURL } from 'ufo'
33
import type { PackumentVersion, NpmVersionDist } from '#shared/types'
4+
import type { JsrPackageInfo } from '#shared/types/jsr'
45
56
definePageMeta({
67
name: 'package',
@@ -59,6 +60,13 @@ const { data: readmeData } = useLazyFetch<{ html: string }>(
5960
{ default: () => ({ html: '' }) },
6061
)
6162
63+
// Check if package exists on JSR (only for scoped packages)
64+
const { data: jsrInfo } = useLazyFetch<JsrPackageInfo>(() => `/api/jsr/${packageName.value}`, {
65+
default: () => ({ exists: false }),
66+
// Only fetch for scoped packages (JSR requirement)
67+
immediate: computed(() => packageName.value.startsWith('@')).value,
68+
})
69+
6270
// Get the version to display (requested or latest)
6371
const displayVersion = computed(() => {
6472
if (!pkg.value) return null
@@ -415,6 +423,18 @@ defineOgImageComponent('Package', {
415423
npm
416424
</a>
417425
</li>
426+
<li v-if="jsrInfo?.exists && jsrInfo.url">
427+
<a
428+
:href="jsrInfo.url"
429+
target="_blank"
430+
rel="noopener noreferrer"
431+
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
432+
title="Also available on JSR"
433+
>
434+
<span class="i-simple-icons-jsr w-4 h-4" />
435+
jsr
436+
</a>
437+
</li>
418438
<li>
419439
<a
420440
:href="`https://socket.dev/npm/package/${pkg.name}/overview/${displayVersion?.version ?? 'latest'}`"

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"citty": "^0.2.0",
2727
"consola": "^3.4.2",
2828
"defu": "^6.1.4",
29-
"h3": "^2.0.1-rc.11",
29+
"h3-next": "npm:h3@^2.0.1-rc.11",
3030
"ofetch": "^1.5.1",
3131
"picocolors": "^1.1.1",
3232
"srvx": "^0.10.1",

cli/src/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import crypto from 'node:crypto'
2-
import { H3, HTTPError, handleCors, type H3Event } from 'h3'
3-
import type { CorsOptions } from 'h3'
2+
import { H3, HTTPError, handleCors, type H3Event } from 'h3-next'
3+
import type { CorsOptions } from 'h3-next'
44

55
import type { ConnectorState, PendingOperation, OperationType, ApiResponse } from './types.ts'
66
import {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"test:unit": "vitest --project unit"
2727
},
2828
"dependencies": {
29+
"@iconify-json/simple-icons": "^1.2.67",
2930
"@iconify-json/vscode-icons": "^1.2.40",
3031
"@nuxt/fonts": "^0.13.0",
3132
"@nuxt/scripts": "^0.13.2",

pnpm-lock.yaml

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { JsrPackageInfo } from '#shared/types/jsr'
2+
3+
/**
4+
* Check if an npm package exists on JSR.
5+
*
6+
* GET /api/jsr/:pkg
7+
*
8+
* @example GET /api/jsr/@std/fs → { exists: true, scope: "std", name: "fs", ... }
9+
* @example GET /api/jsr/lodash → { exists: false }
10+
*/
11+
export default defineCachedEventHandler<Promise<JsrPackageInfo>>(
12+
async event => {
13+
const pkgPath = getRouterParam(event, 'pkg')
14+
if (!pkgPath) {
15+
throw createError({ statusCode: 400, message: 'Package name is required' })
16+
}
17+
18+
return await fetchJsrPackageInfo(pkgPath)
19+
},
20+
{
21+
maxAge: 60 * 60, // 1 hour
22+
name: 'api-jsr-package',
23+
getKey: event => getRouterParam(event, 'pkg') ?? '',
24+
},
25+
)

server/utils/jsr.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { JsrPackageMeta, JsrPackageInfo } from '#shared/types/jsr'
2+
3+
const JSR_REGISTRY = 'https://jsr.io'
4+
5+
/**
6+
* Check if a scoped npm package exists on JSR with the same name.
7+
*
8+
* This only works for scoped packages (@scope/name) since:
9+
* 1. JSR only has scoped packages
10+
* 2. We can only authoritatively match when names are identical
11+
*
12+
* Unscoped npm packages (e.g., "hono") may exist on JSR under a different
13+
* name (e.g., "@hono/hono"), but we don't attempt to guess these mappings.
14+
*
15+
* @param npmPackageName - The npm package name (e.g., "@hono/hono")
16+
* @returns JsrPackageInfo with existence status and metadata
17+
*/
18+
export const fetchJsrPackageInfo = defineCachedFunction(
19+
async (npmPackageName: string): Promise<JsrPackageInfo> => {
20+
// Only check scoped packages - we can't authoritatively map unscoped names
21+
if (!npmPackageName.startsWith('@')) {
22+
return { exists: false }
23+
}
24+
25+
// Parse scope and name from @scope/name format
26+
const match = npmPackageName.match(/^@([^/]+)\/(.+)$/)
27+
if (!match) {
28+
return { exists: false }
29+
}
30+
31+
const [, scope, name] = match
32+
33+
try {
34+
// Fetch JSR package metadata
35+
const meta = await $fetch<JsrPackageMeta>(`${JSR_REGISTRY}/@${scope}/${name}/meta.json`, {
36+
// Short timeout since this is a nice-to-have feature
37+
timeout: 3000,
38+
})
39+
40+
// Find latest non-yanked version
41+
const versions = Object.entries(meta.versions)
42+
.filter(([, v]) => !v.yanked)
43+
.map(([version]) => version)
44+
45+
versions.sort()
46+
const latestVersion = versions[versions.length - 1]
47+
48+
return {
49+
exists: true,
50+
scope: meta.scope,
51+
name: meta.name,
52+
url: `${JSR_REGISTRY}/@${meta.scope}/${meta.name}`,
53+
latestVersion,
54+
}
55+
} catch {
56+
// Package doesn't exist on JSR or API error
57+
return { exists: false }
58+
}
59+
},
60+
{
61+
// Cache for 1 hour - JSR info doesn't change often
62+
maxAge: 60 * 60,
63+
name: 'jsr-package-info',
64+
getKey: (name: string) => name,
65+
},
66+
)

shared/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './npm-registry'
2+
export * from './jsr'

shared/types/jsr.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* JSR (jsr.io) Registry API Types
3+
*
4+
* @see https://jsr.io/docs/api
5+
*/
6+
7+
/**
8+
* JSR package metadata from meta.json
9+
* GET https://jsr.io/@<scope>/<package-name>/meta.json
10+
*/
11+
export interface JsrPackageMeta {
12+
/** Package scope (without @) */
13+
scope: string
14+
/** Package name */
15+
name: string
16+
/** Map of versions to version metadata */
17+
versions: Record<string, JsrVersionMeta>
18+
}
19+
20+
/**
21+
* JSR version metadata (minimal, from meta.json)
22+
*/
23+
export interface JsrVersionMeta {
24+
/** If true, the version has been yanked */
25+
yanked?: boolean
26+
}
27+
28+
/**
29+
* JSR package info response for our API
30+
* Indicates whether a package exists on JSR
31+
*/
32+
export interface JsrPackageInfo {
33+
/** Whether the package exists on JSR */
34+
exists: boolean
35+
/** JSR scope (without @) */
36+
scope?: string
37+
/** JSR package name */
38+
name?: string
39+
/** Full JSR URL */
40+
url?: string
41+
/** Latest version on JSR (non-yanked) */
42+
latestVersion?: string
43+
}

0 commit comments

Comments
 (0)