From 7099e4b6585d9fa05e06e9e30dcfcb543f16c1c4 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Fri, 27 Feb 2026 19:51:25 -0500 Subject: [PATCH] feat: add per-entrypoint API docs pages for multi-export packages Packages with only subpath exports (no root export) previously got no docs because esm.sh returns 404 for their root URL. Fix by falling back to the npm registry field to discover typed subpath entries. Additionally, multi-entrypoint packages now get separate docs pages per subpath with an EntrypointSelector dropdown, instead of dumping all symbols into one flat page. The base URL redirects to the first entrypoint. URL structure: /package-docs/{pkg}/v/{version}/{entrypoint} Closes #1479 --- app/components/EntrypointSelector.stories.ts | 80 ++++++ app/components/EntrypointSelector.vue | 64 +++++ app/composables/useCommandPaletteCommands.ts | 5 + .../useCommandPaletteEntrypointCommands.ts | 51 ++++ app/pages/package-docs/[...path].vue | 83 ++++-- app/types/command-palette.ts | 1 + app/utils/router.ts | 22 +- i18n/locales/en.json | 11 +- i18n/locales/fr-FR.json | 11 +- i18n/schema.json | 21 ++ server/api/registry/docs/[...pkg].get.ts | 48 +++- server/utils/docs/client.ts | 160 ++++++++++- server/utils/docs/index.ts | 49 +++- shared/types/docs.ts | 4 + test/nuxt/a11y.spec.ts | 29 ++ .../components/EntrypointSelector.spec.ts | 68 +++++ test/nuxt/components/VersionSelector.spec.ts | 5 - .../server/api/registry/docs/pkg.get.spec.ts | 222 +++++++++++++++ test/unit/server/utils/docs/client.spec.ts | 263 ++++++++++++++++++ test/unit/server/utils/docs/index.spec.ts | 129 +++++++++ 20 files changed, 1270 insertions(+), 56 deletions(-) create mode 100644 app/components/EntrypointSelector.stories.ts create mode 100644 app/components/EntrypointSelector.vue create mode 100644 app/composables/useCommandPaletteEntrypointCommands.ts create mode 100644 test/nuxt/components/EntrypointSelector.spec.ts create mode 100644 test/unit/server/api/registry/docs/pkg.get.spec.ts create mode 100644 test/unit/server/utils/docs/client.spec.ts create mode 100644 test/unit/server/utils/docs/index.spec.ts diff --git a/app/components/EntrypointSelector.stories.ts b/app/components/EntrypointSelector.stories.ts new file mode 100644 index 0000000000..830746c170 --- /dev/null +++ b/app/components/EntrypointSelector.stories.ts @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook-vue/nuxt' +import EntrypointSelector from './EntrypointSelector.vue' + +const meta = { + component: EntrypointSelector, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const SingleEntrypoint: Story = { + args: { + packageName: 'vue', + version: '3.5.0', + currentEntrypoint: '.', + entrypoints: ['.'], + }, +} + +export const MultipleEntrypoints: Story = { + args: { + packageName: '@nuxt/kit', + version: '4.3.1', + currentEntrypoint: '.', + entrypoints: ['.', 'compatibility', 'loader'], + }, +} + +export const ManyEntrypoints: Story = { + args: { + packageName: '@radix-ui/themes', + version: '3.0.0', + currentEntrypoint: 'button', + entrypoints: [ + 'accordion', + 'alert-dialog', + 'avatar', + 'badge', + 'box', + 'button', + 'callout', + 'card', + 'checkbox', + 'container', + 'dialog', + 'flex', + 'grid', + 'heading', + 'icon-button', + 'inset', + 'link', + 'popover', + 'progress', + 'radio-group', + 'scroll-area', + 'select', + 'separator', + 'skeleton', + 'slider', + 'spinner', + 'switch', + 'table', + 'tabs', + 'text', + 'text-area', + 'text-field', + 'theme', + 'tooltip', + ], + }, +} + +export const NestedEntrypoint: Story = { + args: { + packageName: '@nuxt/kit', + version: '4.3.1', + currentEntrypoint: 'compat/utils', + entrypoints: ['.', 'compat/utils', 'compat/legacy'], + }, +} diff --git a/app/components/EntrypointSelector.vue b/app/components/EntrypointSelector.vue new file mode 100644 index 0000000000..1a97dc62d5 --- /dev/null +++ b/app/components/EntrypointSelector.vue @@ -0,0 +1,64 @@ + + + diff --git a/app/composables/useCommandPaletteCommands.ts b/app/composables/useCommandPaletteCommands.ts index 9c07149131..d52212d227 100644 --- a/app/composables/useCommandPaletteCommands.ts +++ b/app/composables/useCommandPaletteCommands.ts @@ -17,6 +17,7 @@ const GROUP_ORDER: CommandPaletteGroup[] = [ 'settings', 'help', 'npmx', + 'entrypoints', 'versions', ] @@ -68,6 +69,10 @@ export function useCommandPaletteCommands() { return packageName ? t('command_palette.groups.versions_with_name', { name: packageName }) : t('command_palette.groups.versions') + case 'entrypoints': + return packageName + ? t('command_palette.groups.entrypoints_with_name', { name: packageName }) + : t('command_palette.groups.entrypoints') } } diff --git a/app/composables/useCommandPaletteEntrypointCommands.ts b/app/composables/useCommandPaletteEntrypointCommands.ts new file mode 100644 index 0000000000..3c3a337b6f --- /dev/null +++ b/app/composables/useCommandPaletteEntrypointCommands.ts @@ -0,0 +1,51 @@ +// @unocss-include +import type { MaybeRefOrGetter } from 'vue' +import type { + CommandPaletteContextCommandInput, + CommandPalettePackageContext, +} from '~/types/command-palette' + +interface EntrypointContext { + packageContext: CommandPalettePackageContext + entrypoints: string[] + currentEntrypoint: string | null +} + +function getEntrypointLabel(entrypoint: string): string { + return entrypoint === '.' ? '.' : `./${entrypoint}` +} + +export function useCommandPaletteEntrypointCommands( + context: MaybeRefOrGetter, +) { + const { t } = useI18n() + + useCommandPaletteContextCommands( + computed((): CommandPaletteContextCommandInput[] => { + const ctx = toValue(context) + if (!ctx?.packageContext.resolvedVersion) return [] + if (ctx.entrypoints.length === 0) return [] + + return ctx.entrypoints.map(entrypoint => ({ + id: `entrypoint:${entrypoint}`, + group: 'entrypoints' as const, + label: t('command_palette.entrypoint.label', { + entrypoint: getEntrypointLabel(entrypoint), + }), + keywords: [ + ctx.packageContext.packageName, + entrypoint, + getEntrypointLabel(entrypoint), + t('command_palette.groups.entrypoints'), + ], + iconClass: 'i-lucide:package', + active: entrypoint === ctx.currentEntrypoint, + to: docsRoute( + ctx.packageContext.packageName, + ctx.packageContext.resolvedVersion!, + entrypoint, + ), + })) + }), + ) +} diff --git a/app/pages/package-docs/[...path].vue b/app/pages/package-docs/[...path].vue index d07a3d023d..ee8bf85fa4 100644 --- a/app/pages/package-docs/[...path].vue +++ b/app/pages/package-docs/[...path].vue @@ -1,5 +1,6 @@