Skip to content

Commit

Permalink
refactor(core): internalize, simplify and optimize the SSG logic (#9798)
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber authored Feb 8, 2024
1 parent d740be0 commit 34297bc
Show file tree
Hide file tree
Showing 25 changed files with 1,239 additions and 698 deletions.
11 changes: 6 additions & 5 deletions packages/docusaurus/bin/docusaurus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

// @ts-check

import {inspect} from 'node:util';
import logger from '@docusaurus/logger';
import cli from 'commander';
import {DOCUSAURUS_VERSION} from '@docusaurus/utils';
Expand Down Expand Up @@ -61,8 +62,6 @@ cli
'--no-minify',
'build website without minimizing JS bundles (default: false)',
)
// @ts-expect-error: Promise<string> is not assignable to Promise<void>... but
// good enough here.
.action(build);

cli
Expand Down Expand Up @@ -269,9 +268,11 @@ cli.parse(process.argv);

process.on('unhandledRejection', (err) => {
console.log('');
// Do not use logger.error here: it does not print error causes
console.error(err);
console.log('');

// We need to use inspect with increased depth to log the full causal chain
// By default Node logging has depth=2
// see also https://github.com/nodejs/node/issues/51637
logger.error(inspect(err, {depth: Infinity}));

logger.info`Docusaurus version: number=${DOCUSAURUS_VERSION}
Node version: number=${process.version}`;
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"@docusaurus/utils": "3.0.0",
"@docusaurus/utils-common": "3.0.0",
"@docusaurus/utils-validation": "3.0.0",
"@slorber/static-site-generator-webpack-plugin": "^4.0.7",
"@svgr/webpack": "^6.5.1",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3",
Expand All @@ -70,6 +69,7 @@
"del": "^6.1.1",
"detect-port": "^1.5.1",
"escape-html": "^1.0.3",
"eval": "^0.1.8",
"eta": "^2.2.0",
"file-loader": "^6.2.0",
"fs-extra": "^11.1.1",
Expand All @@ -79,6 +79,7 @@
"leven": "^3.1.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.7.6",
"p-map": "^4.0.0",
"postcss": "^8.4.26",
"postcss-loader": "^7.3.3",
"prompts": "^2.4.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {ReactNode} from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {Writable} from 'stream';

export async function renderStaticApp(app: ReactNode): Promise<string> {
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
Expand Down
159 changes: 14 additions & 145 deletions packages/docusaurus/src/client/serverEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,103 +6,31 @@
*/

import React from 'react';
import path from 'path';
import fs from 'fs-extra';
// eslint-disable-next-line no-restricted-imports
import _ from 'lodash';
import * as eta from 'eta';
import {StaticRouter} from 'react-router-dom';
import {HelmetProvider, type FilledContext} from 'react-helmet-async';
import {getBundles, type Manifest} from 'react-loadable-ssr-addon-v5-slorber';
import Loadable from 'react-loadable';
import {minify} from 'html-minifier-terser';
import {renderStaticApp} from './serverRenderer';
import {renderToHtml} from './renderToHtml';
import preload from './preload';
import App from './App';
import {
createStatefulBrokenLinks,
BrokenLinksProvider,
} from './BrokenLinksContext';
import type {Locals} from '@slorber/static-site-generator-webpack-plugin';
import type {PageCollectedData, AppRenderer} from '../common';

const getCompiledSSRTemplate = _.memoize((template: string) =>
eta.compile(template.trim(), {
rmWhitespace: true,
}),
);
const render: AppRenderer = async ({pathname}) => {
await preload(pathname);

function renderSSRTemplate(ssrTemplate: string, data: object) {
const compiled = getCompiledSSRTemplate(ssrTemplate);
return compiled(data, eta.defaultConfig);
}

function buildSSRErrorMessage({
error,
pathname,
}: {
error: Error;
pathname: string;
}): string {
const parts = [
`Docusaurus server-side rendering could not render static page with path ${pathname} because of error: ${error.message}`,
];

const isNotDefinedErrorRegex =
/(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;

if (isNotDefinedErrorRegex.test(error.message)) {
// prettier-ignore
parts.push(`It looks like you are using code that should run on the client-side only.
To get around it, try using \`<BrowserOnly>\` (https://docusaurus.io/docs/docusaurus-core/#browseronly) or \`ExecutionEnvironment\` (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).
It might also require to wrap your client code in \`useEffect\` hook and/or import a third-party library dynamically (if any).`);
}

return parts.join('\n');
}

export default async function render(
locals: Locals & {path: string},
): Promise<string> {
try {
return await doRender(locals);
} catch (errorUnknown) {
const error = errorUnknown as Error;
const message = buildSSRErrorMessage({error, pathname: locals.path});
const ssrError = new Error(message, {cause: error});
// It is important to log the error here because the stacktrace causal chain
// is not available anymore upper in the tree (this SSR runs in eval)
console.error(ssrError);
throw ssrError;
}
}

// Renderer for static-site-generator-webpack-plugin (async rendering).
async function doRender(locals: Locals & {path: string}) {
const {
routesLocation,
headTags,
preBodyTags,
postBodyTags,
onLinksCollected,
onHeadTagsCollected,
baseUrl,
ssrTemplate,
noIndex,
DOCUSAURUS_VERSION,
} = locals;
const location = routesLocation[locals.path]!;
await preload(location);
const modules = new Set<string>();
const routerContext = {};
const helmetContext = {};

const statefulBrokenLinks = createStatefulBrokenLinks();

const app = (
// @ts-expect-error: we are migrating away from react-loadable anyways
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
<HelmetProvider context={helmetContext}>
<StaticRouter location={location} context={routerContext}>
<StaticRouter location={pathname} context={routerContext}>
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
<App />
</BrokenLinksProvider>
Expand All @@ -111,75 +39,16 @@ async function doRender(locals: Locals & {path: string}) {
</Loadable.Capture>
);

const appHtml = await renderStaticApp(app);
onLinksCollected({
staticPagePath: location,
const html = await renderToHtml(app);

const collectedData: PageCollectedData = {
helmet: (helmetContext as FilledContext).helmet,
anchors: statefulBrokenLinks.getCollectedAnchors(),
links: statefulBrokenLinks.getCollectedLinks(),
});

const {helmet} = helmetContext as FilledContext;
const htmlAttributes = helmet.htmlAttributes.toString();
const bodyAttributes = helmet.bodyAttributes.toString();
const metaStrings = [
helmet.title.toString(),
helmet.meta.toString(),
helmet.link.toString(),
helmet.script.toString(),
];
onHeadTagsCollected(location, helmet);
const metaAttributes = metaStrings.filter(Boolean);

const {generatedFilesDir} = locals;
const manifestPath = path.join(generatedFilesDir, 'client-manifest.json');
// Using readJSON seems to fail for users of some plugins, possibly because of
// the eval sandbox having a different `Buffer` instance (native one instead
// of polyfilled one)
const manifest = (await fs
.readFile(manifestPath, 'utf-8')
.then(JSON.parse)) as Manifest;

// Get all required assets for this particular page based on client
// manifest information.
const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)];
const bundles = getBundles(manifest, modulesToBeLoaded);
const stylesheets = (bundles.css ?? []).map((b) => b.file);
const scripts = (bundles.js ?? []).map((b) => b.file);

const renderedHtml = renderSSRTemplate(ssrTemplate, {
appHtml,
baseUrl,
htmlAttributes,
bodyAttributes,
headTags,
preBodyTags,
postBodyTags,
metaAttributes,
scripts,
stylesheets,
noIndex,
version: DOCUSAURUS_VERSION,
});
modules: Array.from(modules),
};

try {
if (process.env.SKIP_HTML_MINIFICATION === 'true') {
return renderedHtml;
}
return {html, collectedData};
};

// Minify html with https://github.com/DanielRuf/html-minifier-terser
return await minify(renderedHtml, {
removeComments: false,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyJS: true,
});
} catch (err) {
// prettier-ignore
console.error(`Minification of page ${locals.path} failed.`);
console.error(err);
throw err;
}
}
export default render;
Loading

0 comments on commit 34297bc

Please sign in to comment.