Skip to content

Commit

Permalink
refactor(core): improve dev perf, fine-grained site reloads - part 3 (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber authored Mar 28, 2024
1 parent 06e70a4 commit efbe474
Show file tree
Hide file tree
Showing 22 changed files with 359 additions and 286 deletions.
1 change: 1 addition & 0 deletions packages/docusaurus-types/src/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};

export type LoadContext = {
siteDir: string;
siteVersion: string | undefined;
generatedFilesDir: string;
siteConfig: DocusaurusConfig;
siteConfigPath: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus-types/src/plugin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 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 {CustomizeRuleString} from 'webpack-merge/dist/types';
import type {CommanderStatic} from 'commander';
Expand Down Expand Up @@ -185,6 +185,7 @@ export type LoadedPlugin = InitializedPlugin & {
readonly content: unknown;
readonly globalData: unknown;
readonly routes: RouteConfig[];
readonly defaultCodeTranslations: CodeTranslations;
};

export type PluginModule<Content = unknown> = {
Expand Down
10 changes: 7 additions & 3 deletions packages/docusaurus-utils/src/emitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {findAsyncSequential} from './jsUtils';

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
* differs from cache (for hot reload performance).
Expand All @@ -38,7 +42,7 @@ export async function generate(
// first "A" remains in cache. But if the file never existed in cache, no
// need to register it.
if (fileHash.get(filepath)) {
fileHash.set(filepath, createHash('md5').update(content).digest('hex'));
fileHash.set(filepath, hashContent(content));
}
return;
}
Expand All @@ -50,11 +54,11 @@ export async function generate(
// overwriting and we can reuse old file.
if (!lastHash && (await fs.pathExists(filepath))) {
const lastContent = await fs.readFile(filepath, 'utf8');
lastHash = createHash('md5').update(lastContent).digest('hex');
lastHash = hashContent(lastContent);
fileHash.set(filepath, lastHash);
}

const currentHash = createHash('md5').update(content).digest('hex');
const currentHash = hashContent(content);

if (lastHash !== currentHash) {
await fs.outputFile(filepath, content);
Expand Down
15 changes: 12 additions & 3 deletions packages/docusaurus-utils/src/gitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@
*/

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`. */
export class GitNotFoundError extends Error {}
Expand Down Expand Up @@ -86,13 +95,13 @@ export async function getFileCommitDate(
timestamp: number;
author?: string;
}> {
if (!shell.which('git')) {
if (!hasGit()) {
throw new GitNotFoundError(
`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(
`Failed to retrieve git history for "${file}" because the file does not exist.`,
);
Expand Down
208 changes: 94 additions & 114 deletions packages/docusaurus/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,15 @@ export async function build(
process.on(sig, () => process.exit());
});

async function tryToBuildLocale({
locale,
isLastLocale,
}: {
locale: string;
isLastLocale: boolean;
}) {
async function tryToBuildLocale({locale}: {locale: string}) {
try {
PerfLogger.start(`Building site for locale ${locale}`);
await buildLocale({
siteDir,
locale,
cliOptions,
forceTerminate,
isLastLocale,
});
PerfLogger.end(`Building site for locale ${locale}`);
await PerfLogger.async(`${logger.name(locale)}`, () =>
buildLocale({
siteDir,
locale,
cliOptions,
}),
);
} catch (err) {
throw new Error(
logger.interpolate`Unable to build website for locale name=${locale}.`,
Expand All @@ -91,20 +83,28 @@ export async function build(
}
}

PerfLogger.start(`Get locales to build`);
const locales = await getLocalesToBuild({siteDir, cliOptions});
PerfLogger.end(`Get locales to build`);
const locales = await PerfLogger.async('Get locales to build', () =>
getLocalesToBuild({siteDir, cliOptions}),
);

if (locales.length > 1) {
logger.info`Website will be built for all these locales: ${locales}`;
}

PerfLogger.start(`Building ${locales.length} locales`);
await mapAsyncSequential(locales, (locale) => {
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
return tryToBuildLocale({locale, isLastLocale});
});
PerfLogger.end(`Building ${locales.length} locales`);
await PerfLogger.async(`Build`, () =>
mapAsyncSequential(locales, async (locale) => {
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
await tryToBuildLocale({locale});
if (isLastLocale) {
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({
Expand Down Expand Up @@ -144,14 +144,10 @@ async function buildLocale({
siteDir,
locale,
cliOptions,
forceTerminate,
isLastLocale,
}: {
siteDir: string;
locale: string;
cliOptions: Partial<BuildCLIOptions>;
forceTerminate: boolean;
isLastLocale: boolean;
}): Promise<string> {
// Temporary workaround to unlock the ability to translate the site config
// We'll remove it if a better official API can be designed
Expand All @@ -160,81 +156,66 @@ async function buildLocale({

logger.info`name=${`[${locale}]`} Creating an optimized production build...`;

PerfLogger.start('Loading site');
const site = await loadSite({
siteDir,
outDir: cliOptions.outDir,
config: cliOptions.config,
locale,
localizePath: cliOptions.locale ? false : undefined,
});
PerfLogger.end('Loading site');
const site = await PerfLogger.async('Load site', () =>
loadSite({
siteDir,
outDir: cliOptions.outDir,
config: cliOptions.config,
locale,
localizePath: cliOptions.locale ? false : undefined,
}),
);

const {props} = site;
const {outDir, plugins} = props;

// We can build the 2 configs in parallel
PerfLogger.start('Creating webpack configs');
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
await Promise.all([
getBuildClientConfig({
props,
cliOptions,
}),
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');
await PerfLogger.async('Creating webpack configs', () =>
Promise.all([
getBuildClientConfig({
props,
cliOptions,
}),
getBuildServerConfig({
props,
}),
]),
);

// Run webpack to build JS bundle (client) and static html files (server).
PerfLogger.start('Bundling');
await compile([clientConfig, serverConfig]);
PerfLogger.end('Bundling');
await PerfLogger.async('Bundling with Webpack', () =>
compile([clientConfig, serverConfig]),
);

PerfLogger.start('Executing static site generation');
const {collectedData} = await executeSSG({
props,
serverBundlePath,
clientManifestPath,
});
PerfLogger.end('Executing static site generation');
const {collectedData} = await PerfLogger.async('SSG', () =>
executeSSG({
props,
serverBundlePath,
clientManifestPath,
}),
);

// Remove server.bundle.js because it is not needed.
PerfLogger.start('Deleting server bundle');
await ensureUnlink(serverBundlePath);
PerfLogger.end('Deleting server bundle');
await PerfLogger.async('Deleting server bundle', () =>
ensureUnlink(serverBundlePath),
);

// Plugin Lifecycle - postBuild.
PerfLogger.start('Executing postBuild()');
await executePluginsPostBuild({plugins, props, collectedData});
PerfLogger.end('Executing postBuild()');
await PerfLogger.async('postBuild()', () =>
executePluginsPostBuild({plugins, props, collectedData}),
);

// TODO execute this in parallel to postBuild?
PerfLogger.start('Executing broken links checker');
await executeBrokenLinksCheck({props, collectedData});
PerfLogger.end('Executing broken links checker');
await PerfLogger.async('Broken links checker', () =>
executeBrokenLinksCheck({props, collectedData}),
);

logger.success`Generated static files in path=${path.relative(
process.cwd(),
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;
}

Expand All @@ -247,40 +228,39 @@ async function executeSSG({
serverBundlePath: string;
clientManifestPath: string;
}) {
PerfLogger.start('Reading client manifest');
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
PerfLogger.end('Reading client manifest');
const manifest: Manifest = await PerfLogger.async(
'Read client manifest',
() => fs.readJSON(clientManifestPath, 'utf-8'),
);

PerfLogger.start('Compiling SSR template');
const ssrTemplate = await compileSSRTemplate(
props.siteConfig.ssrTemplate ?? defaultSSRTemplate,
const ssrTemplate = await PerfLogger.async('Compile SSR template', () =>
compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
);
PerfLogger.end('Compiling SSR template');

PerfLogger.start('Loading App renderer');
const renderer = await loadAppRenderer({
serverBundlePath,
});
PerfLogger.end('Loading App renderer');

PerfLogger.start('Generate static files');
const ssgResult = await generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params: {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
},
});
PerfLogger.end('Generate static files');
const renderer = await PerfLogger.async('Load App renderer', () =>
loadAppRenderer({
serverBundlePath,
}),
);

const ssgResult = await PerfLogger.async('Generate static files', () =>
generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params: {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
},
}),
);

return ssgResult;
}
Expand Down
Loading

0 comments on commit efbe474

Please sign in to comment.