From 062195b549dcee787d68c89a39249fc053ab84c8 Mon Sep 17 00:00:00 2001 From: Alexander Wondwossen Date: Sat, 28 Mar 2026 09:43:57 -0400 Subject: [PATCH 1/2] feat: add dependents page for package reverse-dep lookup Adds /package//dependents showing which packages list this one as a dependency, using the npm search API's dependencies: text filter. - New server API at /api/registry/dependents/[...pkg] (paginated, cached) - New page at /package/[[org]]/[name]/dependents.vue - Dependents tab added to the package navigation header - i18n keys for the new page and nav label --- README.md | 4 +- app/components/Package/Header.vue | 22 ++- .../package/[[org]]/[name]/dependents.vue | 183 ++++++++++++++++++ i18n/locales/en.json | 10 +- i18n/schema.json | 24 +++ .../api/registry/dependents/[...pkg].get.ts | 54 ++++++ 6 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 app/pages/package/[[org]]/[name]/dependents.vue create mode 100644 server/api/registry/dependents/[...pkg].get.ts diff --git a/README.md b/README.md index 05efb54876..14b6576b1d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ What npmx offers: - **Version range resolution** – dependency ranges (e.g., `^1.0.0`) resolve to actual installed versions - **Claim new packages** – register new package names directly from search results (via local connector) - **Clickable version tags** – navigate directly to any version from the versions list +- **Dependents list** – browse packages that depend on a given package ### User & org pages @@ -90,7 +91,7 @@ What npmx offers: | Keyboard navigation | ❌ | ✅ | | Multi-provider repo support | ❌ | ✅ | | Version range resolution | ❌ | ✅ | -| Dependents list | ✅ | 🚧 | +| Dependents list | ✅ | ✅ | | Package admin (access/owners) | ✅ | 🚧 | | Org/team management | ✅ | 🚧 | | 2FA/account settings | ✅ | ❌ | @@ -119,7 +120,6 @@ npmx.dev supports npm permalinks – just replace `npmjs.com` with `npmx.dev #### Not yet supported - `/package//access` – package access settings -- `/package//dependents` – dependent packages list - `/settings/*` – account settings pages ### Simpler URLs diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue index 7cf6b4c8bc..bed47dd4e5 100644 --- a/app/components/Package/Header.vue +++ b/app/components/Package/Header.vue @@ -10,7 +10,7 @@ const props = defineProps<{ latestVersion?: SlimVersion | null provenanceData?: ProvenanceDetails | null provenanceStatus?: string | null - page: 'main' | 'docs' | 'code' | 'diff' + page: 'main' | 'docs' | 'code' | 'diff' | 'dependents' versionUrlPattern: string }>() @@ -108,6 +108,18 @@ const mainLink = computed((): RouteLocationRaw | null => { return packageRoute(props.pkg.name, props.resolvedVersion) }) +const dependentsLink = computed((): RouteLocationRaw | null => { + if (props.pkg == null) return null + const split = props.pkg.name.split('/') + return { + name: 'package-dependents', + params: { + org: split.length === 2 ? split[0] : undefined, + name: split.length === 2 ? split[1]! : split[0]!, + }, + } +}) + const diffLink = computed((): RouteLocationRaw | null => { if ( props.pkg == null || @@ -343,6 +355,14 @@ const fundingUrl = computed(() => { > {{ $t('compare.compare_versions') }} + + {{ $t('package.links.dependents') }} + diff --git a/app/pages/package/[[org]]/[name]/dependents.vue b/app/pages/package/[[org]]/[name]/dependents.vue new file mode 100644 index 0000000000..38254b340a --- /dev/null +++ b/app/pages/package/[[org]]/[name]/dependents.vue @@ -0,0 +1,183 @@ + + + diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 02c1af000f..4255496862 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -318,7 +318,15 @@ "docs": "docs", "fund": "fund", "compare": "compare", - "compare_this_package": "compare this package" + "compare_this_package": "compare this package", + "dependents": "dependents" + }, + "dependents": { + "title": "Dependents", + "subtitle": "Packages that list {name} as a dependency", + "count": "{count} dependent | {count} dependents", + "none": "No packages depend on {name} yet", + "error": "Failed to load dependents" }, "likes": { "like": "Like this package", diff --git a/i18n/schema.json b/i18n/schema.json index 885bd6eb5e..c80fd13ebd 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -960,6 +960,30 @@ }, "compare_this_package": { "type": "string" + }, + "dependents": { + "type": "string" + } + }, + "additionalProperties": false + }, + "dependents": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "count": { + "type": "string" + }, + "none": { + "type": "string" + }, + "error": { + "type": "string" } }, "additionalProperties": false diff --git a/server/api/registry/dependents/[...pkg].get.ts b/server/api/registry/dependents/[...pkg].get.ts new file mode 100644 index 0000000000..fb0984fc61 --- /dev/null +++ b/server/api/registry/dependents/[...pkg].get.ts @@ -0,0 +1,54 @@ +/** + * Returns a paginated list of packages that depend on the given package. + * + * Uses the npm search API with a `dependencies:` text filter. + * + * URL patterns: + * - /api/registry/dependents/packageName?page=0&size=20 + * - /api/registry/dependents/@scope/packageName?page=0&size=20 + */ +export default defineCachedEventHandler( + async event => { + const pkgParam = getRouterParam(event, 'pkg') + if (!pkgParam) { + throw createError({ statusCode: 400, message: 'Package name is required' }) + } + + const packageName = decodeURIComponent(pkgParam) + const query = getQuery(event) + const page = Number(query.page ?? 0) + const size = Math.min(Number(query.size ?? 20), 50) + + const params = new URLSearchParams({ + text: `dependencies:${packageName}`, + size: String(size), + from: String(page * size), + }) + + const response = await $fetch( + `${NPM_REGISTRY}/-/v1/search?${params}`, + ) + + return { + total: response.total, + page, + size, + packages: response.objects.map(obj => ({ + name: obj.package.name, + version: obj.package.version, + description: obj.package.description ?? null, + date: obj.package.date ?? null, + score: obj.score?.final ?? 0, + })), + } + }, + { + maxAge: CACHE_MAX_AGE_FIVE_MINUTES, + swr: true, + getKey: event => { + const pkg = getRouterParam(event, 'pkg') ?? '' + const query = getQuery(event) + return `dependents:${pkg}:${query.page ?? 0}:${query.size ?? 20}` + }, + }, +) From 0103b77b5cfecf55798a5fe8eb4ddf950a2664b3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:45:20 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- server/api/registry/dependents/[...pkg].get.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/api/registry/dependents/[...pkg].get.ts b/server/api/registry/dependents/[...pkg].get.ts index fb0984fc61..208a72be0e 100644 --- a/server/api/registry/dependents/[...pkg].get.ts +++ b/server/api/registry/dependents/[...pkg].get.ts @@ -25,9 +25,7 @@ export default defineCachedEventHandler( from: String(page * size), }) - const response = await $fetch( - `${NPM_REGISTRY}/-/v1/search?${params}`, - ) + const response = await $fetch(`${NPM_REGISTRY}/-/v1/search?${params}`) return { total: response.total,