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): internalize, simplify and optimize the SSG logic #9798

Merged
merged 45 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
92c1a74
Stabilized version of SSG plugin internalization
slorber Jan 26, 2024
2fb7cf9
simplify SSG plugin even more
slorber Jan 26, 2024
7e9acf1
rename SSG plugin option from preferFoldersOutput to trailingSlash
slorber Jan 26, 2024
72c69b2
cache reading of client manifest during SSG
slorber Jan 26, 2024
f7cf058
fix comment
slorber Jan 26, 2024
684d95e
split webpack server config
slorber Jan 26, 2024
29c0bc5
move SSG logic out of webpack
slorber Jan 26, 2024
7f6996f
move SSG logic out of webpack
slorber Jan 27, 2024
9d761d3
cleanup useless routeLocations attribute
slorber Jan 27, 2024
67b9223
more refactor
slorber Jan 27, 2024
dc26d0f
refactor: apply lint autofix
slorber Jan 27, 2024
073f467
Merge branch 'main' into slorber/internalize-ssg-logic
slorber Feb 1, 2024
1dfce98
Ensure all SSG errors are properly logged
slorber Feb 1, 2024
de70d67
Stable refactor of Docusaurus SSG
slorber Feb 1, 2024
fabf483
Move logic for static dir CopyWebpackPlugin to appropriate file
slorber Feb 1, 2024
258ec2d
extract createBuildClientConfig to client.ts
slorber Feb 1, 2024
7dd3c56
extract createStartClientConfig
slorber Feb 1, 2024
0e130b0
stable refactor of start/build commands
slorber Feb 1, 2024
dcf9886
refactor webpack configs
slorber Feb 2, 2024
9674798
stable refactor of webpack configs
slorber Feb 2, 2024
7893f60
Simplify start/build + add build basic perf logging
slorber Feb 2, 2024
25faa59
fix server tests
slorber Feb 2, 2024
81613e7
more start/build commands refactor
slorber Feb 2, 2024
5e7692d
Add basic perf logging system
slorber Feb 2, 2024
8a9cdcc
refactor: apply lint autofix
slorber Feb 2, 2024
7cab720
Move eta, html-minifier, lodash and other dependencies outside the se…
slorber Feb 2, 2024
bb7e96c
Merge remote-tracking branch 'origin/slorber/internalize-ssg-logic' i…
slorber Feb 2, 2024
ea534b9
typo
slorber Feb 2, 2024
5f8c038
rename renderToHtml
slorber Feb 2, 2024
bef6b87
refactor: apply lint autofix
slorber Feb 2, 2024
5ece1ce
Almost complete refactor of build/SSG pipeline?
slorber Feb 2, 2024
5abda78
Merge remote-tracking branch 'origin/slorber/internalize-ssg-logic' i…
slorber Feb 2, 2024
170e321
revert unwanted script change
slorber Feb 2, 2024
9595a30
refactor: apply lint autofix
slorber Feb 2, 2024
77fa391
Fix weird TS compilation issue
slorber Feb 2, 2024
0510a3c
Merge remote-tracking branch 'origin/slorber/internalize-ssg-logic' i…
slorber Feb 2, 2024
9662027
simplify webpack config minify usage
slorber Feb 2, 2024
a245b67
improve error logging
slorber Feb 2, 2024
3cbabc0
refactor: apply lint autofix
slorber Feb 2, 2024
d185bde
rename headTags to helmet to avoid confusion with siteConfig headTags
slorber Feb 2, 2024
cfbe056
Merge remote-tracking branch 'origin/slorber/internalize-ssg-logic' i…
slorber Feb 2, 2024
4c76357
rename bad error message
slorber Feb 6, 2024
72af15b
add missing await
slorber Feb 6, 2024
7953746
SSG: use createRequire() instead of "require-like" package
slorber Feb 6, 2024
0d2f756
cleanup, create new webpack ForceTerminatePlugin
slorber Feb 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading