Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): refactor SSG infrastructure #10593

Merged
merged 13 commits into from
Oct 18, 2024
6 changes: 5 additions & 1 deletion packages/docusaurus-bundler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export {
} from './currentBundler';

export {getMinimizers} from './minification';
export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml';
export {
getHtmlMinifier,
type HtmlMinifier,
type HtmlMinifierType,
} from './minifyHtml';
export {createJsLoaderFactory} from './loaders/jsLoader';
export {createStyleLoadersFactory} from './loaders/styleLoader';
18 changes: 5 additions & 13 deletions packages/docusaurus-bundler/src/minifyHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

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

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

export type HtmlMinifierType = 'swc' | 'terser';

export type HtmlMinifierResult = {
code: string;
warnings: string[];
Expand All @@ -25,24 +26,15 @@ const NoopMinifier: HtmlMinifier = {
minify: async (html: string) => ({code: html, warnings: []}),
};

type SiteConfigSlice = {
future: {
experimental_faster: Pick<
DocusaurusConfig['future']['experimental_faster'],
'swcHtmlMinimizer'
>;
};
};

export async function getHtmlMinifier({
siteConfig,
type,
}: {
siteConfig: SiteConfigSlice;
type: HtmlMinifierType;
}): Promise<HtmlMinifier> {
if (SkipHtmlMinification) {
return NoopMinifier;
}
if (siteConfig.future.experimental_faster.swcHtmlMinimizer) {
if (type === 'swc') {
return getSwcMinifier();
} else {
return getTerserMinifier();
Expand Down
69 changes: 48 additions & 21 deletions packages/docusaurus-logger/src/perfLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import logger from './logger';

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

const Thresholds = {
min: 5,
yellow: 100,
red: 1000,
};

const PerfPrefix = logger.yellow(`[PERF] `);
const PerfPrefix = logger.yellow(`[PERF]`);

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

function getMemory(): NodeJS.MemoryUsage {
// Before reading memory stats, we explicitly call the GC
// Note: this only works when Node.js option "--expose-gc" is provided
globalThis.gc?.();

return process.memoryUsage();
}

function createPerfLogger(): PerfLoggerAPI {
if (!PerfDebuggingEnabled) {
const noop = () => {};
Expand Down Expand Up @@ -73,29 +82,35 @@ function createPerfLogger(): PerfLoggerAPI {
);
};

const formatStatus = (error: Error | undefined): string => {
return error ? logger.red('[KO]') : ''; // logger.green('[OK]');
};

const printPerfLog = ({
label,
duration,
memory,
error,
}: {
label: string;
duration: number;
memory: Memory;
error: Error | undefined;
}) => {
if (duration < Thresholds.min) {
return;
}
console.log(
`${PerfPrefix + label} - ${formatDuration(duration)} - ${formatMemory(
memory,
)}`,
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
duration,
)} - ${formatMemory(memory)}`,
);
};

const start: PerfLoggerAPI['start'] = (label) =>
performance.mark(label, {
detail: {
memoryUsage: process.memoryUsage(),
memoryUsage: getMemory(),
},
});

Expand All @@ -110,30 +125,42 @@ function createPerfLogger(): PerfLoggerAPI {
duration,
memory: {
before: memoryUsage,
after: process.memoryUsage(),
after: getMemory(),
},
error: undefined,
});
};

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

const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
const finalLabel = applyParentPrefix(label);
const before = performance.now();
const memoryBefore = process.memoryUsage();
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
const memoryAfter = process.memoryUsage();
const duration = performance.now() - before;
printPerfLog({
label: finalLabel,
duration,
memory: {
before: memoryBefore,
after: memoryAfter,
},
});
return result;
const memoryBefore = getMemory();

const asyncEnd = ({error}: {error: Error | undefined}) => {
const memoryAfter = getMemory();
const duration = performance.now() - before;
printPerfLog({
error,
label: finalLabel,
duration,
memory: {
before: memoryBefore,
after: memoryAfter,
},
});
};

try {
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
asyncEnd({error: undefined});
return result;
} catch (e) {
asyncEnd({error: e as Error});
throw e;
}
};

return {
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-types/src/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ export type DocusaurusConfig = {
*
* @see https://docusaurus.io/docs/api/docusaurus-config#ssrTemplate
*/
// TODO Docusaurus v4 - rename to ssgTemplate?
ssrTemplate?: string;
/**
* Will be used as title delimiter in the generated `<title>` tag.
Expand Down
82 changes: 15 additions & 67 deletions packages/docusaurus/src/client/renderToHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,74 +7,22 @@

import type {ReactNode} from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {Writable} from 'stream';
import {PassThrough} from 'node:stream';
import {text} from 'node:stream/consumers';

// See also https://github.com/facebook/react/issues/31134
// See also https://github.com/facebook/docusaurus/issues/9985#issuecomment-2396367797
export async function renderToHtml(app: ReactNode): Promise<string> {
// Inspired from
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
const writableStream = new WritableAsPromise();

const {pipe} = renderToPipeableStream(app, {
onError(error) {
writableStream.destroy(error as Error);
},
onAllReady() {
pipe(writableStream);
},
});

return writableStream.getPromise();
}

// WritableAsPromise inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js

/* eslint-disable no-underscore-dangle */
class WritableAsPromise extends Writable {
private _output: string;
private _deferred: {
promise: Promise<string> | null;
resolve: (value: string) => void;
reject: (reason: Error) => void;
};

constructor() {
super();
this._output = ``;
this._deferred = {
promise: null,
resolve: () => null,
reject: () => null,
};
this._deferred.promise = new Promise((resolve, reject) => {
this._deferred.resolve = resolve;
this._deferred.reject = reject;
return new Promise((resolve, reject) => {
const passThrough = new PassThrough();
const {pipe} = renderToPipeableStream(app, {
onError(error) {
reject(error);
},
onAllReady() {
pipe(passThrough);
text(passThrough).then(resolve, reject);
},
});
}

override _write(
chunk: {toString: () => string},
_enc: unknown,
next: () => void,
) {
this._output += chunk.toString();
next();
}

override _destroy(error: Error | null, next: (error?: Error | null) => void) {
if (error instanceof Error) {
this._deferred.reject(error);
} else {
next();
}
}

override end() {
this._deferred.resolve(this._output);
return this.destroy();
}

getPromise(): Promise<string> {
return this._deferred.promise!;
}
});
}
3 changes: 3 additions & 0 deletions packages/docusaurus/src/client/serverEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const render: AppRenderer = async ({pathname}) => {
const html = await renderToHtml(app);

const collectedData: PageCollectedData = {
// TODO Docusaurus v4 refactor: helmet state is non-serializable
// this makes it impossible to run SSG in a worker thread
helmet: (helmetContext as FilledContext).helmet,

anchors: statefulBrokenLinks.getCollectedAnchors(),
links: statefulBrokenLinks.getCollectedLinks(),
modules: Array.from(modules),
Expand Down
Loading