Skip to content

Commit efbe474

Browse files
authored
refactor(core): improve dev perf, fine-grained site reloads - part 3 (#9975)
1 parent 06e70a4 commit efbe474

File tree

22 files changed

+359
-286
lines changed

22 files changed

+359
-286
lines changed

packages/docusaurus-types/src/context.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};
3131

3232
export type LoadContext = {
3333
siteDir: string;
34+
siteVersion: string | undefined;
3435
generatedFilesDir: string;
3536
siteConfig: DocusaurusConfig;
3637
siteConfigPath: string;

packages/docusaurus-types/src/plugin.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import type {TranslationFile} from './i18n';
8+
import type {CodeTranslations, TranslationFile} from './i18n';
99
import type {RuleSetRule, Configuration as WebpackConfiguration} from 'webpack';
1010
import type {CustomizeRuleString} from 'webpack-merge/dist/types';
1111
import type {CommanderStatic} from 'commander';
@@ -185,6 +185,7 @@ export type LoadedPlugin = InitializedPlugin & {
185185
readonly content: unknown;
186186
readonly globalData: unknown;
187187
readonly routes: RouteConfig[];
188+
readonly defaultCodeTranslations: CodeTranslations;
188189
};
189190

190191
export type PluginModule<Content = unknown> = {

packages/docusaurus-utils/src/emitUtils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {findAsyncSequential} from './jsUtils';
1212

1313
const fileHash = new Map<string, string>();
1414

15+
const hashContent = (content: string): string => {
16+
return createHash('md5').update(content).digest('hex');
17+
};
18+
1519
/**
1620
* Outputs a file to the generated files directory. Only writes files if content
1721
* differs from cache (for hot reload performance).
@@ -38,7 +42,7 @@ export async function generate(
3842
// first "A" remains in cache. But if the file never existed in cache, no
3943
// need to register it.
4044
if (fileHash.get(filepath)) {
41-
fileHash.set(filepath, createHash('md5').update(content).digest('hex'));
45+
fileHash.set(filepath, hashContent(content));
4246
}
4347
return;
4448
}
@@ -50,11 +54,11 @@ export async function generate(
5054
// overwriting and we can reuse old file.
5155
if (!lastHash && (await fs.pathExists(filepath))) {
5256
const lastContent = await fs.readFile(filepath, 'utf8');
53-
lastHash = createHash('md5').update(lastContent).digest('hex');
57+
lastHash = hashContent(lastContent);
5458
fileHash.set(filepath, lastHash);
5559
}
5660

57-
const currentHash = createHash('md5').update(content).digest('hex');
61+
const currentHash = hashContent(content);
5862

5963
if (lastHash !== currentHash) {
6064
await fs.outputFile(filepath, content);

packages/docusaurus-utils/src/gitUtils.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@
66
*/
77

88
import path from 'path';
9-
import shell from 'shelljs';
9+
import fs from 'fs-extra';
10+
import _ from 'lodash';
11+
import shell from 'shelljs'; // TODO replace with async-first version
12+
13+
const realHasGitFn = () => !!shell.which('git');
14+
15+
// The hasGit call is synchronous IO so we memoize it
16+
// The user won't install Git in the middle of a build anyway...
17+
const hasGit =
18+
process.env.NODE_ENV === 'test' ? realHasGitFn : _.memoize(realHasGitFn);
1019

1120
/** Custom error thrown when git is not found in `PATH`. */
1221
export class GitNotFoundError extends Error {}
@@ -86,13 +95,13 @@ export async function getFileCommitDate(
8695
timestamp: number;
8796
author?: string;
8897
}> {
89-
if (!shell.which('git')) {
98+
if (!hasGit()) {
9099
throw new GitNotFoundError(
91100
`Failed to retrieve git history for "${file}" because git is not installed.`,
92101
);
93102
}
94103

95-
if (!shell.test('-f', file)) {
104+
if (!(await fs.pathExists(file))) {
96105
throw new Error(
97106
`Failed to retrieve git history for "${file}" because the file does not exist.`,
98107
);

packages/docusaurus/src/commands/build.ts

Lines changed: 94 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,15 @@ export async function build(
6464
process.on(sig, () => process.exit());
6565
});
6666

67-
async function tryToBuildLocale({
68-
locale,
69-
isLastLocale,
70-
}: {
71-
locale: string;
72-
isLastLocale: boolean;
73-
}) {
67+
async function tryToBuildLocale({locale}: {locale: string}) {
7468
try {
75-
PerfLogger.start(`Building site for locale ${locale}`);
76-
await buildLocale({
77-
siteDir,
78-
locale,
79-
cliOptions,
80-
forceTerminate,
81-
isLastLocale,
82-
});
83-
PerfLogger.end(`Building site for locale ${locale}`);
69+
await PerfLogger.async(`${logger.name(locale)}`, () =>
70+
buildLocale({
71+
siteDir,
72+
locale,
73+
cliOptions,
74+
}),
75+
);
8476
} catch (err) {
8577
throw new Error(
8678
logger.interpolate`Unable to build website for locale name=${locale}.`,
@@ -91,20 +83,28 @@ export async function build(
9183
}
9284
}
9385

94-
PerfLogger.start(`Get locales to build`);
95-
const locales = await getLocalesToBuild({siteDir, cliOptions});
96-
PerfLogger.end(`Get locales to build`);
86+
const locales = await PerfLogger.async('Get locales to build', () =>
87+
getLocalesToBuild({siteDir, cliOptions}),
88+
);
9789

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

102-
PerfLogger.start(`Building ${locales.length} locales`);
103-
await mapAsyncSequential(locales, (locale) => {
104-
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
105-
return tryToBuildLocale({locale, isLastLocale});
106-
});
107-
PerfLogger.end(`Building ${locales.length} locales`);
94+
await PerfLogger.async(`Build`, () =>
95+
mapAsyncSequential(locales, async (locale) => {
96+
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
97+
await tryToBuildLocale({locale});
98+
if (isLastLocale) {
99+
logger.info`Use code=${'npm run serve'} command to test your build locally.`;
100+
}
101+
102+
// TODO do we really need this historical forceTerminate exit???
103+
if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) {
104+
process.exit(0);
105+
}
106+
}),
107+
);
108108
}
109109

110110
async function getLocalesToBuild({
@@ -144,14 +144,10 @@ async function buildLocale({
144144
siteDir,
145145
locale,
146146
cliOptions,
147-
forceTerminate,
148-
isLastLocale,
149147
}: {
150148
siteDir: string;
151149
locale: string;
152150
cliOptions: Partial<BuildCLIOptions>;
153-
forceTerminate: boolean;
154-
isLastLocale: boolean;
155151
}): Promise<string> {
156152
// Temporary workaround to unlock the ability to translate the site config
157153
// We'll remove it if a better official API can be designed
@@ -160,81 +156,66 @@ async function buildLocale({
160156

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

163-
PerfLogger.start('Loading site');
164-
const site = await loadSite({
165-
siteDir,
166-
outDir: cliOptions.outDir,
167-
config: cliOptions.config,
168-
locale,
169-
localizePath: cliOptions.locale ? false : undefined,
170-
});
171-
PerfLogger.end('Loading site');
159+
const site = await PerfLogger.async('Load site', () =>
160+
loadSite({
161+
siteDir,
162+
outDir: cliOptions.outDir,
163+
config: cliOptions.config,
164+
locale,
165+
localizePath: cliOptions.locale ? false : undefined,
166+
}),
167+
);
172168

173169
const {props} = site;
174170
const {outDir, plugins} = props;
175171

176172
// We can build the 2 configs in parallel
177-
PerfLogger.start('Creating webpack configs');
178173
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
179-
await Promise.all([
180-
getBuildClientConfig({
181-
props,
182-
cliOptions,
183-
}),
184-
getBuildServerConfig({
185-
props,
186-
}),
187-
]);
188-
PerfLogger.end('Creating webpack configs');
189-
190-
// Make sure generated client-manifest is cleaned first, so we don't reuse
191-
// the one from previous builds.
192-
// TODO do we really need this? .docusaurus folder is cleaned between builds
193-
PerfLogger.start('Deleting previous client manifest');
194-
await ensureUnlink(clientManifestPath);
195-
PerfLogger.end('Deleting previous client manifest');
174+
await PerfLogger.async('Creating webpack configs', () =>
175+
Promise.all([
176+
getBuildClientConfig({
177+
props,
178+
cliOptions,
179+
}),
180+
getBuildServerConfig({
181+
props,
182+
}),
183+
]),
184+
);
196185

197186
// Run webpack to build JS bundle (client) and static html files (server).
198-
PerfLogger.start('Bundling');
199-
await compile([clientConfig, serverConfig]);
200-
PerfLogger.end('Bundling');
187+
await PerfLogger.async('Bundling with Webpack', () =>
188+
compile([clientConfig, serverConfig]),
189+
);
201190

202-
PerfLogger.start('Executing static site generation');
203-
const {collectedData} = await executeSSG({
204-
props,
205-
serverBundlePath,
206-
clientManifestPath,
207-
});
208-
PerfLogger.end('Executing static site generation');
191+
const {collectedData} = await PerfLogger.async('SSG', () =>
192+
executeSSG({
193+
props,
194+
serverBundlePath,
195+
clientManifestPath,
196+
}),
197+
);
209198

210199
// Remove server.bundle.js because it is not needed.
211-
PerfLogger.start('Deleting server bundle');
212-
await ensureUnlink(serverBundlePath);
213-
PerfLogger.end('Deleting server bundle');
200+
await PerfLogger.async('Deleting server bundle', () =>
201+
ensureUnlink(serverBundlePath),
202+
);
214203

215204
// Plugin Lifecycle - postBuild.
216-
PerfLogger.start('Executing postBuild()');
217-
await executePluginsPostBuild({plugins, props, collectedData});
218-
PerfLogger.end('Executing postBuild()');
205+
await PerfLogger.async('postBuild()', () =>
206+
executePluginsPostBuild({plugins, props, collectedData}),
207+
);
219208

220209
// TODO execute this in parallel to postBuild?
221-
PerfLogger.start('Executing broken links checker');
222-
await executeBrokenLinksCheck({props, collectedData});
223-
PerfLogger.end('Executing broken links checker');
210+
await PerfLogger.async('Broken links checker', () =>
211+
executeBrokenLinksCheck({props, collectedData}),
212+
);
224213

225214
logger.success`Generated static files in path=${path.relative(
226215
process.cwd(),
227216
outDir,
228217
)}.`;
229218

230-
if (isLastLocale) {
231-
logger.info`Use code=${'npm run serve'} command to test your build locally.`;
232-
}
233-
234-
if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) {
235-
process.exit(0);
236-
}
237-
238219
return outDir;
239220
}
240221

@@ -247,40 +228,39 @@ async function executeSSG({
247228
serverBundlePath: string;
248229
clientManifestPath: string;
249230
}) {
250-
PerfLogger.start('Reading client manifest');
251-
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
252-
PerfLogger.end('Reading client manifest');
231+
const manifest: Manifest = await PerfLogger.async(
232+
'Read client manifest',
233+
() => fs.readJSON(clientManifestPath, 'utf-8'),
234+
);
253235

254-
PerfLogger.start('Compiling SSR template');
255-
const ssrTemplate = await compileSSRTemplate(
256-
props.siteConfig.ssrTemplate ?? defaultSSRTemplate,
236+
const ssrTemplate = await PerfLogger.async('Compile SSR template', () =>
237+
compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
257238
);
258-
PerfLogger.end('Compiling SSR template');
259239

260-
PerfLogger.start('Loading App renderer');
261-
const renderer = await loadAppRenderer({
262-
serverBundlePath,
263-
});
264-
PerfLogger.end('Loading App renderer');
265-
266-
PerfLogger.start('Generate static files');
267-
const ssgResult = await generateStaticFiles({
268-
pathnames: props.routesPaths,
269-
renderer,
270-
params: {
271-
trailingSlash: props.siteConfig.trailingSlash,
272-
outDir: props.outDir,
273-
baseUrl: props.baseUrl,
274-
manifest,
275-
headTags: props.headTags,
276-
preBodyTags: props.preBodyTags,
277-
postBodyTags: props.postBodyTags,
278-
ssrTemplate,
279-
noIndex: props.siteConfig.noIndex,
280-
DOCUSAURUS_VERSION,
281-
},
282-
});
283-
PerfLogger.end('Generate static files');
240+
const renderer = await PerfLogger.async('Load App renderer', () =>
241+
loadAppRenderer({
242+
serverBundlePath,
243+
}),
244+
);
245+
246+
const ssgResult = await PerfLogger.async('Generate static files', () =>
247+
generateStaticFiles({
248+
pathnames: props.routesPaths,
249+
renderer,
250+
params: {
251+
trailingSlash: props.siteConfig.trailingSlash,
252+
outDir: props.outDir,
253+
baseUrl: props.baseUrl,
254+
manifest,
255+
headTags: props.headTags,
256+
preBodyTags: props.preBodyTags,
257+
postBodyTags: props.postBodyTags,
258+
ssrTemplate,
259+
noIndex: props.siteConfig.noIndex,
260+
DOCUSAURUS_VERSION,
261+
},
262+
}),
263+
);
284264

285265
return ssgResult;
286266
}

0 commit comments

Comments
 (0)