refactor(core): refactor SSG infrastructure (#10593)

This commit is contained in:
Sébastien Lorber 2024-10-18 18:55:09 +02:00 committed by GitHub
parent 14579cbda8
commit c9f231afb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 356 additions and 309 deletions

View file

@ -15,6 +15,10 @@ export {
} from './currentBundler';
export {getMinimizers} from './minification';
export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml';
export {
getHtmlMinifier,
type HtmlMinifier,
type HtmlMinifierType,
} from './minifyHtml';
export {createJsLoaderFactory} from './loaders/jsLoader';
export {createStyleLoadersFactory} from './loaders/styleLoader';

View file

@ -7,11 +7,12 @@
import {minify as terserHtmlMinifier} from 'html-minifier-terser';
import {importSwcHtmlMinifier} from './importFaster';
import type {DocusaurusConfig} from '@docusaurus/types';
// Historical env variable
const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true';
export type HtmlMinifierType = 'swc' | 'terser';
export type HtmlMinifierResult = {
code: string;
warnings: string[];
@ -25,24 +26,15 @@ const NoopMinifier: HtmlMinifier = {
minify: async (html: string) => ({code: html, warnings: []}),
};
type SiteConfigSlice = {
future: {
experimental_faster: Pick<
DocusaurusConfig['future']['experimental_faster'],
'swcHtmlMinimizer'
>;
};
};
export async function getHtmlMinifier({
siteConfig,
type,
}: {
siteConfig: SiteConfigSlice;
type: HtmlMinifierType;
}): Promise<HtmlMinifier> {
if (SkipHtmlMinification) {
return NoopMinifier;
}
if (siteConfig.future.experimental_faster.swcHtmlMinimizer) {
if (type === 'swc') {
return getSwcMinifier();
} else {
return getTerserMinifier();

View file

@ -9,7 +9,8 @@ import logger from './logger';
// For now this is a private env variable we use internally
// But we'll want to expose this feature officially some day
const PerfDebuggingEnabled: boolean = !!process.env.DOCUSAURUS_PERF_LOGGER;
const PerfDebuggingEnabled: boolean =
process.env.DOCUSAURUS_PERF_LOGGER === 'true';
const Thresholds = {
min: 5,
@ -17,7 +18,7 @@ const Thresholds = {
red: 1000,
};
const PerfPrefix = logger.yellow(`[PERF] `);
const PerfPrefix = logger.yellow(`[PERF]`);
// This is what enables to "see the parent stack" for each log
// Parent1 > Parent2 > Parent3 > child trace
@ -42,6 +43,14 @@ type Memory = {
after: NodeJS.MemoryUsage;
};
function getMemory(): NodeJS.MemoryUsage {
// Before reading memory stats, we explicitly call the GC
// Note: this only works when Node.js option "--expose-gc" is provided
globalThis.gc?.();
return process.memoryUsage();
}
function createPerfLogger(): PerfLoggerAPI {
if (!PerfDebuggingEnabled) {
const noop = () => {};
@ -73,29 +82,35 @@ function createPerfLogger(): PerfLoggerAPI {
);
};
const formatStatus = (error: Error | undefined): string => {
return error ? logger.red('[KO]') : ''; // logger.green('[OK]');
};
const printPerfLog = ({
label,
duration,
memory,
error,
}: {
label: string;
duration: number;
memory: Memory;
error: Error | undefined;
}) => {
if (duration < Thresholds.min) {
return;
}
console.log(
`${PerfPrefix + label} - ${formatDuration(duration)} - ${formatMemory(
memory,
)}`,
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
duration,
)} - ${formatMemory(memory)}`,
);
};
const start: PerfLoggerAPI['start'] = (label) =>
performance.mark(label, {
detail: {
memoryUsage: process.memoryUsage(),
memoryUsage: getMemory(),
},
});
@ -110,30 +125,42 @@ function createPerfLogger(): PerfLoggerAPI {
duration,
memory: {
before: memoryUsage,
after: process.memoryUsage(),
after: getMemory(),
},
error: undefined,
});
};
const log: PerfLoggerAPI['log'] = (label: string) =>
console.log(PerfPrefix + applyParentPrefix(label));
console.log(`${PerfPrefix} ${applyParentPrefix(label)}`);
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
const finalLabel = applyParentPrefix(label);
const before = performance.now();
const memoryBefore = process.memoryUsage();
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
const memoryAfter = process.memoryUsage();
const duration = performance.now() - before;
printPerfLog({
label: finalLabel,
duration,
memory: {
before: memoryBefore,
after: memoryAfter,
},
});
return result;
const memoryBefore = getMemory();
const asyncEnd = ({error}: {error: Error | undefined}) => {
const memoryAfter = getMemory();
const duration = performance.now() - before;
printPerfLog({
error,
label: finalLabel,
duration,
memory: {
before: memoryBefore,
after: memoryAfter,
},
});
};
try {
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
asyncEnd({error: undefined});
return result;
} catch (e) {
asyncEnd({error: e as Error});
throw e;
}
};
return {

View file

@ -403,6 +403,7 @@ export type DocusaurusConfig = {
*
* @see https://docusaurus.io/docs/api/docusaurus-config#ssrTemplate
*/
// TODO Docusaurus v4 - rename to ssgTemplate?
ssrTemplate?: string;
/**
* Will be used as title delimiter in the generated `<title>` tag.

View file

@ -7,74 +7,22 @@
import type {ReactNode} from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {Writable} from 'stream';
import {PassThrough} from 'node:stream';
import {text} from 'node:stream/consumers';
// See also https://github.com/facebook/react/issues/31134
// See also https://github.com/facebook/docusaurus/issues/9985#issuecomment-2396367797
export async function renderToHtml(app: ReactNode): Promise<string> {
// Inspired from
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
const writableStream = new WritableAsPromise();
const {pipe} = renderToPipeableStream(app, {
onError(error) {
writableStream.destroy(error as Error);
},
onAllReady() {
pipe(writableStream);
},
});
return writableStream.getPromise();
}
// WritableAsPromise inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js
/* eslint-disable no-underscore-dangle */
class WritableAsPromise extends Writable {
private _output: string;
private _deferred: {
promise: Promise<string> | null;
resolve: (value: string) => void;
reject: (reason: Error) => void;
};
constructor() {
super();
this._output = ``;
this._deferred = {
promise: null,
resolve: () => null,
reject: () => null,
};
this._deferred.promise = new Promise((resolve, reject) => {
this._deferred.resolve = resolve;
this._deferred.reject = reject;
return new Promise((resolve, reject) => {
const passThrough = new PassThrough();
const {pipe} = renderToPipeableStream(app, {
onError(error) {
reject(error);
},
onAllReady() {
pipe(passThrough);
text(passThrough).then(resolve, reject);
},
});
}
override _write(
chunk: {toString: () => string},
_enc: unknown,
next: () => void,
) {
this._output += chunk.toString();
next();
}
override _destroy(error: Error | null, next: (error?: Error | null) => void) {
if (error instanceof Error) {
this._deferred.reject(error);
} else {
next();
}
}
override end() {
this._deferred.resolve(this._output);
return this.destroy();
}
getPromise(): Promise<string> {
return this._deferred.promise!;
}
});
}

View file

@ -42,7 +42,10 @@ const render: AppRenderer = async ({pathname}) => {
const html = await renderToHtml(app);
const collectedData: PageCollectedData = {
// TODO Docusaurus v4 refactor: helmet state is non-serializable
// this makes it impossible to run SSG in a worker thread
helmet: (helmetContext as FilledContext).helmet,
anchors: statefulBrokenLinks.getCollectedAnchors(),
links: statefulBrokenLinks.getCollectedLinks(),
modules: Array.from(modules),

View file

@ -8,9 +8,9 @@
import fs from 'fs-extra';
import path from 'path';
import _ from 'lodash';
import {compile, getHtmlMinifier} from '@docusaurus/bundler';
import {compile} from '@docusaurus/bundler';
import logger, {PerfLogger} from '@docusaurus/logger';
import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils';
import {mapAsyncSequential} from '@docusaurus/utils';
import {loadSite, loadContext, type LoadContextParams} from '../server/site';
import {handleBrokenLinks} from '../server/brokenLinks';
import {createBuildClientConfig} from '../webpack/client';
@ -19,26 +19,12 @@ import {
createConfigureWebpackUtils,
executePluginsConfigureWebpack,
} from '../webpack/configure';
import {loadI18n} from '../server/i18n';
import {
generateHashRouterEntrypoint,
generateStaticFiles,
loadAppRenderer,
} from '../ssg';
import {
compileSSRTemplate,
renderHashRouterTemplate,
} from '../templates/templates';
import defaultSSRTemplate from '../templates/ssr.html.template';
import type {SSGParams} from '../ssg';
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
import {executeSSG} from '../ssg/ssgExecutor';
import type {
ConfigureWebpackUtils,
LoadedPlugin,
Props,
RouterType,
} from '@docusaurus/types';
import type {SiteCollectedData} from '../common';
@ -147,7 +133,7 @@ async function buildLocale({
siteDir: string;
locale: string;
cliOptions: Partial<BuildCLIOptions>;
}): Promise<string> {
}): Promise<void> {
// Temporary workaround to unlock the ability to translate the site config
// We'll remove it if a better official API can be designed
// See https://github.com/facebook/docusaurus/issues/4542
@ -225,72 +211,6 @@ async function buildLocale({
process.cwd(),
outDir,
)}.`;
return outDir;
}
async function executeSSG({
props,
serverBundlePath,
clientManifestPath,
router,
}: {
props: Props;
serverBundlePath: string;
clientManifestPath: string;
router: RouterType;
}): Promise<{collectedData: SiteCollectedData}> {
const manifest: Manifest = await PerfLogger.async(
'Read client manifest',
() => fs.readJSON(clientManifestPath, 'utf-8'),
);
const ssrTemplate = await PerfLogger.async('Compile SSR template', () =>
compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
);
const params: SSGParams = {
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,
};
if (router === 'hash') {
PerfLogger.start('Generate Hash Router entry point');
const content = renderHashRouterTemplate({params});
await generateHashRouterEntrypoint({content, params});
PerfLogger.end('Generate Hash Router entry point');
return {collectedData: {}};
}
const [renderer, htmlMinifier] = await Promise.all([
PerfLogger.async('Load App renderer', () =>
loadAppRenderer({
serverBundlePath,
}),
),
PerfLogger.async('Load HTML minifier', () =>
getHtmlMinifier({siteConfig: props.siteConfig}),
),
]);
const ssgResult = await PerfLogger.async('Generate static files', () =>
generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params,
htmlMinifier,
}),
);
return ssgResult;
}
async function executePluginsPostBuild({

View file

@ -20,7 +20,10 @@ export type AppRenderer = (params: {
}) => Promise<AppRenderResult>;
export type PageCollectedData = {
// TODO Docusaurus v4 refactor: helmet state is non-serializable
// this makes it impossible to run SSG in a worker thread
helmet: HelmetServerState;
links: string[];
anchors: string[];
modules: string[];

View file

@ -12,34 +12,37 @@ import _ from 'lodash';
import evaluate from 'eval';
import pMap from 'p-map';
import logger, {PerfLogger} from '@docusaurus/logger';
import {renderSSRTemplate} from './templates/templates';
import type {AppRenderer, AppRenderResult, SiteCollectedData} from './common';
import {getHtmlMinifier} from '@docusaurus/bundler';
import {
compileSSGTemplate,
renderSSGTemplate,
type SSGTemplateCompiled,
} from './ssgTemplate';
import {SSGConcurrency, writeStaticFile} from './ssgUtils';
import type {SSGParams} from './ssgParams';
import type {AppRenderer, AppRenderResult, SiteCollectedData} from '../common';
import type {HtmlMinifier} from '@docusaurus/bundler';
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;
type SSGSuccessResult = {
collectedData: AppRenderResult['collectedData'];
// html: we don't include it on purpose!
// we don't need to aggregate all html contents in memory!
// html contents can be GC as soon as they are written to disk
};
// 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;
type SSGSuccess = {
pathname: string;
error: null;
result: SSGSuccessResult;
warnings: string[];
};
type SSGError = {
pathname: string;
error: Error;
result: null;
warnings: string[];
};
type SSGResult = SSGSuccess | SSGError;
export async function loadAppRenderer({
serverBundlePath,
@ -86,30 +89,6 @@ export async function loadAppRenderer({
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 function printSSGWarnings(
results: {
pathname: string;
@ -163,28 +142,26 @@ Troubleshooting guide: https://github.com/facebook/docusaurus/discussions/10580
export async function generateStaticFiles({
pathnames,
renderer,
params,
htmlMinifier,
}: {
pathnames: string[];
renderer: AppRenderer;
params: SSGParams;
htmlMinifier: HtmlMinifier;
}): Promise<{collectedData: SiteCollectedData}> {
type SSGSuccess = {
pathname: string;
error: null;
result: AppRenderResult;
warnings: string[];
};
type SSGError = {
pathname: string;
error: Error;
result: null;
warnings: string[];
};
type SSGResult = SSGSuccess | SSGError;
const [renderer, htmlMinifier, ssgTemplate] = await Promise.all([
PerfLogger.async('Load App renderer', () =>
loadAppRenderer({
serverBundlePath: params.serverBundlePath,
}),
),
PerfLogger.async('Load HTML minifier', () =>
getHtmlMinifier({
type: params.htmlMinifierType,
}),
),
PerfLogger.async('Compile SSG template', () =>
compileSSGTemplate(params.ssgTemplateContent),
),
]);
// Note that we catch all async errors on purpose
// Docusaurus presents all the SSG errors to the user, not just the first one
@ -196,6 +173,7 @@ export async function generateStaticFiles({
renderer,
params,
htmlMinifier,
ssgTemplate,
}).then(
(result) => ({
pathname,
@ -210,7 +188,7 @@ export async function generateStaticFiles({
warnings: [],
}),
),
{concurrency: Concurrency},
{concurrency: SSGConcurrency},
);
printSSGWarnings(results);
@ -247,21 +225,24 @@ async function generateStaticFile({
renderer,
params,
htmlMinifier,
ssgTemplate,
}: {
pathname: string;
renderer: AppRenderer;
params: SSGParams;
htmlMinifier: HtmlMinifier;
}): Promise<AppRenderResult & {warnings: string[]}> {
ssgTemplate: SSGTemplateCompiled;
}): Promise<SSGSuccessResult & {warnings: string[]}> {
try {
// This only renders the app HTML
const result = await renderer({
pathname,
});
// This renders the full page HTML, including head tags...
const fullPageHtml = renderSSRTemplate({
const fullPageHtml = renderSSGTemplate({
params,
result,
ssgTemplate,
});
const minifierResult = await htmlMinifier.minify(fullPageHtml);
await writeStaticFile({
@ -270,7 +251,7 @@ async function generateStaticFile({
params,
});
return {
...result,
collectedData: result.collectedData,
// As of today, only the html minifier can emit SSG warnings
warnings: minifierResult.warnings,
};
@ -307,40 +288,3 @@ It might also require to wrap your client code in ${logger.code(
return parts.join('\n');
}
export async function generateHashRouterEntrypoint({
content,
params,
}: {
content: string;
params: SSGParams;
}): Promise<void> {
await writeStaticFile({
pathname: '/',
content,
params,
});
}
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);
}

View file

@ -0,0 +1,50 @@
/**
* 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 {PerfLogger} from '@docusaurus/logger';
import {createSSGParams} from './ssgParams';
import {generateStaticFiles} from './ssg';
import {renderHashRouterTemplate} from './ssgTemplate';
import {generateHashRouterEntrypoint} from './ssgUtils';
import type {Props, RouterType} from '@docusaurus/types';
import type {SiteCollectedData} from '../common';
// TODO Docusaurus v4 - introduce SSG worker threads
export async function executeSSG({
props,
serverBundlePath,
clientManifestPath,
router,
}: {
props: Props;
serverBundlePath: string;
clientManifestPath: string;
router: RouterType;
}): Promise<{collectedData: SiteCollectedData}> {
const params = await createSSGParams({
serverBundlePath,
clientManifestPath,
props,
});
if (router === 'hash') {
PerfLogger.start('Generate Hash Router entry point');
const content = await renderHashRouterTemplate({params});
await generateHashRouterEntrypoint({content, params});
PerfLogger.end('Generate Hash Router entry point');
return {collectedData: {}};
}
const ssgResult = await PerfLogger.async('Generate static files', () =>
generateStaticFiles({
pathnames: props.routesPaths,
params,
}),
);
return ssgResult;
}

View file

@ -0,0 +1,69 @@
/**
* 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 {DOCUSAURUS_VERSION} from '@docusaurus/utils';
import {PerfLogger} from '@docusaurus/logger';
import DefaultSSGTemplate from './ssgTemplate.html';
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
import type {Props} from '@docusaurus/types';
import type {HtmlMinifierType} from '@docusaurus/bundler';
// Keep these params serializable
// This makes it possible to use workers
export type SSGParams = {
trailingSlash: boolean | undefined;
manifest: Manifest;
headTags: string;
preBodyTags: string;
postBodyTags: string;
outDir: string;
baseUrl: string;
noIndex: boolean;
DOCUSAURUS_VERSION: string;
htmlMinifierType: HtmlMinifierType;
serverBundlePath: string;
ssgTemplateContent: string;
};
export async function createSSGParams({
props,
serverBundlePath,
clientManifestPath,
}: {
props: Props;
serverBundlePath: string;
clientManifestPath: string;
}): Promise<SSGParams> {
const manifest: Manifest = await PerfLogger.async(
'Read client manifest',
() => fs.readJSON(clientManifestPath, 'utf-8'),
);
const params: SSGParams = {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssgTemplateContent: props.siteConfig.ssrTemplate ?? DefaultSSGTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
serverBundlePath,
htmlMinifierType: props.siteConfig.future.experimental_faster
.swcHtmlMinimizer
? 'swc'
: 'terser',
};
// Useless but ensures that SSG params remain serializable
return structuredClone(params);
}

View file

@ -7,14 +7,15 @@
import * as eta from 'eta';
import {getBundles} from 'react-loadable-ssr-addon-v5-slorber';
import type {SSGParams} from '../ssg';
import {PerfLogger} from '@docusaurus/logger';
import type {SSGParams} from './ssgParams';
import type {AppRenderResult} from '../common';
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
// TODO this is historical server template data
// TODO Docusaurus v4 breaking change - 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 = {
export type SSGTemplateData = {
appHtml: string;
baseUrl: string;
htmlAttributes: string;
@ -29,16 +30,16 @@ export type SSRTemplateData = {
version: string;
};
export type SSRTemplateCompiled = (data: SSRTemplateData) => string;
export type SSGTemplateCompiled = (data: SSGTemplateData) => string;
export async function compileSSRTemplate(
export async function compileSSGTemplate(
template: string,
): Promise<SSRTemplateCompiled> {
): Promise<SSGTemplateCompiled> {
const compiledTemplate = eta.compile(template.trim(), {
rmWhitespace: true,
});
return (data: SSRTemplateData) => compiledTemplate(data, eta.defaultConfig);
return (data: SSGTemplateData) => compiledTemplate(data, eta.defaultConfig);
}
/**
@ -62,12 +63,14 @@ function getScriptsAndStylesheets({
return {scripts, stylesheets};
}
export function renderSSRTemplate({
export function renderSSGTemplate({
params,
result,
ssgTemplate,
}: {
params: SSGParams;
result: AppRenderResult;
ssgTemplate: SSGTemplateCompiled;
}): string {
const {
baseUrl,
@ -77,7 +80,6 @@ export function renderSSRTemplate({
manifest,
noIndex,
DOCUSAURUS_VERSION,
ssrTemplate,
} = params;
const {
html: appHtml,
@ -96,7 +98,7 @@ export function renderSSRTemplate({
];
const metaAttributes = metaStrings.filter(Boolean);
const data: SSRTemplateData = {
const data: SSGTemplateData = {
appHtml,
baseUrl,
htmlAttributes,
@ -111,14 +113,14 @@ export function renderSSRTemplate({
version: DOCUSAURUS_VERSION,
};
return ssrTemplate(data);
return ssgTemplate(data);
}
export function renderHashRouterTemplate({
export async function renderHashRouterTemplate({
params,
}: {
params: SSGParams;
}): string {
}): Promise<string> {
const {
// baseUrl,
headTags,
@ -126,15 +128,19 @@ export function renderHashRouterTemplate({
postBodyTags,
manifest,
DOCUSAURUS_VERSION,
ssrTemplate,
ssgTemplateContent,
} = params;
const ssgTemplate = await PerfLogger.async('Compile SSG template', () =>
compileSSGTemplate(ssgTemplateContent),
);
const {scripts, stylesheets} = getScriptsAndStylesheets({
manifest,
modules: [],
});
const data: SSRTemplateData = {
const data: SSGTemplateData = {
appHtml: '',
baseUrl: './',
htmlAttributes: '',
@ -149,5 +155,5 @@ export function renderHashRouterTemplate({
version: DOCUSAURUS_VERSION,
};
return ssrTemplate(data);
return ssgTemplate(data);
}

View file

@ -0,0 +1,80 @@
/**
* 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 path from 'path';
import type {SSGParams} from './ssgParams';
// Secret way to set SSR plugin concurrency option
// Waiting for feedback before documenting this officially?
export const SSGConcurrency = 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;
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 generateHashRouterEntrypoint({
content,
params,
}: {
content: string;
params: SSGParams;
}): Promise<void> {
await writeStaticFile({
pathname: '/',
content,
params,
});
}
export async function writeStaticFile({
content,
pathname,
params,
}: {
content: string;
pathname: string;
params: SSGParams;
}): Promise<void> {
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);
}

View file

@ -109,7 +109,7 @@ export async function createStartClientConfig({
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({
template: path.join(__dirname, '../templates/dev.html.template.ejs'),
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',