refactor(core): improve dev perf, fine-grained site reloads - part 3 (#9975)

This commit is contained in:
Sébastien Lorber 2024-03-28 12:39:07 +01:00 committed by GitHub
parent 06e70a4f9a
commit efbe474e9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 359 additions and 286 deletions

View file

@ -31,6 +31,7 @@ export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};
export type LoadContext = { export type LoadContext = {
siteDir: string; siteDir: string;
siteVersion: string | undefined;
generatedFilesDir: string; generatedFilesDir: string;
siteConfig: DocusaurusConfig; siteConfig: DocusaurusConfig;
siteConfigPath: string; siteConfigPath: string;

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import type {TranslationFile} from './i18n'; import type {CodeTranslations, TranslationFile} from './i18n';
import type {RuleSetRule, Configuration as WebpackConfiguration} from 'webpack'; import type {RuleSetRule, Configuration as WebpackConfiguration} from 'webpack';
import type {CustomizeRuleString} from 'webpack-merge/dist/types'; import type {CustomizeRuleString} from 'webpack-merge/dist/types';
import type {CommanderStatic} from 'commander'; import type {CommanderStatic} from 'commander';
@ -185,6 +185,7 @@ export type LoadedPlugin = InitializedPlugin & {
readonly content: unknown; readonly content: unknown;
readonly globalData: unknown; readonly globalData: unknown;
readonly routes: RouteConfig[]; readonly routes: RouteConfig[];
readonly defaultCodeTranslations: CodeTranslations;
}; };
export type PluginModule<Content = unknown> = { export type PluginModule<Content = unknown> = {

View file

@ -12,6 +12,10 @@ import {findAsyncSequential} from './jsUtils';
const fileHash = new Map<string, string>(); const fileHash = new Map<string, string>();
const hashContent = (content: string): string => {
return createHash('md5').update(content).digest('hex');
};
/** /**
* Outputs a file to the generated files directory. Only writes files if content * Outputs a file to the generated files directory. Only writes files if content
* differs from cache (for hot reload performance). * differs from cache (for hot reload performance).
@ -38,7 +42,7 @@ export async function generate(
// first "A" remains in cache. But if the file never existed in cache, no // first "A" remains in cache. But if the file never existed in cache, no
// need to register it. // need to register it.
if (fileHash.get(filepath)) { if (fileHash.get(filepath)) {
fileHash.set(filepath, createHash('md5').update(content).digest('hex')); fileHash.set(filepath, hashContent(content));
} }
return; return;
} }
@ -50,11 +54,11 @@ export async function generate(
// overwriting and we can reuse old file. // overwriting and we can reuse old file.
if (!lastHash && (await fs.pathExists(filepath))) { if (!lastHash && (await fs.pathExists(filepath))) {
const lastContent = await fs.readFile(filepath, 'utf8'); const lastContent = await fs.readFile(filepath, 'utf8');
lastHash = createHash('md5').update(lastContent).digest('hex'); lastHash = hashContent(lastContent);
fileHash.set(filepath, lastHash); fileHash.set(filepath, lastHash);
} }
const currentHash = createHash('md5').update(content).digest('hex'); const currentHash = hashContent(content);
if (lastHash !== currentHash) { if (lastHash !== currentHash) {
await fs.outputFile(filepath, content); await fs.outputFile(filepath, content);

View file

@ -6,7 +6,16 @@
*/ */
import path from 'path'; import path from 'path';
import shell from 'shelljs'; import fs from 'fs-extra';
import _ from 'lodash';
import shell from 'shelljs'; // TODO replace with async-first version
const realHasGitFn = () => !!shell.which('git');
// The hasGit call is synchronous IO so we memoize it
// The user won't install Git in the middle of a build anyway...
const hasGit =
process.env.NODE_ENV === 'test' ? realHasGitFn : _.memoize(realHasGitFn);
/** Custom error thrown when git is not found in `PATH`. */ /** Custom error thrown when git is not found in `PATH`. */
export class GitNotFoundError extends Error {} export class GitNotFoundError extends Error {}
@ -86,13 +95,13 @@ export async function getFileCommitDate(
timestamp: number; timestamp: number;
author?: string; author?: string;
}> { }> {
if (!shell.which('git')) { if (!hasGit()) {
throw new GitNotFoundError( throw new GitNotFoundError(
`Failed to retrieve git history for "${file}" because git is not installed.`, `Failed to retrieve git history for "${file}" because git is not installed.`,
); );
} }
if (!shell.test('-f', file)) { if (!(await fs.pathExists(file))) {
throw new Error( throw new Error(
`Failed to retrieve git history for "${file}" because the file does not exist.`, `Failed to retrieve git history for "${file}" because the file does not exist.`,
); );

View file

@ -64,23 +64,15 @@ export async function build(
process.on(sig, () => process.exit()); process.on(sig, () => process.exit());
}); });
async function tryToBuildLocale({ async function tryToBuildLocale({locale}: {locale: string}) {
locale,
isLastLocale,
}: {
locale: string;
isLastLocale: boolean;
}) {
try { try {
PerfLogger.start(`Building site for locale ${locale}`); await PerfLogger.async(`${logger.name(locale)}`, () =>
await buildLocale({ buildLocale({
siteDir, siteDir,
locale, locale,
cliOptions, cliOptions,
forceTerminate, }),
isLastLocale, );
});
PerfLogger.end(`Building site for locale ${locale}`);
} catch (err) { } catch (err) {
throw new Error( throw new Error(
logger.interpolate`Unable to build website for locale name=${locale}.`, logger.interpolate`Unable to build website for locale name=${locale}.`,
@ -91,20 +83,28 @@ export async function build(
} }
} }
PerfLogger.start(`Get locales to build`); const locales = await PerfLogger.async('Get locales to build', () =>
const locales = await getLocalesToBuild({siteDir, cliOptions}); getLocalesToBuild({siteDir, cliOptions}),
PerfLogger.end(`Get locales to build`); );
if (locales.length > 1) { if (locales.length > 1) {
logger.info`Website will be built for all these locales: ${locales}`; logger.info`Website will be built for all these locales: ${locales}`;
} }
PerfLogger.start(`Building ${locales.length} locales`); await PerfLogger.async(`Build`, () =>
await mapAsyncSequential(locales, (locale) => { mapAsyncSequential(locales, async (locale) => {
const isLastLocale = locales.indexOf(locale) === locales.length - 1; const isLastLocale = locales.indexOf(locale) === locales.length - 1;
return tryToBuildLocale({locale, isLastLocale}); await tryToBuildLocale({locale});
}); if (isLastLocale) {
PerfLogger.end(`Building ${locales.length} locales`); logger.info`Use code=${'npm run serve'} command to test your build locally.`;
}
// TODO do we really need this historical forceTerminate exit???
if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) {
process.exit(0);
}
}),
);
} }
async function getLocalesToBuild({ async function getLocalesToBuild({
@ -144,14 +144,10 @@ async function buildLocale({
siteDir, siteDir,
locale, locale,
cliOptions, cliOptions,
forceTerminate,
isLastLocale,
}: { }: {
siteDir: string; siteDir: string;
locale: string; locale: string;
cliOptions: Partial<BuildCLIOptions>; cliOptions: Partial<BuildCLIOptions>;
forceTerminate: boolean;
isLastLocale: boolean;
}): Promise<string> { }): Promise<string> {
// Temporary workaround to unlock the ability to translate the site config // Temporary workaround to unlock the ability to translate the site config
// We'll remove it if a better official API can be designed // We'll remove it if a better official API can be designed
@ -160,81 +156,66 @@ async function buildLocale({
logger.info`name=${`[${locale}]`} Creating an optimized production build...`; logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
PerfLogger.start('Loading site'); const site = await PerfLogger.async('Load site', () =>
const site = await loadSite({ loadSite({
siteDir, siteDir,
outDir: cliOptions.outDir, outDir: cliOptions.outDir,
config: cliOptions.config, config: cliOptions.config,
locale, locale,
localizePath: cliOptions.locale ? false : undefined, localizePath: cliOptions.locale ? false : undefined,
}); }),
PerfLogger.end('Loading site'); );
const {props} = site; const {props} = site;
const {outDir, plugins} = props; const {outDir, plugins} = props;
// We can build the 2 configs in parallel // We can build the 2 configs in parallel
PerfLogger.start('Creating webpack configs');
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] = const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
await Promise.all([ await PerfLogger.async('Creating webpack configs', () =>
getBuildClientConfig({ Promise.all([
props, getBuildClientConfig({
cliOptions, props,
}), cliOptions,
getBuildServerConfig({ }),
props, getBuildServerConfig({
}), props,
]); }),
PerfLogger.end('Creating webpack configs'); ]),
);
// Make sure generated client-manifest is cleaned first, so we don't reuse
// the one from previous builds.
// TODO do we really need this? .docusaurus folder is cleaned between builds
PerfLogger.start('Deleting previous client manifest');
await ensureUnlink(clientManifestPath);
PerfLogger.end('Deleting previous client manifest');
// Run webpack to build JS bundle (client) and static html files (server). // Run webpack to build JS bundle (client) and static html files (server).
PerfLogger.start('Bundling'); await PerfLogger.async('Bundling with Webpack', () =>
await compile([clientConfig, serverConfig]); compile([clientConfig, serverConfig]),
PerfLogger.end('Bundling'); );
PerfLogger.start('Executing static site generation'); const {collectedData} = await PerfLogger.async('SSG', () =>
const {collectedData} = await executeSSG({ executeSSG({
props, props,
serverBundlePath, serverBundlePath,
clientManifestPath, clientManifestPath,
}); }),
PerfLogger.end('Executing static site generation'); );
// Remove server.bundle.js because it is not needed. // Remove server.bundle.js because it is not needed.
PerfLogger.start('Deleting server bundle'); await PerfLogger.async('Deleting server bundle', () =>
await ensureUnlink(serverBundlePath); ensureUnlink(serverBundlePath),
PerfLogger.end('Deleting server bundle'); );
// Plugin Lifecycle - postBuild. // Plugin Lifecycle - postBuild.
PerfLogger.start('Executing postBuild()'); await PerfLogger.async('postBuild()', () =>
await executePluginsPostBuild({plugins, props, collectedData}); executePluginsPostBuild({plugins, props, collectedData}),
PerfLogger.end('Executing postBuild()'); );
// TODO execute this in parallel to postBuild? // TODO execute this in parallel to postBuild?
PerfLogger.start('Executing broken links checker'); await PerfLogger.async('Broken links checker', () =>
await executeBrokenLinksCheck({props, collectedData}); executeBrokenLinksCheck({props, collectedData}),
PerfLogger.end('Executing broken links checker'); );
logger.success`Generated static files in path=${path.relative( logger.success`Generated static files in path=${path.relative(
process.cwd(), process.cwd(),
outDir, outDir,
)}.`; )}.`;
if (isLastLocale) {
logger.info`Use code=${'npm run serve'} command to test your build locally.`;
}
if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) {
process.exit(0);
}
return outDir; return outDir;
} }
@ -247,40 +228,39 @@ async function executeSSG({
serverBundlePath: string; serverBundlePath: string;
clientManifestPath: string; clientManifestPath: string;
}) { }) {
PerfLogger.start('Reading client manifest'); const manifest: Manifest = await PerfLogger.async(
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8'); 'Read client manifest',
PerfLogger.end('Reading client manifest'); () => fs.readJSON(clientManifestPath, 'utf-8'),
PerfLogger.start('Compiling SSR template');
const ssrTemplate = await compileSSRTemplate(
props.siteConfig.ssrTemplate ?? defaultSSRTemplate,
); );
PerfLogger.end('Compiling SSR template');
PerfLogger.start('Loading App renderer'); const ssrTemplate = await PerfLogger.async('Compile SSR template', () =>
const renderer = await loadAppRenderer({ compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
serverBundlePath, );
});
PerfLogger.end('Loading App renderer');
PerfLogger.start('Generate static files'); const renderer = await PerfLogger.async('Load App renderer', () =>
const ssgResult = await generateStaticFiles({ loadAppRenderer({
pathnames: props.routesPaths, serverBundlePath,
renderer, }),
params: { );
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir, const ssgResult = await PerfLogger.async('Generate static files', () =>
baseUrl: props.baseUrl, generateStaticFiles({
manifest, pathnames: props.routesPaths,
headTags: props.headTags, renderer,
preBodyTags: props.preBodyTags, params: {
postBodyTags: props.postBodyTags, trailingSlash: props.siteConfig.trailingSlash,
ssrTemplate, outDir: props.outDir,
noIndex: props.siteConfig.noIndex, baseUrl: props.baseUrl,
DOCUSAURUS_VERSION, manifest,
}, headTags: props.headTags,
}); preBodyTags: props.preBodyTags,
PerfLogger.end('Generate static files'); postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
},
}),
);
return ssgResult; return ssgResult;
} }

View file

@ -18,6 +18,7 @@ import {
reloadSite, reloadSite,
reloadSitePlugin, reloadSitePlugin,
} from '../../server/site'; } from '../../server/site';
import {formatPluginName} from '../../server/plugins/pluginsUtils';
import type {StartCLIOptions} from './start'; import type {StartCLIOptions} from './start';
import type {LoadedPlugin} from '@docusaurus/types'; import type {LoadedPlugin} from '@docusaurus/types';
@ -69,10 +70,13 @@ async function createLoadSiteParams({
export async function createReloadableSite(startParams: StartParams) { export async function createReloadableSite(startParams: StartParams) {
const openUrlContext = await createOpenUrlContext(startParams); const openUrlContext = await createOpenUrlContext(startParams);
let site = await PerfLogger.async('Loading site', async () => { const loadSiteParams = await PerfLogger.async('createLoadSiteParams', () =>
const params = await createLoadSiteParams(startParams); createLoadSiteParams(startParams),
return loadSite(params); );
});
let site = await PerfLogger.async('Load site', () =>
loadSite(loadSiteParams),
);
const get = () => site; const get = () => site;
@ -89,7 +93,7 @@ export async function createReloadableSite(startParams: StartParams) {
const reloadBase = async () => { const reloadBase = async () => {
try { try {
const oldSite = site; const oldSite = site;
site = await PerfLogger.async('Reloading site', () => reloadSite(site)); site = await PerfLogger.async('Reload site', () => reloadSite(site));
if (oldSite.props.baseUrl !== site.props.baseUrl) { if (oldSite.props.baseUrl !== site.props.baseUrl) {
printOpenUrlMessage(); printOpenUrlMessage();
} }
@ -108,7 +112,7 @@ export async function createReloadableSite(startParams: StartParams) {
const reloadPlugin = async (plugin: LoadedPlugin) => { const reloadPlugin = async (plugin: LoadedPlugin) => {
try { try {
site = await PerfLogger.async( site = await PerfLogger.async(
`Reloading site plugin ${plugin.name}@${plugin.options.id}`, `Reload site plugin ${formatPluginName(plugin)}`,
() => { () => {
const pluginIdentifier = {name: plugin.name, id: plugin.options.id}; const pluginIdentifier = {name: plugin.name, id: plugin.options.id};
return reloadSitePlugin(site, pluginIdentifier); return reloadSitePlugin(site, pluginIdentifier);
@ -116,7 +120,7 @@ export async function createReloadableSite(startParams: StartParams) {
); );
} catch (e) { } catch (e) {
logger.error( logger.error(
`Site plugin reload failure - Plugin ${plugin.name}@${plugin.options.id}`, `Site plugin reload failure - Plugin ${formatPluginName(plugin)}`,
); );
console.error(e); console.error(e);
} }

View file

@ -13,7 +13,7 @@ import {
writePluginTranslations, writePluginTranslations,
writeCodeTranslations, writeCodeTranslations,
type WriteTranslationsOptions, type WriteTranslationsOptions,
getPluginsDefaultCodeTranslationMessages, loadPluginsDefaultCodeTranslationMessages,
applyDefaultCodeTranslations, applyDefaultCodeTranslations,
} from '../server/translations/translations'; } from '../server/translations/translations';
import { import {
@ -114,7 +114,7 @@ Available locales are: ${context.i18n.locales.join(',')}.`,
await getExtraSourceCodeFilePaths(), await getExtraSourceCodeFilePaths(),
); );
const defaultCodeMessages = await getPluginsDefaultCodeTranslationMessages( const defaultCodeMessages = await loadPluginsDefaultCodeTranslationMessages(
plugins, plugins,
); );

View file

@ -36,6 +36,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
"plugins": [ "plugins": [
{ {
"content": undefined, "content": undefined,
"defaultCodeTranslations": {},
"getClientModules": [Function], "getClientModules": [Function],
"globalData": undefined, "globalData": undefined,
"injectHtmlTags": [Function], "injectHtmlTags": [Function],
@ -52,6 +53,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
{ {
"configureWebpack": [Function], "configureWebpack": [Function],
"content": undefined, "content": undefined,
"defaultCodeTranslations": {},
"globalData": undefined, "globalData": undefined,
"name": "docusaurus-mdx-fallback-plugin", "name": "docusaurus-mdx-fallback-plugin",
"options": { "options": {
@ -132,5 +134,6 @@ exports[`load loads props for site with custom i18n path 1`] = `
"pluginVersions": {}, "pluginVersions": {},
"siteVersion": undefined, "siteVersion": undefined,
}, },
"siteVersion": undefined,
} }
`; `;

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {loadClientModules} from '../clientModules'; import {getAllClientModules} from '../clientModules';
import type {LoadedPlugin} from '@docusaurus/types'; import type {LoadedPlugin} from '@docusaurus/types';
const pluginEmpty = { const pluginEmpty = {
@ -33,14 +33,14 @@ const pluginHelloWorld = {
}, },
} as unknown as LoadedPlugin; } as unknown as LoadedPlugin;
describe('loadClientModules', () => { describe('getAllClientModules', () => {
it('loads an empty plugin', () => { it('loads an empty plugin', () => {
const clientModules = loadClientModules([pluginEmpty]); const clientModules = getAllClientModules([pluginEmpty]);
expect(clientModules).toMatchInlineSnapshot(`[]`); expect(clientModules).toMatchInlineSnapshot(`[]`);
}); });
it('loads a non-empty plugin', () => { it('loads a non-empty plugin', () => {
const clientModules = loadClientModules([pluginFooBar]); const clientModules = getAllClientModules([pluginFooBar]);
expect(clientModules).toMatchInlineSnapshot(` expect(clientModules).toMatchInlineSnapshot(`
[ [
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo", "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
@ -50,7 +50,7 @@ describe('loadClientModules', () => {
}); });
it('loads multiple non-empty plugins', () => { it('loads multiple non-empty plugins', () => {
const clientModules = loadClientModules([pluginFooBar, pluginHelloWorld]); const clientModules = getAllClientModules([pluginFooBar, pluginHelloWorld]);
expect(clientModules).toMatchInlineSnapshot(` expect(clientModules).toMatchInlineSnapshot(`
[ [
"<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo", "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/foo",
@ -62,7 +62,7 @@ describe('loadClientModules', () => {
}); });
it('loads multiple non-empty plugins in different order', () => { it('loads multiple non-empty plugins in different order', () => {
const clientModules = loadClientModules([pluginHelloWorld, pluginFooBar]); const clientModules = getAllClientModules([pluginHelloWorld, pluginFooBar]);
expect(clientModules).toMatchInlineSnapshot(` expect(clientModules).toMatchInlineSnapshot(`
[ [
"/hello", "/hello",
@ -74,7 +74,7 @@ describe('loadClientModules', () => {
}); });
it('loads both empty and non-empty plugins', () => { it('loads both empty and non-empty plugins', () => {
const clientModules = loadClientModules([ const clientModules = getAllClientModules([
pluginHelloWorld, pluginHelloWorld,
pluginEmpty, pluginEmpty,
pluginFooBar, pluginFooBar,
@ -90,7 +90,7 @@ describe('loadClientModules', () => {
}); });
it('loads empty and non-empty in a different order', () => { it('loads empty and non-empty in a different order', () => {
const clientModules = loadClientModules([ const clientModules = getAllClientModules([
pluginHelloWorld, pluginHelloWorld,
pluginFooBar, pluginFooBar,
pluginEmpty, pluginEmpty,

View file

@ -7,13 +7,13 @@
import path from 'path'; import path from 'path';
import {DOCUSAURUS_VERSION} from '@docusaurus/utils'; import {DOCUSAURUS_VERSION} from '@docusaurus/utils';
import {getPluginVersion, loadSiteMetadata} from '../siteMetadata'; import {loadPluginVersion, createSiteMetadata} from '../siteMetadata';
import type {LoadedPlugin} from '@docusaurus/types'; import type {LoadedPlugin} from '@docusaurus/types';
describe('getPluginVersion', () => { describe('loadPluginVersion', () => {
it('detects external packages plugins versions', async () => { it('detects external packages plugins versions', async () => {
await expect( await expect(
getPluginVersion( loadPluginVersion(
path.join(__dirname, '__fixtures__/siteMetadata/dummy-plugin.js'), path.join(__dirname, '__fixtures__/siteMetadata/dummy-plugin.js'),
// Make the plugin appear external. // Make the plugin appear external.
path.join(__dirname, '..', '..', '..', '..', '..', '..', 'website'), path.join(__dirname, '..', '..', '..', '..', '..', '..', 'website'),
@ -23,7 +23,7 @@ describe('getPluginVersion', () => {
it('detects project plugins versions', async () => { it('detects project plugins versions', async () => {
await expect( await expect(
getPluginVersion( loadPluginVersion(
path.join(__dirname, '__fixtures__/siteMetadata/dummy-plugin.js'), path.join(__dirname, '__fixtures__/siteMetadata/dummy-plugin.js'),
// Make the plugin appear project local. // Make the plugin appear project local.
path.join(__dirname, '__fixtures__/siteMetadata'), path.join(__dirname, '__fixtures__/siteMetadata'),
@ -32,14 +32,14 @@ describe('getPluginVersion', () => {
}); });
it('detects local packages versions', async () => { it('detects local packages versions', async () => {
await expect(getPluginVersion('/', '/')).resolves.toEqual({type: 'local'}); await expect(loadPluginVersion('/', '/')).resolves.toEqual({type: 'local'});
}); });
}); });
describe('loadSiteMetadata', () => { describe('createSiteMetadata', () => {
it('throws if plugin versions mismatch', async () => { it('throws if plugin versions mismatch', () => {
await expect( expect(() =>
loadSiteMetadata({ createSiteMetadata({
plugins: [ plugins: [
{ {
name: 'docusaurus-plugin-content-docs', name: 'docusaurus-plugin-content-docs',
@ -50,10 +50,9 @@ describe('loadSiteMetadata', () => {
}, },
}, },
] as LoadedPlugin[], ] as LoadedPlugin[],
siteDir: path.join(__dirname, '__fixtures__/siteMetadata'), siteVersion: 'some-random-version',
}), }),
).rejects ).toThrow(`Invalid name=docusaurus-plugin-content-docs version number=1.0.0.
.toThrow(`Invalid name=docusaurus-plugin-content-docs version number=1.0.0.
All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${DOCUSAURUS_VERSION}). All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${DOCUSAURUS_VERSION}).
Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`); Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`);
}); });

View file

@ -12,7 +12,7 @@ import type {LoadedPlugin} from '@docusaurus/types';
* Runs the `getClientModules` lifecycle. The returned file paths are all * Runs the `getClientModules` lifecycle. The returned file paths are all
* absolute. * absolute.
*/ */
export function loadClientModules(plugins: LoadedPlugin[]): string[] { export function getAllClientModules(plugins: LoadedPlugin[]): string[] {
return plugins.flatMap( return plugins.flatMap(
(plugin) => (plugin) =>
plugin.getClientModules?.().map((p) => path.resolve(plugin.path, p)) ?? plugin.getClientModules?.().map((p) => path.resolve(plugin.path, p)) ??

View file

@ -48,6 +48,7 @@ export async function createPluginActionsUtils({
dataDir, dataDir,
`${docuHash('pluginRouteContextModule')}.json`, `${docuHash('pluginRouteContextModule')}.json`,
); );
// TODO not ideal place to generate that file
await generate( await generate(
'/', '/',
pluginRouteContextModulePath, pluginRouteContextModulePath,

View file

@ -12,7 +12,7 @@ import {
normalizePluginOptions, normalizePluginOptions,
normalizeThemeConfig, normalizeThemeConfig,
} from '@docusaurus/utils-validation'; } from '@docusaurus/utils-validation';
import {getPluginVersion} from '../siteMetadata'; import {loadPluginVersion} from '../siteMetadata';
import {ensureUniquePluginInstanceIds} from './pluginIds'; import {ensureUniquePluginInstanceIds} from './pluginIds';
import {loadPluginConfigs, type NormalizedPluginConfig} from './configs'; import {loadPluginConfigs, type NormalizedPluginConfig} from './configs';
import type { import type {
@ -61,14 +61,14 @@ export async function initPlugins(
const pluginRequire = createRequire(context.siteConfigPath); const pluginRequire = createRequire(context.siteConfigPath);
const pluginConfigs = await loadPluginConfigs(context); const pluginConfigs = await loadPluginConfigs(context);
async function doGetPluginVersion( async function doLoadPluginVersion(
normalizedPluginConfig: NormalizedPluginConfig, normalizedPluginConfig: NormalizedPluginConfig,
): Promise<PluginVersionInformation> { ): Promise<PluginVersionInformation> {
if (normalizedPluginConfig.pluginModule?.path) { if (normalizedPluginConfig.pluginModule?.path) {
const pluginPath = pluginRequire.resolve( const pluginPath = pluginRequire.resolve(
normalizedPluginConfig.pluginModule.path, normalizedPluginConfig.pluginModule.path,
); );
return getPluginVersion(pluginPath, context.siteDir); return loadPluginVersion(pluginPath, context.siteDir);
} }
return {type: 'local'}; return {type: 'local'};
} }
@ -109,7 +109,7 @@ export async function initPlugins(
async function initializePlugin( async function initializePlugin(
normalizedPluginConfig: NormalizedPluginConfig, normalizedPluginConfig: NormalizedPluginConfig,
): Promise<InitializedPlugin> { ): Promise<InitializedPlugin> {
const pluginVersion: PluginVersionInformation = await doGetPluginVersion( const pluginVersion: PluginVersionInformation = await doLoadPluginVersion(
normalizedPluginConfig, normalizedPluginConfig,
); );
const pluginOptions = doValidatePluginOptions(normalizedPluginConfig); const pluginOptions = doValidatePluginOptions(normalizedPluginConfig);

View file

@ -15,6 +15,7 @@ import {
aggregateAllContent, aggregateAllContent,
aggregateGlobalData, aggregateGlobalData,
aggregateRoutes, aggregateRoutes,
formatPluginName,
getPluginByIdentifier, getPluginByIdentifier,
mergeGlobalData, mergeGlobalData,
} from './pluginsUtils'; } from './pluginsUtils';
@ -73,46 +74,57 @@ async function executePluginContentLoading({
plugin: InitializedPlugin; plugin: InitializedPlugin;
context: LoadContext; context: LoadContext;
}): Promise<LoadedPlugin> { }): Promise<LoadedPlugin> {
return PerfLogger.async( return PerfLogger.async(`Load ${formatPluginName(plugin)}`, async () => {
`Plugins - single plugin content loading - ${plugin.name}@${plugin.options.id}`, let content = await PerfLogger.async('loadContent()', () =>
async () => { plugin.loadContent?.(),
let content = await plugin.loadContent?.(); );
content = await translatePluginContent({ content = await PerfLogger.async('translatePluginContent()', () =>
translatePluginContent({
plugin, plugin,
content, content,
context, context,
}); }),
);
if (!plugin.contentLoaded) { const defaultCodeTranslations =
return { (await PerfLogger.async('getDefaultCodeTranslationMessages()', () =>
...plugin, plugin.getDefaultCodeTranslationMessages?.(),
content, )) ?? {};
routes: [],
globalData: undefined,
};
}
const pluginActionsUtils = await createPluginActionsUtils({
plugin,
generatedFilesDir: context.generatedFilesDir,
baseUrl: context.siteConfig.baseUrl,
trailingSlash: context.siteConfig.trailingSlash,
});
await plugin.contentLoaded({
content,
actions: pluginActionsUtils.getActions(),
});
if (!plugin.contentLoaded) {
return { return {
...plugin, ...plugin,
content, content,
routes: pluginActionsUtils.getRoutes(), defaultCodeTranslations,
globalData: pluginActionsUtils.getGlobalData(), routes: [],
globalData: undefined,
}; };
}, }
);
const pluginActionsUtils = await createPluginActionsUtils({
plugin,
generatedFilesDir: context.generatedFilesDir,
baseUrl: context.siteConfig.baseUrl,
trailingSlash: context.siteConfig.trailingSlash,
});
await PerfLogger.async('contentLoaded()', () =>
// @ts-expect-error: should autofix with TS 5.4
plugin.contentLoaded({
content,
actions: pluginActionsUtils.getActions(),
}),
);
return {
...plugin,
content,
defaultCodeTranslations,
routes: pluginActionsUtils.getRoutes(),
globalData: pluginActionsUtils.getGlobalData(),
};
});
} }
async function executeAllPluginsContentLoading({ async function executeAllPluginsContentLoading({
@ -122,7 +134,7 @@ async function executeAllPluginsContentLoading({
plugins: InitializedPlugin[]; plugins: InitializedPlugin[];
context: LoadContext; context: LoadContext;
}): Promise<LoadedPlugin[]> { }): Promise<LoadedPlugin[]> {
return PerfLogger.async(`Plugins - all plugins content loading`, () => { return PerfLogger.async(`Load plugins content`, () => {
return Promise.all( return Promise.all(
plugins.map((plugin) => executePluginContentLoading({plugin, context})), plugins.map((plugin) => executePluginContentLoading({plugin, context})),
); );
@ -139,7 +151,7 @@ async function executePluginAllContentLoaded({
allContent: AllContent; allContent: AllContent;
}): Promise<{routes: RouteConfig[]; globalData: unknown}> { }): Promise<{routes: RouteConfig[]; globalData: unknown}> {
return PerfLogger.async( return PerfLogger.async(
`Plugins - allContentLoaded - ${plugin.name}@${plugin.options.id}`, `allContentLoaded() - ${formatPluginName(plugin)}`,
async () => { async () => {
if (!plugin.allContentLoaded) { if (!plugin.allContentLoaded) {
return {routes: [], globalData: undefined}; return {routes: [], globalData: undefined};
@ -171,7 +183,7 @@ async function executeAllPluginsAllContentLoaded({
plugins: LoadedPlugin[]; plugins: LoadedPlugin[];
context: LoadContext; context: LoadContext;
}): Promise<AllContentLoadedResult> { }): Promise<AllContentLoadedResult> {
return PerfLogger.async(`Plugins - allContentLoaded`, async () => { return PerfLogger.async(`allContentLoaded()`, async () => {
const allContent = aggregateAllContent(plugins); const allContent = aggregateAllContent(plugins);
const routes: RouteConfig[] = []; const routes: RouteConfig[] = [];
@ -199,6 +211,9 @@ async function executeAllPluginsAllContentLoaded({
}); });
} }
// This merges plugins routes and global data created from both lifecycles:
// - contentLoaded()
// - allContentLoaded()
function mergeResults({ function mergeResults({
plugins, plugins,
allContentLoadedResult, allContentLoadedResult,
@ -232,9 +247,9 @@ export type LoadPluginsResult = {
export async function loadPlugins( export async function loadPlugins(
context: LoadContext, context: LoadContext,
): Promise<LoadPluginsResult> { ): Promise<LoadPluginsResult> {
return PerfLogger.async('Plugins - loadPlugins', async () => { return PerfLogger.async('Load plugins', async () => {
const initializedPlugins: InitializedPlugin[] = await PerfLogger.async( const initializedPlugins: InitializedPlugin[] = await PerfLogger.async(
'Plugins - initPlugins', 'Init plugins',
() => initPlugins(context), () => initPlugins(context),
); );
@ -272,36 +287,39 @@ export async function reloadPlugin({
plugins: LoadedPlugin[]; plugins: LoadedPlugin[];
context: LoadContext; context: LoadContext;
}): Promise<LoadPluginsResult> { }): Promise<LoadPluginsResult> {
return PerfLogger.async('Plugins - reloadPlugin', async () => { return PerfLogger.async(
const previousPlugin = getPluginByIdentifier({ `Reload plugin ${formatPluginName(pluginIdentifier)}`,
plugins: previousPlugins, async () => {
pluginIdentifier, const previousPlugin = getPluginByIdentifier({
}); plugins: previousPlugins,
const plugin = await executePluginContentLoading({ pluginIdentifier,
plugin: previousPlugin, });
context, const plugin = await executePluginContentLoading({
}); plugin: previousPlugin,
context,
});
/* /*
// TODO Docusaurus v4 - upgrade to Node 20, use array.with() // TODO Docusaurus v4 - upgrade to Node 20, use array.with()
const plugins = previousPlugins.with( const plugins = previousPlugins.with(
previousPlugins.indexOf(previousPlugin), previousPlugins.indexOf(previousPlugin),
plugin, plugin,
); );
*/ */
const plugins = [...previousPlugins]; const plugins = [...previousPlugins];
plugins[previousPlugins.indexOf(previousPlugin)] = plugin; plugins[previousPlugins.indexOf(previousPlugin)] = plugin;
const allContentLoadedResult = await executeAllPluginsAllContentLoaded({ const allContentLoadedResult = await executeAllPluginsAllContentLoaded({
plugins, plugins,
context, context,
}); });
const {routes, globalData} = mergeResults({ const {routes, globalData} = mergeResults({
plugins, plugins,
allContentLoadedResult, allContentLoadedResult,
}); });
return {plugins, routes, globalData}; return {plugins, routes, globalData};
}); },
);
} }

View file

@ -29,7 +29,9 @@ export function getPluginByIdentifier<P extends InitializedPlugin>({
); );
if (!plugin) { if (!plugin) {
throw new Error( throw new Error(
logger.interpolate`Plugin not found for identifier ${pluginIdentifier.name}@${pluginIdentifier.id}`, logger.interpolate`Plugin not found for identifier ${formatPluginName(
pluginIdentifier,
)}`,
); );
} }
return plugin; return plugin;
@ -85,3 +87,22 @@ export function mergeGlobalData(...globalDataList: GlobalData[]): GlobalData {
return result; return result;
} }
// This is primarily useful for colored logging purpose
// Do not rely on this for logic
export function formatPluginName(
plugin: InitializedPlugin | PluginIdentifier,
): string {
let formattedName = plugin.name;
// Hacky way to reduce string size for logging purpose
formattedName = formattedName.replace('docusaurus-plugin-content-', '');
formattedName = formattedName.replace('docusaurus-plugin-', '');
formattedName = formattedName.replace('docusaurus-theme-', '');
formattedName = formattedName.replace('-plugin', '');
formattedName = logger.name(formattedName);
const id = 'id' in plugin ? plugin.id : plugin.options.id;
const formattedId = logger.subdue(id);
return `${formattedName}@${formattedId}`;
}

View file

@ -13,14 +13,14 @@ import {
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import combinePromises from 'combine-promises'; import combinePromises from 'combine-promises';
import {loadSiteConfig} from './config'; import {loadSiteConfig} from './config';
import {loadClientModules} from './clientModules'; import {getAllClientModules} from './clientModules';
import {loadPlugins, reloadPlugin} from './plugins/plugins'; import {loadPlugins, reloadPlugin} from './plugins/plugins';
import {loadHtmlTags} from './htmlTags'; import {loadHtmlTags} from './htmlTags';
import {loadSiteMetadata} from './siteMetadata'; import {createSiteMetadata, loadSiteVersion} from './siteMetadata';
import {loadI18n} from './i18n'; import {loadI18n} from './i18n';
import { import {
loadSiteCodeTranslations, loadSiteCodeTranslations,
getPluginsDefaultCodeTranslationMessages, getPluginsDefaultCodeTranslations,
} from './translations/translations'; } from './translations/translations';
import {PerfLogger} from '../utils'; import {PerfLogger} from '../utils';
import {generateSiteFiles} from './codegen/codegen'; import {generateSiteFiles} from './codegen/codegen';
@ -76,9 +76,15 @@ export async function loadContext(
} = params; } = params;
const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME); const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({ const {
siteDir, siteVersion,
customConfigFilePath, loadSiteConfig: {siteConfig: initialSiteConfig, siteConfigPath},
} = await combinePromises({
siteVersion: loadSiteVersion(siteDir),
loadSiteConfig: loadSiteConfig({
siteDir,
customConfigFilePath,
}),
}); });
const i18n = await loadI18n(initialSiteConfig, {locale}); const i18n = await loadI18n(initialSiteConfig, {locale});
@ -107,6 +113,7 @@ export async function loadContext(
return { return {
siteDir, siteDir,
siteVersion,
generatedFilesDir, generatedFilesDir,
localizationDir, localizationDir,
siteConfig, siteConfig,
@ -118,13 +125,14 @@ export async function loadContext(
}; };
} }
async function createSiteProps( function createSiteProps(
params: LoadPluginsResult & {context: LoadContext}, params: LoadPluginsResult & {context: LoadContext},
): Promise<Props> { ): Props {
const {plugins, routes, context} = params; const {plugins, routes, context} = params;
const { const {
generatedFilesDir, generatedFilesDir,
siteDir, siteDir,
siteVersion,
siteConfig, siteConfig,
siteConfigPath, siteConfigPath,
outDir, outDir,
@ -136,19 +144,12 @@ async function createSiteProps(
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins); const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
const {codeTranslations, siteMetadata} = await combinePromises({ const siteMetadata = createSiteMetadata({plugins, siteVersion});
// TODO code translations should be loaded as part of LoadedPlugin?
codeTranslations: PerfLogger.async( const codeTranslations = {
'Load - loadCodeTranslations', ...getPluginsDefaultCodeTranslations({plugins}),
async () => ({ ...siteCodeTranslations,
...(await getPluginsDefaultCodeTranslationMessages(plugins)), };
...siteCodeTranslations,
}),
),
siteMetadata: PerfLogger.async('Load - loadSiteMetadata', () =>
loadSiteMetadata({plugins, siteDir}),
),
});
handleDuplicateRoutes(routes, siteConfig.onDuplicateRoutes); handleDuplicateRoutes(routes, siteConfig.onDuplicateRoutes);
const routesPaths = getRoutesPaths(routes, baseUrl); const routesPaths = getRoutesPaths(routes, baseUrl);
@ -157,6 +158,7 @@ async function createSiteProps(
siteConfig, siteConfig,
siteConfigPath, siteConfigPath,
siteMetadata, siteMetadata,
siteVersion,
siteDir, siteDir,
outDir, outDir,
baseUrl, baseUrl,
@ -181,7 +183,7 @@ async function createSiteFiles({
site: Site; site: Site;
globalData: GlobalData; globalData: GlobalData;
}) { }) {
return PerfLogger.async('Load - createSiteFiles', async () => { return PerfLogger.async('Create site files', async () => {
const { const {
props: { props: {
plugins, plugins,
@ -194,7 +196,7 @@ async function createSiteFiles({
baseUrl, baseUrl,
}, },
} = site; } = site;
const clientModules = loadClientModules(plugins); const clientModules = getAllClientModules(plugins);
await generateSiteFiles({ await generateSiteFiles({
generatedFilesDir, generatedFilesDir,
clientModules, clientModules,
@ -216,13 +218,11 @@ async function createSiteFiles({
* it generates temp files in the `.docusaurus` folder for the bundler. * it generates temp files in the `.docusaurus` folder for the bundler.
*/ */
export async function loadSite(params: LoadContextParams): Promise<Site> { export async function loadSite(params: LoadContextParams): Promise<Site> {
PerfLogger.start('Load - loadContext'); const context = await PerfLogger.async('Load context', () =>
const context = await loadContext(params); loadContext(params),
PerfLogger.end('Load - loadContext'); );
PerfLogger.start('Load - loadPlugins');
const {plugins, routes, globalData} = await loadPlugins(context); const {plugins, routes, globalData} = await loadPlugins(context);
PerfLogger.end('Load - loadPlugins');
const props = await createSiteProps({plugins, routes, globalData, context}); const props = await createSiteProps({plugins, routes, globalData, context});

View file

@ -14,7 +14,7 @@ import type {
SiteMetadata, SiteMetadata,
} from '@docusaurus/types'; } from '@docusaurus/types';
async function getPackageJsonVersion( async function loadPackageJsonVersion(
packageJsonPath: string, packageJsonPath: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
if (await fs.pathExists(packageJsonPath)) { if (await fs.pathExists(packageJsonPath)) {
@ -24,14 +24,20 @@ async function getPackageJsonVersion(
return undefined; return undefined;
} }
async function getPackageJsonName( async function loadPackageJsonName(
packageJsonPath: string, packageJsonPath: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require
return (require(packageJsonPath) as {name?: string}).name; return (require(packageJsonPath) as {name?: string}).name;
} }
export async function getPluginVersion( export async function loadSiteVersion(
siteDir: string,
): Promise<string | undefined> {
return loadPackageJsonVersion(path.join(siteDir, 'package.json'));
}
export async function loadPluginVersion(
pluginPath: string, pluginPath: string,
siteDir: string, siteDir: string,
): Promise<PluginVersionInformation> { ): Promise<PluginVersionInformation> {
@ -52,8 +58,8 @@ export async function getPluginVersion(
} }
return { return {
type: 'package', type: 'package',
name: await getPackageJsonName(packageJsonPath), name: await loadPackageJsonName(packageJsonPath),
version: await getPackageJsonVersion(packageJsonPath), version: await loadPackageJsonVersion(packageJsonPath),
}; };
} }
potentialPluginPackageJsonDirectory = path.dirname( potentialPluginPackageJsonDirectory = path.dirname(
@ -89,18 +95,16 @@ Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?
); );
} }
export async function loadSiteMetadata({ export function createSiteMetadata({
siteVersion,
plugins, plugins,
siteDir,
}: { }: {
siteVersion: string | undefined;
plugins: LoadedPlugin[]; plugins: LoadedPlugin[];
siteDir: string; }): SiteMetadata {
}): Promise<SiteMetadata> {
const siteMetadata: SiteMetadata = { const siteMetadata: SiteMetadata = {
docusaurusVersion: DOCUSAURUS_VERSION, docusaurusVersion: DOCUSAURUS_VERSION,
siteVersion: await getPackageJsonVersion( siteVersion,
path.join(siteDir, 'package.json'),
),
pluginVersions: Object.fromEntries( pluginVersions: Object.fromEntries(
plugins plugins
.filter(({version: {type}}) => type !== 'synthetic') .filter(({version: {type}}) => type !== 'synthetic')

View file

@ -15,7 +15,7 @@ import {
readCodeTranslationFileContent, readCodeTranslationFileContent,
type WriteTranslationsOptions, type WriteTranslationsOptions,
localizePluginTranslationFile, localizePluginTranslationFile,
getPluginsDefaultCodeTranslationMessages, loadPluginsDefaultCodeTranslationMessages,
applyDefaultCodeTranslations, applyDefaultCodeTranslations,
} from '../translations'; } from '../translations';
import type { import type {
@ -537,7 +537,7 @@ describe('readCodeTranslationFileContent', () => {
}); });
}); });
describe('getPluginsDefaultCodeTranslationMessages', () => { describe('loadPluginsDefaultCodeTranslationMessages', () => {
function createTestPlugin( function createTestPlugin(
fn: InitializedPlugin['getDefaultCodeTranslationMessages'], fn: InitializedPlugin['getDefaultCodeTranslationMessages'],
): InitializedPlugin { ): InitializedPlugin {
@ -547,14 +547,14 @@ describe('getPluginsDefaultCodeTranslationMessages', () => {
it('works for empty plugins', async () => { it('works for empty plugins', async () => {
const plugins: InitializedPlugin[] = []; const plugins: InitializedPlugin[] = [];
await expect( await expect(
getPluginsDefaultCodeTranslationMessages(plugins), loadPluginsDefaultCodeTranslationMessages(plugins),
).resolves.toEqual({}); ).resolves.toEqual({});
}); });
it('works for 1 plugin without lifecycle', async () => { it('works for 1 plugin without lifecycle', async () => {
const plugins: InitializedPlugin[] = [createTestPlugin(undefined)]; const plugins: InitializedPlugin[] = [createTestPlugin(undefined)];
await expect( await expect(
getPluginsDefaultCodeTranslationMessages(plugins), loadPluginsDefaultCodeTranslationMessages(plugins),
).resolves.toEqual({}); ).resolves.toEqual({});
}); });
@ -566,7 +566,7 @@ describe('getPluginsDefaultCodeTranslationMessages', () => {
})), })),
]; ];
await expect( await expect(
getPluginsDefaultCodeTranslationMessages(plugins), loadPluginsDefaultCodeTranslationMessages(plugins),
).resolves.toEqual({ ).resolves.toEqual({
a: '1', a: '1',
b: '2', b: '2',
@ -585,7 +585,7 @@ describe('getPluginsDefaultCodeTranslationMessages', () => {
})), })),
]; ];
await expect( await expect(
getPluginsDefaultCodeTranslationMessages(plugins), loadPluginsDefaultCodeTranslationMessages(plugins),
).resolves.toEqual({ ).resolves.toEqual({
a: '1', a: '1',
b: '2', b: '2',
@ -613,7 +613,7 @@ describe('getPluginsDefaultCodeTranslationMessages', () => {
createTestPlugin(undefined), createTestPlugin(undefined),
]; ];
await expect( await expect(
getPluginsDefaultCodeTranslationMessages(plugins), loadPluginsDefaultCodeTranslationMessages(plugins),
).resolves.toEqual({ ).resolves.toEqual({
// merge, last plugin wins // merge, last plugin wins
b: '2', b: '2',

View file

@ -20,6 +20,7 @@ import type {
TranslationFile, TranslationFile,
CodeTranslations, CodeTranslations,
InitializedPlugin, InitializedPlugin,
LoadedPlugin,
} from '@docusaurus/types'; } from '@docusaurus/types';
export type WriteTranslationsOptions = { export type WriteTranslationsOptions = {
@ -242,17 +243,33 @@ export async function localizePluginTranslationFile({
return translationFile; return translationFile;
} }
export async function getPluginsDefaultCodeTranslationMessages( export function mergeCodeTranslations(
codeTranslations: CodeTranslations[],
): CodeTranslations {
return codeTranslations.reduce(
(allCodeTranslations, current) => ({
...allCodeTranslations,
...current,
}),
{},
);
}
export async function loadPluginsDefaultCodeTranslationMessages(
plugins: InitializedPlugin[], plugins: InitializedPlugin[],
): Promise<CodeTranslations> { ): Promise<CodeTranslations> {
const pluginsMessages = await Promise.all( const pluginsMessages = await Promise.all(
plugins.map((plugin) => plugin.getDefaultCodeTranslationMessages?.() ?? {}), plugins.map((plugin) => plugin.getDefaultCodeTranslationMessages?.() ?? {}),
); );
return mergeCodeTranslations(pluginsMessages);
}
return pluginsMessages.reduce( export function getPluginsDefaultCodeTranslations({
(allMessages, pluginMessages) => ({...allMessages, ...pluginMessages}), plugins,
{}, }: {
); plugins: LoadedPlugin[];
}): CodeTranslations {
return mergeCodeTranslations(plugins.map((p) => p.defaultCodeTranslations));
} }
export function applyDefaultCodeTranslations({ export function applyDefaultCodeTranslations({

View file

@ -47,12 +47,11 @@ export async function loadAppRenderer({
}: { }: {
serverBundlePath: string; serverBundlePath: string;
}): Promise<AppRenderer> { }): Promise<AppRenderer> {
console.log(`SSG - Load server bundle`); const source = await PerfLogger.async(`Load server bundle`, () =>
PerfLogger.start(`SSG - Load server bundle`); fs.readFile(serverBundlePath),
const source = await fs.readFile(serverBundlePath); );
PerfLogger.end(`SSG - Load server bundle`);
PerfLogger.log( PerfLogger.log(
`SSG - Server bundle size = ${(source.length / 1024000).toFixed(3)} MB`, `Server bundle size = ${(source.length / 1024000).toFixed(3)} MB`,
); );
const filename = path.basename(serverBundlePath); const filename = path.basename(serverBundlePath);
@ -69,14 +68,16 @@ export async function loadAppRenderer({
require: createRequire(serverBundlePath), require: createRequire(serverBundlePath),
}; };
PerfLogger.start(`SSG - Evaluate server bundle`); const serverEntry = await PerfLogger.async(
const serverEntry = evaluate( `Evaluate server bundle`,
source, () =>
/* filename: */ filename, evaluate(
/* scope: */ globals, source,
/* includeGlobals: */ true, /* filename: */ filename,
) as {default?: AppRenderer}; /* scope: */ globals,
PerfLogger.end(`SSG - Evaluate server bundle`); /* includeGlobals: */ true,
) as {default?: AppRenderer},
);
if (!serverEntry?.default || typeof serverEntry.default !== 'function') { if (!serverEntry?.default || typeof serverEntry.default !== 'function') {
throw new Error( throw new Error(

View file

@ -4,6 +4,7 @@
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {AsyncLocalStorage} from 'async_hooks';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
// For now this is a private env variable we use internally // For now this is a private env variable we use internally
@ -17,6 +18,16 @@ const Thresholds = {
red: 1000, red: 1000,
}; };
const PerfPrefix = logger.yellow(`[PERF] `);
// This is what enables to "see the parent stack" for each log
// Parent1 > Parent2 > Parent3 > child trace
const ParentPrefix = new AsyncLocalStorage<string>();
function applyParentPrefix(label: string) {
const parentPrefix = ParentPrefix.getStore();
return parentPrefix ? `${parentPrefix} > ${label}` : label;
}
type PerfLoggerAPI = { type PerfLoggerAPI = {
start: (label: string) => void; start: (label: string) => void;
end: (label: string) => void; end: (label: string) => void;
@ -38,8 +49,6 @@ function createPerfLogger(): PerfLoggerAPI {
}; };
} }
const prefix = logger.yellow(`[PERF] `);
const formatDuration = (duration: number): string => { const formatDuration = (duration: number): string => {
if (duration > Thresholds.red) { if (duration > Thresholds.red) {
return logger.red(`${(duration / 1000).toFixed(2)} seconds!`); return logger.red(`${(duration / 1000).toFixed(2)} seconds!`);
@ -54,7 +63,7 @@ function createPerfLogger(): PerfLoggerAPI {
if (duration < Thresholds.min) { if (duration < Thresholds.min) {
return; return;
} }
console.log(`${prefix + label} - ${formatDuration(duration)}`); console.log(`${PerfPrefix + label} - ${formatDuration(duration)}`);
}; };
const start: PerfLoggerAPI['start'] = (label) => performance.mark(label); const start: PerfLoggerAPI['start'] = (label) => performance.mark(label);
@ -62,18 +71,18 @@ function createPerfLogger(): PerfLoggerAPI {
const end: PerfLoggerAPI['end'] = (label) => { const end: PerfLoggerAPI['end'] = (label) => {
const {duration} = performance.measure(label); const {duration} = performance.measure(label);
performance.clearMarks(label); performance.clearMarks(label);
logDuration(label, duration); logDuration(applyParentPrefix(label), duration);
}; };
const log: PerfLoggerAPI['log'] = (label: string) => const log: PerfLoggerAPI['log'] = (label: string) =>
console.log(prefix + label); console.log(PerfPrefix + applyParentPrefix(label));
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => { const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
start(label); const finalLabel = applyParentPrefix(label);
const before = performance.now(); const before = performance.now();
const result = await asyncFn(); const result = await ParentPrefix.run(finalLabel, () => asyncFn());
const duration = performance.now() - before; const duration = performance.now() - before;
logDuration(label, duration); logDuration(finalLabel, duration);
return result; return result;
}; };

View file

@ -16,6 +16,7 @@ architecting
Astro Astro
atrule atrule
Autoconverted Autoconverted
autofix
Autogen Autogen
autogen autogen
autogenerating autogenerating