Skip to content

Commit c9f231a

Browse files
authored
refactor(core): refactor SSG infrastructure (facebook#10593)
1 parent 14579cb commit c9f231a

16 files changed

Lines changed: 356 additions & 309 deletions

File tree

packages/docusaurus-bundler/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export {
1515
} from './currentBundler';
1616

1717
export {getMinimizers} from './minification';
18-
export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml';
18+
export {
19+
getHtmlMinifier,
20+
type HtmlMinifier,
21+
type HtmlMinifierType,
22+
} from './minifyHtml';
1923
export {createJsLoaderFactory} from './loaders/jsLoader';
2024
export {createStyleLoadersFactory} from './loaders/styleLoader';

packages/docusaurus-bundler/src/minifyHtml.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77

88
import {minify as terserHtmlMinifier} from 'html-minifier-terser';
99
import {importSwcHtmlMinifier} from './importFaster';
10-
import type {DocusaurusConfig} from '@docusaurus/types';
1110

1211
// Historical env variable
1312
const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true';
1413

14+
export type HtmlMinifierType = 'swc' | 'terser';
15+
1516
export type HtmlMinifierResult = {
1617
code: string;
1718
warnings: string[];
@@ -25,24 +26,15 @@ const NoopMinifier: HtmlMinifier = {
2526
minify: async (html: string) => ({code: html, warnings: []}),
2627
};
2728

28-
type SiteConfigSlice = {
29-
future: {
30-
experimental_faster: Pick<
31-
DocusaurusConfig['future']['experimental_faster'],
32-
'swcHtmlMinimizer'
33-
>;
34-
};
35-
};
36-
3729
export async function getHtmlMinifier({
38-
siteConfig,
30+
type,
3931
}: {
40-
siteConfig: SiteConfigSlice;
32+
type: HtmlMinifierType;
4133
}): Promise<HtmlMinifier> {
4234
if (SkipHtmlMinification) {
4335
return NoopMinifier;
4436
}
45-
if (siteConfig.future.experimental_faster.swcHtmlMinimizer) {
37+
if (type === 'swc') {
4638
return getSwcMinifier();
4739
} else {
4840
return getTerserMinifier();

packages/docusaurus-logger/src/perfLogger.ts

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ import logger from './logger';
99

1010
// For now this is a private env variable we use internally
1111
// But we'll want to expose this feature officially some day
12-
const PerfDebuggingEnabled: boolean = !!process.env.DOCUSAURUS_PERF_LOGGER;
12+
const PerfDebuggingEnabled: boolean =
13+
process.env.DOCUSAURUS_PERF_LOGGER === 'true';
1314

1415
const Thresholds = {
1516
min: 5,
1617
yellow: 100,
1718
red: 1000,
1819
};
1920

20-
const PerfPrefix = logger.yellow(`[PERF] `);
21+
const PerfPrefix = logger.yellow(`[PERF]`);
2122

2223
// This is what enables to "see the parent stack" for each log
2324
// Parent1 > Parent2 > Parent3 > child trace
@@ -42,6 +43,14 @@ type Memory = {
4243
after: NodeJS.MemoryUsage;
4344
};
4445

46+
function getMemory(): NodeJS.MemoryUsage {
47+
// Before reading memory stats, we explicitly call the GC
48+
// Note: this only works when Node.js option "--expose-gc" is provided
49+
globalThis.gc?.();
50+
51+
return process.memoryUsage();
52+
}
53+
4554
function createPerfLogger(): PerfLoggerAPI {
4655
if (!PerfDebuggingEnabled) {
4756
const noop = () => {};
@@ -73,29 +82,35 @@ function createPerfLogger(): PerfLoggerAPI {
7382
);
7483
};
7584

85+
const formatStatus = (error: Error | undefined): string => {
86+
return error ? logger.red('[KO]') : ''; // logger.green('[OK]');
87+
};
88+
7689
const printPerfLog = ({
7790
label,
7891
duration,
7992
memory,
93+
error,
8094
}: {
8195
label: string;
8296
duration: number;
8397
memory: Memory;
98+
error: Error | undefined;
8499
}) => {
85100
if (duration < Thresholds.min) {
86101
return;
87102
}
88103
console.log(
89-
`${PerfPrefix + label} - ${formatDuration(duration)} - ${formatMemory(
90-
memory,
91-
)}`,
104+
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
105+
duration,
106+
)} - ${formatMemory(memory)}`,
92107
);
93108
};
94109

95110
const start: PerfLoggerAPI['start'] = (label) =>
96111
performance.mark(label, {
97112
detail: {
98-
memoryUsage: process.memoryUsage(),
113+
memoryUsage: getMemory(),
99114
},
100115
});
101116

@@ -110,30 +125,42 @@ function createPerfLogger(): PerfLoggerAPI {
110125
duration,
111126
memory: {
112127
before: memoryUsage,
113-
after: process.memoryUsage(),
128+
after: getMemory(),
114129
},
130+
error: undefined,
115131
});
116132
};
117133

118134
const log: PerfLoggerAPI['log'] = (label: string) =>
119-
console.log(PerfPrefix + applyParentPrefix(label));
135+
console.log(`${PerfPrefix} ${applyParentPrefix(label)}`);
120136

121137
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
122138
const finalLabel = applyParentPrefix(label);
123139
const before = performance.now();
124-
const memoryBefore = process.memoryUsage();
125-
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
126-
const memoryAfter = process.memoryUsage();
127-
const duration = performance.now() - before;
128-
printPerfLog({
129-
label: finalLabel,
130-
duration,
131-
memory: {
132-
before: memoryBefore,
133-
after: memoryAfter,
134-
},
135-
});
136-
return result;
140+
const memoryBefore = getMemory();
141+
142+
const asyncEnd = ({error}: {error: Error | undefined}) => {
143+
const memoryAfter = getMemory();
144+
const duration = performance.now() - before;
145+
printPerfLog({
146+
error,
147+
label: finalLabel,
148+
duration,
149+
memory: {
150+
before: memoryBefore,
151+
after: memoryAfter,
152+
},
153+
});
154+
};
155+
156+
try {
157+
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
158+
asyncEnd({error: undefined});
159+
return result;
160+
} catch (e) {
161+
asyncEnd({error: e as Error});
162+
throw e;
163+
}
137164
};
138165

139166
return {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ export type DocusaurusConfig = {
403403
*
404404
* @see https://docusaurus.io/docs/api/docusaurus-config#ssrTemplate
405405
*/
406+
// TODO Docusaurus v4 - rename to ssgTemplate?
406407
ssrTemplate?: string;
407408
/**
408409
* Will be used as title delimiter in the generated `<title>` tag.

packages/docusaurus/src/client/renderToHtml.tsx

Lines changed: 15 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,74 +7,22 @@
77

88
import type {ReactNode} from 'react';
99
import {renderToPipeableStream} from 'react-dom/server';
10-
import {Writable} from 'stream';
10+
import {PassThrough} from 'node:stream';
11+
import {text} from 'node:stream/consumers';
1112

13+
// See also https://github.com/facebook/react/issues/31134
14+
// See also https://github.com/facebook/docusaurus/issues/9985#issuecomment-2396367797
1215
export async function renderToHtml(app: ReactNode): Promise<string> {
13-
// Inspired from
14-
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
15-
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
16-
const writableStream = new WritableAsPromise();
17-
18-
const {pipe} = renderToPipeableStream(app, {
19-
onError(error) {
20-
writableStream.destroy(error as Error);
21-
},
22-
onAllReady() {
23-
pipe(writableStream);
24-
},
25-
});
26-
27-
return writableStream.getPromise();
28-
}
29-
30-
// WritableAsPromise inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js
31-
32-
/* eslint-disable no-underscore-dangle */
33-
class WritableAsPromise extends Writable {
34-
private _output: string;
35-
private _deferred: {
36-
promise: Promise<string> | null;
37-
resolve: (value: string) => void;
38-
reject: (reason: Error) => void;
39-
};
40-
41-
constructor() {
42-
super();
43-
this._output = ``;
44-
this._deferred = {
45-
promise: null,
46-
resolve: () => null,
47-
reject: () => null,
48-
};
49-
this._deferred.promise = new Promise((resolve, reject) => {
50-
this._deferred.resolve = resolve;
51-
this._deferred.reject = reject;
16+
return new Promise((resolve, reject) => {
17+
const passThrough = new PassThrough();
18+
const {pipe} = renderToPipeableStream(app, {
19+
onError(error) {
20+
reject(error);
21+
},
22+
onAllReady() {
23+
pipe(passThrough);
24+
text(passThrough).then(resolve, reject);
25+
},
5226
});
53-
}
54-
55-
override _write(
56-
chunk: {toString: () => string},
57-
_enc: unknown,
58-
next: () => void,
59-
) {
60-
this._output += chunk.toString();
61-
next();
62-
}
63-
64-
override _destroy(error: Error | null, next: (error?: Error | null) => void) {
65-
if (error instanceof Error) {
66-
this._deferred.reject(error);
67-
} else {
68-
next();
69-
}
70-
}
71-
72-
override end() {
73-
this._deferred.resolve(this._output);
74-
return this.destroy();
75-
}
76-
77-
getPromise(): Promise<string> {
78-
return this._deferred.promise!;
79-
}
27+
});
8028
}

packages/docusaurus/src/client/serverEntry.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ const render: AppRenderer = async ({pathname}) => {
4242
const html = await renderToHtml(app);
4343

4444
const collectedData: PageCollectedData = {
45+
// TODO Docusaurus v4 refactor: helmet state is non-serializable
46+
// this makes it impossible to run SSG in a worker thread
4547
helmet: (helmetContext as FilledContext).helmet,
48+
4649
anchors: statefulBrokenLinks.getCollectedAnchors(),
4750
links: statefulBrokenLinks.getCollectedLinks(),
4851
modules: Array.from(modules),

0 commit comments

Comments
 (0)