refactor(docs): extract loadVersion() without changing the behavior (#11230)

* refactor, extract loadVersion() without changing behavior

* Add minimal test case for loadVersion()

* Add minimal test case for loadVersion()

* more refactor, rename index.ts to version.ts

* fix tests

* empty
This commit is contained in:
Sébastien Lorber 2025-06-02 18:44:07 +02:00 committed by GitHub
parent dacfc17fb4
commit 18b47fdfc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 281 additions and 121 deletions

View file

@ -25,7 +25,7 @@ import {
type DocEnv, type DocEnv,
} from '../docs'; } from '../docs';
import {loadSidebars} from '../sidebars'; import {loadSidebars} from '../sidebars';
import {readVersionsMetadata} from '../versions'; import {readVersionsMetadata} from '../versions/version';
import {DEFAULT_OPTIONS} from '../options'; import {DEFAULT_OPTIONS} from '../options';
import type {Sidebars} from '../sidebars/types'; import type {Sidebars} from '../sidebars/types';
import type {DocFile} from '../types'; import type {DocFile} from '../types';

View file

@ -7,7 +7,6 @@
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import _ from 'lodash';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import { import {
normalizeUrl, normalizeUrl,
@ -17,30 +16,19 @@ import {
posixPath, posixPath,
addTrailingPathSeparator, addTrailingPathSeparator,
createAbsoluteFilePathMatcher, createAbsoluteFilePathMatcher,
createSlugger,
resolveMarkdownLinkPathname, resolveMarkdownLinkPathname,
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
type TagsFile,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import { import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation';
getTagsFile,
getTagsFilePathsToWatch,
} from '@docusaurus/utils-validation';
import {createMDXLoaderRule} from '@docusaurus/mdx-loader'; import {createMDXLoaderRule} from '@docusaurus/mdx-loader';
import {loadSidebars, resolveSidebarPathOption} from './sidebars'; import {resolveSidebarPathOption} from './sidebars';
import {CategoryMetadataFilenamePattern} from './sidebars/generator'; import {CategoryMetadataFilenamePattern} from './sidebars/generator';
import { import {type DocEnv} from './docs';
readVersionDocs,
processDocMetadata,
addDocNavigation,
type DocEnv,
createDocsByIdIndex,
} from './docs';
import { import {
getVersionFromSourceFilePath, getVersionFromSourceFilePath,
readVersionsMetadata, readVersionsMetadata,
toFullVersion, toFullVersion,
} from './versions'; } from './versions/version';
import cliDocs from './cli'; import cliDocs from './cli';
import {VERSIONS_JSON_FILE} from './constants'; import {VERSIONS_JSON_FILE} from './constants';
import {toGlobalDataVersion} from './globalData'; import {toGlobalDataVersion} from './globalData';
@ -49,19 +37,17 @@ import {
getLoadedContentTranslationFiles, getLoadedContentTranslationFiles,
} from './translations'; } from './translations';
import {createAllRoutes} from './routes'; import {createAllRoutes} from './routes';
import {createSidebarsUtils} from './sidebars/utils';
import {createContentHelpers} from './contentHelpers'; import {createContentHelpers} from './contentHelpers';
import {loadVersion} from './versions/loadVersion';
import type { import type {
PluginOptions, PluginOptions,
DocMetadataBase,
VersionMetadata, VersionMetadata,
DocFrontMatter, DocFrontMatter,
LoadedContent, LoadedContent,
LoadedVersion,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import type {LoadContext, Plugin} from '@docusaurus/types'; import type {LoadContext, Plugin} from '@docusaurus/types';
import type {DocFile, FullVersion} from './types'; import type {FullVersion} from './types';
import type {RuleSetRule} from 'webpack'; import type {RuleSetRule} from 'webpack';
// MDX loader is not 100% deterministic, leading to cache invalidation issue // MDX loader is not 100% deterministic, leading to cache invalidation issue
@ -243,102 +229,17 @@ export default async function pluginContentDocs(
}, },
async loadContent() { async loadContent() {
async function loadVersionDocsBase( return {
versionMetadata: VersionMetadata, loadedVersions: await Promise.all(
tagsFile: TagsFile | null, versionsMetadata.map((versionMetadata) =>
): Promise<DocMetadataBase[]> { loadVersion({
const docFiles = await readVersionDocs(versionMetadata, options);
if (docFiles.length === 0) {
throw new Error(
`Docs version "${
versionMetadata.versionName
}" has no docs! At least one doc should exist at "${path.relative(
siteDir,
versionMetadata.contentPath,
)}".`,
);
}
function processVersionDoc(docFile: DocFile) {
return processDocMetadata({
docFile,
versionMetadata,
context, context,
options, options,
env, env,
tagsFile,
});
}
return Promise.all(docFiles.map(processVersionDoc));
}
async function doLoadVersion(
versionMetadata: VersionMetadata,
): Promise<LoadedVersion> {
const tagsFile = await getTagsFile({
contentPaths: versionMetadata,
tags: options.tags,
});
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
versionMetadata, versionMetadata,
tagsFile,
);
// TODO we only ever need draftIds in further code, not full draft items
// To simplify and prevent mistakes, avoid exposing draft
// replace draft=>draftIds in content loaded
const [drafts, docs] = _.partition(docsBase, (doc) => doc.draft);
const sidebars = await loadSidebars(versionMetadata.sidebarFilePath, {
sidebarItemsGenerator: options.sidebarItemsGenerator,
numberPrefixParser: options.numberPrefixParser,
docs,
drafts,
version: versionMetadata,
sidebarOptions: {
sidebarCollapsed: options.sidebarCollapsed,
sidebarCollapsible: options.sidebarCollapsible,
},
categoryLabelSlugger: createSlugger(),
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const docsById = createDocsByIdIndex(docs);
const allDocIds = Object.keys(docsById);
sidebarsUtils.checkLegacyVersionedSidebarNames({
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
sidebarsUtils.checkSidebarsDocIds({
allDocIds,
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
return {
...versionMetadata,
docs: addDocNavigation({
docs,
sidebarsUtils,
}), }),
drafts, ),
sidebars, ),
};
}
async function loadVersion(versionMetadata: VersionMetadata) {
try {
return await doLoadVersion(versionMetadata);
} catch (err) {
logger.error`Loading of version failed for version name=${versionMetadata.versionName}`;
throw err;
}
}
return {
loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)),
}; };
}, },

View file

@ -22,5 +22,5 @@ export {
getDefaultVersionBanner, getDefaultVersionBanner,
getVersionBadge, getVersionBadge,
getVersionBanner, getVersionBanner,
} from './versions'; } from './versions/version';
export {readVersionNames} from './versions/files'; export {readVersionNames} from './versions/files';

View file

@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`minimal site can load current version 1`] = `
{
"badge": false,
"banner": null,
"className": "docs-version-current",
"contentPath": "<PROJECT_ROOT>/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/minimal-site/docs",
"contentPathLocalized": "<PROJECT_ROOT>/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/minimal-site/i18n/en/docusaurus-plugin-content-docs/current",
"docs": [
{
"description": "World",
"draft": false,
"editUrl": undefined,
"frontMatter": {},
"id": "hello",
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"next": undefined,
"permalink": "/docs/hello",
"previous": undefined,
"sidebar": "defaultSidebar",
"sidebarPosition": undefined,
"slug": "/hello",
"source": "@site/docs/hello.md",
"sourceDirName": ".",
"tags": [],
"title": "Hello",
"unlisted": false,
"version": "current",
},
],
"drafts": [],
"editUrl": undefined,
"editUrlLocalized": undefined,
"isLast": true,
"label": "Next",
"noIndex": false,
"path": "/docs",
"routePriority": -1,
"sidebarFilePath": undefined,
"sidebars": {
"defaultSidebar": [
{
"id": "hello",
"type": "doc",
},
],
},
"tagsPath": "/docs/tags",
"versionName": "current",
}
`;

View file

@ -0,0 +1,73 @@
/**
* 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 path from 'path';
import {fromPartial} from '@total-typescript/shoehorn';
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils/src';
import {readVersionsMetadata} from '../version';
import {DEFAULT_OPTIONS} from '../../options';
import {loadVersion} from '../loadVersion';
import type {I18n, LoadContext} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
const DefaultI18N: I18n = {
path: 'i18n',
currentLocale: 'en',
locales: ['en'],
defaultLocale: 'en',
localeConfigs: {},
};
describe('minimal site', () => {
async function loadSite() {
const siteDir = path.resolve(
path.join(__dirname, './__fixtures__', 'minimal-site'),
);
const options: PluginOptions = fromPartial<PluginOptions>({
...DEFAULT_OPTIONS,
});
const context = fromPartial<LoadContext>({
siteDir,
baseUrl: '/',
i18n: DefaultI18N,
localizationDir: path.join(siteDir, 'i18n/en'),
siteConfig: {
markdown: {
parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
},
},
});
return {
siteDir,
options,
context,
};
}
it('can load current version', async () => {
const {options, context} = await loadSite();
const versionsMetadata = await readVersionsMetadata({
options,
context,
});
expect(versionsMetadata).toHaveLength(1);
expect(versionsMetadata[0]!.versionName).toBe('current');
const versionMetadata = versionsMetadata[0]!;
const loadedVersion = loadVersion({
context,
options,
versionMetadata,
env: 'production',
});
await expect(loadedVersion).resolves.toMatchSnapshot();
});
});

View file

@ -8,7 +8,7 @@
import {jest} from '@jest/globals'; import {jest} from '@jest/globals';
import path from 'path'; import path from 'path';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {readVersionsMetadata} from '../index'; import {readVersionsMetadata} from '../version';
import {DEFAULT_OPTIONS} from '../../options'; import {DEFAULT_OPTIONS} from '../../options';
import type {I18n, LoadContext} from '@docusaurus/types'; import type {I18n, LoadContext} from '@docusaurus/types';
import type { import type {

View file

@ -19,7 +19,7 @@ import type {
PluginOptions, PluginOptions,
VersionMetadata, VersionMetadata,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import type {VersionContext} from './index'; import type {VersionContext} from './version';
/** Add a prefix like `community_version-1.0.0`. No-op for default instance. */ /** Add a prefix like `community_version-1.0.0`. No-op for default instance. */
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string { function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {

View file

@ -0,0 +1,130 @@
/**
* 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 path from 'path';
import _ from 'lodash';
import {createSlugger} from '@docusaurus/utils';
import {getTagsFile} from '@docusaurus/utils-validation';
import logger from '@docusaurus/logger';
import {
addDocNavigation,
createDocsByIdIndex,
type DocEnv,
processDocMetadata,
readVersionDocs,
} from '../docs';
import {loadSidebars} from '../sidebars';
import {createSidebarsUtils} from '../sidebars/utils';
import type {TagsFile} from '@docusaurus/utils';
import type {
DocMetadataBase,
LoadedVersion,
PluginOptions,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
import type {DocFile} from '../types';
import type {LoadContext} from '@docusaurus/types';
export async function loadVersion({
context,
options,
versionMetadata,
env,
}: {
context: LoadContext;
options: PluginOptions;
versionMetadata: VersionMetadata;
env: DocEnv;
}): Promise<LoadedVersion> {
const {siteDir} = context;
async function loadVersionDocsBase(
tagsFile: TagsFile | null,
): Promise<DocMetadataBase[]> {
const docFiles = await readVersionDocs(versionMetadata, options);
if (docFiles.length === 0) {
throw new Error(
`Docs version "${
versionMetadata.versionName
}" has no docs! At least one doc should exist at "${path.relative(
siteDir,
versionMetadata.contentPath,
)}".`,
);
}
function processVersionDoc(docFile: DocFile) {
return processDocMetadata({
docFile,
versionMetadata,
context,
options,
env,
tagsFile,
});
}
return Promise.all(docFiles.map(processVersionDoc));
}
async function doLoadVersion(): Promise<LoadedVersion> {
const tagsFile = await getTagsFile({
contentPaths: versionMetadata,
tags: options.tags,
});
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(tagsFile);
// TODO we only ever need draftIds in further code, not full draft items
// To simplify and prevent mistakes, avoid exposing draft
// replace draft=>draftIds in content loaded
const [drafts, docs] = _.partition(docsBase, (doc) => doc.draft);
const sidebars = await loadSidebars(versionMetadata.sidebarFilePath, {
sidebarItemsGenerator: options.sidebarItemsGenerator,
numberPrefixParser: options.numberPrefixParser,
docs,
drafts,
version: versionMetadata,
sidebarOptions: {
sidebarCollapsed: options.sidebarCollapsed,
sidebarCollapsible: options.sidebarCollapsible,
},
categoryLabelSlugger: createSlugger(),
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const docsById = createDocsByIdIndex(docs);
const allDocIds = Object.keys(docsById);
sidebarsUtils.checkLegacyVersionedSidebarNames({
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
sidebarsUtils.checkSidebarsDocIds({
allDocIds,
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
return {
...versionMetadata,
docs: addDocNavigation({
docs,
sidebarsUtils,
}),
drafts,
sidebars,
};
}
try {
return await doLoadVersion();
} catch (err) {
logger.error`Loading of version failed for version name=${versionMetadata.versionName}`;
throw err;
}
}

View file

@ -243,7 +243,7 @@ export async function readVersionsMetadata({
validateVersionsOptions(allVersionNames, options); validateVersionsOptions(allVersionNames, options);
const versionNames = filterVersions(allVersionNames, options); const versionNames = filterVersions(allVersionNames, options);
const lastVersionName = getLastVersionName({versionNames, options}); const lastVersionName = getLastVersionName({versionNames, options});
const versionsMetadata = await Promise.all( return Promise.all(
versionNames.map((versionName) => versionNames.map((versionName) =>
createVersionMetadata({ createVersionMetadata({
versionName, versionName,
@ -254,7 +254,6 @@ export async function readVersionsMetadata({
}), }),
), ),
); );
return versionsMetadata;
} }
export function toFullVersion(version: LoadedVersion): FullVersion { export function toFullVersion(version: LoadedVersion): FullVersion {