add VcsConfig + default retro-compatible implementation + config option + config validation tests

This commit is contained in:
sebastien 2025-10-24 17:38:15 +02:00
parent a4742594a9
commit bb85b80813
7 changed files with 359 additions and 0 deletions

View File

@ -40,6 +40,22 @@ export type FutureV4Config = {
useCssCascadeLayers: boolean;
};
// VCS (Version Control System) info about a given change, e.g., a git commit.
// The agnostic term "VCS" is used instead of "git" to acknowledge the existence
// of other version control systems, and external systems like CMSs and i18n
// translation SaaS (e.g., Crowdin)
type VcsChangeInfo = {timestamp: number; author: string};
// VCS (Version Control System) config hooks to get file change info.
// This lets you override and customize the default Docusaurus behavior.
// This can be useful to optimize calls or when using something else than git
// See https://github.com/facebook/docusaurus/issues/11208
// See https://github.com/e18e/ecosystem-issues/issues/216
export type VcsConfig = {
getFileCreationInfo: (filePath: string) => Promise<VcsChangeInfo | null>;
getFileLastUpdateInfo: (filePath: string) => Promise<VcsChangeInfo | null>;
};
export type FutureConfig = {
/**
* Turns v4 future flags on
@ -50,6 +66,8 @@ export type FutureConfig = {
experimental_storage: StorageConfig;
experimental_vcs: VcsConfig;
/**
* Docusaurus can work with 2 router types.
*

View File

@ -13,6 +13,7 @@ export {
FutureV4Config,
FasterConfig,
StorageConfig,
VcsConfig,
Config,
} from './config';

View File

@ -126,6 +126,7 @@ export async function getFileCommitDate(
timestamp: number;
author?: string;
}> {
console.log({file});
if (!hasGit()) {
throw new GitNotFoundError(
`Failed to retrieve git history for "${file}" because git is not installed.`,

View File

@ -129,4 +129,6 @@ export {
type FrontMatterLastUpdate,
} from './lastUpdateUtils';
export {DEFAULT_VCS_CONFIG} from './vcsUtils';
export {normalizeTags, reportInlineTags} from './tags';

View File

@ -0,0 +1,32 @@
/**
* 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 {getFileCommitDate} from './gitUtils';
import {getLastUpdate} from './lastUpdateUtils';
import type {VcsConfig} from '@docusaurus/types';
export const DEFAULT_VCS_CONFIG: VcsConfig = {
getFileCreationInfo: async (filePath: string) => {
return getFileCommitDate(filePath, {
age: 'oldest',
includeAuthor: true,
});
},
getFileLastUpdateInfo: async (filePath: string) => {
// TODO non-ideal integration but good enough for now
// This keeps this new VscConfig system retro-compatible with the existing
// historical Docusaurus behavior based on Git
const result = await getLastUpdate(filePath);
if (result === null) {
return null;
}
return {
timestamp: result.lastUpdatedAt!,
author: result.lastUpdatedBy!,
};
},
};

View File

@ -6,6 +6,7 @@
*/
import {jest} from '@jest/globals';
import {DEFAULT_VCS_CONFIG} from '@docusaurus/utils';
import {
ConfigSchema,
DEFAULT_CONFIG,
@ -29,6 +30,7 @@ import type {
PluginConfig,
I18nConfig,
I18nLocaleConfig,
VcsConfig,
} from '@docusaurus/types';
import type {DeepPartial} from 'utility-types';
@ -73,6 +75,10 @@ describe('normalizeConfig', () => {
type: 'sessionStorage',
namespace: true,
},
experimental_vcs: {
getFileCreationInfo: (_filePath) => null,
getFileLastUpdateInfo: (_filePath) => null,
},
experimental_router: 'hash',
},
tagline: 'my awesome site',
@ -1077,6 +1083,10 @@ describe('future', () => {
rspackPersistentCache: true,
ssgWorkerThreads: true,
},
experimental_vcs: {
getFileCreationInfo: (_filePath) => null,
getFileLastUpdateInfo: (_filePath) => null,
},
experimental_storage: {
type: 'sessionStorage',
namespace: 'myNamespace',
@ -1394,6 +1404,284 @@ describe('future', () => {
});
});
describe('vcs', () => {
function vcsContaining(vcs: Partial<VcsConfig>) {
return futureContaining({
experimental_vcs: expect.objectContaining(vcs),
});
}
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 = {
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<VcsConfig> = 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<VcsConfig> = 42;
expect(() =>
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_vcs" must be of type object
"
`);
});
describe('getFileCreationInfo', () => {
it('accepts fn(filePath)', () => {
const vcs: Partial<VcsConfig> = {
getFileCreationInfo: (_filePath) => null,
};
expect(
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toEqual(
vcsContaining({
...DEFAULT_VCS_CONFIG,
...vcs,
}),
);
});
it('accepts undefined', () => {
const vcs: Partial<VcsConfig> = {
getFileCreationInfo: undefined,
};
expect(
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toEqual(
vcsContaining({
...DEFAULT_VCS_CONFIG,
}),
);
});
it('rejects null', () => {
const vcs: Partial<VcsConfig> = {
// @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<VcsConfig> = {
// @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<VcsConfig> = {
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<VcsConfig> = {
// @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<VcsConfig> = {
getFileLastUpdateInfo: (_filePath) => null,
};
expect(
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toEqual(
vcsContaining({
...DEFAULT_VCS_CONFIG,
...vcs,
}),
);
});
it('accepts undefined', () => {
const vcs: Partial<VcsConfig> = {
getFileLastUpdateInfo: undefined,
};
expect(
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toEqual(
vcsContaining({
...DEFAULT_VCS_CONFIG,
}),
);
});
it('rejects null', () => {
const vcs: Partial<VcsConfig> = {
// @ts-expect-error: invalid
getFileLastUpdateInfo: null,
};
expect(() =>
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_vcs.getFileLastUpdateInfo" must be of type function
"
`);
});
it('rejects number', () => {
const vcs: Partial<VcsConfig> = {
// @ts-expect-error: invalid
getFileLastUpdateInfo: 42,
};
expect(() =>
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_vcs.getFileLastUpdateInfo" must be of type function
"
`);
});
it('rejects fn()', () => {
const vcs: Partial<VcsConfig> = {
getFileLastUpdateInfo: () => null,
};
expect(() =>
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_vcs.getFileLastUpdateInfo" must have an arity of 1
"
`);
});
it('rejects fn(filePath, anotherArg)', () => {
const vcs: Partial<VcsConfig> = {
// @ts-expect-error: invalid
getFileLastUpdateInfo: (_filePath, _anotherArg) => null,
};
expect(() =>
normalizeConfig({
future: {
experimental_vcs: vcs,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_vcs.getFileLastUpdateInfo" must have an arity of 1
"
`);
});
});
});
describe('faster', () => {
function fasterContaining(faster: Partial<FasterConfig>) {
return futureContaining({

View File

@ -9,6 +9,7 @@ import {
DEFAULT_PARSE_FRONT_MATTER,
DEFAULT_STATIC_DIR_NAME,
DEFAULT_I18N_DIR_NAME,
DEFAULT_VCS_CONFIG,
} from '@docusaurus/utils';
import {Joi, printWarning} from '@docusaurus/utils-validation';
import {
@ -27,6 +28,7 @@ import type {
MarkdownConfig,
MarkdownHooks,
I18nLocaleConfig,
VcsConfig,
} from '@docusaurus/types';
const DEFAULT_I18N_LOCALE = 'en';
@ -106,6 +108,7 @@ 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,
experimental_router: 'browser',
};
@ -331,10 +334,24 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({
.optional()
.default(DEFAULT_STORAGE_CONFIG);
const VCS_CONFIG_SCHEMA = Joi.object<VcsConfig>({
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 FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
v4: FUTURE_V4_SCHEMA,
experimental_faster: FASTER_CONFIG_SCHEMA,
experimental_storage: STORAGE_CONFIG_SCHEMA,
experimental_vcs: VCS_CONFIG_SCHEMA,
experimental_router: Joi.string()
.equal('browser', 'hash')
.default(DEFAULT_FUTURE_CONFIG.experimental_router),