mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 11:47:23 +02:00
refactor(core): internalize, simplify and optimize the SSG logic (#9798)
This commit is contained in:
parent
d740be0e9c
commit
34297bc56d
25 changed files with 1263 additions and 722 deletions
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import {inspect} from 'node:util';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import cli from 'commander';
|
import cli from 'commander';
|
||||||
import {DOCUSAURUS_VERSION} from '@docusaurus/utils';
|
import {DOCUSAURUS_VERSION} from '@docusaurus/utils';
|
||||||
|
@ -61,8 +62,6 @@ cli
|
||||||
'--no-minify',
|
'--no-minify',
|
||||||
'build website without minimizing JS bundles (default: false)',
|
'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);
|
.action(build);
|
||||||
|
|
||||||
cli
|
cli
|
||||||
|
@ -269,9 +268,11 @@ cli.parse(process.argv);
|
||||||
|
|
||||||
process.on('unhandledRejection', (err) => {
|
process.on('unhandledRejection', (err) => {
|
||||||
console.log('');
|
console.log('');
|
||||||
// Do not use logger.error here: it does not print error causes
|
|
||||||
console.error(err);
|
// We need to use inspect with increased depth to log the full causal chain
|
||||||
console.log('');
|
// 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}
|
logger.info`Docusaurus version: number=${DOCUSAURUS_VERSION}
|
||||||
Node version: number=${process.version}`;
|
Node version: number=${process.version}`;
|
||||||
|
|
|
@ -50,7 +50,6 @@
|
||||||
"@docusaurus/utils": "3.0.0",
|
"@docusaurus/utils": "3.0.0",
|
||||||
"@docusaurus/utils-common": "3.0.0",
|
"@docusaurus/utils-common": "3.0.0",
|
||||||
"@docusaurus/utils-validation": "3.0.0",
|
"@docusaurus/utils-validation": "3.0.0",
|
||||||
"@slorber/static-site-generator-webpack-plugin": "^4.0.7",
|
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.1.3",
|
||||||
|
@ -70,6 +69,7 @@
|
||||||
"del": "^6.1.1",
|
"del": "^6.1.1",
|
||||||
"detect-port": "^1.5.1",
|
"detect-port": "^1.5.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"eval": "^0.1.8",
|
||||||
"eta": "^2.2.0",
|
"eta": "^2.2.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
|
@ -79,6 +79,7 @@
|
||||||
"leven": "^3.1.0",
|
"leven": "^3.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mini-css-extract-plugin": "^2.7.6",
|
"mini-css-extract-plugin": "^2.7.6",
|
||||||
|
"p-map": "^4.0.0",
|
||||||
"postcss": "^8.4.26",
|
"postcss": "^8.4.26",
|
||||||
"postcss-loader": "^7.3.3",
|
"postcss-loader": "^7.3.3",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type {ReactNode} from 'react';
|
||||||
import {renderToPipeableStream} from 'react-dom/server';
|
import {renderToPipeableStream} from 'react-dom/server';
|
||||||
import {Writable} from 'stream';
|
import {Writable} from 'stream';
|
||||||
|
|
||||||
export async function renderStaticApp(app: ReactNode): Promise<string> {
|
export async function renderToHtml(app: ReactNode): Promise<string> {
|
||||||
// Inspired from
|
// Inspired from
|
||||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
// 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
|
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
|
|
@ -6,103 +6,31 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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 {StaticRouter} from 'react-router-dom';
|
||||||
import {HelmetProvider, type FilledContext} from 'react-helmet-async';
|
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 Loadable from 'react-loadable';
|
||||||
import {minify} from 'html-minifier-terser';
|
import {renderToHtml} from './renderToHtml';
|
||||||
import {renderStaticApp} from './serverRenderer';
|
|
||||||
import preload from './preload';
|
import preload from './preload';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import {
|
import {
|
||||||
createStatefulBrokenLinks,
|
createStatefulBrokenLinks,
|
||||||
BrokenLinksProvider,
|
BrokenLinksProvider,
|
||||||
} from './BrokenLinksContext';
|
} from './BrokenLinksContext';
|
||||||
import type {Locals} from '@slorber/static-site-generator-webpack-plugin';
|
import type {PageCollectedData, AppRenderer} from '../common';
|
||||||
|
|
||||||
const getCompiledSSRTemplate = _.memoize((template: string) =>
|
const render: AppRenderer = async ({pathname}) => {
|
||||||
eta.compile(template.trim(), {
|
await preload(pathname);
|
||||||
rmWhitespace: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
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 modules = new Set<string>();
|
||||||
const routerContext = {};
|
const routerContext = {};
|
||||||
const helmetContext = {};
|
const helmetContext = {};
|
||||||
|
|
||||||
const statefulBrokenLinks = createStatefulBrokenLinks();
|
const statefulBrokenLinks = createStatefulBrokenLinks();
|
||||||
|
|
||||||
const app = (
|
const app = (
|
||||||
// @ts-expect-error: we are migrating away from react-loadable anyways
|
// @ts-expect-error: we are migrating away from react-loadable anyways
|
||||||
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
||||||
<HelmetProvider context={helmetContext}>
|
<HelmetProvider context={helmetContext}>
|
||||||
<StaticRouter location={location} context={routerContext}>
|
<StaticRouter location={pathname} context={routerContext}>
|
||||||
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
||||||
<App />
|
<App />
|
||||||
</BrokenLinksProvider>
|
</BrokenLinksProvider>
|
||||||
|
@ -111,75 +39,16 @@ async function doRender(locals: Locals & {path: string}) {
|
||||||
</Loadable.Capture>
|
</Loadable.Capture>
|
||||||
);
|
);
|
||||||
|
|
||||||
const appHtml = await renderStaticApp(app);
|
const html = await renderToHtml(app);
|
||||||
onLinksCollected({
|
|
||||||
staticPagePath: location,
|
const collectedData: PageCollectedData = {
|
||||||
|
helmet: (helmetContext as FilledContext).helmet,
|
||||||
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
||||||
links: statefulBrokenLinks.getCollectedLinks(),
|
links: statefulBrokenLinks.getCollectedLinks(),
|
||||||
});
|
modules: Array.from(modules),
|
||||||
|
};
|
||||||
|
|
||||||
const {helmet} = helmetContext as FilledContext;
|
return {html, collectedData};
|
||||||
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;
|
export default render;
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (process.env.SKIP_HTML_MINIFICATION === 'true') {
|
|
||||||
return renderedHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,27 +7,29 @@
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import _ from 'lodash';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {mapAsyncSequential} from '@docusaurus/utils';
|
import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils';
|
||||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
|
||||||
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
|
|
||||||
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
|
|
||||||
import merge from 'webpack-merge';
|
|
||||||
import {load, loadContext, type LoadContextOptions} from '../server';
|
import {load, loadContext, type LoadContextOptions} from '../server';
|
||||||
import {handleBrokenLinks} from '../server/brokenLinks';
|
import {handleBrokenLinks} from '../server/brokenLinks';
|
||||||
|
|
||||||
import createClientConfig from '../webpack/client';
|
import {createBuildClientConfig} from '../webpack/client';
|
||||||
import createServerConfig from '../webpack/server';
|
import createServerConfig from '../webpack/server';
|
||||||
import {
|
import {
|
||||||
applyConfigurePostCss,
|
executePluginsConfigurePostCss,
|
||||||
applyConfigureWebpack,
|
executePluginsConfigureWebpack,
|
||||||
compile,
|
compile,
|
||||||
} from '../webpack/utils';
|
} from '../webpack/utils';
|
||||||
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
|
import {PerfLogger} from '../utils';
|
||||||
|
|
||||||
import {loadI18n} from '../server/i18n';
|
import {loadI18n} from '../server/i18n';
|
||||||
import type {HelmetServerState} from 'react-helmet-async';
|
import {generateStaticFiles, loadAppRenderer} from '../ssg';
|
||||||
import type {Configuration} from 'webpack';
|
import {compileSSRTemplate} from '../templates/templates';
|
||||||
import type {Props} from '@docusaurus/types';
|
import defaultSSRTemplate from '../templates/ssr.html.template';
|
||||||
|
|
||||||
|
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
||||||
|
import type {LoadedPlugin, Props} from '@docusaurus/types';
|
||||||
|
import type {SiteCollectedData} from '../common';
|
||||||
|
|
||||||
export type BuildCLIOptions = Pick<
|
export type BuildCLIOptions = Pick<
|
||||||
LoadContextOptions,
|
LoadContextOptions,
|
||||||
|
@ -46,7 +48,7 @@ export async function build(
|
||||||
// deploy, we have to let deploy finish.
|
// deploy, we have to let deploy finish.
|
||||||
// See https://github.com/facebook/docusaurus/pull/2496
|
// See https://github.com/facebook/docusaurus/pull/2496
|
||||||
forceTerminate: boolean = true,
|
forceTerminate: boolean = true,
|
||||||
): Promise<string> {
|
): Promise<void> {
|
||||||
process.env.BABEL_ENV = 'production';
|
process.env.BABEL_ENV = 'production';
|
||||||
process.env.NODE_ENV = 'production';
|
process.env.NODE_ENV = 'production';
|
||||||
process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale;
|
process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale;
|
||||||
|
@ -70,13 +72,15 @@ export async function build(
|
||||||
isLastLocale: boolean;
|
isLastLocale: boolean;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
return await buildLocale({
|
PerfLogger.start(`Building site for locale ${locale}`);
|
||||||
|
await buildLocale({
|
||||||
siteDir,
|
siteDir,
|
||||||
locale,
|
locale,
|
||||||
cliOptions,
|
cliOptions,
|
||||||
forceTerminate,
|
forceTerminate,
|
||||||
isLastLocale,
|
isLastLocale,
|
||||||
});
|
});
|
||||||
|
PerfLogger.end(`Building site for locale ${locale}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
logger.interpolate`Unable to build website for locale name=${locale}.`,
|
logger.interpolate`Unable to build website for locale name=${locale}.`,
|
||||||
|
@ -86,6 +90,34 @@ export async function build(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PerfLogger.start(`Get locales to build`);
|
||||||
|
const locales = await getLocalesToBuild({siteDir, cliOptions});
|
||||||
|
PerfLogger.end(`Get locales to build`);
|
||||||
|
|
||||||
|
if (locales.length > 1) {
|
||||||
|
logger.info`Website will be built for all these locales: ${locales}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
PerfLogger.start(`Building ${locales.length} locales`);
|
||||||
|
await mapAsyncSequential(locales, (locale) => {
|
||||||
|
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
|
||||||
|
return tryToBuildLocale({locale, isLastLocale});
|
||||||
|
});
|
||||||
|
PerfLogger.end(`Building ${locales.length} locales`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLocalesToBuild({
|
||||||
|
siteDir,
|
||||||
|
cliOptions,
|
||||||
|
}: {
|
||||||
|
siteDir: string;
|
||||||
|
cliOptions: BuildCLIOptions;
|
||||||
|
}): Promise<[string, ...string[]]> {
|
||||||
|
if (cliOptions.locale) {
|
||||||
|
return [cliOptions.locale];
|
||||||
|
}
|
||||||
|
|
||||||
const context = await loadContext({
|
const context = await loadContext({
|
||||||
siteDir,
|
siteDir,
|
||||||
outDir: cliOptions.outDir,
|
outDir: cliOptions.outDir,
|
||||||
|
@ -96,26 +128,16 @@ export async function build(
|
||||||
const i18n = await loadI18n(context.siteConfig, {
|
const i18n = await loadI18n(context.siteConfig, {
|
||||||
locale: cliOptions.locale,
|
locale: cliOptions.locale,
|
||||||
});
|
});
|
||||||
if (cliOptions.locale) {
|
|
||||||
return tryToBuildLocale({locale: cliOptions.locale, isLastLocale: true});
|
|
||||||
}
|
|
||||||
if (i18n.locales.length > 1) {
|
if (i18n.locales.length > 1) {
|
||||||
logger.info`Website will be built for all these locales: ${i18n.locales}`;
|
logger.info`Website will be built for all these locales: ${i18n.locales}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need the default locale to always be the 1st in the list. If we build it
|
// We need the default locale to always be the 1st in the list. If we build it
|
||||||
// last, it would "erase" the localized sites built in sub-folders
|
// last, it would "erase" the localized sites built in sub-folders
|
||||||
const orderedLocales: [string, ...string[]] = [
|
return [
|
||||||
i18n.defaultLocale,
|
i18n.defaultLocale,
|
||||||
...i18n.locales.filter((locale) => locale !== i18n.defaultLocale),
|
...i18n.locales.filter((locale) => locale !== i18n.defaultLocale),
|
||||||
];
|
];
|
||||||
|
|
||||||
const results = await mapAsyncSequential(orderedLocales, (locale) => {
|
|
||||||
const isLastLocale =
|
|
||||||
orderedLocales.indexOf(locale) === orderedLocales.length - 1;
|
|
||||||
return tryToBuildLocale({locale, isLastLocale});
|
|
||||||
});
|
|
||||||
return results[0]!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildLocale({
|
async function buildLocale({
|
||||||
|
@ -138,6 +160,7 @@ async function buildLocale({
|
||||||
|
|
||||||
logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
|
logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
|
||||||
|
|
||||||
|
PerfLogger.start('Loading site');
|
||||||
const props: Props = await load({
|
const props: Props = await load({
|
||||||
siteDir,
|
siteDir,
|
||||||
outDir: cliOptions.outDir,
|
outDir: cliOptions.outDir,
|
||||||
|
@ -145,156 +168,59 @@ async function buildLocale({
|
||||||
locale,
|
locale,
|
||||||
localizePath: cliOptions.locale ? false : undefined,
|
localizePath: cliOptions.locale ? false : undefined,
|
||||||
});
|
});
|
||||||
|
PerfLogger.end('Loading site');
|
||||||
|
|
||||||
// Apply user webpack config.
|
// Apply user webpack config.
|
||||||
const {
|
const {outDir, plugins} = props;
|
||||||
outDir,
|
|
||||||
generatedFilesDir,
|
|
||||||
plugins,
|
|
||||||
siteConfig: {
|
|
||||||
onBrokenLinks,
|
|
||||||
onBrokenAnchors,
|
|
||||||
staticDirectories: staticDirectoriesOption,
|
|
||||||
},
|
|
||||||
routes,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const clientManifestPath = path.join(
|
// We can build the 2 configs in parallel
|
||||||
generatedFilesDir,
|
PerfLogger.start('Creating webpack configs');
|
||||||
'client-manifest.json',
|
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
|
||||||
);
|
await Promise.all([
|
||||||
let clientConfig: Configuration = merge(
|
getBuildClientConfig({
|
||||||
await createClientConfig(props, cliOptions.minify, true),
|
props,
|
||||||
{
|
cliOptions,
|
||||||
plugins: [
|
|
||||||
// Remove/clean build folders before building bundles.
|
|
||||||
new CleanWebpackPlugin({verbose: false}),
|
|
||||||
// Visualize size of webpack output files with an interactive zoomable
|
|
||||||
// tree map.
|
|
||||||
cliOptions.bundleAnalyzer && new BundleAnalyzerPlugin(),
|
|
||||||
// Generate client manifests file that will be used for server bundle.
|
|
||||||
new ReactLoadableSSRAddon({
|
|
||||||
filename: clientManifestPath,
|
|
||||||
}),
|
|
||||||
].filter(<T>(x: T | undefined | false): x is T => Boolean(x)),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const collectedLinks: {
|
|
||||||
[pathname: string]: {links: string[]; anchors: string[]};
|
|
||||||
} = {};
|
|
||||||
const headTags: {[location: string]: HelmetServerState} = {};
|
|
||||||
|
|
||||||
let serverConfig: Configuration = await createServerConfig({
|
|
||||||
props,
|
|
||||||
onLinksCollected: ({staticPagePath, links, anchors}) => {
|
|
||||||
collectedLinks[staticPagePath] = {links, anchors};
|
|
||||||
},
|
|
||||||
onHeadTagsCollected: (staticPagePath, tags) => {
|
|
||||||
headTags[staticPagePath] = tags;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// The staticDirectories option can contain empty directories, or non-existent
|
|
||||||
// directories (e.g. user deleted `static`). Instead of issuing an error, we
|
|
||||||
// just silently filter them out, because user could have never configured it
|
|
||||||
// in the first place (the default option should always "work").
|
|
||||||
const staticDirectories = (
|
|
||||||
await Promise.all(
|
|
||||||
staticDirectoriesOption.map(async (dir) => {
|
|
||||||
const staticDir = path.resolve(siteDir, dir);
|
|
||||||
if (
|
|
||||||
(await fs.pathExists(staticDir)) &&
|
|
||||||
(await fs.readdir(staticDir)).length > 0
|
|
||||||
) {
|
|
||||||
return staticDir;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}),
|
}),
|
||||||
)
|
getBuildServerConfig({
|
||||||
).filter(Boolean);
|
props,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
PerfLogger.end('Creating webpack configs');
|
||||||
|
|
||||||
if (staticDirectories.length > 0) {
|
// Make sure generated client-manifest is cleaned first, so we don't reuse
|
||||||
serverConfig = merge(serverConfig, {
|
|
||||||
plugins: [
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: staticDirectories.map((dir) => ({
|
|
||||||
from: dir,
|
|
||||||
to: outDir,
|
|
||||||
toType: 'dir',
|
|
||||||
})),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugin Lifecycle - configureWebpack and configurePostCss.
|
|
||||||
plugins.forEach((plugin) => {
|
|
||||||
const {configureWebpack, configurePostCss} = plugin;
|
|
||||||
|
|
||||||
if (configurePostCss) {
|
|
||||||
clientConfig = applyConfigurePostCss(
|
|
||||||
configurePostCss.bind(plugin),
|
|
||||||
clientConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configureWebpack) {
|
|
||||||
clientConfig = applyConfigureWebpack(
|
|
||||||
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
|
|
||||||
clientConfig,
|
|
||||||
false,
|
|
||||||
props.siteConfig.webpack?.jsLoader,
|
|
||||||
plugin.content,
|
|
||||||
);
|
|
||||||
|
|
||||||
serverConfig = applyConfigureWebpack(
|
|
||||||
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
|
|
||||||
serverConfig,
|
|
||||||
true,
|
|
||||||
props.siteConfig.webpack?.jsLoader,
|
|
||||||
plugin.content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure generated client-manifest is cleaned first so we don't reuse
|
|
||||||
// the one from previous builds.
|
// the one from previous builds.
|
||||||
if (await fs.pathExists(clientManifestPath)) {
|
// TODO do we really need this? .docusaurus folder is cleaned between builds
|
||||||
await fs.unlink(clientManifestPath);
|
PerfLogger.start('Deleting previous client manifest');
|
||||||
}
|
await ensureUnlink(clientManifestPath);
|
||||||
|
PerfLogger.end('Deleting previous client manifest');
|
||||||
|
|
||||||
// Run webpack to build JS bundle (client) and static html files (server).
|
// Run webpack to build JS bundle (client) and static html files (server).
|
||||||
|
PerfLogger.start('Bundling');
|
||||||
await compile([clientConfig, serverConfig]);
|
await compile([clientConfig, serverConfig]);
|
||||||
|
PerfLogger.end('Bundling');
|
||||||
|
|
||||||
|
PerfLogger.start('Executing static site generation');
|
||||||
|
const {collectedData} = await executeSSG({
|
||||||
|
props,
|
||||||
|
serverBundlePath,
|
||||||
|
clientManifestPath,
|
||||||
|
});
|
||||||
|
PerfLogger.end('Executing static site generation');
|
||||||
|
|
||||||
// Remove server.bundle.js because it is not needed.
|
// Remove server.bundle.js because it is not needed.
|
||||||
if (typeof serverConfig.output?.filename === 'string') {
|
PerfLogger.start('Deleting server bundle');
|
||||||
const serverBundle = path.join(outDir, serverConfig.output.filename);
|
await ensureUnlink(serverBundlePath);
|
||||||
if (await fs.pathExists(serverBundle)) {
|
PerfLogger.end('Deleting server bundle');
|
||||||
await fs.unlink(serverBundle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugin Lifecycle - postBuild.
|
// Plugin Lifecycle - postBuild.
|
||||||
await Promise.all(
|
PerfLogger.start('Executing postBuild()');
|
||||||
plugins.map(async (plugin) => {
|
await executePluginsPostBuild({plugins, props, collectedData});
|
||||||
if (!plugin.postBuild) {
|
PerfLogger.end('Executing postBuild()');
|
||||||
return;
|
|
||||||
}
|
|
||||||
await plugin.postBuild({
|
|
||||||
...props,
|
|
||||||
head: headTags,
|
|
||||||
content: plugin.content,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await handleBrokenLinks({
|
// TODO execute this in parallel to postBuild?
|
||||||
collectedLinks,
|
PerfLogger.start('Executing broken links checker');
|
||||||
routes,
|
await executeBrokenLinksCheck({props, collectedData});
|
||||||
onBrokenLinks,
|
PerfLogger.end('Executing broken links checker');
|
||||||
onBrokenAnchors,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.success`Generated static files in path=${path.relative(
|
logger.success`Generated static files in path=${path.relative(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
|
@ -311,3 +237,144 @@ async function buildLocale({
|
||||||
|
|
||||||
return outDir;
|
return outDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeSSG({
|
||||||
|
props,
|
||||||
|
serverBundlePath,
|
||||||
|
clientManifestPath,
|
||||||
|
}: {
|
||||||
|
props: Props;
|
||||||
|
serverBundlePath: string;
|
||||||
|
clientManifestPath: string;
|
||||||
|
}) {
|
||||||
|
PerfLogger.start('Reading client manifest');
|
||||||
|
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
|
||||||
|
PerfLogger.end('Reading client manifest');
|
||||||
|
|
||||||
|
PerfLogger.start('Compiling SSR template');
|
||||||
|
const ssrTemplate = await compileSSRTemplate(
|
||||||
|
props.siteConfig.ssrTemplate ?? defaultSSRTemplate,
|
||||||
|
);
|
||||||
|
PerfLogger.end('Compiling SSR template');
|
||||||
|
|
||||||
|
PerfLogger.start('Loading App renderer');
|
||||||
|
const renderer = await loadAppRenderer({
|
||||||
|
serverBundlePath,
|
||||||
|
});
|
||||||
|
PerfLogger.end('Loading App renderer');
|
||||||
|
|
||||||
|
PerfLogger.start('Generate static files');
|
||||||
|
const ssgResult = await generateStaticFiles({
|
||||||
|
pathnames: props.routesPaths,
|
||||||
|
renderer,
|
||||||
|
params: {
|
||||||
|
trailingSlash: props.siteConfig.trailingSlash,
|
||||||
|
outDir: props.outDir,
|
||||||
|
baseUrl: props.baseUrl,
|
||||||
|
manifest,
|
||||||
|
headTags: props.headTags,
|
||||||
|
preBodyTags: props.preBodyTags,
|
||||||
|
postBodyTags: props.postBodyTags,
|
||||||
|
ssrTemplate,
|
||||||
|
noIndex: props.siteConfig.noIndex,
|
||||||
|
DOCUSAURUS_VERSION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
PerfLogger.end('Generate static files');
|
||||||
|
|
||||||
|
return ssgResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePluginsPostBuild({
|
||||||
|
plugins,
|
||||||
|
props,
|
||||||
|
collectedData,
|
||||||
|
}: {
|
||||||
|
plugins: LoadedPlugin[];
|
||||||
|
props: Props;
|
||||||
|
collectedData: SiteCollectedData;
|
||||||
|
}) {
|
||||||
|
const head = _.mapValues(collectedData, (d) => d.helmet);
|
||||||
|
await Promise.all(
|
||||||
|
plugins.map(async (plugin) => {
|
||||||
|
if (!plugin.postBuild) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await plugin.postBuild({
|
||||||
|
...props,
|
||||||
|
head,
|
||||||
|
content: plugin.content,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeBrokenLinksCheck({
|
||||||
|
props: {
|
||||||
|
routes,
|
||||||
|
siteConfig: {onBrokenLinks, onBrokenAnchors},
|
||||||
|
},
|
||||||
|
collectedData,
|
||||||
|
}: {
|
||||||
|
props: Props;
|
||||||
|
collectedData: SiteCollectedData;
|
||||||
|
}) {
|
||||||
|
const collectedLinks = _.mapValues(collectedData, (d) => ({
|
||||||
|
links: d.links,
|
||||||
|
anchors: d.anchors,
|
||||||
|
}));
|
||||||
|
await handleBrokenLinks({
|
||||||
|
collectedLinks,
|
||||||
|
routes,
|
||||||
|
onBrokenLinks,
|
||||||
|
onBrokenAnchors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuildClientConfig({
|
||||||
|
props,
|
||||||
|
cliOptions,
|
||||||
|
}: {
|
||||||
|
props: Props;
|
||||||
|
cliOptions: BuildCLIOptions;
|
||||||
|
}) {
|
||||||
|
const {plugins} = props;
|
||||||
|
const result = await createBuildClientConfig({
|
||||||
|
props,
|
||||||
|
minify: cliOptions.minify ?? true,
|
||||||
|
bundleAnalyzer: cliOptions.bundleAnalyzer ?? false,
|
||||||
|
});
|
||||||
|
let {config} = result;
|
||||||
|
config = executePluginsConfigureWebpack({
|
||||||
|
plugins,
|
||||||
|
config,
|
||||||
|
isServer: false,
|
||||||
|
jsLoader: props.siteConfig.webpack?.jsLoader,
|
||||||
|
});
|
||||||
|
return {clientConfig: config, clientManifestPath: result.clientManifestPath};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuildServerConfig({props}: {props: Props}) {
|
||||||
|
const {plugins} = props;
|
||||||
|
const result = await createServerConfig({
|
||||||
|
props,
|
||||||
|
});
|
||||||
|
let {config} = result;
|
||||||
|
config = executePluginsConfigurePostCss({
|
||||||
|
plugins,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
config = executePluginsConfigureWebpack({
|
||||||
|
plugins,
|
||||||
|
config,
|
||||||
|
isServer: true,
|
||||||
|
jsLoader: props.siteConfig.webpack?.jsLoader,
|
||||||
|
});
|
||||||
|
return {serverConfig: config, serverBundlePath: result.serverBundlePath};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureUnlink(filepath: string) {
|
||||||
|
if (await fs.pathExists(filepath)) {
|
||||||
|
await fs.unlink(filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -254,7 +254,7 @@ You can also set the deploymentBranch property in docusaurus.config.js .`);
|
||||||
if (!cliOptions.skipBuild) {
|
if (!cliOptions.skipBuild) {
|
||||||
// Build site, then push to deploymentBranch branch of specified repo.
|
// Build site, then push to deploymentBranch branch of specified repo.
|
||||||
try {
|
try {
|
||||||
await build(siteDir, cliOptions, false).then(runDeploy);
|
await build(siteDir, cliOptions, false).then(() => runDeploy(outDir));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Deployment of the build output failed.');
|
logger.error('Deployment of the build output failed.');
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -31,14 +31,14 @@ export async function serve(
|
||||||
const siteDir = await fs.realpath(siteDirParam);
|
const siteDir = await fs.realpath(siteDirParam);
|
||||||
|
|
||||||
const buildDir = cliOptions.dir ?? DEFAULT_BUILD_DIR_NAME;
|
const buildDir = cliOptions.dir ?? DEFAULT_BUILD_DIR_NAME;
|
||||||
let dir = path.resolve(siteDir, buildDir);
|
const outDir = path.resolve(siteDir, buildDir);
|
||||||
|
|
||||||
if (cliOptions.build) {
|
if (cliOptions.build) {
|
||||||
dir = await build(
|
await build(
|
||||||
siteDir,
|
siteDir,
|
||||||
{
|
{
|
||||||
config: cliOptions.config,
|
config: cliOptions.config,
|
||||||
outDir: dir,
|
outDir,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -75,7 +75,7 @@ export async function serve(
|
||||||
|
|
||||||
serveHandler(req, res, {
|
serveHandler(req, res, {
|
||||||
cleanUrls: true,
|
cleanUrls: true,
|
||||||
public: dir,
|
public: outDir,
|
||||||
trailingSlash,
|
trailingSlash,
|
||||||
directoryListing: false,
|
directoryListing: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,6 @@ import _ from 'lodash';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {normalizeUrl, posixPath} from '@docusaurus/utils';
|
import {normalizeUrl, posixPath} from '@docusaurus/utils';
|
||||||
import chokidar from 'chokidar';
|
import chokidar from 'chokidar';
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
|
||||||
import openBrowser from 'react-dev-utils/openBrowser';
|
import openBrowser from 'react-dev-utils/openBrowser';
|
||||||
import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils';
|
import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils';
|
||||||
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
|
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
|
||||||
|
@ -19,15 +18,18 @@ import webpack from 'webpack';
|
||||||
import WebpackDevServer from 'webpack-dev-server';
|
import WebpackDevServer from 'webpack-dev-server';
|
||||||
import merge from 'webpack-merge';
|
import merge from 'webpack-merge';
|
||||||
import {load, type LoadContextOptions} from '../server';
|
import {load, type LoadContextOptions} from '../server';
|
||||||
import createClientConfig from '../webpack/client';
|
import {createStartClientConfig} from '../webpack/client';
|
||||||
import {
|
import {
|
||||||
applyConfigureWebpack,
|
|
||||||
applyConfigurePostCss,
|
|
||||||
getHttpsConfig,
|
getHttpsConfig,
|
||||||
formatStatsErrorMessage,
|
formatStatsErrorMessage,
|
||||||
printStatsWarnings,
|
printStatsWarnings,
|
||||||
|
executePluginsConfigurePostCss,
|
||||||
|
executePluginsConfigureWebpack,
|
||||||
} from '../webpack/utils';
|
} from '../webpack/utils';
|
||||||
import {getHostPort, type HostPortOptions} from '../server/getHostPort';
|
import {getHostPort, type HostPortOptions} from '../server/getHostPort';
|
||||||
|
import {PerfLogger} from '../utils';
|
||||||
|
import type {Compiler} from 'webpack';
|
||||||
|
import type {Props} from '@docusaurus/types';
|
||||||
|
|
||||||
export type StartCLIOptions = HostPortOptions &
|
export type StartCLIOptions = HostPortOptions &
|
||||||
Pick<LoadContextOptions, 'locale' | 'config'> & {
|
Pick<LoadContextOptions, 'locale' | 'config'> & {
|
||||||
|
@ -50,29 +52,23 @@ export async function start(
|
||||||
|
|
||||||
logger.info('Starting the development server...');
|
logger.info('Starting the development server...');
|
||||||
|
|
||||||
function loadSite() {
|
async function loadSite() {
|
||||||
return load({
|
PerfLogger.start('Loading site');
|
||||||
|
const result = await load({
|
||||||
siteDir,
|
siteDir,
|
||||||
config: cliOptions.config,
|
config: cliOptions.config,
|
||||||
locale: cliOptions.locale,
|
locale: cliOptions.locale,
|
||||||
localizePath: undefined, // Should this be configurable?
|
localizePath: undefined, // Should this be configurable?
|
||||||
});
|
});
|
||||||
|
PerfLogger.end('Loading site');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all related files as a prop.
|
// Process all related files as a prop.
|
||||||
const props = await loadSite();
|
const props = await loadSite();
|
||||||
|
|
||||||
const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http';
|
const {host, port, getOpenUrl} = await createUrlUtils({cliOptions});
|
||||||
|
const openUrl = getOpenUrl({baseUrl: props.baseUrl});
|
||||||
const {host, port} = await getHostPort(cliOptions);
|
|
||||||
|
|
||||||
if (port === null) {
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const {baseUrl, headTags, preBodyTags, postBodyTags} = props;
|
|
||||||
const urls = prepareUrls(protocol, host, port);
|
|
||||||
const openUrl = normalizeUrl([urls.localUrlForBrowser, baseUrl]);
|
|
||||||
|
|
||||||
logger.success`Docusaurus website is running at: url=${openUrl}`;
|
logger.success`Docusaurus website is running at: url=${openUrl}`;
|
||||||
|
|
||||||
|
@ -80,7 +76,7 @@ export async function start(
|
||||||
const reload = _.debounce(() => {
|
const reload = _.debounce(() => {
|
||||||
loadSite()
|
loadSite()
|
||||||
.then(({baseUrl: newBaseUrl}) => {
|
.then(({baseUrl: newBaseUrl}) => {
|
||||||
const newOpenUrl = normalizeUrl([urls.localUrlForBrowser, newBaseUrl]);
|
const newOpenUrl = getOpenUrl({baseUrl: newBaseUrl});
|
||||||
if (newOpenUrl !== openUrl) {
|
if (newOpenUrl !== openUrl) {
|
||||||
logger.success`Docusaurus website is running at: url=${newOpenUrl}`;
|
logger.success`Docusaurus website is running at: url=${newOpenUrl}`;
|
||||||
}
|
}
|
||||||
|
@ -89,7 +85,89 @@ export async function start(
|
||||||
logger.error(err.stack);
|
logger.error(err.stack);
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
const {siteConfig, plugins, localizationDir} = props;
|
|
||||||
|
// TODO this is historically not optimized!
|
||||||
|
// When any site file changes, we reload absolutely everything :/
|
||||||
|
// At least we should try to reload only one plugin individually?
|
||||||
|
setupFileWatchers({
|
||||||
|
props,
|
||||||
|
cliOptions,
|
||||||
|
onFileChange: () => {
|
||||||
|
reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = await getStartClientConfig({
|
||||||
|
props,
|
||||||
|
minify: cliOptions.minify ?? true,
|
||||||
|
poll: cliOptions.poll,
|
||||||
|
});
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
registerE2ETestHook(compiler);
|
||||||
|
|
||||||
|
const defaultDevServerConfig = await createDevServerConfig({
|
||||||
|
cliOptions,
|
||||||
|
props,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow plugin authors to customize/override devServer config
|
||||||
|
const devServerConfig: WebpackDevServer.Configuration = merge(
|
||||||
|
[defaultDevServerConfig, config.devServer].filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
const devServer = new WebpackDevServer(devServerConfig, compiler);
|
||||||
|
devServer.startCallback(() => {
|
||||||
|
if (cliOptions.open) {
|
||||||
|
openBrowser(openUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
['SIGINT', 'SIGTERM'].forEach((sig) => {
|
||||||
|
process.on(sig, () => {
|
||||||
|
devServer.stop();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPollingOptions({cliOptions}: {cliOptions: StartCLIOptions}) {
|
||||||
|
return {
|
||||||
|
usePolling: !!cliOptions.poll,
|
||||||
|
interval: Number.isInteger(cliOptions.poll)
|
||||||
|
? (cliOptions.poll as number)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFileWatchers({
|
||||||
|
props,
|
||||||
|
cliOptions,
|
||||||
|
onFileChange,
|
||||||
|
}: {
|
||||||
|
props: Props;
|
||||||
|
cliOptions: StartCLIOptions;
|
||||||
|
onFileChange: () => void;
|
||||||
|
}) {
|
||||||
|
const {siteDir} = props;
|
||||||
|
const pathsToWatch = getPathsToWatch({props});
|
||||||
|
|
||||||
|
const pollingOptions = createPollingOptions({cliOptions});
|
||||||
|
const fsWatcher = chokidar.watch(pathsToWatch, {
|
||||||
|
cwd: siteDir,
|
||||||
|
ignoreInitial: true,
|
||||||
|
...{pollingOptions},
|
||||||
|
});
|
||||||
|
|
||||||
|
['add', 'change', 'unlink', 'addDir', 'unlinkDir'].forEach((event) =>
|
||||||
|
fsWatcher.on(event, onFileChange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathsToWatch({props}: {props: Props}): string[] {
|
||||||
|
const {siteDir, siteConfigPath, plugins, localizationDir} = props;
|
||||||
|
|
||||||
const normalizeToSiteDir = (filepath: string) => {
|
const normalizeToSiteDir = (filepath: string) => {
|
||||||
if (filepath && path.isAbsolute(filepath)) {
|
if (filepath && path.isAbsolute(filepath)) {
|
||||||
|
@ -98,100 +176,49 @@ export async function start(
|
||||||
return posixPath(filepath);
|
return posixPath(filepath);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pluginPaths = plugins
|
const pluginsPaths = plugins
|
||||||
.flatMap((plugin) => plugin.getPathsToWatch?.() ?? [])
|
.flatMap((plugin) => plugin.getPathsToWatch?.() ?? [])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(normalizeToSiteDir);
|
.map(normalizeToSiteDir);
|
||||||
|
|
||||||
const pathsToWatch = [...pluginPaths, props.siteConfigPath, localizationDir];
|
return [...pluginsPaths, siteConfigPath, localizationDir];
|
||||||
|
}
|
||||||
|
|
||||||
const pollingOptions = {
|
async function createUrlUtils({cliOptions}: {cliOptions: StartCLIOptions}) {
|
||||||
usePolling: !!cliOptions.poll,
|
const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||||
interval: Number.isInteger(cliOptions.poll)
|
|
||||||
? (cliOptions.poll as number)
|
const {host, port} = await getHostPort(cliOptions);
|
||||||
: undefined,
|
if (port === null) {
|
||||||
|
return process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOpenUrl = ({baseUrl}: {baseUrl: string}) => {
|
||||||
|
const urls = prepareUrls(protocol, host, port);
|
||||||
|
return normalizeUrl([urls.localUrlForBrowser, baseUrl]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {host, port, getOpenUrl};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDevServerConfig({
|
||||||
|
cliOptions,
|
||||||
|
props,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
}: {
|
||||||
|
cliOptions: StartCLIOptions;
|
||||||
|
props: Props;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}): Promise<WebpackDevServer.Configuration> {
|
||||||
|
const {baseUrl, siteDir, siteConfig} = props;
|
||||||
|
|
||||||
|
const pollingOptions = createPollingOptions({cliOptions});
|
||||||
|
|
||||||
const httpsConfig = await getHttpsConfig();
|
const httpsConfig = await getHttpsConfig();
|
||||||
const fsWatcher = chokidar.watch(pathsToWatch, {
|
|
||||||
cwd: siteDir,
|
|
||||||
ignoreInitial: true,
|
|
||||||
...{pollingOptions},
|
|
||||||
});
|
|
||||||
|
|
||||||
['add', 'change', 'unlink', 'addDir', 'unlinkDir'].forEach((event) =>
|
|
||||||
fsWatcher.on(event, reload),
|
|
||||||
);
|
|
||||||
|
|
||||||
let config: webpack.Configuration = merge(
|
|
||||||
await createClientConfig(props, cliOptions.minify, false),
|
|
||||||
{
|
|
||||||
watchOptions: {
|
|
||||||
ignored: /node_modules\/(?!@docusaurus)/,
|
|
||||||
poll: cliOptions.poll,
|
|
||||||
},
|
|
||||||
infrastructureLogging: {
|
|
||||||
// Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105
|
|
||||||
level: 'warn',
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// Generates an `index.html` file with the <script> injected.
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: path.join(
|
|
||||||
__dirname,
|
|
||||||
'../webpack/templates/index.html.template.ejs',
|
|
||||||
),
|
|
||||||
// So we can define the position where the scripts are injected.
|
|
||||||
inject: false,
|
|
||||||
filename: 'index.html',
|
|
||||||
title: siteConfig.title,
|
|
||||||
headTags,
|
|
||||||
preBodyTags,
|
|
||||||
postBodyTags,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Plugin Lifecycle - configureWebpack and configurePostCss.
|
|
||||||
plugins.forEach((plugin) => {
|
|
||||||
const {configureWebpack, configurePostCss} = plugin;
|
|
||||||
|
|
||||||
if (configurePostCss) {
|
|
||||||
config = applyConfigurePostCss(configurePostCss.bind(plugin), config);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configureWebpack) {
|
|
||||||
config = applyConfigureWebpack(
|
|
||||||
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
|
|
||||||
config,
|
|
||||||
false,
|
|
||||||
props.siteConfig.webpack?.jsLoader,
|
|
||||||
plugin.content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const compiler = webpack(config);
|
|
||||||
compiler.hooks.done.tap('done', (stats) => {
|
|
||||||
const errorsWarnings = stats.toJson('errors-warnings');
|
|
||||||
const statsErrorMessage = formatStatsErrorMessage(errorsWarnings);
|
|
||||||
if (statsErrorMessage) {
|
|
||||||
console.error(statsErrorMessage);
|
|
||||||
}
|
|
||||||
printStatsWarnings(errorsWarnings);
|
|
||||||
|
|
||||||
if (process.env.E2E_TEST) {
|
|
||||||
if (stats.hasErrors()) {
|
|
||||||
logger.error('E2E_TEST: Project has compiler errors.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
logger.success('E2E_TEST: Project can compile.');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://webpack.js.org/configuration/dev-server
|
// https://webpack.js.org/configuration/dev-server
|
||||||
const defaultDevServerConfig: WebpackDevServer.Configuration = {
|
return {
|
||||||
hot: cliOptions.hotOnly ? 'only' : true,
|
hot: cliOptions.hotOnly ? 'only' : true,
|
||||||
liveReload: false,
|
liveReload: false,
|
||||||
client: {
|
client: {
|
||||||
|
@ -245,23 +272,50 @@ export async function start(
|
||||||
return middlewares;
|
return middlewares;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Allow plugin authors to customize/override devServer config
|
// E2E_TEST=true docusaurus start
|
||||||
const devServerConfig: WebpackDevServer.Configuration = merge(
|
// Makes "docusaurus start" exit immediately on success/error, for E2E test
|
||||||
[defaultDevServerConfig, config.devServer].filter(Boolean),
|
function registerE2ETestHook(compiler: Compiler) {
|
||||||
);
|
compiler.hooks.done.tap('done', (stats) => {
|
||||||
|
const errorsWarnings = stats.toJson('errors-warnings');
|
||||||
const devServer = new WebpackDevServer(devServerConfig, compiler);
|
const statsErrorMessage = formatStatsErrorMessage(errorsWarnings);
|
||||||
devServer.startCallback(() => {
|
if (statsErrorMessage) {
|
||||||
if (cliOptions.open) {
|
console.error(statsErrorMessage);
|
||||||
openBrowser(openUrl);
|
}
|
||||||
|
printStatsWarnings(errorsWarnings);
|
||||||
|
if (process.env.E2E_TEST) {
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
logger.error('E2E_TEST: Project has compiler errors.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.success('E2E_TEST: Project can compile.');
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
['SIGINT', 'SIGTERM'].forEach((sig) => {
|
|
||||||
process.on(sig, () => {
|
async function getStartClientConfig({
|
||||||
devServer.stop();
|
props,
|
||||||
process.exit();
|
minify,
|
||||||
});
|
poll,
|
||||||
});
|
}: {
|
||||||
|
props: Props;
|
||||||
|
minify: boolean;
|
||||||
|
poll: number | boolean | undefined;
|
||||||
|
}) {
|
||||||
|
const {plugins, siteConfig} = props;
|
||||||
|
let {clientConfig: config} = await createStartClientConfig({
|
||||||
|
props,
|
||||||
|
minify,
|
||||||
|
poll,
|
||||||
|
});
|
||||||
|
config = executePluginsConfigurePostCss({plugins, config});
|
||||||
|
config = executePluginsConfigureWebpack({
|
||||||
|
plugins,
|
||||||
|
config,
|
||||||
|
isServer: false,
|
||||||
|
jsLoader: siteConfig.webpack?.jsLoader,
|
||||||
|
});
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
31
packages/docusaurus/src/common.d.ts
vendored
Normal file
31
packages/docusaurus/src/common.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This file is for types that are common between client/server
|
||||||
|
// In particular the interface between SSG and serverEntry code
|
||||||
|
|
||||||
|
import type {HelmetServerState} from 'react-helmet-async';
|
||||||
|
|
||||||
|
export type AppRenderResult = {
|
||||||
|
html: string;
|
||||||
|
collectedData: PageCollectedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppRenderer = (params: {
|
||||||
|
pathname: string;
|
||||||
|
}) => Promise<AppRenderResult>;
|
||||||
|
|
||||||
|
export type PageCollectedData = {
|
||||||
|
helmet: HelmetServerState;
|
||||||
|
links: string[];
|
||||||
|
anchors: string[];
|
||||||
|
modules: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SiteCollectedData = {
|
||||||
|
[pathname: string]: PageCollectedData;
|
||||||
|
};
|
40
packages/docusaurus/src/deps.d.ts
vendored
40
packages/docusaurus/src/deps.d.ts
vendored
|
@ -32,46 +32,6 @@ declare module 'react-loadable-ssr-addon-v5-slorber' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@slorber/static-site-generator-webpack-plugin' {
|
|
||||||
import type {WebpackPluginInstance, Compiler} from 'webpack';
|
|
||||||
import type {HelmetServerState} from 'react-helmet-async';
|
|
||||||
|
|
||||||
export type Locals = {
|
|
||||||
routesLocation: {[filePath: string]: string};
|
|
||||||
generatedFilesDir: string;
|
|
||||||
headTags: string;
|
|
||||||
preBodyTags: string;
|
|
||||||
postBodyTags: string;
|
|
||||||
onLinksCollected: (params: {
|
|
||||||
staticPagePath: string;
|
|
||||||
links: string[];
|
|
||||||
anchors: string[];
|
|
||||||
}) => void;
|
|
||||||
onHeadTagsCollected: (
|
|
||||||
staticPagePath: string,
|
|
||||||
tags: HelmetServerState,
|
|
||||||
) => void;
|
|
||||||
baseUrl: string;
|
|
||||||
ssrTemplate: string;
|
|
||||||
noIndex: boolean;
|
|
||||||
DOCUSAURUS_VERSION: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class StaticSiteGeneratorPlugin
|
|
||||||
implements WebpackPluginInstance
|
|
||||||
{
|
|
||||||
constructor(props: {
|
|
||||||
entry: string;
|
|
||||||
locals: Locals;
|
|
||||||
paths: string[];
|
|
||||||
preferFoldersOutput?: boolean;
|
|
||||||
globals: {[key: string]: unknown};
|
|
||||||
concurrency?: number;
|
|
||||||
});
|
|
||||||
apply(compiler: Compiler): void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'webpack/lib/HotModuleReplacementPlugin' {
|
declare module 'webpack/lib/HotModuleReplacementPlugin' {
|
||||||
import type {HotModuleReplacementPlugin} from 'webpack';
|
import type {HotModuleReplacementPlugin} from 'webpack';
|
||||||
|
|
||||||
|
|
270
packages/docusaurus/src/ssg.ts
Normal file
270
packages/docusaurus/src/ssg.ts
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import {createRequire} from 'module';
|
||||||
|
import path from 'path';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import evaluate from 'eval';
|
||||||
|
import pMap from 'p-map';
|
||||||
|
import {minify} from 'html-minifier-terser';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
|
import {PerfLogger} from './utils';
|
||||||
|
import {renderSSRTemplate} from './templates/templates';
|
||||||
|
import type {AppRenderer, AppRenderResult, SiteCollectedData} from './common';
|
||||||
|
|
||||||
|
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
||||||
|
import type {SSRTemplateCompiled} from './templates/templates';
|
||||||
|
|
||||||
|
export type SSGParams = {
|
||||||
|
trailingSlash: boolean | undefined;
|
||||||
|
manifest: Manifest;
|
||||||
|
headTags: string;
|
||||||
|
preBodyTags: string;
|
||||||
|
postBodyTags: string;
|
||||||
|
outDir: string;
|
||||||
|
baseUrl: string;
|
||||||
|
noIndex: boolean;
|
||||||
|
DOCUSAURUS_VERSION: string;
|
||||||
|
ssrTemplate: SSRTemplateCompiled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Secret way to set SSR plugin concurrency option
|
||||||
|
// Waiting for feedback before documenting this officially?
|
||||||
|
const Concurrency = process.env.DOCUSAURUS_SSR_CONCURRENCY
|
||||||
|
? parseInt(process.env.DOCUSAURUS_SSR_CONCURRENCY, 10)
|
||||||
|
: // Not easy to define a reasonable option default
|
||||||
|
// Will still be better than Infinity
|
||||||
|
// See also https://github.com/sindresorhus/p-map/issues/24
|
||||||
|
32;
|
||||||
|
|
||||||
|
export async function loadAppRenderer({
|
||||||
|
serverBundlePath,
|
||||||
|
}: {
|
||||||
|
serverBundlePath: string;
|
||||||
|
}): Promise<AppRenderer> {
|
||||||
|
console.log(`SSG - Load server bundle`);
|
||||||
|
PerfLogger.start(`SSG - Load server bundle`);
|
||||||
|
const source = await fs.readFile(serverBundlePath);
|
||||||
|
PerfLogger.end(`SSG - Load server bundle`);
|
||||||
|
PerfLogger.log(
|
||||||
|
`SSG - Server bundle size = ${(source.length / 1024000).toFixed(3)} MB`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filename = path.basename(serverBundlePath);
|
||||||
|
|
||||||
|
const globals = {
|
||||||
|
// When using "new URL('file.js', import.meta.url)", Webpack will emit
|
||||||
|
// __filename, and this plugin will throw. not sure the __filename value
|
||||||
|
// has any importance for this plugin, just using an empty string to
|
||||||
|
// avoid the error. See https://github.com/facebook/docusaurus/issues/4922
|
||||||
|
__filename: '',
|
||||||
|
|
||||||
|
// This uses module.createRequire() instead of very old "require-like" lib
|
||||||
|
// See also: https://github.com/pierrec/node-eval/issues/33
|
||||||
|
require: createRequire(serverBundlePath),
|
||||||
|
};
|
||||||
|
|
||||||
|
PerfLogger.start(`SSG - Evaluate server bundle`);
|
||||||
|
const serverEntry = evaluate(
|
||||||
|
source,
|
||||||
|
/* filename: */ filename,
|
||||||
|
/* scope: */ globals,
|
||||||
|
/* includeGlobals: */ true,
|
||||||
|
) as {default?: AppRenderer};
|
||||||
|
PerfLogger.end(`SSG - Evaluate server bundle`);
|
||||||
|
|
||||||
|
if (!serverEntry?.default || typeof serverEntry.default !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`Server bundle export from "${filename}" must be a function that renders the Docusaurus React app.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return serverEntry.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathnameToFilename({
|
||||||
|
pathname,
|
||||||
|
trailingSlash,
|
||||||
|
}: {
|
||||||
|
pathname: string;
|
||||||
|
trailingSlash?: boolean;
|
||||||
|
}): string {
|
||||||
|
const outputFileName = pathname.replace(/^[/\\]/, ''); // Remove leading slashes for webpack-dev-server
|
||||||
|
// Paths ending with .html are left untouched
|
||||||
|
if (/\.html?$/i.test(outputFileName)) {
|
||||||
|
return outputFileName;
|
||||||
|
}
|
||||||
|
// Legacy retro-compatible behavior
|
||||||
|
if (typeof trailingSlash === 'undefined') {
|
||||||
|
return path.join(outputFileName, 'index.html');
|
||||||
|
}
|
||||||
|
// New behavior: we can say if we prefer file/folder output
|
||||||
|
// Useful resource: https://github.com/slorber/trailing-slash-guide
|
||||||
|
if (pathname === '' || pathname.endsWith('/') || trailingSlash) {
|
||||||
|
return path.join(outputFileName, 'index.html');
|
||||||
|
}
|
||||||
|
return `${outputFileName}.html`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticFiles({
|
||||||
|
pathnames,
|
||||||
|
renderer,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
pathnames: string[];
|
||||||
|
renderer: AppRenderer;
|
||||||
|
params: SSGParams;
|
||||||
|
}): Promise<{collectedData: SiteCollectedData}> {
|
||||||
|
type SSGSuccess = {pathname: string; error: null; result: AppRenderResult};
|
||||||
|
type SSGError = {pathname: string; error: Error; result: null};
|
||||||
|
type SSGResult = SSGSuccess | SSGError;
|
||||||
|
|
||||||
|
// Note that we catch all async errors on purpose
|
||||||
|
// Docusaurus presents all the SSG errors to the user, not just the first one
|
||||||
|
const results: SSGResult[] = await pMap(
|
||||||
|
pathnames,
|
||||||
|
async (pathname) =>
|
||||||
|
generateStaticFile({
|
||||||
|
pathname,
|
||||||
|
renderer,
|
||||||
|
params,
|
||||||
|
}).then(
|
||||||
|
(result) => ({pathname, result, error: null}),
|
||||||
|
(error) => ({pathname, result: null, error: error as Error}),
|
||||||
|
),
|
||||||
|
{concurrency: Concurrency},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [allSSGErrors, allSSGSuccesses] = _.partition(
|
||||||
|
results,
|
||||||
|
(r): r is SSGError => !!r.error,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allSSGErrors.length > 0) {
|
||||||
|
const message = `Docusaurus static site generation failed for ${
|
||||||
|
allSSGErrors.length
|
||||||
|
} path${allSSGErrors.length ? 's' : ''}:\n- ${allSSGErrors
|
||||||
|
.map((ssgError) => logger.path(ssgError.pathname))
|
||||||
|
.join('\n- ')}`;
|
||||||
|
|
||||||
|
// Note logging this error properly require using inspect(error,{depth})
|
||||||
|
// See https://github.com/nodejs/node/issues/51637
|
||||||
|
throw new Error(message, {
|
||||||
|
cause: new AggregateError(allSSGErrors.map((ssgError) => ssgError.error)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectedData: SiteCollectedData = _.chain(allSSGSuccesses)
|
||||||
|
.keyBy((success) => success.pathname)
|
||||||
|
.mapValues((ssgSuccess) => ssgSuccess.result.collectedData)
|
||||||
|
.value();
|
||||||
|
|
||||||
|
return {collectedData};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateStaticFile({
|
||||||
|
pathname,
|
||||||
|
renderer,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
pathname: string;
|
||||||
|
renderer: AppRenderer;
|
||||||
|
params: SSGParams;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
// This only renders the app HTML
|
||||||
|
const result = await renderer({
|
||||||
|
pathname,
|
||||||
|
});
|
||||||
|
// This renders the full page HTML, including head tags...
|
||||||
|
const fullPageHtml = renderSSRTemplate({
|
||||||
|
params,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
const content = await minifyHtml(fullPageHtml);
|
||||||
|
await writeStaticFile({
|
||||||
|
pathname,
|
||||||
|
content,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (errorUnknown) {
|
||||||
|
const error = errorUnknown as Error;
|
||||||
|
const tips = getSSGErrorTips(error);
|
||||||
|
const message = logger.interpolate`Can't render static file for pathname path=${pathname}${
|
||||||
|
tips ? `\n\n${tips}` : ''
|
||||||
|
}`;
|
||||||
|
throw new Error(message, {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSSGErrorTips(error: Error): string {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
const isNotDefinedErrorRegex =
|
||||||
|
/(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;
|
||||||
|
if (isNotDefinedErrorRegex.test(error.message)) {
|
||||||
|
parts.push(`It looks like you are using code that should run on the client-side only.
|
||||||
|
To get around it, try using one of:
|
||||||
|
- ${logger.code('<BrowserOnly>')} (${logger.url(
|
||||||
|
'https://docusaurus.io/docs/docusaurus-core/#browseronly',
|
||||||
|
)})
|
||||||
|
- ${logger.code('ExecutionEnvironment')} (${logger.url(
|
||||||
|
'https://docusaurus.io/docs/docusaurus-core/#executionenvironment',
|
||||||
|
)}).
|
||||||
|
It might also require to wrap your client code in ${logger.code(
|
||||||
|
'useEffect',
|
||||||
|
)} hook and/or import a third-party library dynamically (if any).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeStaticFile({
|
||||||
|
content,
|
||||||
|
pathname,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
pathname: string;
|
||||||
|
params: SSGParams;
|
||||||
|
}) {
|
||||||
|
function removeBaseUrl(p: string, baseUrl: string): string {
|
||||||
|
return baseUrl === '/' ? p : p.replace(new RegExp(`^${baseUrl}`), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = pathnameToFilename({
|
||||||
|
pathname: removeBaseUrl(pathname, params.baseUrl),
|
||||||
|
trailingSlash: params.trailingSlash,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = path.join(params.outDir, filename);
|
||||||
|
await fs.ensureDir(path.dirname(filePath));
|
||||||
|
await fs.writeFile(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function minifyHtml(html: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
if (process.env.SKIP_HTML_MINIFICATION === 'true') {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
// Minify html with https://github.com/DanielRuf/html-minifier-terser
|
||||||
|
return await minify(html, {
|
||||||
|
removeComments: false,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
minifyJS: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('HTML minification failed', {cause: err as Error});
|
||||||
|
}
|
||||||
|
}
|
115
packages/docusaurus/src/templates/templates.ts
Normal file
115
packages/docusaurus/src/templates/templates.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as eta from 'eta';
|
||||||
|
import {getBundles} from 'react-loadable-ssr-addon-v5-slorber';
|
||||||
|
import type {SSGParams} from '../ssg';
|
||||||
|
import type {AppRenderResult} from '../common';
|
||||||
|
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
||||||
|
|
||||||
|
// TODO this is historical server template data
|
||||||
|
// that does not look super clean nor typesafe
|
||||||
|
// Note: changing it is a breaking change because template is configurable
|
||||||
|
export type SSRTemplateData = {
|
||||||
|
appHtml: string;
|
||||||
|
baseUrl: string;
|
||||||
|
htmlAttributes: string;
|
||||||
|
bodyAttributes: string;
|
||||||
|
headTags: string;
|
||||||
|
preBodyTags: string;
|
||||||
|
postBodyTags: string;
|
||||||
|
metaAttributes: string[];
|
||||||
|
scripts: string[];
|
||||||
|
stylesheets: string[];
|
||||||
|
noIndex: boolean;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSRTemplateCompiled = (data: SSRTemplateData) => string;
|
||||||
|
|
||||||
|
export async function compileSSRTemplate(
|
||||||
|
template: string,
|
||||||
|
): Promise<SSRTemplateCompiled> {
|
||||||
|
const compiledTemplate = eta.compile(template.trim(), {
|
||||||
|
rmWhitespace: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (data: SSRTemplateData) => compiledTemplate(data, eta.defaultConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of modules that were SSR an d
|
||||||
|
* @param modules
|
||||||
|
* @param manifest
|
||||||
|
*/
|
||||||
|
function getScriptsAndStylesheets({
|
||||||
|
modules,
|
||||||
|
manifest,
|
||||||
|
}: {
|
||||||
|
modules: string[];
|
||||||
|
manifest: 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);
|
||||||
|
return {scripts, stylesheets};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSSRTemplate({
|
||||||
|
params,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
params: SSGParams;
|
||||||
|
result: AppRenderResult;
|
||||||
|
}): string {
|
||||||
|
const {
|
||||||
|
baseUrl,
|
||||||
|
headTags,
|
||||||
|
preBodyTags,
|
||||||
|
postBodyTags,
|
||||||
|
manifest,
|
||||||
|
noIndex,
|
||||||
|
DOCUSAURUS_VERSION,
|
||||||
|
ssrTemplate,
|
||||||
|
} = params;
|
||||||
|
const {
|
||||||
|
html: appHtml,
|
||||||
|
collectedData: {modules, helmet},
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
const {scripts, stylesheets} = getScriptsAndStylesheets({manifest, modules});
|
||||||
|
|
||||||
|
const htmlAttributes = helmet.htmlAttributes.toString();
|
||||||
|
const bodyAttributes = helmet.bodyAttributes.toString();
|
||||||
|
const metaStrings = [
|
||||||
|
helmet.title.toString(),
|
||||||
|
helmet.meta.toString(),
|
||||||
|
helmet.link.toString(),
|
||||||
|
helmet.script.toString(),
|
||||||
|
];
|
||||||
|
const metaAttributes = metaStrings.filter(Boolean);
|
||||||
|
|
||||||
|
const data: SSRTemplateData = {
|
||||||
|
appHtml,
|
||||||
|
baseUrl,
|
||||||
|
htmlAttributes,
|
||||||
|
bodyAttributes,
|
||||||
|
headTags,
|
||||||
|
preBodyTags,
|
||||||
|
postBodyTags,
|
||||||
|
metaAttributes,
|
||||||
|
scripts,
|
||||||
|
stylesheets,
|
||||||
|
noIndex,
|
||||||
|
version: DOCUSAURUS_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ssrTemplate(data);
|
||||||
|
}
|
38
packages/docusaurus/src/utils.ts
Normal file
38
packages/docusaurus/src/utils.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
|
|
||||||
|
// For now this is a private env variable we use internally
|
||||||
|
// But we'll want to expose this feature officially some day
|
||||||
|
export const PerfDebuggingEnabled: boolean =
|
||||||
|
!!process.env.DOCUSAURUS_PERF_LOGGER;
|
||||||
|
|
||||||
|
type PerfLoggerAPI = {
|
||||||
|
start: (label: string) => void;
|
||||||
|
end: (label: string) => void;
|
||||||
|
log: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPerfLogger(): PerfLoggerAPI {
|
||||||
|
if (!PerfDebuggingEnabled) {
|
||||||
|
const noop = () => {};
|
||||||
|
return {
|
||||||
|
start: noop,
|
||||||
|
end: noop,
|
||||||
|
log: noop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = logger.yellow(`[PERF] `);
|
||||||
|
return {
|
||||||
|
start: (label) => console.time(prefix + label),
|
||||||
|
end: (label) => console.timeEnd(prefix + label),
|
||||||
|
log: (label) => console.log(prefix + label),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerfLogger: PerfLoggerAPI = createPerfLogger();
|
|
@ -105,8 +105,9 @@ describe('base webpack config', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates webpack aliases', async () => {
|
it('creates webpack aliases', async () => {
|
||||||
const aliases = ((await createBaseConfig(props, true)).resolve?.alias ??
|
const aliases = ((
|
||||||
{}) as {[alias: string]: string};
|
await createBaseConfig({props, isServer: true, minify: true})
|
||||||
|
).resolve?.alias ?? {}) as {[alias: string]: string};
|
||||||
// Make aliases relative so that test work on all computers
|
// Make aliases relative so that test work on all computers
|
||||||
const relativeAliases = _.mapValues(aliases, (a) =>
|
const relativeAliases = _.mapValues(aliases, (a) =>
|
||||||
posixPath(path.relative(props.siteDir, a)),
|
posixPath(path.relative(props.siteDir, a)),
|
||||||
|
@ -121,7 +122,7 @@ describe('base webpack config', () => {
|
||||||
.spyOn(utils, 'getFileLoaderUtils')
|
.spyOn(utils, 'getFileLoaderUtils')
|
||||||
.mockImplementation(() => fileLoaderUtils);
|
.mockImplementation(() => fileLoaderUtils);
|
||||||
|
|
||||||
await createBaseConfig(props, false, false);
|
await createBaseConfig({props, isServer: false, minify: false});
|
||||||
expect(mockSvg).toHaveBeenCalled();
|
expect(mockSvg).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,21 +7,31 @@
|
||||||
|
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
|
|
||||||
import createClientConfig from '../client';
|
import {createBuildClientConfig, createStartClientConfig} from '../client';
|
||||||
import {loadSetup} from '../../server/__tests__/testUtils';
|
import {loadSetup} from '../../server/__tests__/testUtils';
|
||||||
|
|
||||||
describe('webpack dev config', () => {
|
describe('webpack dev config', () => {
|
||||||
it('simple', async () => {
|
it('simple start', async () => {
|
||||||
const props = await loadSetup('simple-site');
|
const props = await loadSetup('simple-site');
|
||||||
const config = await createClientConfig(props);
|
const {clientConfig} = await createStartClientConfig({props});
|
||||||
const errors = webpack.validate(config);
|
webpack.validate(clientConfig);
|
||||||
expect(errors).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('custom', async () => {
|
it('simple build', async () => {
|
||||||
|
const props = await loadSetup('simple-site');
|
||||||
|
const {config} = await createBuildClientConfig({props});
|
||||||
|
webpack.validate(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom start', async () => {
|
||||||
const props = await loadSetup('custom-site');
|
const props = await loadSetup('custom-site');
|
||||||
const config = await createClientConfig(props);
|
const {clientConfig} = await createStartClientConfig({props});
|
||||||
const errors = webpack.validate(config);
|
webpack.validate(clientConfig);
|
||||||
expect(errors).toBeUndefined();
|
});
|
||||||
|
|
||||||
|
it('custom build', async () => {
|
||||||
|
const props = await loadSetup('custom-site');
|
||||||
|
const {config} = await createBuildClientConfig({props});
|
||||||
|
webpack.validate(config);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,24 +15,18 @@ describe('webpack production config', () => {
|
||||||
it('simple', async () => {
|
it('simple', async () => {
|
||||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
const props = await loadSetup('simple-site');
|
const props = await loadSetup('simple-site');
|
||||||
const config = await createServerConfig({
|
const {config} = await createServerConfig({
|
||||||
props,
|
props,
|
||||||
onHeadTagsCollected: () => {},
|
|
||||||
onLinksCollected: () => {},
|
|
||||||
});
|
});
|
||||||
const errors = webpack.validate(config);
|
webpack.validate(config);
|
||||||
expect(errors).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('custom', async () => {
|
it('custom', async () => {
|
||||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
const props = await loadSetup('custom-site');
|
const props = await loadSetup('custom-site');
|
||||||
const config = await createServerConfig({
|
const {config} = await createServerConfig({
|
||||||
props,
|
props,
|
||||||
onHeadTagsCollected: () => {},
|
|
||||||
onLinksCollected: () => {},
|
|
||||||
});
|
});
|
||||||
const errors = webpack.validate(config);
|
webpack.validate(config);
|
||||||
expect(errors).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,8 +13,8 @@ import {
|
||||||
getCustomizableJSLoader,
|
getCustomizableJSLoader,
|
||||||
getStyleLoaders,
|
getStyleLoaders,
|
||||||
getCustomBabelConfigFilePath,
|
getCustomBabelConfigFilePath,
|
||||||
getMinimizer,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import {getMinimizer} from './minification';
|
||||||
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
|
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
|
||||||
import type {Configuration} from 'webpack';
|
import type {Configuration} from 'webpack';
|
||||||
import type {Props} from '@docusaurus/types';
|
import type {Props} from '@docusaurus/types';
|
||||||
|
@ -44,11 +44,15 @@ export function excludeJS(modulePath: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBaseConfig(
|
export async function createBaseConfig({
|
||||||
props: Props,
|
props,
|
||||||
isServer: boolean,
|
isServer,
|
||||||
minify: boolean = true,
|
minify,
|
||||||
): Promise<Configuration> {
|
}: {
|
||||||
|
props: Props;
|
||||||
|
isServer: boolean;
|
||||||
|
minify: boolean;
|
||||||
|
}): Promise<Configuration> {
|
||||||
const {
|
const {
|
||||||
outDir,
|
outDir,
|
||||||
siteDir,
|
siteDir,
|
||||||
|
@ -62,8 +66,7 @@ export async function createBaseConfig(
|
||||||
} = props;
|
} = props;
|
||||||
const totalPages = routesPaths.length;
|
const totalPages = routesPaths.length;
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
const minimizeEnabled = minify && isProd && !isServer;
|
const minimizeEnabled = minify && isProd;
|
||||||
const useSimpleCssMinifier = process.env.USE_SIMPLE_CSS_MINIFIER === 'true';
|
|
||||||
|
|
||||||
const fileLoaderUtils = getFileLoaderUtils();
|
const fileLoaderUtils = getFileLoaderUtils();
|
||||||
|
|
||||||
|
@ -156,9 +159,7 @@ export async function createBaseConfig(
|
||||||
// Only minimize client bundle in production because server bundle is only
|
// Only minimize client bundle in production because server bundle is only
|
||||||
// used for static site generation
|
// used for static site generation
|
||||||
minimize: minimizeEnabled,
|
minimize: minimizeEnabled,
|
||||||
minimizer: minimizeEnabled
|
minimizer: minimizeEnabled ? getMinimizer() : undefined,
|
||||||
? getMinimizer(useSimpleCssMinifier)
|
|
||||||
: undefined,
|
|
||||||
splitChunks: isServer
|
splitChunks: isServer
|
||||||
? false
|
? false
|
||||||
: {
|
: {
|
||||||
|
|
|
@ -9,22 +9,47 @@ import path from 'path';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import merge from 'webpack-merge';
|
import merge from 'webpack-merge';
|
||||||
import WebpackBar from 'webpackbar';
|
import WebpackBar from 'webpackbar';
|
||||||
import {DefinePlugin} from 'webpack';
|
import webpack from 'webpack';
|
||||||
|
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
|
||||||
|
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
import {createBaseConfig} from './base';
|
import {createBaseConfig} from './base';
|
||||||
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
|
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
|
||||||
import {formatStatsErrorMessage} from './utils';
|
import {formatStatsErrorMessage} from './utils';
|
||||||
|
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
|
||||||
import type {Props} from '@docusaurus/types';
|
import type {Props} from '@docusaurus/types';
|
||||||
import type {Configuration} from 'webpack';
|
import type {Configuration} from 'webpack';
|
||||||
|
|
||||||
export default async function createClientConfig(
|
// When building, include the plugin to force terminate building if errors
|
||||||
props: Props,
|
// happened in the client bundle.
|
||||||
minify: boolean = true,
|
class ForceTerminatePlugin implements webpack.WebpackPluginInstance {
|
||||||
hydrate: boolean = true,
|
apply(compiler: webpack.Compiler) {
|
||||||
): Promise<Configuration> {
|
compiler.hooks.done.tap('client:done', (stats) => {
|
||||||
const isBuilding = process.argv[2] === 'build';
|
if (stats.hasErrors()) {
|
||||||
const config = await createBaseConfig(props, false, minify);
|
const errorsWarnings = stats.toJson('errors-warnings');
|
||||||
|
logger.error(
|
||||||
|
`Client bundle compiled with errors therefore further build is impossible.\n${formatStatsErrorMessage(
|
||||||
|
errorsWarnings,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const clientConfig = merge(config, {
|
async function createBaseClientConfig({
|
||||||
|
props,
|
||||||
|
hydrate,
|
||||||
|
minify,
|
||||||
|
}: {
|
||||||
|
props: Props;
|
||||||
|
hydrate: boolean;
|
||||||
|
minify: boolean;
|
||||||
|
}): Promise<Configuration> {
|
||||||
|
const baseConfig = await createBaseConfig({props, isServer: false, minify});
|
||||||
|
|
||||||
|
return merge(baseConfig, {
|
||||||
// Useless, disabled on purpose (errors on existing sites with no
|
// Useless, disabled on purpose (errors on existing sites with no
|
||||||
// browserslist config)
|
// browserslist config)
|
||||||
// target: 'browserslist',
|
// target: 'browserslist',
|
||||||
|
@ -35,7 +60,7 @@ export default async function createClientConfig(
|
||||||
runtimeChunk: true,
|
runtimeChunk: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.HYDRATE_CLIENT_ENTRY': JSON.stringify(hydrate),
|
'process.env.HYDRATE_CLIENT_ENTRY': JSON.stringify(hydrate),
|
||||||
}),
|
}),
|
||||||
new ChunkAssetPlugin(),
|
new ChunkAssetPlugin(),
|
||||||
|
@ -45,26 +70,89 @@ export default async function createClientConfig(
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// When building, include the plugin to force terminate building if errors
|
|
||||||
// happened in the client bundle.
|
// client config when running "docusaurus start"
|
||||||
if (isBuilding) {
|
export async function createStartClientConfig({
|
||||||
clientConfig.plugins?.push({
|
props,
|
||||||
apply: (compiler) => {
|
minify,
|
||||||
compiler.hooks.done.tap('client:done', (stats) => {
|
poll,
|
||||||
if (stats.hasErrors()) {
|
}: {
|
||||||
const errorsWarnings = stats.toJson('errors-warnings');
|
props: Props;
|
||||||
logger.error(
|
minify: boolean;
|
||||||
`Client bundle compiled with errors therefore further build is impossible.\n${formatStatsErrorMessage(
|
poll: number | boolean | undefined;
|
||||||
errorsWarnings,
|
}): Promise<{clientConfig: Configuration}> {
|
||||||
)}`,
|
const {siteConfig, headTags, preBodyTags, postBodyTags} = props;
|
||||||
);
|
|
||||||
process.exit(1);
|
const clientConfig: webpack.Configuration = merge(
|
||||||
}
|
await createBaseClientConfig({
|
||||||
});
|
props,
|
||||||
},
|
minify,
|
||||||
});
|
hydrate: false,
|
||||||
}
|
}),
|
||||||
|
{
|
||||||
return clientConfig;
|
watchOptions: {
|
||||||
|
ignored: /node_modules\/(?!@docusaurus)/,
|
||||||
|
poll,
|
||||||
|
},
|
||||||
|
infrastructureLogging: {
|
||||||
|
// Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105
|
||||||
|
level: 'warn',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Generates an `index.html` file with the <script> injected.
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: path.join(__dirname, '../templates/dev.html.template.ejs'),
|
||||||
|
// So we can define the position where the scripts are injected.
|
||||||
|
inject: false,
|
||||||
|
filename: 'index.html',
|
||||||
|
title: siteConfig.title,
|
||||||
|
headTags,
|
||||||
|
preBodyTags,
|
||||||
|
postBodyTags,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {clientConfig};
|
||||||
|
}
|
||||||
|
|
||||||
|
// client config when running "docusaurus build"
|
||||||
|
export async function createBuildClientConfig({
|
||||||
|
props,
|
||||||
|
minify,
|
||||||
|
bundleAnalyzer,
|
||||||
|
}: {
|
||||||
|
props: Props;
|
||||||
|
minify: boolean;
|
||||||
|
bundleAnalyzer: boolean;
|
||||||
|
}): Promise<{config: Configuration; clientManifestPath: string}> {
|
||||||
|
// Apply user webpack config.
|
||||||
|
const {generatedFilesDir} = props;
|
||||||
|
|
||||||
|
const clientManifestPath = path.join(
|
||||||
|
generatedFilesDir,
|
||||||
|
'client-manifest.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
const config: Configuration = merge(
|
||||||
|
await createBaseClientConfig({props, minify, hydrate: true}),
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
new ForceTerminatePlugin(),
|
||||||
|
// Remove/clean build folders before building bundles.
|
||||||
|
new CleanWebpackPlugin({verbose: false}),
|
||||||
|
// Visualize size of webpack output files with an interactive zoomable
|
||||||
|
// tree map.
|
||||||
|
bundleAnalyzer && new BundleAnalyzerPlugin(),
|
||||||
|
// Generate client manifests file that will be used for server bundle.
|
||||||
|
new ReactLoadableSSRAddon({
|
||||||
|
filename: clientManifestPath,
|
||||||
|
}),
|
||||||
|
].filter(<T>(x: T | undefined | false): x is T => Boolean(x)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {config, clientManifestPath};
|
||||||
}
|
}
|
||||||
|
|
102
packages/docusaurus/src/webpack/minification.ts
Normal file
102
packages/docusaurus/src/webpack/minification.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
|
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
||||||
|
import type {CustomOptions, CssNanoOptions} from 'css-minimizer-webpack-plugin';
|
||||||
|
import type {WebpackPluginInstance} from 'webpack';
|
||||||
|
|
||||||
|
// See https://github.com/webpack-contrib/terser-webpack-plugin#parallel
|
||||||
|
function getTerserParallel() {
|
||||||
|
let terserParallel: boolean | number = true;
|
||||||
|
if (process.env.TERSER_PARALLEL === 'false') {
|
||||||
|
terserParallel = false;
|
||||||
|
} else if (
|
||||||
|
process.env.TERSER_PARALLEL &&
|
||||||
|
parseInt(process.env.TERSER_PARALLEL, 10) > 0
|
||||||
|
) {
|
||||||
|
terserParallel = parseInt(process.env.TERSER_PARALLEL, 10);
|
||||||
|
}
|
||||||
|
return terserParallel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsMinifierPlugin() {
|
||||||
|
return new TerserPlugin({
|
||||||
|
parallel: getTerserParallel(),
|
||||||
|
terserOptions: {
|
||||||
|
parse: {
|
||||||
|
// We want uglify-js to parse ecma 8 code. However, we don't want it
|
||||||
|
// to apply any minification steps that turns valid ecma 5 code
|
||||||
|
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||||
|
// sections only apply transformations that are ecma 5 safe
|
||||||
|
// https://github.com/facebook/create-react-app/pull/4234
|
||||||
|
ecma: 2020,
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
ecma: 5,
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
safari10: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
ecma: 5,
|
||||||
|
comments: false,
|
||||||
|
// Turned on because emoji and regex is not minified properly using
|
||||||
|
// default. See https://github.com/facebook/create-react-app/issues/2488
|
||||||
|
ascii_only: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdvancedCssMinifier() {
|
||||||
|
// Using the array syntax to add 2 minimizers
|
||||||
|
// see https://github.com/webpack-contrib/css-minimizer-webpack-plugin#array
|
||||||
|
return new CssMinimizerPlugin<[CssNanoOptions, CustomOptions]>({
|
||||||
|
minimizerOptions: [
|
||||||
|
// CssNano options
|
||||||
|
{
|
||||||
|
preset: require.resolve('@docusaurus/cssnano-preset'),
|
||||||
|
},
|
||||||
|
// CleanCss options
|
||||||
|
{
|
||||||
|
inline: false,
|
||||||
|
level: {
|
||||||
|
1: {
|
||||||
|
all: false,
|
||||||
|
removeWhitespace: true,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
all: true,
|
||||||
|
restructureRules: true,
|
||||||
|
removeUnusedAtRules: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
minify: [
|
||||||
|
CssMinimizerPlugin.cssnanoMinify,
|
||||||
|
CssMinimizerPlugin.cleanCssMinify,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMinimizer(): WebpackPluginInstance[] {
|
||||||
|
// This is an historical env variable to opt-out of the advanced minifier
|
||||||
|
// Sometimes there's a bug in it and people are happy to disable it
|
||||||
|
const useSimpleCssMinifier = process.env.USE_SIMPLE_CSS_MINIFIER === 'true';
|
||||||
|
|
||||||
|
const minimizer: WebpackPluginInstance[] = [getJsMinifierPlugin()];
|
||||||
|
|
||||||
|
if (useSimpleCssMinifier) {
|
||||||
|
minimizer.push(new CssMinimizerPlugin());
|
||||||
|
} else {
|
||||||
|
minimizer.push(getAdvancedCssMinifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
return minimizer;
|
||||||
|
}
|
|
@ -6,104 +6,90 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import merge from 'webpack-merge';
|
import merge from 'webpack-merge';
|
||||||
import {
|
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
||||||
NODE_MAJOR_VERSION,
|
|
||||||
NODE_MINOR_VERSION,
|
|
||||||
DOCUSAURUS_VERSION,
|
|
||||||
} from '@docusaurus/utils';
|
|
||||||
// Forked for Docusaurus: https://github.com/slorber/static-site-generator-webpack-plugin
|
|
||||||
import StaticSiteGeneratorPlugin, {
|
|
||||||
type Locals,
|
|
||||||
} from '@slorber/static-site-generator-webpack-plugin';
|
|
||||||
import WebpackBar from 'webpackbar';
|
import WebpackBar from 'webpackbar';
|
||||||
|
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||||
import {createBaseConfig} from './base';
|
import {createBaseConfig} from './base';
|
||||||
import WaitPlugin from './plugins/WaitPlugin';
|
|
||||||
import ssrDefaultTemplate from './templates/ssr.html.template';
|
|
||||||
import type {Props} from '@docusaurus/types';
|
import type {Props} from '@docusaurus/types';
|
||||||
import type {Configuration} from 'webpack';
|
import type {Configuration} from 'webpack';
|
||||||
|
|
||||||
export default async function createServerConfig({
|
export default async function createServerConfig(params: {
|
||||||
props,
|
|
||||||
onLinksCollected,
|
|
||||||
onHeadTagsCollected,
|
|
||||||
}: Pick<Locals, 'onLinksCollected' | 'onHeadTagsCollected'> & {
|
|
||||||
props: Props;
|
props: Props;
|
||||||
}): Promise<Configuration> {
|
}): Promise<{config: Configuration; serverBundlePath: string}> {
|
||||||
const {
|
const {props} = params;
|
||||||
baseUrl,
|
|
||||||
routesPaths,
|
|
||||||
generatedFilesDir,
|
|
||||||
headTags,
|
|
||||||
preBodyTags,
|
|
||||||
postBodyTags,
|
|
||||||
siteConfig: {noIndex, trailingSlash, ssrTemplate},
|
|
||||||
} = props;
|
|
||||||
const config = await createBaseConfig(props, true);
|
|
||||||
|
|
||||||
const routesLocation: {[filePath: string]: string} = {};
|
const baseConfig = await createBaseConfig({
|
||||||
// Array of paths to be rendered. Relative to output directory
|
props,
|
||||||
const ssgPaths = routesPaths.map((str) => {
|
isServer: true,
|
||||||
const ssgPath =
|
|
||||||
baseUrl === '/' ? str : str.replace(new RegExp(`^${baseUrl}`), '/');
|
// Minification of server bundle reduces size but doubles bundle time :/
|
||||||
routesLocation[ssgPath] = str;
|
minify: false,
|
||||||
return ssgPath;
|
|
||||||
});
|
});
|
||||||
const serverConfig = merge(config, {
|
|
||||||
|
const outputFilename = 'server.bundle.js';
|
||||||
|
const serverBundlePath = path.join(props.outDir, outputFilename);
|
||||||
|
|
||||||
|
const config = merge(baseConfig, {
|
||||||
target: `node${NODE_MAJOR_VERSION}.${NODE_MINOR_VERSION}`,
|
target: `node${NODE_MAJOR_VERSION}.${NODE_MINOR_VERSION}`,
|
||||||
entry: {
|
entry: {
|
||||||
main: path.resolve(__dirname, '../client/serverEntry.js'),
|
main: path.resolve(__dirname, '../client/serverEntry.js'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: 'server.bundle.js',
|
filename: outputFilename,
|
||||||
libraryTarget: 'commonjs2',
|
libraryTarget: 'commonjs2',
|
||||||
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
|
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
|
||||||
globalObject: 'this',
|
globalObject: 'this',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// Wait until manifest from client bundle is generated
|
|
||||||
new WaitPlugin({
|
|
||||||
filepath: path.join(generatedFilesDir, 'client-manifest.json'),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Static site generator webpack plugin.
|
|
||||||
new StaticSiteGeneratorPlugin({
|
|
||||||
entry: 'main',
|
|
||||||
locals: {
|
|
||||||
baseUrl,
|
|
||||||
generatedFilesDir,
|
|
||||||
routesLocation,
|
|
||||||
headTags,
|
|
||||||
preBodyTags,
|
|
||||||
postBodyTags,
|
|
||||||
onLinksCollected,
|
|
||||||
onHeadTagsCollected,
|
|
||||||
ssrTemplate: ssrTemplate ?? ssrDefaultTemplate,
|
|
||||||
noIndex,
|
|
||||||
DOCUSAURUS_VERSION,
|
|
||||||
},
|
|
||||||
paths: ssgPaths,
|
|
||||||
preferFoldersOutput: trailingSlash,
|
|
||||||
|
|
||||||
// When using "new URL('file.js', import.meta.url)", Webpack will emit
|
|
||||||
// __filename, and this plugin will throw. not sure the __filename value
|
|
||||||
// has any importance for this plugin, just using an empty string to
|
|
||||||
// avoid the error. See https://github.com/facebook/docusaurus/issues/4922
|
|
||||||
globals: {__filename: ''},
|
|
||||||
|
|
||||||
// Secret way to set SSR plugin concurrency option
|
|
||||||
// Waiting for feedback before documenting this officially?
|
|
||||||
concurrency: process.env.DOCUSAURUS_SSR_CONCURRENCY
|
|
||||||
? parseInt(process.env.DOCUSAURUS_SSR_CONCURRENCY, 10)
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Show compilation progress bar.
|
// Show compilation progress bar.
|
||||||
new WebpackBar({
|
new WebpackBar({
|
||||||
name: 'Server',
|
name: 'Server',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
}),
|
}),
|
||||||
],
|
await createStaticDirectoriesCopyPlugin(params),
|
||||||
|
].filter(Boolean),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {config, serverBundlePath};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createStaticDirectoriesCopyPlugin({props}: {props: Props}) {
|
||||||
|
const {
|
||||||
|
outDir,
|
||||||
|
siteDir,
|
||||||
|
siteConfig: {staticDirectories: staticDirectoriesOption},
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// The staticDirectories option can contain empty directories, or non-existent
|
||||||
|
// directories (e.g. user deleted `static`). Instead of issuing an error, we
|
||||||
|
// just silently filter them out, because user could have never configured it
|
||||||
|
// in the first place (the default option should always "work").
|
||||||
|
const staticDirectories: string[] = (
|
||||||
|
await Promise.all(
|
||||||
|
staticDirectoriesOption.map(async (dir) => {
|
||||||
|
const staticDir = path.resolve(siteDir, dir);
|
||||||
|
if (
|
||||||
|
(await fs.pathExists(staticDir)) &&
|
||||||
|
(await fs.readdir(staticDir)).length > 0
|
||||||
|
) {
|
||||||
|
return staticDir;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter(Boolean);
|
||||||
|
|
||||||
|
if (staticDirectories.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CopyWebpackPlugin({
|
||||||
|
patterns: staticDirectories.map((dir) => ({
|
||||||
|
from: dir,
|
||||||
|
to: outDir,
|
||||||
|
toType: 'dir',
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
return serverConfig;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,20 +16,14 @@ import {
|
||||||
customizeArray,
|
customizeArray,
|
||||||
customizeObject,
|
customizeObject,
|
||||||
} from 'webpack-merge';
|
} from 'webpack-merge';
|
||||||
import webpack, {
|
import webpack, {type Configuration, type RuleSetRule} from 'webpack';
|
||||||
type Configuration,
|
|
||||||
type RuleSetRule,
|
|
||||||
type WebpackPluginInstance,
|
|
||||||
} from 'webpack';
|
|
||||||
import TerserPlugin from 'terser-webpack-plugin';
|
|
||||||
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
|
||||||
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
|
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
|
||||||
import type {CustomOptions, CssNanoOptions} from 'css-minimizer-webpack-plugin';
|
|
||||||
import type {TransformOptions} from '@babel/core';
|
import type {TransformOptions} from '@babel/core';
|
||||||
import type {
|
import type {
|
||||||
Plugin,
|
Plugin,
|
||||||
PostCssOptions,
|
PostCssOptions,
|
||||||
ConfigureWebpackUtils,
|
ConfigureWebpackUtils,
|
||||||
|
LoadedPlugin,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
export function formatStatsErrorMessage(
|
export function formatStatsErrorMessage(
|
||||||
|
@ -259,6 +253,57 @@ export function applyConfigurePostCss(
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin Lifecycle - configurePostCss()
|
||||||
|
export function executePluginsConfigurePostCss({
|
||||||
|
plugins,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
plugins: LoadedPlugin[];
|
||||||
|
config: Configuration;
|
||||||
|
}): Configuration {
|
||||||
|
let resultConfig = config;
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
const {configurePostCss} = plugin;
|
||||||
|
if (configurePostCss) {
|
||||||
|
resultConfig = applyConfigurePostCss(
|
||||||
|
configurePostCss.bind(plugin),
|
||||||
|
resultConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return resultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin Lifecycle - configureWebpack()
|
||||||
|
export function executePluginsConfigureWebpack({
|
||||||
|
plugins,
|
||||||
|
config,
|
||||||
|
isServer,
|
||||||
|
jsLoader,
|
||||||
|
}: {
|
||||||
|
plugins: LoadedPlugin[];
|
||||||
|
config: Configuration;
|
||||||
|
isServer: boolean;
|
||||||
|
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined;
|
||||||
|
}): Configuration {
|
||||||
|
let resultConfig = config;
|
||||||
|
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
const {configureWebpack} = plugin;
|
||||||
|
if (configureWebpack) {
|
||||||
|
resultConfig = applyConfigureWebpack(
|
||||||
|
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
|
||||||
|
resultConfig,
|
||||||
|
isServer,
|
||||||
|
jsLoader,
|
||||||
|
plugin.content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return resultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Error {
|
interface Error {
|
||||||
/** @see https://webpack.js.org/api/node/#error-handling */
|
/** @see https://webpack.js.org/api/node/#error-handling */
|
||||||
|
@ -266,7 +311,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compile(config: Configuration[]): Promise<void> {
|
export function compile(config: Configuration[]): Promise<webpack.MultiStats> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const compiler = webpack(config);
|
const compiler = webpack(config);
|
||||||
compiler.run((err, stats) => {
|
compiler.run((err, stats) => {
|
||||||
|
@ -296,7 +341,7 @@ export function compile(config: Configuration[]): Promise<void> {
|
||||||
logger.error(`Error while closing Webpack compiler: ${errClose}`);
|
logger.error(`Error while closing Webpack compiler: ${errClose}`);
|
||||||
reject(errClose);
|
reject(errClose);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve(stats!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -366,87 +411,3 @@ export async function getHttpsConfig(): Promise<
|
||||||
}
|
}
|
||||||
return isHttps;
|
return isHttps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/webpack-contrib/terser-webpack-plugin#parallel
|
|
||||||
function getTerserParallel() {
|
|
||||||
let terserParallel: boolean | number = true;
|
|
||||||
if (process.env.TERSER_PARALLEL === 'false') {
|
|
||||||
terserParallel = false;
|
|
||||||
} else if (
|
|
||||||
process.env.TERSER_PARALLEL &&
|
|
||||||
parseInt(process.env.TERSER_PARALLEL, 10) > 0
|
|
||||||
) {
|
|
||||||
terserParallel = parseInt(process.env.TERSER_PARALLEL, 10);
|
|
||||||
}
|
|
||||||
return terserParallel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMinimizer(
|
|
||||||
useSimpleCssMinifier = false,
|
|
||||||
): WebpackPluginInstance[] {
|
|
||||||
const minimizer: WebpackPluginInstance[] = [
|
|
||||||
new TerserPlugin({
|
|
||||||
parallel: getTerserParallel(),
|
|
||||||
terserOptions: {
|
|
||||||
parse: {
|
|
||||||
// We want uglify-js to parse ecma 8 code. However, we don't want it
|
|
||||||
// to apply any minification steps that turns valid ecma 5 code
|
|
||||||
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
|
||||||
// sections only apply transformations that are ecma 5 safe
|
|
||||||
// https://github.com/facebook/create-react-app/pull/4234
|
|
||||||
ecma: 2020,
|
|
||||||
},
|
|
||||||
compress: {
|
|
||||||
ecma: 5,
|
|
||||||
},
|
|
||||||
mangle: {
|
|
||||||
safari10: true,
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
ecma: 5,
|
|
||||||
comments: false,
|
|
||||||
// Turned on because emoji and regex is not minified properly using
|
|
||||||
// default. See https://github.com/facebook/create-react-app/issues/2488
|
|
||||||
ascii_only: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
if (useSimpleCssMinifier) {
|
|
||||||
minimizer.push(new CssMinimizerPlugin());
|
|
||||||
} else {
|
|
||||||
minimizer.push(
|
|
||||||
// Using the array syntax to add 2 minimizers
|
|
||||||
// see https://github.com/webpack-contrib/css-minimizer-webpack-plugin#array
|
|
||||||
new CssMinimizerPlugin<[CssNanoOptions, CustomOptions]>({
|
|
||||||
minimizerOptions: [
|
|
||||||
// CssNano options
|
|
||||||
{
|
|
||||||
preset: require.resolve('@docusaurus/cssnano-preset'),
|
|
||||||
},
|
|
||||||
// CleanCss options
|
|
||||||
{
|
|
||||||
inline: false,
|
|
||||||
level: {
|
|
||||||
1: {
|
|
||||||
all: false,
|
|
||||||
removeWhitespace: true,
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
all: true,
|
|
||||||
restructureRules: true,
|
|
||||||
removeUnusedAtRules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
minify: [
|
|
||||||
CssMinimizerPlugin.cssnanoMinify,
|
|
||||||
CssMinimizerPlugin.cleanCssMinify,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return minimizer;
|
|
||||||
}
|
|
||||||
|
|
|
@ -363,6 +363,7 @@ triaging
|
||||||
TSES
|
TSES
|
||||||
twoslash
|
twoslash
|
||||||
typecheck
|
typecheck
|
||||||
|
typesafe
|
||||||
Typesense
|
Typesense
|
||||||
typesense
|
typesense
|
||||||
Unavatar
|
Unavatar
|
||||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -2746,15 +2746,6 @@
|
||||||
micromark-util-character "^1.1.0"
|
micromark-util-character "^1.1.0"
|
||||||
micromark-util-symbol "^1.0.1"
|
micromark-util-symbol "^1.0.1"
|
||||||
|
|
||||||
"@slorber/static-site-generator-webpack-plugin@^4.0.7":
|
|
||||||
version "4.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz#fc1678bddefab014e2145cbe25b3ce4e1cfc36f3"
|
|
||||||
integrity sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==
|
|
||||||
dependencies:
|
|
||||||
eval "^0.1.8"
|
|
||||||
p-map "^4.0.0"
|
|
||||||
webpack-sources "^3.2.2"
|
|
||||||
|
|
||||||
"@surma/rollup-plugin-off-main-thread@^2.2.3":
|
"@surma/rollup-plugin-off-main-thread@^2.2.3":
|
||||||
version "2.2.3"
|
version "2.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
|
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
|
||||||
|
@ -16602,7 +16593,7 @@ webpack-merge@^5.9.0:
|
||||||
clone-deep "^4.0.1"
|
clone-deep "^4.0.1"
|
||||||
wildcard "^2.0.0"
|
wildcard "^2.0.0"
|
||||||
|
|
||||||
webpack-sources@^3.2.2, webpack-sources@^3.2.3:
|
webpack-sources@^3.2.3:
|
||||||
version "3.2.3"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
Loading…
Add table
Reference in a new issue