mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +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
|
||||
|
||||
import {inspect} from 'node:util';
|
||||
import logger from '@docusaurus/logger';
|
||||
import cli from 'commander';
|
||||
import {DOCUSAURUS_VERSION} from '@docusaurus/utils';
|
||||
|
@ -61,8 +62,6 @@ cli
|
|||
'--no-minify',
|
||||
'build website without minimizing JS bundles (default: false)',
|
||||
)
|
||||
// @ts-expect-error: Promise<string> is not assignable to Promise<void>... but
|
||||
// good enough here.
|
||||
.action(build);
|
||||
|
||||
cli
|
||||
|
@ -269,9 +268,11 @@ cli.parse(process.argv);
|
|||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.log('');
|
||||
// Do not use logger.error here: it does not print error causes
|
||||
console.error(err);
|
||||
console.log('');
|
||||
|
||||
// We need to use inspect with increased depth to log the full causal chain
|
||||
// By default Node logging has depth=2
|
||||
// see also https://github.com/nodejs/node/issues/51637
|
||||
logger.error(inspect(err, {depth: Infinity}));
|
||||
|
||||
logger.info`Docusaurus version: number=${DOCUSAURUS_VERSION}
|
||||
Node version: number=${process.version}`;
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
"@docusaurus/utils": "3.0.0",
|
||||
"@docusaurus/utils-common": "3.0.0",
|
||||
"@docusaurus/utils-validation": "3.0.0",
|
||||
"@slorber/static-site-generator-webpack-plugin": "^4.0.7",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.3",
|
||||
|
@ -70,6 +69,7 @@
|
|||
"del": "^6.1.1",
|
||||
"detect-port": "^1.5.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"eval": "^0.1.8",
|
||||
"eta": "^2.2.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
|
@ -79,6 +79,7 @@
|
|||
"leven": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"p-map": "^4.0.0",
|
||||
"postcss": "^8.4.26",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"prompts": "^2.4.2",
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {ReactNode} from 'react';
|
|||
import {renderToPipeableStream} from 'react-dom/server';
|
||||
import {Writable} from 'stream';
|
||||
|
||||
export async function renderStaticApp(app: ReactNode): Promise<string> {
|
||||
export async function renderToHtml(app: ReactNode): Promise<string> {
|
||||
// Inspired from
|
||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
|
|
@ -6,103 +6,31 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import _ from 'lodash';
|
||||
import * as eta from 'eta';
|
||||
import {StaticRouter} from 'react-router-dom';
|
||||
import {HelmetProvider, type FilledContext} from 'react-helmet-async';
|
||||
import {getBundles, type Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
||||
import Loadable from 'react-loadable';
|
||||
import {minify} from 'html-minifier-terser';
|
||||
import {renderStaticApp} from './serverRenderer';
|
||||
import {renderToHtml} from './renderToHtml';
|
||||
import preload from './preload';
|
||||
import App from './App';
|
||||
import {
|
||||
createStatefulBrokenLinks,
|
||||
BrokenLinksProvider,
|
||||
} from './BrokenLinksContext';
|
||||
import type {Locals} from '@slorber/static-site-generator-webpack-plugin';
|
||||
import type {PageCollectedData, AppRenderer} from '../common';
|
||||
|
||||
const getCompiledSSRTemplate = _.memoize((template: string) =>
|
||||
eta.compile(template.trim(), {
|
||||
rmWhitespace: true,
|
||||
}),
|
||||
);
|
||||
const render: AppRenderer = async ({pathname}) => {
|
||||
await preload(pathname);
|
||||
|
||||
function renderSSRTemplate(ssrTemplate: string, data: object) {
|
||||
const compiled = getCompiledSSRTemplate(ssrTemplate);
|
||||
return compiled(data, eta.defaultConfig);
|
||||
}
|
||||
|
||||
function buildSSRErrorMessage({
|
||||
error,
|
||||
pathname,
|
||||
}: {
|
||||
error: Error;
|
||||
pathname: string;
|
||||
}): string {
|
||||
const parts = [
|
||||
`Docusaurus server-side rendering could not render static page with path ${pathname} because of error: ${error.message}`,
|
||||
];
|
||||
|
||||
const isNotDefinedErrorRegex =
|
||||
/(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;
|
||||
|
||||
if (isNotDefinedErrorRegex.test(error.message)) {
|
||||
// prettier-ignore
|
||||
parts.push(`It looks like you are using code that should run on the client-side only.
|
||||
To get around it, try using \`<BrowserOnly>\` (https://docusaurus.io/docs/docusaurus-core/#browseronly) or \`ExecutionEnvironment\` (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).
|
||||
It might also require to wrap your client code in \`useEffect\` hook and/or import a third-party library dynamically (if any).`);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
export default async function render(
|
||||
locals: Locals & {path: string},
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await doRender(locals);
|
||||
} catch (errorUnknown) {
|
||||
const error = errorUnknown as Error;
|
||||
const message = buildSSRErrorMessage({error, pathname: locals.path});
|
||||
const ssrError = new Error(message, {cause: error});
|
||||
// It is important to log the error here because the stacktrace causal chain
|
||||
// is not available anymore upper in the tree (this SSR runs in eval)
|
||||
console.error(ssrError);
|
||||
throw ssrError;
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer for static-site-generator-webpack-plugin (async rendering).
|
||||
async function doRender(locals: Locals & {path: string}) {
|
||||
const {
|
||||
routesLocation,
|
||||
headTags,
|
||||
preBodyTags,
|
||||
postBodyTags,
|
||||
onLinksCollected,
|
||||
onHeadTagsCollected,
|
||||
baseUrl,
|
||||
ssrTemplate,
|
||||
noIndex,
|
||||
DOCUSAURUS_VERSION,
|
||||
} = locals;
|
||||
const location = routesLocation[locals.path]!;
|
||||
await preload(location);
|
||||
const modules = new Set<string>();
|
||||
const routerContext = {};
|
||||
const helmetContext = {};
|
||||
|
||||
const statefulBrokenLinks = createStatefulBrokenLinks();
|
||||
|
||||
const app = (
|
||||
// @ts-expect-error: we are migrating away from react-loadable anyways
|
||||
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
||||
<HelmetProvider context={helmetContext}>
|
||||
<StaticRouter location={location} context={routerContext}>
|
||||
<StaticRouter location={pathname} context={routerContext}>
|
||||
<BrokenLinksProvider brokenLinks={statefulBrokenLinks}>
|
||||
<App />
|
||||
</BrokenLinksProvider>
|
||||
|
@ -111,75 +39,16 @@ async function doRender(locals: Locals & {path: string}) {
|
|||
</Loadable.Capture>
|
||||
);
|
||||
|
||||
const appHtml = await renderStaticApp(app);
|
||||
onLinksCollected({
|
||||
staticPagePath: location,
|
||||
const html = await renderToHtml(app);
|
||||
|
||||
const collectedData: PageCollectedData = {
|
||||
helmet: (helmetContext as FilledContext).helmet,
|
||||
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
||||
links: statefulBrokenLinks.getCollectedLinks(),
|
||||
});
|
||||
modules: Array.from(modules),
|
||||
};
|
||||
|
||||
const {helmet} = helmetContext as FilledContext;
|
||||
const htmlAttributes = helmet.htmlAttributes.toString();
|
||||
const bodyAttributes = helmet.bodyAttributes.toString();
|
||||
const metaStrings = [
|
||||
helmet.title.toString(),
|
||||
helmet.meta.toString(),
|
||||
helmet.link.toString(),
|
||||
helmet.script.toString(),
|
||||
];
|
||||
onHeadTagsCollected(location, helmet);
|
||||
const metaAttributes = metaStrings.filter(Boolean);
|
||||
return {html, collectedData};
|
||||
};
|
||||
|
||||
const {generatedFilesDir} = locals;
|
||||
const manifestPath = path.join(generatedFilesDir, 'client-manifest.json');
|
||||
// Using readJSON seems to fail for users of some plugins, possibly because of
|
||||
// the eval sandbox having a different `Buffer` instance (native one instead
|
||||
// of polyfilled one)
|
||||
const manifest = (await fs
|
||||
.readFile(manifestPath, 'utf-8')
|
||||
.then(JSON.parse)) as Manifest;
|
||||
|
||||
// Get all required assets for this particular page based on client
|
||||
// manifest information.
|
||||
const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)];
|
||||
const bundles = getBundles(manifest, modulesToBeLoaded);
|
||||
const stylesheets = (bundles.css ?? []).map((b) => b.file);
|
||||
const scripts = (bundles.js ?? []).map((b) => b.file);
|
||||
|
||||
const renderedHtml = renderSSRTemplate(ssrTemplate, {
|
||||
appHtml,
|
||||
baseUrl,
|
||||
htmlAttributes,
|
||||
bodyAttributes,
|
||||
headTags,
|
||||
preBodyTags,
|
||||
postBodyTags,
|
||||
metaAttributes,
|
||||
scripts,
|
||||
stylesheets,
|
||||
noIndex,
|
||||
version: DOCUSAURUS_VERSION,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
export default render;
|
||||
|
|
|
@ -7,27 +7,29 @@
|
|||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {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 {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils';
|
||||
import {load, loadContext, type LoadContextOptions} from '../server';
|
||||
import {handleBrokenLinks} from '../server/brokenLinks';
|
||||
|
||||
import createClientConfig from '../webpack/client';
|
||||
import {createBuildClientConfig} from '../webpack/client';
|
||||
import createServerConfig from '../webpack/server';
|
||||
import {
|
||||
applyConfigurePostCss,
|
||||
applyConfigureWebpack,
|
||||
executePluginsConfigurePostCss,
|
||||
executePluginsConfigureWebpack,
|
||||
compile,
|
||||
} from '../webpack/utils';
|
||||
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
|
||||
import {PerfLogger} from '../utils';
|
||||
|
||||
import {loadI18n} from '../server/i18n';
|
||||
import type {HelmetServerState} from 'react-helmet-async';
|
||||
import type {Configuration} from 'webpack';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
import {generateStaticFiles, loadAppRenderer} from '../ssg';
|
||||
import {compileSSRTemplate} from '../templates/templates';
|
||||
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<
|
||||
LoadContextOptions,
|
||||
|
@ -46,7 +48,7 @@ export async function build(
|
|||
// deploy, we have to let deploy finish.
|
||||
// See https://github.com/facebook/docusaurus/pull/2496
|
||||
forceTerminate: boolean = true,
|
||||
): Promise<string> {
|
||||
): Promise<void> {
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale;
|
||||
|
@ -70,13 +72,15 @@ export async function build(
|
|||
isLastLocale: boolean;
|
||||
}) {
|
||||
try {
|
||||
return await buildLocale({
|
||||
PerfLogger.start(`Building site for locale ${locale}`);
|
||||
await buildLocale({
|
||||
siteDir,
|
||||
locale,
|
||||
cliOptions,
|
||||
forceTerminate,
|
||||
isLastLocale,
|
||||
});
|
||||
PerfLogger.end(`Building site for locale ${locale}`);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
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({
|
||||
siteDir,
|
||||
outDir: cliOptions.outDir,
|
||||
|
@ -96,26 +128,16 @@ export async function build(
|
|||
const i18n = await loadI18n(context.siteConfig, {
|
||||
locale: cliOptions.locale,
|
||||
});
|
||||
if (cliOptions.locale) {
|
||||
return tryToBuildLocale({locale: cliOptions.locale, isLastLocale: true});
|
||||
}
|
||||
if (i18n.locales.length > 1) {
|
||||
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
|
||||
// last, it would "erase" the localized sites built in sub-folders
|
||||
const orderedLocales: [string, ...string[]] = [
|
||||
return [
|
||||
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({
|
||||
|
@ -138,6 +160,7 @@ async function buildLocale({
|
|||
|
||||
logger.info`name=${`[${locale}]`} Creating an optimized production build...`;
|
||||
|
||||
PerfLogger.start('Loading site');
|
||||
const props: Props = await load({
|
||||
siteDir,
|
||||
outDir: cliOptions.outDir,
|
||||
|
@ -145,156 +168,59 @@ async function buildLocale({
|
|||
locale,
|
||||
localizePath: cliOptions.locale ? false : undefined,
|
||||
});
|
||||
PerfLogger.end('Loading site');
|
||||
|
||||
// Apply user webpack config.
|
||||
const {
|
||||
outDir,
|
||||
generatedFilesDir,
|
||||
plugins,
|
||||
siteConfig: {
|
||||
onBrokenLinks,
|
||||
onBrokenAnchors,
|
||||
staticDirectories: staticDirectoriesOption,
|
||||
},
|
||||
routes,
|
||||
} = props;
|
||||
const {outDir, plugins} = props;
|
||||
|
||||
const clientManifestPath = path.join(
|
||||
generatedFilesDir,
|
||||
'client-manifest.json',
|
||||
);
|
||||
let clientConfig: Configuration = merge(
|
||||
await createClientConfig(props, cliOptions.minify, true),
|
||||
{
|
||||
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 '';
|
||||
// We can build the 2 configs in parallel
|
||||
PerfLogger.start('Creating webpack configs');
|
||||
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
|
||||
await Promise.all([
|
||||
getBuildClientConfig({
|
||||
props,
|
||||
cliOptions,
|
||||
}),
|
||||
)
|
||||
).filter(Boolean);
|
||||
getBuildServerConfig({
|
||||
props,
|
||||
}),
|
||||
]);
|
||||
PerfLogger.end('Creating webpack configs');
|
||||
|
||||
if (staticDirectories.length > 0) {
|
||||
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
|
||||
// Make sure generated client-manifest is cleaned first, so we don't reuse
|
||||
// the one from previous builds.
|
||||
if (await fs.pathExists(clientManifestPath)) {
|
||||
await fs.unlink(clientManifestPath);
|
||||
}
|
||||
// TODO do we really need this? .docusaurus folder is cleaned between builds
|
||||
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).
|
||||
PerfLogger.start('Bundling');
|
||||
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.
|
||||
if (typeof serverConfig.output?.filename === 'string') {
|
||||
const serverBundle = path.join(outDir, serverConfig.output.filename);
|
||||
if (await fs.pathExists(serverBundle)) {
|
||||
await fs.unlink(serverBundle);
|
||||
}
|
||||
}
|
||||
PerfLogger.start('Deleting server bundle');
|
||||
await ensureUnlink(serverBundlePath);
|
||||
PerfLogger.end('Deleting server bundle');
|
||||
|
||||
// Plugin Lifecycle - postBuild.
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
if (!plugin.postBuild) {
|
||||
return;
|
||||
}
|
||||
await plugin.postBuild({
|
||||
...props,
|
||||
head: headTags,
|
||||
content: plugin.content,
|
||||
});
|
||||
}),
|
||||
);
|
||||
PerfLogger.start('Executing postBuild()');
|
||||
await executePluginsPostBuild({plugins, props, collectedData});
|
||||
PerfLogger.end('Executing postBuild()');
|
||||
|
||||
await handleBrokenLinks({
|
||||
collectedLinks,
|
||||
routes,
|
||||
onBrokenLinks,
|
||||
onBrokenAnchors,
|
||||
});
|
||||
// TODO execute this in parallel to postBuild?
|
||||
PerfLogger.start('Executing broken links checker');
|
||||
await executeBrokenLinksCheck({props, collectedData});
|
||||
PerfLogger.end('Executing broken links checker');
|
||||
|
||||
logger.success`Generated static files in path=${path.relative(
|
||||
process.cwd(),
|
||||
|
@ -311,3 +237,144 @@ async function buildLocale({
|
|||
|
||||
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) {
|
||||
// Build site, then push to deploymentBranch branch of specified repo.
|
||||
try {
|
||||
await build(siteDir, cliOptions, false).then(runDeploy);
|
||||
await build(siteDir, cliOptions, false).then(() => runDeploy(outDir));
|
||||
} catch (err) {
|
||||
logger.error('Deployment of the build output failed.');
|
||||
throw err;
|
||||
|
|
|
@ -31,14 +31,14 @@ export async function serve(
|
|||
const siteDir = await fs.realpath(siteDirParam);
|
||||
|
||||
const buildDir = cliOptions.dir ?? DEFAULT_BUILD_DIR_NAME;
|
||||
let dir = path.resolve(siteDir, buildDir);
|
||||
const outDir = path.resolve(siteDir, buildDir);
|
||||
|
||||
if (cliOptions.build) {
|
||||
dir = await build(
|
||||
await build(
|
||||
siteDir,
|
||||
{
|
||||
config: cliOptions.config,
|
||||
outDir: dir,
|
||||
outDir,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
@ -75,7 +75,7 @@ export async function serve(
|
|||
|
||||
serveHandler(req, res, {
|
||||
cleanUrls: true,
|
||||
public: dir,
|
||||
public: outDir,
|
||||
trailingSlash,
|
||||
directoryListing: false,
|
||||
});
|
||||
|
|
|
@ -11,7 +11,6 @@ import _ from 'lodash';
|
|||
import logger from '@docusaurus/logger';
|
||||
import {normalizeUrl, posixPath} from '@docusaurus/utils';
|
||||
import chokidar from 'chokidar';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils';
|
||||
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
|
||||
|
@ -19,15 +18,18 @@ import webpack from 'webpack';
|
|||
import WebpackDevServer from 'webpack-dev-server';
|
||||
import merge from 'webpack-merge';
|
||||
import {load, type LoadContextOptions} from '../server';
|
||||
import createClientConfig from '../webpack/client';
|
||||
import {createStartClientConfig} from '../webpack/client';
|
||||
import {
|
||||
applyConfigureWebpack,
|
||||
applyConfigurePostCss,
|
||||
getHttpsConfig,
|
||||
formatStatsErrorMessage,
|
||||
printStatsWarnings,
|
||||
executePluginsConfigurePostCss,
|
||||
executePluginsConfigureWebpack,
|
||||
} from '../webpack/utils';
|
||||
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 &
|
||||
Pick<LoadContextOptions, 'locale' | 'config'> & {
|
||||
|
@ -50,29 +52,23 @@ export async function start(
|
|||
|
||||
logger.info('Starting the development server...');
|
||||
|
||||
function loadSite() {
|
||||
return load({
|
||||
async function loadSite() {
|
||||
PerfLogger.start('Loading site');
|
||||
const result = await load({
|
||||
siteDir,
|
||||
config: cliOptions.config,
|
||||
locale: cliOptions.locale,
|
||||
localizePath: undefined, // Should this be configurable?
|
||||
});
|
||||
PerfLogger.end('Loading site');
|
||||
return result;
|
||||
}
|
||||
|
||||
// Process all related files as a prop.
|
||||
const props = await loadSite();
|
||||
|
||||
const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
|
||||
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]);
|
||||
const {host, port, getOpenUrl} = await createUrlUtils({cliOptions});
|
||||
const openUrl = getOpenUrl({baseUrl: props.baseUrl});
|
||||
|
||||
logger.success`Docusaurus website is running at: url=${openUrl}`;
|
||||
|
||||
|
@ -80,7 +76,7 @@ export async function start(
|
|||
const reload = _.debounce(() => {
|
||||
loadSite()
|
||||
.then(({baseUrl: newBaseUrl}) => {
|
||||
const newOpenUrl = normalizeUrl([urls.localUrlForBrowser, newBaseUrl]);
|
||||
const newOpenUrl = getOpenUrl({baseUrl: newBaseUrl});
|
||||
if (newOpenUrl !== openUrl) {
|
||||
logger.success`Docusaurus website is running at: url=${newOpenUrl}`;
|
||||
}
|
||||
|
@ -89,7 +85,89 @@ export async function start(
|
|||
logger.error(err.stack);
|
||||
});
|
||||
}, 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) => {
|
||||
if (filepath && path.isAbsolute(filepath)) {
|
||||
|
@ -98,100 +176,49 @@ export async function start(
|
|||
return posixPath(filepath);
|
||||
};
|
||||
|
||||
const pluginPaths = plugins
|
||||
const pluginsPaths = plugins
|
||||
.flatMap((plugin) => plugin.getPathsToWatch?.() ?? [])
|
||||
.filter(Boolean)
|
||||
.map(normalizeToSiteDir);
|
||||
|
||||
const pathsToWatch = [...pluginPaths, props.siteConfigPath, localizationDir];
|
||||
return [...pluginsPaths, siteConfigPath, localizationDir];
|
||||
}
|
||||
|
||||
const pollingOptions = {
|
||||
usePolling: !!cliOptions.poll,
|
||||
interval: Number.isInteger(cliOptions.poll)
|
||||
? (cliOptions.poll as number)
|
||||
: undefined,
|
||||
async function createUrlUtils({cliOptions}: {cliOptions: StartCLIOptions}) {
|
||||
const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
|
||||
const {host, port} = await getHostPort(cliOptions);
|
||||
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 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
|
||||
const defaultDevServerConfig: WebpackDevServer.Configuration = {
|
||||
return {
|
||||
hot: cliOptions.hotOnly ? 'only' : true,
|
||||
liveReload: false,
|
||||
client: {
|
||||
|
@ -245,23 +272,50 @@ export async function start(
|
|||
return middlewares;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
// E2E_TEST=true docusaurus start
|
||||
// Makes "docusaurus start" exit immediately on success/error, for E2E test
|
||||
function registerE2ETestHook(compiler: Compiler) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach((sig) => {
|
||||
process.on(sig, () => {
|
||||
devServer.stop();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getStartClientConfig({
|
||||
props,
|
||||
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' {
|
||||
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 () => {
|
||||
const aliases = ((await createBaseConfig(props, true)).resolve?.alias ??
|
||||
{}) as {[alias: string]: string};
|
||||
const aliases = ((
|
||||
await createBaseConfig({props, isServer: true, minify: true})
|
||||
).resolve?.alias ?? {}) as {[alias: string]: string};
|
||||
// Make aliases relative so that test work on all computers
|
||||
const relativeAliases = _.mapValues(aliases, (a) =>
|
||||
posixPath(path.relative(props.siteDir, a)),
|
||||
|
@ -121,7 +122,7 @@ describe('base webpack config', () => {
|
|||
.spyOn(utils, 'getFileLoaderUtils')
|
||||
.mockImplementation(() => fileLoaderUtils);
|
||||
|
||||
await createBaseConfig(props, false, false);
|
||||
await createBaseConfig({props, isServer: false, minify: false});
|
||||
expect(mockSvg).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,21 +7,31 @@
|
|||
|
||||
import webpack from 'webpack';
|
||||
|
||||
import createClientConfig from '../client';
|
||||
import {createBuildClientConfig, createStartClientConfig} from '../client';
|
||||
import {loadSetup} from '../../server/__tests__/testUtils';
|
||||
|
||||
describe('webpack dev config', () => {
|
||||
it('simple', async () => {
|
||||
it('simple start', async () => {
|
||||
const props = await loadSetup('simple-site');
|
||||
const config = await createClientConfig(props);
|
||||
const errors = webpack.validate(config);
|
||||
expect(errors).toBeUndefined();
|
||||
const {clientConfig} = await createStartClientConfig({props});
|
||||
webpack.validate(clientConfig);
|
||||
});
|
||||
|
||||
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 config = await createClientConfig(props);
|
||||
const errors = webpack.validate(config);
|
||||
expect(errors).toBeUndefined();
|
||||
const {clientConfig} = await createStartClientConfig({props});
|
||||
webpack.validate(clientConfig);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const props = await loadSetup('simple-site');
|
||||
const config = await createServerConfig({
|
||||
const {config} = await createServerConfig({
|
||||
props,
|
||||
onHeadTagsCollected: () => {},
|
||||
onLinksCollected: () => {},
|
||||
});
|
||||
const errors = webpack.validate(config);
|
||||
expect(errors).toBeUndefined();
|
||||
webpack.validate(config);
|
||||
});
|
||||
|
||||
it('custom', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const props = await loadSetup('custom-site');
|
||||
const config = await createServerConfig({
|
||||
const {config} = await createServerConfig({
|
||||
props,
|
||||
onHeadTagsCollected: () => {},
|
||||
onLinksCollected: () => {},
|
||||
});
|
||||
const errors = webpack.validate(config);
|
||||
expect(errors).toBeUndefined();
|
||||
webpack.validate(config);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,8 +13,8 @@ import {
|
|||
getCustomizableJSLoader,
|
||||
getStyleLoaders,
|
||||
getCustomBabelConfigFilePath,
|
||||
getMinimizer,
|
||||
} from './utils';
|
||||
import {getMinimizer} from './minification';
|
||||
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
|
||||
import type {Configuration} from 'webpack';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
|
@ -44,11 +44,15 @@ export function excludeJS(modulePath: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export async function createBaseConfig(
|
||||
props: Props,
|
||||
isServer: boolean,
|
||||
minify: boolean = true,
|
||||
): Promise<Configuration> {
|
||||
export async function createBaseConfig({
|
||||
props,
|
||||
isServer,
|
||||
minify,
|
||||
}: {
|
||||
props: Props;
|
||||
isServer: boolean;
|
||||
minify: boolean;
|
||||
}): Promise<Configuration> {
|
||||
const {
|
||||
outDir,
|
||||
siteDir,
|
||||
|
@ -62,8 +66,7 @@ export async function createBaseConfig(
|
|||
} = props;
|
||||
const totalPages = routesPaths.length;
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const minimizeEnabled = minify && isProd && !isServer;
|
||||
const useSimpleCssMinifier = process.env.USE_SIMPLE_CSS_MINIFIER === 'true';
|
||||
const minimizeEnabled = minify && isProd;
|
||||
|
||||
const fileLoaderUtils = getFileLoaderUtils();
|
||||
|
||||
|
@ -156,9 +159,7 @@ export async function createBaseConfig(
|
|||
// Only minimize client bundle in production because server bundle is only
|
||||
// used for static site generation
|
||||
minimize: minimizeEnabled,
|
||||
minimizer: minimizeEnabled
|
||||
? getMinimizer(useSimpleCssMinifier)
|
||||
: undefined,
|
||||
minimizer: minimizeEnabled ? getMinimizer() : undefined,
|
||||
splitChunks: isServer
|
||||
? false
|
||||
: {
|
||||
|
|
|
@ -9,22 +9,47 @@ import path from 'path';
|
|||
import logger from '@docusaurus/logger';
|
||||
import merge from 'webpack-merge';
|
||||
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 ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
|
||||
import {formatStatsErrorMessage} from './utils';
|
||||
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
import type {Configuration} from 'webpack';
|
||||
|
||||
export default async function createClientConfig(
|
||||
props: Props,
|
||||
minify: boolean = true,
|
||||
hydrate: boolean = true,
|
||||
): Promise<Configuration> {
|
||||
const isBuilding = process.argv[2] === 'build';
|
||||
const config = await createBaseConfig(props, false, minify);
|
||||
// When building, include the plugin to force terminate building if errors
|
||||
// happened in the client bundle.
|
||||
class ForceTerminatePlugin implements webpack.WebpackPluginInstance {
|
||||
apply(compiler: webpack.Compiler) {
|
||||
compiler.hooks.done.tap('client:done', (stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
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
|
||||
// browserslist config)
|
||||
// target: 'browserslist',
|
||||
|
@ -35,7 +60,7 @@ export default async function createClientConfig(
|
|||
runtimeChunk: true,
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.HYDRATE_CLIENT_ENTRY': JSON.stringify(hydrate),
|
||||
}),
|
||||
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.
|
||||
if (isBuilding) {
|
||||
clientConfig.plugins?.push({
|
||||
apply: (compiler) => {
|
||||
compiler.hooks.done.tap('client:done', (stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
const errorsWarnings = stats.toJson('errors-warnings');
|
||||
logger.error(
|
||||
`Client bundle compiled with errors therefore further build is impossible.\n${formatStatsErrorMessage(
|
||||
errorsWarnings,
|
||||
)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return clientConfig;
|
||||
}
|
||||
|
||||
// client config when running "docusaurus start"
|
||||
export async function createStartClientConfig({
|
||||
props,
|
||||
minify,
|
||||
poll,
|
||||
}: {
|
||||
props: Props;
|
||||
minify: boolean;
|
||||
poll: number | boolean | undefined;
|
||||
}): Promise<{clientConfig: Configuration}> {
|
||||
const {siteConfig, headTags, preBodyTags, postBodyTags} = props;
|
||||
|
||||
const clientConfig: webpack.Configuration = merge(
|
||||
await createBaseClientConfig({
|
||||
props,
|
||||
minify,
|
||||
hydrate: false,
|
||||
}),
|
||||
{
|
||||
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 fs from 'fs-extra';
|
||||
import merge from 'webpack-merge';
|
||||
import {
|
||||
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 {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
||||
import WebpackBar from 'webpackbar';
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import {createBaseConfig} from './base';
|
||||
import WaitPlugin from './plugins/WaitPlugin';
|
||||
import ssrDefaultTemplate from './templates/ssr.html.template';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
import type {Configuration} from 'webpack';
|
||||
|
||||
export default async function createServerConfig({
|
||||
props,
|
||||
onLinksCollected,
|
||||
onHeadTagsCollected,
|
||||
}: Pick<Locals, 'onLinksCollected' | 'onHeadTagsCollected'> & {
|
||||
export default async function createServerConfig(params: {
|
||||
props: Props;
|
||||
}): Promise<Configuration> {
|
||||
const {
|
||||
baseUrl,
|
||||
routesPaths,
|
||||
generatedFilesDir,
|
||||
headTags,
|
||||
preBodyTags,
|
||||
postBodyTags,
|
||||
siteConfig: {noIndex, trailingSlash, ssrTemplate},
|
||||
} = props;
|
||||
const config = await createBaseConfig(props, true);
|
||||
}): Promise<{config: Configuration; serverBundlePath: string}> {
|
||||
const {props} = params;
|
||||
|
||||
const routesLocation: {[filePath: string]: string} = {};
|
||||
// Array of paths to be rendered. Relative to output directory
|
||||
const ssgPaths = routesPaths.map((str) => {
|
||||
const ssgPath =
|
||||
baseUrl === '/' ? str : str.replace(new RegExp(`^${baseUrl}`), '/');
|
||||
routesLocation[ssgPath] = str;
|
||||
return ssgPath;
|
||||
const baseConfig = await createBaseConfig({
|
||||
props,
|
||||
isServer: true,
|
||||
|
||||
// Minification of server bundle reduces size but doubles bundle time :/
|
||||
minify: false,
|
||||
});
|
||||
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}`,
|
||||
entry: {
|
||||
main: path.resolve(__dirname, '../client/serverEntry.js'),
|
||||
},
|
||||
output: {
|
||||
filename: 'server.bundle.js',
|
||||
filename: outputFilename,
|
||||
libraryTarget: 'commonjs2',
|
||||
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
|
||||
globalObject: 'this',
|
||||
},
|
||||
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.
|
||||
new WebpackBar({
|
||||
name: 'Server',
|
||||
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,
|
||||
customizeObject,
|
||||
} from 'webpack-merge';
|
||||
import webpack, {
|
||||
type Configuration,
|
||||
type RuleSetRule,
|
||||
type WebpackPluginInstance,
|
||||
} from 'webpack';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
|
||||
import webpack, {type Configuration, type RuleSetRule} from 'webpack';
|
||||
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
|
||||
import type {CustomOptions, CssNanoOptions} from 'css-minimizer-webpack-plugin';
|
||||
import type {TransformOptions} from '@babel/core';
|
||||
import type {
|
||||
Plugin,
|
||||
PostCssOptions,
|
||||
ConfigureWebpackUtils,
|
||||
LoadedPlugin,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
export function formatStatsErrorMessage(
|
||||
|
@ -259,6 +253,57 @@ export function applyConfigurePostCss(
|
|||
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 {
|
||||
interface Error {
|
||||
/** @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) => {
|
||||
const compiler = webpack(config);
|
||||
compiler.run((err, stats) => {
|
||||
|
@ -296,7 +341,7 @@ export function compile(config: Configuration[]): Promise<void> {
|
|||
logger.error(`Error while closing Webpack compiler: ${errClose}`);
|
||||
reject(errClose);
|
||||
} else {
|
||||
resolve();
|
||||
resolve(stats!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -366,87 +411,3 @@ export async function getHttpsConfig(): Promise<
|
|||
}
|
||||
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
|
||||
twoslash
|
||||
typecheck
|
||||
typesafe
|
||||
Typesense
|
||||
typesense
|
||||
Unavatar
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -2746,15 +2746,6 @@
|
|||
micromark-util-character "^1.1.0"
|
||||
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":
|
||||
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"
|
||||
|
@ -16602,7 +16593,7 @@ webpack-merge@^5.9.0:
|
|||
clone-deep "^4.0.1"
|
||||
wildcard "^2.0.0"
|
||||
|
||||
webpack-sources@^3.2.2, webpack-sources@^3.2.3:
|
||||
webpack-sources@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
|
Loading…
Add table
Reference in a new issue