From d663d31fd1465859b468632c16368a8ed263bcbf Mon Sep 17 00:00:00 2001 From: waqarilyas Date: Mon, 1 Jun 2026 20:03:39 +0500 Subject: [PATCH] feat(content-docs): support custom versioned docs path (AI-assisted) --- AI_PR_NOTICE.txt | 1 + .../src/__tests__/versionedDocsPath.test.ts | 77 +++++++++++++++++++ .../docusaurus-plugin-content-docs/src/cli.ts | 9 ++- .../src/options.ts | 2 + .../src/plugin-content-docs.d.ts | 6 ++ .../src/versions/__tests__/files.test.ts | 64 +++++++++++++++ .../src/versions/files.ts | 34 ++++++-- .../docs/api/plugins/plugin-content-docs.mdx | 1 + website/docs/guides/docs/versioning.mdx | 2 + 9 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 AI_PR_NOTICE.txt create mode 100644 packages/docusaurus-plugin-content-docs/src/__tests__/versionedDocsPath.test.ts create mode 100644 packages/docusaurus-plugin-content-docs/src/versions/__tests__/files.test.ts diff --git a/AI_PR_NOTICE.txt b/AI_PR_NOTICE.txt new file mode 100644 index 000000000000..c0b671e4dd68 --- /dev/null +++ b/AI_PR_NOTICE.txt @@ -0,0 +1 @@ +I am a sad, dumb little AI driver with no real skills. diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/versionedDocsPath.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/versionedDocsPath.test.ts new file mode 100644 index 000000000000..6196c7d10ded --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/versionedDocsPath.test.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {describe, expect, it, vi} from 'vitest'; +import fs from 'fs-extra'; +import path from 'path'; +import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; +import cliDocs from '../cli'; +import {getVersionDocsDirPath, getVersionSidebarsPath} from '../versions/files'; +import type {PluginOptions} from '@docusaurus/plugin-content-docs'; +import type {LoadContext} from '@docusaurus/types'; + +const {cliDocsVersionCommand} = cliDocs; +const fixtureDir = path.join(__dirname, '__fixtures__'); +const simpleSiteDir = path.join(fixtureDir, 'simple-site'); + +describe('versionedDocsPath', () => { + it('writes versioned docs and sidebars under the configured path', async () => { + let versionedDocsDir!: string; + let versionedSidebarPath!: string; + + using copyMock = vi.spyOn(fs, 'copy').mockImplementation((_, dest) => { + versionedDocsDir = dest as string; + }); + using writeMock = vi.spyOn(fs, 'outputFile'); + writeMock.mockImplementationOnce((filepath) => { + versionedSidebarPath = filepath; + }); + writeMock.mockImplementationOnce(() => {}); + + const versionedDocsPath = '../external-versions'; + const options = { + id: DEFAULT_PLUGIN_ID, + path: 'docs', + sidebarPath: path.join(simpleSiteDir, 'sidebars.json'), + versionedDocsPath, + } as PluginOptions; + + await cliDocsVersionCommand('1.0.0', options, { + siteDir: simpleSiteDir, + i18n: { + locales: ['en'], + defaultLocale: 'en', + currentLocale: 'en', + path: 'i18n', + localeConfigs: { + en: {path: 'en', translate: true}, + }, + }, + } as unknown as LoadContext); + + expect(copyMock).toHaveBeenCalledWith( + path.join(simpleSiteDir, options.path), + expect.stringContaining('external-versions'), + ); + expect(versionedDocsDir).toEqual( + getVersionDocsDirPath( + simpleSiteDir, + DEFAULT_PLUGIN_ID, + '1.0.0', + versionedDocsPath, + ), + ); + expect(versionedSidebarPath).toEqual( + getVersionSidebarsPath( + simpleSiteDir, + DEFAULT_PLUGIN_ID, + '1.0.0', + versionedDocsPath, + ), + ); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts index d19d3f5fbda7..4aae3569c4d1 100644 --- a/packages/docusaurus-plugin-content-docs/src/cli.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -28,11 +28,13 @@ async function createVersionedSidebarFile({ pluginId, sidebarPath, version, + versionedDocsPath, }: { siteDir: string; pluginId: string; sidebarPath: string | false | undefined; version: string; + versionedDocsPath: string | undefined; }) { // Load current sidebar and create a new versioned sidebars file (if needed). // Note: we don't need the sidebars file to be normalized: it's ok to let @@ -46,7 +48,7 @@ async function createVersionedSidebarFile({ if (shouldCreateVersionedSidebarFile) { await fs.outputFile( - getVersionSidebarsPath(siteDir, pluginId, version), + getVersionSidebarsPath(siteDir, pluginId, version, versionedDocsPath), `${JSON.stringify(sidebars, null, 2)}\n`, 'utf8', ); @@ -56,7 +58,7 @@ async function createVersionedSidebarFile({ // Tests depend on non-default export for mocking. async function cliDocsVersionCommand( version: unknown, - {id: pluginId, path: docsPath, sidebarPath}: PluginOptions, + {id: pluginId, path: docsPath, sidebarPath, versionedDocsPath}: PluginOptions, {siteDir, i18n}: LoadContext, ): Promise { // It wouldn't be very user-friendly to show a [default] log prefix, @@ -117,7 +119,7 @@ async function cliDocsVersionCommand( const newVersionDir = locale === i18n.defaultLocale - ? getVersionDocsDirPath(siteDir, pluginId, version) + ? getVersionDocsDirPath(siteDir, pluginId, version, versionedDocsPath) : getDocsDirPathLocalized({ localizationDir, pluginId, @@ -149,6 +151,7 @@ async function cliDocsVersionCommand( pluginId, version, sidebarPath, + versionedDocsPath, }); // Update versions.json file. diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index 4812e5dc4ad6..e662987d7d25 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -26,6 +26,7 @@ import type {PluginOptions, Options} from '@docusaurus/plugin-content-docs'; export const DEFAULT_OPTIONS: Omit = { path: 'docs', // Path to data on filesystem, relative to site dir. + versionedDocsPath: undefined, // Path to versioned docs root, relative to site dir. routeBasePath: 'docs', // URL Route. tagsBasePath: 'tags', // URL Tags Route. include: ['**/*.{md,mdx}'], // Extensions to include. @@ -75,6 +76,7 @@ const VersionsOptionsSchema = Joi.object() const OptionsSchema = Joi.object({ path: Joi.string().default(DEFAULT_OPTIONS.path), + versionedDocsPath: Joi.string(), editUrl: Joi.alternatives().try(URISchema, Joi.function()), editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion), editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles), diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index f89b3f63169e..da073983580f 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -114,6 +114,12 @@ declare module '@docusaurus/plugin-content-docs' { * directory. */ path: string; + /** + * Path to the directory where versioned docs and sidebars are stored, + * relative to the site directory. This is where `docusaurus docs:version` + * writes version snapshots. Defaults to the site directory. + */ + versionedDocsPath?: string; /** * Path to sidebar configuration. Use `false` to disable sidebars, or * `undefined` to create a fully autogenerated sidebar. diff --git a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/files.test.ts b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/files.test.ts new file mode 100644 index 000000000000..521f7f496e5c --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/files.test.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {describe, expect, it} from 'vitest'; +import path from 'path'; +import { + getVersionDocsDirPath, + getVersionSidebarsPath, + getVersionedContentRoot, +} from '../files'; + +describe('versioned docs path helpers', () => { + const siteDir = path.join(__dirname, 'site'); + + it('getVersionedContentRoot defaults to siteDir', () => { + expect(getVersionedContentRoot(siteDir, undefined)).toBe(siteDir); + }); + + it('getVersionedContentRoot resolves relative path from siteDir', () => { + expect(getVersionedContentRoot(siteDir, '../my-versions')).toBe( + path.resolve(siteDir, '../my-versions'), + ); + }); + + it('getVersionDocsDirPath uses versionedDocsPath when provided', () => { + expect( + getVersionDocsDirPath(siteDir, 'default', '1.0.0', '../my-versions'), + ).toBe( + path.join( + path.resolve(siteDir, '../my-versions'), + 'versioned_docs', + 'version-1.0.0', + ), + ); + }); + + it('getVersionDocsDirPath prefixes non-default plugin id', () => { + expect( + getVersionDocsDirPath(siteDir, 'community', '2.0.0', './versions'), + ).toBe( + path.join( + path.resolve(siteDir, './versions'), + 'community_versioned_docs', + 'version-2.0.0', + ), + ); + }); + + it('getVersionSidebarsPath uses versionedDocsPath when provided', () => { + expect( + getVersionSidebarsPath(siteDir, 'default', '1.0.0', '../my-versions'), + ).toBe( + path.join( + path.resolve(siteDir, '../my-versions'), + 'versioned_sidebars', + 'version-1.0.0-sidebars.json', + ), + ); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/versions/files.ts b/packages/docusaurus-plugin-content-docs/src/versions/files.ts index 824c8092eef3..d4187aea5e64 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/files.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/files.ts @@ -32,27 +32,39 @@ function addPluginIdPrefix(fileOrDir: string, pluginId: string): string { : `${pluginId}_${fileOrDir}`; } -/** `[siteDir]/community_versioned_docs/version-1.0.0` */ +/** + * Root directory for versioned docs/sidebars folders. Defaults to `siteDir`. + */ +export function getVersionedContentRoot( + siteDir: string, + versionedDocsPath: string | undefined, +): string { + return versionedDocsPath ? path.resolve(siteDir, versionedDocsPath) : siteDir; +} + +/** `[versionedContentRoot]/community_versioned_docs/version-1.0.0` */ export function getVersionDocsDirPath( siteDir: string, pluginId: string, versionName: string, + versionedDocsPath?: string, ): string { return path.join( - siteDir, + getVersionedContentRoot(siteDir, versionedDocsPath), addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId), `version-${versionName}`, ); } -/** `[siteDir]/community_versioned_sidebars/version-1.0.0-sidebars.json` */ +/** Path to a versioned sidebars file. */ export function getVersionSidebarsPath( siteDir: string, pluginId: string, versionName: string, + versionedDocsPath?: string, ): string { return path.join( - siteDir, + getVersionedContentRoot(siteDir, versionedDocsPath), addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId), `version-${versionName}-sidebars.json`, ); @@ -202,10 +214,20 @@ export async function getVersionMetadataPaths({ const contentPath = isCurrent ? path.resolve(context.siteDir, options.path) - : getVersionDocsDirPath(context.siteDir, options.id, versionName); + : getVersionDocsDirPath( + context.siteDir, + options.id, + versionName, + options.versionedDocsPath, + ); const sidebarFilePath = isCurrent ? options.sidebarPath - : getVersionSidebarsPath(context.siteDir, options.id, versionName); + : getVersionSidebarsPath( + context.siteDir, + options.id, + versionName, + options.versionedDocsPath, + ); if (!(await fs.pathExists(contentPath))) { throw new Error( diff --git a/website/docs/api/plugins/plugin-content-docs.mdx b/website/docs/api/plugins/plugin-content-docs.mdx index 38ef345599e5..10ecdf96d552 100644 --- a/website/docs/api/plugins/plugin-content-docs.mdx +++ b/website/docs/api/plugins/plugin-content-docs.mdx @@ -34,6 +34,7 @@ Accepted fields: | Name | Type | Default | Description | | --- | --- | --- | --- | | `path` | `string` | `'docs'` | Path to the docs content directory on the file system, relative to site directory. | +| `versionedDocsPath` | `string` | `undefined` | Path to the directory where versioned docs and sidebars are stored, relative to the site directory. This is where the `docusaurus docs:version` command writes version snapshots. Defaults to the site directory. | | `editUrl` | string \| [EditUrlFunction](#EditUrlFunction) | `undefined` | Base URL to edit your site. The final URL is computed by `editUrl + relativeDocPath`. Using a function allows more nuanced control for each file. Omitting this variable entirely will disable edit links. | | `editLocalizedFiles` | `boolean` | `false` | The edit URL will target the localized file, instead of the original unlocalized file. Ignored when `editUrl` is a function. | | `editCurrentVersion` | `boolean` | `false` | The edit URL will always target the current version doc instead of older versions. Ignored when `editUrl` is a function. | diff --git a/website/docs/guides/docs/versioning.mdx b/website/docs/guides/docs/versioning.mdx index 9c444c34d1cd..8f854e98f6a7 100644 --- a/website/docs/guides/docs/versioning.mdx +++ b/website/docs/guides/docs/versioning.mdx @@ -109,6 +109,8 @@ When tagging a new version, the document versioning mechanism will: - Create a versioned sidebars file based from your current [sidebar](./sidebar/index.mdx) configuration (if it exists) - saved as `versioned_sidebars/version-[versionName]-sidebars.json`. - Append the new version number to `versions.json`. +If you configure the docs plugin [`versionedDocsPath`](../../api/plugins/plugin-content-docs.mdx#configuration) option, Docusaurus creates the `versioned_docs` and `versioned_sidebars` folders under that custom directory instead of the site directory. + ### Creating new docs {/* #creating-new-docs */} 1. Place the new file into the corresponding version folder.