From 94084af808a934df42a78dbfe09e0cd35b73dd25 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 23 Aug 2026 19:57:19 +0000 Subject: [PATCH] feat: source cli command docs from nuxt/cli --- app/components/DocsProseImg.vue | 39 +++++++++++++++++++++++++++++++++ app/pages/docs/[...slug].vue | 3 ++- content.config.ts | 34 +++++++++++++++++++++++----- nuxt.config.ts | 27 ++++++++++++++++++----- patches/@nuxt__content.patch | 22 +++++++++++++++++++ pnpm-lock.yaml | 13 ++++++----- pnpm-workspace.yaml | 6 +++++ shared/utils/cli-docs.ts | 22 +++++++++++++++++++ 8 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 app/components/DocsProseImg.vue create mode 100644 patches/@nuxt__content.patch create mode 100644 shared/utils/cli-docs.ts diff --git a/app/components/DocsProseImg.vue b/app/components/DocsProseImg.vue new file mode 100644 index 000000000..09795efe4 --- /dev/null +++ b/app/components/DocsProseImg.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/pages/docs/[...slug].vue b/app/pages/docs/[...slug].vue index 68567bf18..bae4e6a75 100644 --- a/app/pages/docs/[...slug].vue +++ b/app/pages/docs/[...slug].vue @@ -4,6 +4,7 @@ import { joinURL } from 'ufo' import type { ContentNavigationItem } from '@nuxt/content' import { findPageBreadcrumb } from '@nuxt/content/utils' import { mapContentNavigation } from '@nuxt/ui/utils/content' +import { DocsProseImg } from '#components' import { SUPPORTED_DOCS_PATH_REGEX } from '#shared/utils/docs' definePageMeta({ @@ -250,7 +251,7 @@ const noRightAside = computed(() => route.path.includes('/examples/')) - +
diff --git a/content.config.ts b/content.config.ts index 710345936..604bee29c 100644 --- a/content.config.ts +++ b/content.config.ts @@ -1,10 +1,11 @@ import { defineContentConfig, defineCollection, z } from '@nuxt/content' +import { CLI_DOCS_PREFIX, CLI_DOCS_REFS, CLI_DOCS_REPO } from './shared/utils/cli-docs' const docsV3Source = { cwd: process.env.NUXT_V3_PATH ?? undefined, repository: !process.env.NUXT_V3_PATH ? 'https://github.com/nuxt/nuxt/tree/3.x' : undefined, include: 'docs/**/*', - exclude: ['docs/**/*.json'], + exclude: ['docs/**/*.json', 'docs/4.api/4.commands/**'], prefix: '/docs/3.x' } @@ -12,7 +13,7 @@ const docsV4Source = { cwd: process.env.NUXT_V4_PATH ?? undefined, repository: !process.env.NUXT_V4_PATH ? 'https://github.com/nuxt/nuxt/tree/4.x' : undefined, include: 'docs/**/*', - exclude: ['docs/**/*.json'], + exclude: ['docs/**/*.json', 'docs/4.api/4.commands/**'], prefix: '/docs/4.x' } @@ -20,10 +21,31 @@ const docsV5Source = { cwd: process.env.NUXT_V5_PATH ?? undefined, repository: !process.env.NUXT_V5_PATH ? 'https://github.com/nuxt/nuxt/tree/main' : undefined, include: 'docs/**/*', - exclude: ['docs/**/*.json'], + exclude: ['docs/**/*.json', 'docs/4.api/4.commands/**'], prefix: '/docs/5.x' } +const cliV3Source = { + cwd: process.env.NUXT_CLI_PATH ?? undefined, + repository: !process.env.NUXT_CLI_PATH ? `https://github.com/${CLI_DOCS_REPO}/tree/${CLI_DOCS_REFS.docsv3}` : undefined, + include: 'docs/**/*', + prefix: `/docs/3.x/${CLI_DOCS_PREFIX}` +} + +const cliV4Source = { + cwd: process.env.NUXT_CLI_PATH ?? undefined, + repository: !process.env.NUXT_CLI_PATH ? `https://github.com/${CLI_DOCS_REPO}/tree/${CLI_DOCS_REFS.docsv4}` : undefined, + include: 'docs/**/*', + prefix: `/docs/4.x/${CLI_DOCS_PREFIX}` +} + +const cliV5Source = { + cwd: process.env.NUXT_CLI_PATH ?? undefined, + repository: !process.env.NUXT_CLI_PATH ? `https://github.com/${CLI_DOCS_REPO}/tree/${CLI_DOCS_REFS.docsv5}` : undefined, + include: 'docs/**/*', + prefix: `/docs/5.x/${CLI_DOCS_PREFIX}` +} + const examplesV3Source = { cwd: process.env.NUXT_EXAMPLES_PATH ?? undefined, repository: !process.env.NUXT_EXAMPLES_PATH ? 'https://github.com/nuxt/examples' : undefined, @@ -214,7 +236,7 @@ export default defineContentConfig({ }), docsv5: defineCollection({ type: 'page', - source: [docsV5Source, examplesV5Source], + source: [docsV5Source, cliV5Source, examplesV5Source], schema: z.object({ titleTemplate: z.string().optional(), links: z.array(Button), @@ -223,7 +245,7 @@ export default defineContentConfig({ }), docsv4: defineCollection({ type: 'page', - source: [docsV4Source, examplesV4Source], + source: [docsV4Source, cliV4Source, examplesV4Source], schema: z.object({ titleTemplate: z.string().optional(), links: z.array(Button), @@ -232,7 +254,7 @@ export default defineContentConfig({ }), docsv3: defineCollection({ type: 'page', - source: [docsV3Source, examplesV3Source], + source: [docsV3Source, cliV3Source, examplesV3Source], schema: z.object({ titleTemplate: z.string().optional(), links: z.array(Button), diff --git a/nuxt.config.ts b/nuxt.config.ts index bb9d7dcc9..c77bf08a6 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,5 +1,6 @@ import { createResolver } from 'nuxt/kit' import { parseMdc } from './helpers/mdc-parser.mjs' +import { CLI_DOCS_PREFIX, CLI_DOCS_REFS, CLI_DOCS_REPO } from './shared/utils/cli-docs' const { resolve } = createResolver(import.meta.url) @@ -246,10 +247,10 @@ export default defineNuxtConfig({ '/docs/3.x/api/kit': { redirect: '/docs/3.x/api/kit/modules', prerender: false }, '/docs/4.x/api/kit': { redirect: '/docs/4.x/api/kit/modules', prerender: false }, '/docs/5.x/api/kit': { redirect: '/docs/5.x/api/kit/modules', prerender: false }, - '/docs/api/commands': { redirect: '/docs/api/commands/dev', prerender: false }, - '/docs/3.x/api/commands': { redirect: '/docs/3.x/api/commands/dev', prerender: false }, - '/docs/4.x/api/commands': { redirect: '/docs/4.x/api/commands/dev', prerender: false }, - '/docs/5.x/api/commands': { redirect: '/docs/5.x/api/commands/dev', prerender: false }, + '/docs/api/commands': { redirect: '/docs/api/commands/overview', prerender: false }, + '/docs/3.x/api/commands': { redirect: '/docs/3.x/api/commands/overview', prerender: false }, + '/docs/4.x/api/commands': { redirect: '/docs/4.x/api/commands/overview', prerender: false }, + '/docs/5.x/api/commands': { redirect: '/docs/5.x/api/commands/overview', prerender: false }, '/docs/api/advanced': { redirect: '/docs/api/advanced/hooks', prerender: false }, '/docs/3.x/api/advanced': { redirect: '/docs/3.x/api/advanced/hooks', prerender: false }, '/docs/4.x/api/advanced': { redirect: '/docs/4.x/api/advanced/hooks', prerender: false }, @@ -537,6 +538,15 @@ export default defineNuxtConfig({ }, hooks: { 'content:file:beforeParse': async ({ file }) => { + // Command docs are served from `nuxt/cli`, but Content only ships the markdown, + // not the terminal captures committed beside it. Root-relative image sources are + // repo-relative, so resolve them against the ref this page was parsed from: a + // hardcoded ref would serve `main`'s captures on the 3.x tree. + const collection = file.id.split('/')[0] as keyof typeof CLI_DOCS_REFS + if (file.id.includes(`/${CLI_DOCS_PREFIX}/`) && CLI_DOCS_REFS[collection]) { + const base = `https://raw.githubusercontent.com/${CLI_DOCS_REPO}/${CLI_DOCS_REFS[collection]}` + file.body = file.body.replaceAll(/(!\[[^\]]*\]\()\/(?!\/)/g, `$1${base}/`) + } if (file.id.startsWith('docsv5/')) { file.body = file.body.replaceAll(/\(\/docs\/(?!\d\.x)/g, '(/docs/5.x/') // Pages that only exist on main (5.x) but are linked as /docs/4.x/* from @@ -556,6 +566,9 @@ export default defineNuxtConfig({ if (file.id.startsWith('docsv4/')) { file.body = file.body.replaceAll(/\(\/docs\/(?!\d\.x)/g, '(/docs/4.x/') } + if (file.id.startsWith('docsv3/')) { + file.body = file.body.replaceAll(/\(\/docs\/(?!\d\.x)/g, '(/docs/3.x/') + } }, 'content:file:afterParse': async ({ file, content }) => { if (file.id === 'index/index.yml') { @@ -631,7 +644,11 @@ export default defineNuxtConfig({ }, image: { format: ['webp', 'jpeg', 'jpg', 'png', 'svg'], - provider: 'ipx' + provider: 'ipx', + // Opt-out escape hatch for images IPX must not touch, used by `DocsProseImg`. + providers: { + none: { provider: 'none' } + } }, llms: { domain: 'https://nuxt.com', diff --git a/patches/@nuxt__content.patch b/patches/@nuxt__content.patch new file mode 100644 index 000000000..4c2b2a8b8 --- /dev/null +++ b/patches/@nuxt__content.patch @@ -0,0 +1,22 @@ +diff --git a/dist/module.mjs b/dist/module.mjs +index f1516bab826818eacc9088a24df96cf4ff895502..b14f1f4579af1e502fc57e9834e88feb916222cc 100644 +--- a/dist/module.mjs ++++ b/dist/module.mjs +@@ -2007,11 +2007,16 @@ function defineGitSource(source) { + const repository = source?.repository && gitUrlParse(source.repository.url); + if (repository) { + const { source: gitSource, owner, name } = repository; +- resolvedSource.cwd = join(rootDir, ".data", "content", `${gitSource}-${owner}-${name}-${repository.ref || "main"}`); + let ref; + if (source.repository.branch && source.repository.tag) { + throw new Error("Cannot specify both branch and tag for git repository. Please specify one of `branch` or `tag`."); + } ++ // Keyed on the resolved ref so two refs of one repo do not share a checkout. ++ // Backport of https://github.com/nuxt/content/pull/3839; drop on the next release. ++ const resolvedRef = source.repository.branch || source.repository.tag || repository.ref || "main"; ++ const refKey = /^[\w.-]+$/.test(resolvedRef) ? resolvedRef : `${resolvedRef.replace(/[^\w.-]+/g, "-")}-${hash(resolvedRef).slice(0, 8)}`; ++ const refPrefix = source.repository.tag ? "tag-" : ""; ++ resolvedSource.cwd = join(rootDir, ".data", "content", `${gitSource}-${owner}-${name}-${refPrefix}${refKey}`); + if (source.repository.branch) ref = { branch: source.repository.branch }; + if (source.repository.tag) ref = { tag: source.repository.tag }; + if (!source.repository?.auth && source.authBasic) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7864c2c99..0a45979bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ overrides: '@workflow/serde': 4.1.2 '@nuxt/content>@nuxtjs/mdc': ^0.23.1 +patchedDependencies: + '@nuxt/content': c5b7bb228dea1e45f274e367308107c2f611ad739b4bb5742478643be3a95f9f + importers: .: @@ -54,7 +57,7 @@ importers: version: 2.1.0 '@nuxt/content': specifier: ^3.15.2 - version: 3.15.2(@libsql/client@0.17.4)(@oxc-project/types@0.144.0)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(supports-color@10.2.2)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 3.15.2(patch_hash=c5b7bb228dea1e45f274e367308107c2f611ad739b4bb5742478643be3a95f9f)(@libsql/client@0.17.4)(@oxc-project/types@0.144.0)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(supports-color@10.2.2)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) '@nuxt/hints': specifier: ^1.1.4 version: 1.1.4(@libsql/client@0.17.4)(@vercel/functions@3.9.3(ws@8.21.3))(@vitest/expect@4.1.10)(better-sqlite3@13.0.3)(db0@0.3.4(@libsql/client@0.17.4)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3)))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.4)(rolldown@1.2.4)(rollup@4.62.4)(srvx@0.11.22)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10(@types/node@26.2.0)(happy-dom@20.11.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)))(vue@3.5.41(typescript@6.0.3)) @@ -66,7 +69,7 @@ importers: version: 1.3.5(@oxc-project/types@0.144.0)(@types/youtube@0.3.0)(@unhead/vue@3.3.2(@oxc-project/types@0.144.0)(esbuild@0.28.2)(lightningcss@1.33.0)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)))(@vercel/functions@3.9.3(ws@8.21.3))(db0@0.3.4(@libsql/client@0.17.4)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3)))(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.4)(rolldown@1.2.4)(rollup@4.62.4)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) '@nuxt/ui': specifier: ^4.10.0 - version: 4.10.0(ae7cefd1db2330b460f8c2d512a8cf44) + version: 4.10.0(1c051f5eaf5cc4a909f4318924c3960a) '@nuxthub/core': specifier: ^0.10.8 version: 0.10.8(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@vercel/functions@3.9.3(ws@8.21.3))(db0@0.3.4(@libsql/client@0.17.4)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3)))(ioredis@5.11.1(supports-color@10.2.2))(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(typescript@6.0.3)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.4)(rollup@4.62.4)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)))(vue-tsc@3.3.10(typescript@6.0.3)) @@ -11874,7 +11877,7 @@ snapshots: - magicast - supports-color - '@nuxt/content@3.15.2(@libsql/client@0.17.4)(@oxc-project/types@0.144.0)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(supports-color@10.2.2)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))': + '@nuxt/content@3.15.2(patch_hash=c5b7bb228dea1e45f274e367308107c2f611ad739b4bb5742478643be3a95f9f)(@libsql/client@0.17.4)(@oxc-project/types@0.144.0)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(supports-color@10.2.2)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@nuxt/kit': 4.5.2(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.4)(rollup@4.62.4)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))) '@nuxtjs/mdc': 0.23.1(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(supports-color@10.2.2)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.4)(rollup@4.62.4)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))) @@ -12737,7 +12740,7 @@ snapshots: - vite - webpack - '@nuxt/ui@4.10.0(ae7cefd1db2330b460f8c2d512a8cf44)': + '@nuxt/ui@4.10.0(1c051f5eaf5cc4a909f4318924c3960a)': dependencies: '@floating-ui/dom': 1.8.0 '@iconify/vue': 5.0.1(vue@3.5.41(typescript@6.0.3)) @@ -12806,7 +12809,7 @@ snapshots: optionalDependencies: '@internationalized/date': 3.12.3 '@internationalized/number': 3.6.7 - '@nuxt/content': 3.15.2(@libsql/client@0.17.4)(@oxc-project/types@0.144.0)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(supports-color@10.2.2)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) + '@nuxt/content': 3.15.2(patch_hash=c5b7bb228dea1e45f274e367308107c2f611ad739b4bb5742478643be3a95f9f)(@libsql/client@0.17.4)(@oxc-project/types@0.144.0)(better-sqlite3@13.0.3)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260702.1)(@libsql/client@0.17.4)(better-sqlite3@13.0.3))(esbuild@0.28.2)(magic-string@1.2.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.4)(rollup@4.62.4)(supports-color@10.2.2)(valibot@1.4.2(typescript@6.0.3))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) ai: 7.0.66(zod@4.4.3) valibot: 1.4.2(typescript@6.0.3) zod: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index df9cd1f64..3d46a1065 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -31,3 +31,9 @@ minimumReleaseAgeExclude: - vue - '@vue/*' - '@nuxtjs/mdc@0.23.1' + +patchedDependencies: + # Keys the git checkout directory on the resolved ref, so the `nuxt/cli` sources on + # `3.x` and `main` stop overwriting each other's working tree. + # https://github.com/nuxt/content/pull/3839 + '@nuxt/content': patches/@nuxt__content.patch diff --git a/shared/utils/cli-docs.ts b/shared/utils/cli-docs.ts new file mode 100644 index 000000000..bdee75cba --- /dev/null +++ b/shared/utils/cli-docs.ts @@ -0,0 +1,22 @@ +// Single source of truth for which `nuxt/cli` ref backs each docs version's +// command reference. `3.x` is the released CLI; `main` is where v4 is developed and +// where `@nuxt/cli-nightly` is published from, which is what `nuxt-nightly@5.x` +// depends on, so a 5.x reader sees the CLI they actually install. Consumed by two +// surfaces that must agree, or pages resolve their assets against a different ref +// than they were parsed from: +// +// - content.config.ts → the `cliV*Source` collection sources +// - nuxt.config.ts → `content:file:beforeParse` asset URL rewriting +export const CLI_DOCS_REPO = 'nuxt/cli' + +// TODO: repoint docsv4 to `main` when the CLI v4 releases. +export const CLI_DOCS_REFS = { + docsv3: '3.x', + docsv4: '3.x', + docsv5: 'main' +} as const + +// Command docs live at `docs/` in `nuxt/cli` but mount under the API section of +// each version tree, so this is both the source `prefix` and the marker that +// identifies a CLI-sourced file inside the parse hook. +export const CLI_DOCS_PREFIX = '4.api/4.commands'