From acf5f383ea2ecf2e4b18132bf4d49d1086368097 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 6 Nov 2025 17:26:23 +0100 Subject: [PATCH] Refactor VCS preset + config validation --- package.json | 3 +- packages/docusaurus-types/src/config.d.ts | 7 +- packages/docusaurus-types/src/index.d.ts | 1 + packages/docusaurus-utils/src/index.ts | 7 +- packages/docusaurus-utils/src/vcs/vcs.ts | 61 ++- .../docusaurus-utils/src/vcs/vcsGitEager.ts | 1 + .../server/__tests__/configValidation.test.ts | 384 +++++------------- .../docusaurus/src/server/configValidation.ts | 47 ++- yarn.lock | 6 +- 9 files changed, 186 insertions(+), 331 deletions(-) diff --git a/package.json b/package.json index 0e9d63abf9..12ad1f5ccc 100644 --- a/package.json +++ b/package.json @@ -129,5 +129,6 @@ "stylelint-config-standard": "^29.0.0", "typescript": "~5.8.2" }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", + "dependencies": {} } diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index 3f4a0c6fdc..1f10022d6e 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -76,6 +76,11 @@ export type VcsConfig = { getFileLastUpdateInfo: (filePath: string) => Promise; }; +/** + * List of pre-built VcsConfig that Docusaurus provides. + */ +export type VcsPreset = 'git-ad-hoc' | 'git-eager' | 'hardcoded'; + export type FutureConfig = { /** * Turns v4 future flags on @@ -86,7 +91,7 @@ export type FutureConfig = { experimental_storage: StorageConfig; - experimental_vcs: VcsConfig; + experimental_vcs: VcsPreset | VcsConfig; /** * Docusaurus can work with 2 router types. diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 8b8cb11e40..6d153c387a 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -14,6 +14,7 @@ export { FasterConfig, StorageConfig, VcsConfig, + VcsPreset, VcsChangeInfo, VscInitializeParams, Config, diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 87b9311446..493b6359b5 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -131,6 +131,11 @@ export { type FrontMatterLastUpdate, } from './lastUpdateUtils'; -export {DEFAULT_VCS_CONFIG, DEFAULT_TEST_VCS_CONFIG} from './vcs/vcs'; +export { + VcsPresetNames, + getVcsPreset, + getDefaultVcsConfig, + DEFAULT_TEST_VCS_CONFIG, +} from './vcs/vcs'; export {normalizeTags, reportInlineTags} from './tags'; diff --git a/packages/docusaurus-utils/src/vcs/vcs.ts b/packages/docusaurus-utils/src/vcs/vcs.ts index 0c69588d97..a73084ff20 100644 --- a/packages/docusaurus-utils/src/vcs/vcs.ts +++ b/packages/docusaurus-utils/src/vcs/vcs.ts @@ -8,31 +8,59 @@ import {VcsHardcoded} from './vcsHardcoded'; import {VcsGitAdHoc} from './vcsGitAdHoc'; import {VscGitEager} from './vcsGitEager'; -import type {VcsConfig} from '@docusaurus/types'; +import type {VcsConfig, VcsPreset} from '@docusaurus/types'; -const VcsPresets = { +const VcsPresets: Record = { 'git-ad-hoc': VcsGitAdHoc, 'git-eager': VscGitEager, hardcoded: VcsHardcoded, -} as const satisfies Record; +}; -type VscPresetName = keyof typeof VcsPresets; +export const VcsPresetNames = Object.keys(VcsPresets) as VcsPreset[]; -function getVcsPreset(presetName: VscPresetName): VcsConfig { - return VcsPresets[presetName]; +export function findVcsPreset(presetName: string): VcsConfig | undefined { + return VcsPresets[presetName as VcsPreset]; } -function getDefaultVcsConfig(): VcsConfig { +export function getVcsPreset(presetName: VcsPreset): VcsConfig { + const vcs = findVcsPreset(presetName); + if (vcs) { + return vcs; + } else { + throw new Error( + `Unknown Docusaurus VCS preset name: ${process.env.DOCUSAURUS_VCS}`, + ); + } +} + +export function getDefaultVcsPreset(): VcsPreset { // Escape hatch to override the default VCS preset we use if (process.env.DOCUSAURUS_VCS) { - const vcs = getVcsPreset(process.env.DOCUSAURUS_VCS as VscPresetName); - if (vcs) { - return vcs; - } else { - throw new Error( - `Unknown DOCUSAURUS_VCS preset name: ${process.env.DOCUSAURUS_VCS}`, - ); - } + return process.env.DOCUSAURUS_VCS as VcsPreset; + } + + if (process.env.NODE_ENV === 'production') { + // TODO add feature flag switch for git-eager / git-ad-hoc strategies + // return getVcsPreset('git-ad-hoc'); + return 'git-eager'; + } + // Return hardcoded values in dev to improve DX + if (process.env.NODE_ENV === 'development') { + return 'hardcoded'; + } + + // Return hardcoded values in test to make tests simpler and faster + if (process.env.NODE_ENV === 'test') { + return 'hardcoded'; + } + + return 'git-eager'; +} + +export function getDefaultVcsConfig(): VcsConfig { + // Escape hatch to override the default VCS preset we use + if (process.env.DOCUSAURUS_VCS) { + return getVcsPreset(process.env.DOCUSAURUS_VCS as VcsPreset); } if (process.env.NODE_ENV === 'production') { @@ -40,7 +68,6 @@ function getDefaultVcsConfig(): VcsConfig { // return getVcsPreset('git-ad-hoc'); return getVcsPreset('git-eager'); } - // Return hardcoded values in dev to improve DX if (process.env.NODE_ENV === 'development') { return getVcsPreset('hardcoded'); @@ -55,5 +82,3 @@ function getDefaultVcsConfig(): VcsConfig { } export const DEFAULT_TEST_VCS_CONFIG: VcsConfig = VcsHardcoded; - -export const DEFAULT_VCS_CONFIG: VcsConfig = getDefaultVcsConfig(); diff --git a/packages/docusaurus-utils/src/vcs/vcsGitEager.ts b/packages/docusaurus-utils/src/vcs/vcsGitEager.ts index 1980a253dd..3d25a02431 100644 --- a/packages/docusaurus-utils/src/vcs/vcsGitEager.ts +++ b/packages/docusaurus-utils/src/vcs/vcsGitEager.ts @@ -163,6 +163,7 @@ function createGitVcsConfig(): VcsConfig { return { initialize: ({siteDir}) => { + console.log('git eager init'); // Only pre-init for production builds getGitFileInfo(siteDir).catch((error) => { console.error( diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index a5371a1cde..3defcb9fc8 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -6,7 +6,7 @@ */ import {jest} from '@jest/globals'; -import {DEFAULT_VCS_CONFIG} from '@docusaurus/utils'; +import {DEFAULT_TEST_VCS_CONFIG, getVcsPreset} from '@docusaurus/utils'; import { ConfigSchema, DEFAULT_CONFIG, @@ -31,6 +31,7 @@ import type { I18nConfig, I18nLocaleConfig, VcsConfig, + VcsPreset, } from '@docusaurus/types'; import type {DeepPartial} from 'utility-types'; @@ -1413,128 +1414,25 @@ describe('future', () => { }); } - it('accepts vcs - undefined', () => { - expect( - normalizeConfig({ - future: { - experimental_vcs: undefined, - }, - }), - ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG)); - }); - - it('accepts vcs - empty', () => { - expect( - normalizeConfig({ - future: {experimental_vcs: {}}, - }), - ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG)); - }); - - it('accepts vcs - full', () => { - const vcs: VcsConfig = { - initialize: (_params) => {}, - getFileCreationInfo: (_filePath) => null, - getFileLastUpdateInfo: (_filePath) => null, - }; - expect( - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toEqual(vcsContaining(vcs)); - }); - - it('rejects vcs - boolean', () => { - // @ts-expect-error: invalid - const vcs: Partial = true; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs" must be of type object - " - `); - }); - - it('rejects vcs - number', () => { - // @ts-expect-error: invalid - const vcs: Partial = 42; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs" must be of type object - " - `); - }); - - describe('initialize', () => { - it('accepts fn(params)', () => { - const vcs: Partial = { - initialize: (_params) => null, - }; + describe('base', () => { + it('accepts vcs - undefined', () => { expect( normalizeConfig({ future: { - experimental_vcs: vcs, + experimental_vcs: undefined, }, }), ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - ...vcs, + futureContaining({ + ...DEFAULT_FUTURE_CONFIG, + experimental_vcs: DEFAULT_TEST_VCS_CONFIG, }), ); }); - it('accepts fn()', () => { - const vcs: Partial = { - initialize: () => null, - }; - expect( - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - ...vcs, - }), - ); - }); - - it('accepts undefined', () => { - const vcs: Partial = { - initialize: undefined, - }; - expect( - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - }), - ); - }); - - it('rejects null', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - initialize: null, - }; + it('rejects vcs - boolean', () => { + // @ts-expect-error: invalid + const vcs: Partial = true; expect(() => normalizeConfig({ future: { @@ -1542,16 +1440,14 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.initialize" must be of type function + ""future.experimental_vcs" failed custom validation because "value" must be of type object " `); }); - it('rejects number', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - initialize: 42, - }; + it('rejects vcs - number', () => { + // @ts-expect-error: invalid + const vcs: Partial = 42; expect(() => normalizeConfig({ future: { @@ -1559,136 +1455,67 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.initialize" must be of type function - " - `); - }); - - it('rejects fn(params, anotherArg)', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - initialize: (_params, _anotherArg) => null, - }; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.initialize" must have an arity lesser or equal to 1 + ""future.experimental_vcs" failed custom validation because "value" must be of type object " `); }); }); - describe('getFileCreationInfo', () => { - it('accepts fn(filePath)', () => { - const vcs: Partial = { + describe('presets', () => { + it('accepts git-ad-hoc', () => { + const presetName: VcsPreset = 'git-ad-hoc'; + expect( + normalizeConfig({ + future: { + experimental_vcs: presetName, + }, + }), + ).toEqual(vcsContaining(getVcsPreset(presetName))); + }); + + it('accepts git-eager', () => { + const presetName: VcsPreset = 'git-eager'; + expect( + normalizeConfig({ + future: { + experimental_vcs: presetName, + }, + }), + ).toEqual(vcsContaining(getVcsPreset(presetName))); + }); + + it('accepts hardcoded', () => { + const presetName: VcsPreset = 'hardcoded'; + expect( + normalizeConfig({ + future: { + experimental_vcs: presetName, + }, + }), + ).toEqual(vcsContaining(getVcsPreset(presetName))); + }); + + it('rejects unknown preset name', () => { + // @ts-expect-error: invalid on purpose + const presetName: VcsPreset = 'unknown-preset-name'; + expect(() => + normalizeConfig({ + future: { + experimental_vcs: presetName, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_vcs" failed custom validation because VCS config preset name 'unknown-preset-name' is not valid. + " + `); + }); + }); + + describe('object config', () => { + it('accepts vcs - full', () => { + const vcs: VcsConfig = { + initialize: (_params) => {}, getFileCreationInfo: (_filePath) => null, - }; - expect( - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - ...vcs, - }), - ); - }); - - it('accepts undefined', () => { - const vcs: Partial = { - getFileCreationInfo: undefined, - }; - expect( - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - }), - ); - }); - - it('rejects null', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - getFileCreationInfo: null, - }; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileCreationInfo" must be of type function - " - `); - }); - - it('rejects number', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - getFileCreationInfo: 42, - }; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileCreationInfo" must be of type function - " - `); - }); - - it('rejects fn()', () => { - const vcs: Partial = { - getFileCreationInfo: () => null, - }; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileCreationInfo" must have an arity of 1 - " - `); - }); - - it('rejects fn(filePath, anotherArg)', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - getFileCreationInfo: (_filePath, _anotherArg) => null, - }; - expect(() => - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileCreationInfo" must have an arity of 1 - " - `); - }); - }); - - describe('getFileLastUpdateInfo', () => { - it('accepts fn(filePath)', () => { - const vcs: Partial = { getFileLastUpdateInfo: (_filePath) => null, }; expect( @@ -1697,52 +1524,26 @@ describe('future', () => { experimental_vcs: vcs, }, }), - ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - ...vcs, - }), - ); + ).toEqual(vcsContaining(vcs)); }); - it('accepts undefined', () => { - const vcs: Partial = { - getFileLastUpdateInfo: undefined, - }; - expect( - normalizeConfig({ - future: { - experimental_vcs: vcs, - }, - }), - ).toEqual( - vcsContaining({ - ...DEFAULT_VCS_CONFIG, - }), - ); - }); - - it('rejects null', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - getFileLastUpdateInfo: null, - }; + it('rejects vcs - empty', () => { expect(() => normalizeConfig({ - future: { - experimental_vcs: vcs, - }, + future: {experimental_vcs: {}}, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileLastUpdateInfo" must be of type function + ""future.experimental_vcs" failed custom validation because "initialize" is required " `); }); - it('rejects number', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - getFileLastUpdateInfo: 42, + it('accepts vcs - bad initialize() arity', () => { + const vcs: VcsConfig = { + // @ts-expect-error: invalid arity + initialize: (_params, _extraParam) => {}, + getFileCreationInfo: (_filePath) => null, + getFileLastUpdateInfo: (_filePath) => null, }; expect(() => normalizeConfig({ @@ -1751,14 +1552,17 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileLastUpdateInfo" must be of type function + ""future.experimental_vcs" failed custom validation because "initialize" must have an arity lesser or equal to 1 " `); }); - it('rejects fn()', () => { - const vcs: Partial = { - getFileLastUpdateInfo: () => null, + it('accepts vcs - bad getFileCreationInfo() arity', () => { + const vcs: VcsConfig = { + initialize: (_params) => {}, + // @ts-expect-error: invalid arity + getFileCreationInfo: (_filePath, _extraParam) => null, + getFileLastUpdateInfo: (_filePath) => null, }; expect(() => normalizeConfig({ @@ -1767,15 +1571,17 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileLastUpdateInfo" must have an arity of 1 + ""future.experimental_vcs" failed custom validation because "getFileCreationInfo" must have an arity of 1 " `); }); - it('rejects fn(filePath, anotherArg)', () => { - const vcs: Partial = { - // @ts-expect-error: invalid - getFileLastUpdateInfo: (_filePath, _anotherArg) => null, + it('accepts vcs - bad getFileLastUpdateInfo() arity', () => { + const vcs: VcsConfig = { + initialize: (_params) => {}, + getFileCreationInfo: (_filePath) => null, + // @ts-expect-error: invalid arity + getFileLastUpdateInfo: (_filePath, _extraParam) => null, }; expect(() => normalizeConfig({ @@ -1784,7 +1590,7 @@ describe('future', () => { }, }), ).toThrowErrorMatchingInlineSnapshot(` - ""future.experimental_vcs.getFileLastUpdateInfo" must have an arity of 1 + ""future.experimental_vcs" failed custom validation because "getFileLastUpdateInfo" must have an arity of 1 " `); }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index ad732ea043..a5e5ae78d1 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -9,7 +9,9 @@ import { DEFAULT_PARSE_FRONT_MATTER, DEFAULT_STATIC_DIR_NAME, DEFAULT_I18N_DIR_NAME, - DEFAULT_VCS_CONFIG, + getDefaultVcsConfig, + VcsPresetNames, + getVcsPreset, } from '@docusaurus/utils'; import {Joi, printWarning} from '@docusaurus/utils-validation'; import { @@ -29,6 +31,7 @@ import type { MarkdownHooks, I18nLocaleConfig, VcsConfig, + VcsPreset, } from '@docusaurus/types'; const DEFAULT_I18N_LOCALE = 'en'; @@ -108,7 +111,9 @@ export const DEFAULT_FUTURE_CONFIG: FutureConfig = { v4: DEFAULT_FUTURE_V4_CONFIG, experimental_faster: DEFAULT_FASTER_CONFIG, experimental_storage: DEFAULT_STORAGE_CONFIG, - experimental_vcs: DEFAULT_VCS_CONFIG, + + // Not good, need to be loaded lazily + experimental_vcs: getDefaultVcsConfig(), experimental_router: 'browser', }; @@ -334,22 +339,28 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({ .optional() .default(DEFAULT_STORAGE_CONFIG); -const VCS_CONFIG_SCHEMA = Joi.object({ - initialize: Joi.function() - .maxArity(1) - .optional() - .default(() => DEFAULT_VCS_CONFIG.initialize), - getFileCreationInfo: Joi.function() - .arity(1) - .optional() - .default(() => DEFAULT_VCS_CONFIG.getFileCreationInfo), - getFileLastUpdateInfo: Joi.function() - .arity(1) - .optional() - .default(() => DEFAULT_VCS_CONFIG.getFileLastUpdateInfo), -}) - .optional() - .default(DEFAULT_VCS_CONFIG); +const VCS_CONFIG_OBJECT_SCHEMA = Joi.object({ + // All the fields are required on purpose + // You either provide a full VCS config or nothing + initialize: Joi.function().maxArity(1).required(), + getFileCreationInfo: Joi.function().arity(1).required(), + getFileLastUpdateInfo: Joi.function().arity(1).required(), +}); + +const VCS_CONFIG_SCHEMA = Joi.custom((input) => { + if (typeof input === 'string') { + const presetName = input as VcsPreset; + if (!VcsPresetNames.includes(presetName)) { + throw new Error(`VCS config preset name '${input}' is not valid.`); + } + return getVcsPreset(presetName); + } + const {error, value} = VCS_CONFIG_OBJECT_SCHEMA.validate(input); + if (error) { + throw error; + } + return value; +}).default(() => getDefaultVcsConfig()); const FUTURE_CONFIG_SCHEMA = Joi.object({ v4: FUTURE_V4_SCHEMA, diff --git a/yarn.lock b/yarn.lock index db4d46570d..76cafa7aed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6088,9 +6088,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001718: - version "1.0.30001721" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz#36b90cd96901f8c98dd6698bf5c8af7d4c6872d7" - integrity sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ== + version "1.0.30001754" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz" + integrity sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg== ccount@^2.0.0: version "2.0.1"