mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 18:27:56 +02:00
refactor(core): refactor SSG infrastructure (#10593)
This commit is contained in:
parent
14579cbda8
commit
c9f231afb3
16 changed files with 356 additions and 309 deletions
|
@ -15,6 +15,10 @@ export {
|
||||||
} from './currentBundler';
|
} from './currentBundler';
|
||||||
|
|
||||||
export {getMinimizers} from './minification';
|
export {getMinimizers} from './minification';
|
||||||
export {getHtmlMinifier, type HtmlMinifier} from './minifyHtml';
|
export {
|
||||||
|
getHtmlMinifier,
|
||||||
|
type HtmlMinifier,
|
||||||
|
type HtmlMinifierType,
|
||||||
|
} from './minifyHtml';
|
||||||
export {createJsLoaderFactory} from './loaders/jsLoader';
|
export {createJsLoaderFactory} from './loaders/jsLoader';
|
||||||
export {createStyleLoadersFactory} from './loaders/styleLoader';
|
export {createStyleLoadersFactory} from './loaders/styleLoader';
|
||||||
|
|
|
@ -7,11 +7,12 @@
|
||||||
|
|
||||||
import {minify as terserHtmlMinifier} from 'html-minifier-terser';
|
import {minify as terserHtmlMinifier} from 'html-minifier-terser';
|
||||||
import {importSwcHtmlMinifier} from './importFaster';
|
import {importSwcHtmlMinifier} from './importFaster';
|
||||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
|
||||||
|
|
||||||
// Historical env variable
|
// Historical env variable
|
||||||
const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true';
|
const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true';
|
||||||
|
|
||||||
|
export type HtmlMinifierType = 'swc' | 'terser';
|
||||||
|
|
||||||
export type HtmlMinifierResult = {
|
export type HtmlMinifierResult = {
|
||||||
code: string;
|
code: string;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
|
@ -25,24 +26,15 @@ const NoopMinifier: HtmlMinifier = {
|
||||||
minify: async (html: string) => ({code: html, warnings: []}),
|
minify: async (html: string) => ({code: html, warnings: []}),
|
||||||
};
|
};
|
||||||
|
|
||||||
type SiteConfigSlice = {
|
|
||||||
future: {
|
|
||||||
experimental_faster: Pick<
|
|
||||||
DocusaurusConfig['future']['experimental_faster'],
|
|
||||||
'swcHtmlMinimizer'
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getHtmlMinifier({
|
export async function getHtmlMinifier({
|
||||||
siteConfig,
|
type,
|
||||||
}: {
|
}: {
|
||||||
siteConfig: SiteConfigSlice;
|
type: HtmlMinifierType;
|
||||||
}): Promise<HtmlMinifier> {
|
}): Promise<HtmlMinifier> {
|
||||||
if (SkipHtmlMinification) {
|
if (SkipHtmlMinification) {
|
||||||
return NoopMinifier;
|
return NoopMinifier;
|
||||||
}
|
}
|
||||||
if (siteConfig.future.experimental_faster.swcHtmlMinimizer) {
|
if (type === 'swc') {
|
||||||
return getSwcMinifier();
|
return getSwcMinifier();
|
||||||
} else {
|
} else {
|
||||||
return getTerserMinifier();
|
return getTerserMinifier();
|
||||||
|
|
|
@ -9,7 +9,8 @@ import logger from './logger';
|
||||||
|
|
||||||
// For now this is a private env variable we use internally
|
// For now this is a private env variable we use internally
|
||||||
// But we'll want to expose this feature officially some day
|
// 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 = {
|
const Thresholds = {
|
||||||
min: 5,
|
min: 5,
|
||||||
|
@ -17,7 +18,7 @@ const Thresholds = {
|
||||||
red: 1000,
|
red: 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PerfPrefix = logger.yellow(`[PERF] `);
|
const PerfPrefix = logger.yellow(`[PERF]`);
|
||||||
|
|
||||||
// This is what enables to "see the parent stack" for each log
|
// This is what enables to "see the parent stack" for each log
|
||||||
// Parent1 > Parent2 > Parent3 > child trace
|
// Parent1 > Parent2 > Parent3 > child trace
|
||||||
|
@ -42,6 +43,14 @@ type Memory = {
|
||||||
after: NodeJS.MemoryUsage;
|
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 {
|
function createPerfLogger(): PerfLoggerAPI {
|
||||||
if (!PerfDebuggingEnabled) {
|
if (!PerfDebuggingEnabled) {
|
||||||
const noop = () => {};
|
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 = ({
|
const printPerfLog = ({
|
||||||
label,
|
label,
|
||||||
duration,
|
duration,
|
||||||
memory,
|
memory,
|
||||||
|
error,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
memory: Memory;
|
memory: Memory;
|
||||||
|
error: Error | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
if (duration < Thresholds.min) {
|
if (duration < Thresholds.min) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`${PerfPrefix + label} - ${formatDuration(duration)} - ${formatMemory(
|
`${PerfPrefix}${formatStatus(error)} ${label} - ${formatDuration(
|
||||||
memory,
|
duration,
|
||||||
)}`,
|
)} - ${formatMemory(memory)}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const start: PerfLoggerAPI['start'] = (label) =>
|
const start: PerfLoggerAPI['start'] = (label) =>
|
||||||
performance.mark(label, {
|
performance.mark(label, {
|
||||||
detail: {
|
detail: {
|
||||||
memoryUsage: process.memoryUsage(),
|
memoryUsage: getMemory(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -110,30 +125,42 @@ function createPerfLogger(): PerfLoggerAPI {
|
||||||
duration,
|
duration,
|
||||||
memory: {
|
memory: {
|
||||||
before: memoryUsage,
|
before: memoryUsage,
|
||||||
after: process.memoryUsage(),
|
after: getMemory(),
|
||||||
},
|
},
|
||||||
|
error: undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const log: PerfLoggerAPI['log'] = (label: string) =>
|
const log: PerfLoggerAPI['log'] = (label: string) =>
|
||||||
console.log(PerfPrefix + applyParentPrefix(label));
|
console.log(`${PerfPrefix} ${applyParentPrefix(label)}`);
|
||||||
|
|
||||||
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
|
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
|
||||||
const finalLabel = applyParentPrefix(label);
|
const finalLabel = applyParentPrefix(label);
|
||||||
const before = performance.now();
|
const before = performance.now();
|
||||||
const memoryBefore = process.memoryUsage();
|
const memoryBefore = getMemory();
|
||||||
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
|
|
||||||
const memoryAfter = process.memoryUsage();
|
const asyncEnd = ({error}: {error: Error | undefined}) => {
|
||||||
const duration = performance.now() - before;
|
const memoryAfter = getMemory();
|
||||||
printPerfLog({
|
const duration = performance.now() - before;
|
||||||
label: finalLabel,
|
printPerfLog({
|
||||||
duration,
|
error,
|
||||||
memory: {
|
label: finalLabel,
|
||||||
before: memoryBefore,
|
duration,
|
||||||
after: memoryAfter,
|
memory: {
|
||||||
},
|
before: memoryBefore,
|
||||||
});
|
after: memoryAfter,
|
||||||
return result;
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await ParentPrefix.run(finalLabel, () => asyncFn());
|
||||||
|
asyncEnd({error: undefined});
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
asyncEnd({error: e as Error});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
1
packages/docusaurus-types/src/config.d.ts
vendored
1
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -403,6 +403,7 @@ export type DocusaurusConfig = {
|
||||||
*
|
*
|
||||||
* @see https://docusaurus.io/docs/api/docusaurus-config#ssrTemplate
|
* @see https://docusaurus.io/docs/api/docusaurus-config#ssrTemplate
|
||||||
*/
|
*/
|
||||||
|
// TODO Docusaurus v4 - rename to ssgTemplate?
|
||||||
ssrTemplate?: string;
|
ssrTemplate?: string;
|
||||||
/**
|
/**
|
||||||
* Will be used as title delimiter in the generated `<title>` tag.
|
* Will be used as title delimiter in the generated `<title>` tag.
|
||||||
|
|
|
@ -7,74 +7,22 @@
|
||||||
|
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
import {renderToPipeableStream} from 'react-dom/server';
|
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> {
|
export async function renderToHtml(app: ReactNode): Promise<string> {
|
||||||
// Inspired from
|
return new Promise((resolve, reject) => {
|
||||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
const passThrough = new PassThrough();
|
||||||
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
|
const {pipe} = renderToPipeableStream(app, {
|
||||||
const writableStream = new WritableAsPromise();
|
onError(error) {
|
||||||
|
reject(error);
|
||||||
const {pipe} = renderToPipeableStream(app, {
|
},
|
||||||
onError(error) {
|
onAllReady() {
|
||||||
writableStream.destroy(error as Error);
|
pipe(passThrough);
|
||||||
},
|
text(passThrough).then(resolve, reject);
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
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!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,10 @@ const render: AppRenderer = async ({pathname}) => {
|
||||||
const html = await renderToHtml(app);
|
const html = await renderToHtml(app);
|
||||||
|
|
||||||
const collectedData: PageCollectedData = {
|
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,
|
helmet: (helmetContext as FilledContext).helmet,
|
||||||
|
|
||||||
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
anchors: statefulBrokenLinks.getCollectedAnchors(),
|
||||||
links: statefulBrokenLinks.getCollectedLinks(),
|
links: statefulBrokenLinks.getCollectedLinks(),
|
||||||
modules: Array.from(modules),
|
modules: Array.from(modules),
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {compile, getHtmlMinifier} from '@docusaurus/bundler';
|
import {compile} from '@docusaurus/bundler';
|
||||||
import logger, {PerfLogger} from '@docusaurus/logger';
|
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 {loadSite, loadContext, type LoadContextParams} from '../server/site';
|
||||||
import {handleBrokenLinks} from '../server/brokenLinks';
|
import {handleBrokenLinks} from '../server/brokenLinks';
|
||||||
import {createBuildClientConfig} from '../webpack/client';
|
import {createBuildClientConfig} from '../webpack/client';
|
||||||
|
@ -19,26 +19,12 @@ import {
|
||||||
createConfigureWebpackUtils,
|
createConfigureWebpackUtils,
|
||||||
executePluginsConfigureWebpack,
|
executePluginsConfigureWebpack,
|
||||||
} from '../webpack/configure';
|
} from '../webpack/configure';
|
||||||
|
|
||||||
import {loadI18n} from '../server/i18n';
|
import {loadI18n} from '../server/i18n';
|
||||||
import {
|
import {executeSSG} from '../ssg/ssgExecutor';
|
||||||
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 type {
|
import type {
|
||||||
ConfigureWebpackUtils,
|
ConfigureWebpackUtils,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
Props,
|
Props,
|
||||||
RouterType,
|
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import type {SiteCollectedData} from '../common';
|
import type {SiteCollectedData} from '../common';
|
||||||
|
|
||||||
|
@ -147,7 +133,7 @@ async function buildLocale({
|
||||||
siteDir: string;
|
siteDir: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
cliOptions: Partial<BuildCLIOptions>;
|
cliOptions: Partial<BuildCLIOptions>;
|
||||||
}): Promise<string> {
|
}): Promise<void> {
|
||||||
// Temporary workaround to unlock the ability to translate the site config
|
// Temporary workaround to unlock the ability to translate the site config
|
||||||
// We'll remove it if a better official API can be designed
|
// We'll remove it if a better official API can be designed
|
||||||
// See https://github.com/facebook/docusaurus/issues/4542
|
// See https://github.com/facebook/docusaurus/issues/4542
|
||||||
|
@ -225,72 +211,6 @@ async function buildLocale({
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
outDir,
|
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({
|
async function executePluginsPostBuild({
|
||||||
|
|
3
packages/docusaurus/src/common.d.ts
vendored
3
packages/docusaurus/src/common.d.ts
vendored
|
@ -20,7 +20,10 @@ export type AppRenderer = (params: {
|
||||||
}) => Promise<AppRenderResult>;
|
}) => Promise<AppRenderResult>;
|
||||||
|
|
||||||
export type PageCollectedData = {
|
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;
|
helmet: HelmetServerState;
|
||||||
|
|
||||||
links: string[];
|
links: string[];
|
||||||
anchors: string[];
|
anchors: string[];
|
||||||
modules: string[];
|
modules: string[];
|
||||||
|
|
|
@ -12,34 +12,37 @@ import _ from 'lodash';
|
||||||
import evaluate from 'eval';
|
import evaluate from 'eval';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import logger, {PerfLogger} from '@docusaurus/logger';
|
import logger, {PerfLogger} from '@docusaurus/logger';
|
||||||
import {renderSSRTemplate} from './templates/templates';
|
import {getHtmlMinifier} from '@docusaurus/bundler';
|
||||||
import type {AppRenderer, AppRenderResult, SiteCollectedData} from './common';
|
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 {HtmlMinifier} from '@docusaurus/bundler';
|
||||||
|
|
||||||
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
type SSGSuccessResult = {
|
||||||
import type {SSRTemplateCompiled} from './templates/templates';
|
collectedData: AppRenderResult['collectedData'];
|
||||||
|
// html: we don't include it on purpose!
|
||||||
export type SSGParams = {
|
// we don't need to aggregate all html contents in memory!
|
||||||
trailingSlash: boolean | undefined;
|
// html contents can be GC as soon as they are written to disk
|
||||||
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
|
type SSGSuccess = {
|
||||||
// Waiting for feedback before documenting this officially?
|
pathname: string;
|
||||||
const Concurrency = process.env.DOCUSAURUS_SSR_CONCURRENCY
|
error: null;
|
||||||
? parseInt(process.env.DOCUSAURUS_SSR_CONCURRENCY, 10)
|
result: SSGSuccessResult;
|
||||||
: // Not easy to define a reasonable option default
|
warnings: string[];
|
||||||
// Will still be better than Infinity
|
};
|
||||||
// See also https://github.com/sindresorhus/p-map/issues/24
|
type SSGError = {
|
||||||
32;
|
pathname: string;
|
||||||
|
error: Error;
|
||||||
|
result: null;
|
||||||
|
warnings: string[];
|
||||||
|
};
|
||||||
|
type SSGResult = SSGSuccess | SSGError;
|
||||||
|
|
||||||
export async function loadAppRenderer({
|
export async function loadAppRenderer({
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
|
@ -86,30 +89,6 @@ export async function loadAppRenderer({
|
||||||
return serverEntry.default;
|
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(
|
export function printSSGWarnings(
|
||||||
results: {
|
results: {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
|
@ -163,28 +142,26 @@ Troubleshooting guide: https://github.com/facebook/docusaurus/discussions/10580
|
||||||
|
|
||||||
export async function generateStaticFiles({
|
export async function generateStaticFiles({
|
||||||
pathnames,
|
pathnames,
|
||||||
renderer,
|
|
||||||
params,
|
params,
|
||||||
htmlMinifier,
|
|
||||||
}: {
|
}: {
|
||||||
pathnames: string[];
|
pathnames: string[];
|
||||||
renderer: AppRenderer;
|
|
||||||
params: SSGParams;
|
params: SSGParams;
|
||||||
htmlMinifier: HtmlMinifier;
|
|
||||||
}): Promise<{collectedData: SiteCollectedData}> {
|
}): Promise<{collectedData: SiteCollectedData}> {
|
||||||
type SSGSuccess = {
|
const [renderer, htmlMinifier, ssgTemplate] = await Promise.all([
|
||||||
pathname: string;
|
PerfLogger.async('Load App renderer', () =>
|
||||||
error: null;
|
loadAppRenderer({
|
||||||
result: AppRenderResult;
|
serverBundlePath: params.serverBundlePath,
|
||||||
warnings: string[];
|
}),
|
||||||
};
|
),
|
||||||
type SSGError = {
|
PerfLogger.async('Load HTML minifier', () =>
|
||||||
pathname: string;
|
getHtmlMinifier({
|
||||||
error: Error;
|
type: params.htmlMinifierType,
|
||||||
result: null;
|
}),
|
||||||
warnings: string[];
|
),
|
||||||
};
|
PerfLogger.async('Compile SSG template', () =>
|
||||||
type SSGResult = SSGSuccess | SSGError;
|
compileSSGTemplate(params.ssgTemplateContent),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
// Note that we catch all async errors on purpose
|
// Note that we catch all async errors on purpose
|
||||||
// Docusaurus presents all the SSG errors to the user, not just the first one
|
// Docusaurus presents all the SSG errors to the user, not just the first one
|
||||||
|
@ -196,6 +173,7 @@ export async function generateStaticFiles({
|
||||||
renderer,
|
renderer,
|
||||||
params,
|
params,
|
||||||
htmlMinifier,
|
htmlMinifier,
|
||||||
|
ssgTemplate,
|
||||||
}).then(
|
}).then(
|
||||||
(result) => ({
|
(result) => ({
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -210,7 +188,7 @@ export async function generateStaticFiles({
|
||||||
warnings: [],
|
warnings: [],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{concurrency: Concurrency},
|
{concurrency: SSGConcurrency},
|
||||||
);
|
);
|
||||||
|
|
||||||
printSSGWarnings(results);
|
printSSGWarnings(results);
|
||||||
|
@ -247,21 +225,24 @@ async function generateStaticFile({
|
||||||
renderer,
|
renderer,
|
||||||
params,
|
params,
|
||||||
htmlMinifier,
|
htmlMinifier,
|
||||||
|
ssgTemplate,
|
||||||
}: {
|
}: {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
renderer: AppRenderer;
|
renderer: AppRenderer;
|
||||||
params: SSGParams;
|
params: SSGParams;
|
||||||
htmlMinifier: HtmlMinifier;
|
htmlMinifier: HtmlMinifier;
|
||||||
}): Promise<AppRenderResult & {warnings: string[]}> {
|
ssgTemplate: SSGTemplateCompiled;
|
||||||
|
}): Promise<SSGSuccessResult & {warnings: string[]}> {
|
||||||
try {
|
try {
|
||||||
// This only renders the app HTML
|
// This only renders the app HTML
|
||||||
const result = await renderer({
|
const result = await renderer({
|
||||||
pathname,
|
pathname,
|
||||||
});
|
});
|
||||||
// This renders the full page HTML, including head tags...
|
// This renders the full page HTML, including head tags...
|
||||||
const fullPageHtml = renderSSRTemplate({
|
const fullPageHtml = renderSSGTemplate({
|
||||||
params,
|
params,
|
||||||
result,
|
result,
|
||||||
|
ssgTemplate,
|
||||||
});
|
});
|
||||||
const minifierResult = await htmlMinifier.minify(fullPageHtml);
|
const minifierResult = await htmlMinifier.minify(fullPageHtml);
|
||||||
await writeStaticFile({
|
await writeStaticFile({
|
||||||
|
@ -270,7 +251,7 @@ async function generateStaticFile({
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
...result,
|
collectedData: result.collectedData,
|
||||||
// As of today, only the html minifier can emit SSG warnings
|
// As of today, only the html minifier can emit SSG warnings
|
||||||
warnings: minifierResult.warnings,
|
warnings: minifierResult.warnings,
|
||||||
};
|
};
|
||||||
|
@ -307,40 +288,3 @@ It might also require to wrap your client code in ${logger.code(
|
||||||
|
|
||||||
return parts.join('\n');
|
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);
|
|
||||||
}
|
|
50
packages/docusaurus/src/ssg/ssgExecutor.ts
Normal file
50
packages/docusaurus/src/ssg/ssgExecutor.ts
Normal 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;
|
||||||
|
}
|
69
packages/docusaurus/src/ssg/ssgParams.ts
Normal file
69
packages/docusaurus/src/ssg/ssgParams.ts
Normal 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);
|
||||||
|
}
|
|
@ -7,14 +7,15 @@
|
||||||
|
|
||||||
import * as eta from 'eta';
|
import * as eta from 'eta';
|
||||||
import {getBundles} from 'react-loadable-ssr-addon-v5-slorber';
|
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 {AppRenderResult} from '../common';
|
||||||
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
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
|
// that does not look super clean nor typesafe
|
||||||
// Note: changing it is a breaking change because template is configurable
|
// Note: changing it is a breaking change because template is configurable
|
||||||
export type SSRTemplateData = {
|
export type SSGTemplateData = {
|
||||||
appHtml: string;
|
appHtml: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
htmlAttributes: string;
|
htmlAttributes: string;
|
||||||
|
@ -29,16 +30,16 @@ export type SSRTemplateData = {
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SSRTemplateCompiled = (data: SSRTemplateData) => string;
|
export type SSGTemplateCompiled = (data: SSGTemplateData) => string;
|
||||||
|
|
||||||
export async function compileSSRTemplate(
|
export async function compileSSGTemplate(
|
||||||
template: string,
|
template: string,
|
||||||
): Promise<SSRTemplateCompiled> {
|
): Promise<SSGTemplateCompiled> {
|
||||||
const compiledTemplate = eta.compile(template.trim(), {
|
const compiledTemplate = eta.compile(template.trim(), {
|
||||||
rmWhitespace: true,
|
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};
|
return {scripts, stylesheets};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSSRTemplate({
|
export function renderSSGTemplate({
|
||||||
params,
|
params,
|
||||||
result,
|
result,
|
||||||
|
ssgTemplate,
|
||||||
}: {
|
}: {
|
||||||
params: SSGParams;
|
params: SSGParams;
|
||||||
result: AppRenderResult;
|
result: AppRenderResult;
|
||||||
|
ssgTemplate: SSGTemplateCompiled;
|
||||||
}): string {
|
}): string {
|
||||||
const {
|
const {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
@ -77,7 +80,6 @@ export function renderSSRTemplate({
|
||||||
manifest,
|
manifest,
|
||||||
noIndex,
|
noIndex,
|
||||||
DOCUSAURUS_VERSION,
|
DOCUSAURUS_VERSION,
|
||||||
ssrTemplate,
|
|
||||||
} = params;
|
} = params;
|
||||||
const {
|
const {
|
||||||
html: appHtml,
|
html: appHtml,
|
||||||
|
@ -96,7 +98,7 @@ export function renderSSRTemplate({
|
||||||
];
|
];
|
||||||
const metaAttributes = metaStrings.filter(Boolean);
|
const metaAttributes = metaStrings.filter(Boolean);
|
||||||
|
|
||||||
const data: SSRTemplateData = {
|
const data: SSGTemplateData = {
|
||||||
appHtml,
|
appHtml,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
htmlAttributes,
|
htmlAttributes,
|
||||||
|
@ -111,14 +113,14 @@ export function renderSSRTemplate({
|
||||||
version: DOCUSAURUS_VERSION,
|
version: DOCUSAURUS_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ssrTemplate(data);
|
return ssgTemplate(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderHashRouterTemplate({
|
export async function renderHashRouterTemplate({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: SSGParams;
|
params: SSGParams;
|
||||||
}): string {
|
}): Promise<string> {
|
||||||
const {
|
const {
|
||||||
// baseUrl,
|
// baseUrl,
|
||||||
headTags,
|
headTags,
|
||||||
|
@ -126,15 +128,19 @@ export function renderHashRouterTemplate({
|
||||||
postBodyTags,
|
postBodyTags,
|
||||||
manifest,
|
manifest,
|
||||||
DOCUSAURUS_VERSION,
|
DOCUSAURUS_VERSION,
|
||||||
ssrTemplate,
|
ssgTemplateContent,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
const ssgTemplate = await PerfLogger.async('Compile SSG template', () =>
|
||||||
|
compileSSGTemplate(ssgTemplateContent),
|
||||||
|
);
|
||||||
|
|
||||||
const {scripts, stylesheets} = getScriptsAndStylesheets({
|
const {scripts, stylesheets} = getScriptsAndStylesheets({
|
||||||
manifest,
|
manifest,
|
||||||
modules: [],
|
modules: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const data: SSRTemplateData = {
|
const data: SSGTemplateData = {
|
||||||
appHtml: '',
|
appHtml: '',
|
||||||
baseUrl: './',
|
baseUrl: './',
|
||||||
htmlAttributes: '',
|
htmlAttributes: '',
|
||||||
|
@ -149,5 +155,5 @@ export function renderHashRouterTemplate({
|
||||||
version: DOCUSAURUS_VERSION,
|
version: DOCUSAURUS_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ssrTemplate(data);
|
return ssgTemplate(data);
|
||||||
}
|
}
|
80
packages/docusaurus/src/ssg/ssgUtils.ts
Normal file
80
packages/docusaurus/src/ssg/ssgUtils.ts
Normal 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);
|
||||||
|
}
|
|
@ -109,7 +109,7 @@ export async function createStartClientConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
// Generates an `index.html` file with the <script> injected.
|
// Generates an `index.html` file with the <script> injected.
|
||||||
new HtmlWebpackPlugin({
|
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.
|
// So we can define the position where the scripts are injected.
|
||||||
inject: false,
|
inject: false,
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
|
|
Loading…
Add table
Reference in a new issue