@docusaurus/core 3.5.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bin/beforeCli.mjs +1 -1
  2. package/bin/docusaurus.mjs +15 -4
  3. package/lib/babel/preset.d.ts +2 -2
  4. package/lib/babel/preset.js +10 -71
  5. package/lib/client/renderToHtml.js +15 -51
  6. package/lib/client/serverEntry.d.ts +1 -1
  7. package/lib/client/serverEntry.js +2 -0
  8. package/lib/commands/{build.d.ts → build/build.d.ts} +4 -3
  9. package/lib/commands/build/build.js +94 -0
  10. package/lib/commands/build/buildLocale.d.ts +13 -0
  11. package/lib/commands/build/buildLocale.js +143 -0
  12. package/lib/commands/deploy.d.ts +2 -1
  13. package/lib/commands/deploy.js +2 -2
  14. package/lib/commands/serve.js +2 -2
  15. package/lib/commands/start/utils.js +5 -6
  16. package/lib/commands/start/webpack.js +15 -9
  17. package/lib/commands/writeHeadingIds.js +1 -2
  18. package/lib/commands/writeTranslations.js +6 -6
  19. package/lib/index.d.ts +1 -1
  20. package/lib/index.js +1 -1
  21. package/lib/server/configValidation.d.ts +3 -1
  22. package/lib/server/configValidation.js +35 -4
  23. package/lib/server/i18n.d.ts +1 -1
  24. package/lib/server/i18n.js +1 -1
  25. package/lib/server/plugins/plugins.js +13 -13
  26. package/lib/server/plugins/synthetic.d.ts +1 -1
  27. package/lib/server/plugins/synthetic.js +14 -17
  28. package/lib/server/site.js +10 -4
  29. package/lib/server/translations/translationsExtractor.d.ts +5 -11
  30. package/lib/server/translations/translationsExtractor.js +8 -196
  31. package/lib/ssg/ssg.d.ts +21 -0
  32. package/lib/{ssg.js → ssg/ssg.js} +91 -82
  33. package/lib/ssg/ssgExecutor.d.ts +16 -0
  34. package/lib/ssg/ssgExecutor.js +34 -0
  35. package/lib/{server/utils.d.ts → ssg/ssgNodeRequire.d.ts} +5 -2
  36. package/lib/ssg/ssgNodeRequire.js +40 -0
  37. package/lib/ssg/ssgParams.d.ts +28 -0
  38. package/lib/ssg/ssgParams.js +36 -0
  39. package/lib/{templates/templates.d.ts → ssg/ssgTemplate.d.ts} +7 -6
  40. package/lib/{templates/templates.js → ssg/ssgTemplate.js} +11 -9
  41. package/lib/ssg/ssgUtils.d.ts +17 -0
  42. package/lib/ssg/ssgUtils.js +58 -0
  43. package/lib/webpack/base.d.ts +4 -2
  44. package/lib/webpack/base.js +33 -20
  45. package/lib/webpack/client.d.ts +7 -3
  46. package/lib/webpack/client.js +32 -14
  47. package/lib/webpack/configure.d.ts +20 -6
  48. package/lib/webpack/configure.js +31 -16
  49. package/lib/webpack/plugins/ChunkAssetPlugin.d.ts +0 -11
  50. package/lib/webpack/plugins/ChunkAssetPlugin.js +48 -33
  51. package/lib/webpack/plugins/ForceTerminatePlugin.js +2 -2
  52. package/lib/webpack/plugins/StaticDirectoriesCopyPlugin.d.ts +2 -2
  53. package/lib/webpack/plugins/StaticDirectoriesCopyPlugin.js +5 -2
  54. package/lib/webpack/server.d.ts +3 -2
  55. package/lib/webpack/server.js +12 -10
  56. package/lib/webpack/{minification.d.ts → utils/getHttpsConfig.d.ts} +4 -2
  57. package/lib/webpack/utils/getHttpsConfig.js +60 -0
  58. package/package.json +19 -44
  59. package/lib/commands/build.js +0 -240
  60. package/lib/server/utils.js +0 -20
  61. package/lib/ssg.d.ts +0 -35
  62. package/lib/utils.d.ts +0 -9
  63. package/lib/utils.js +0 -78
  64. package/lib/webpack/minification.js +0 -96
  65. package/lib/webpack/plugins/WaitPlugin.d.ts +0 -16
  66. package/lib/webpack/plugins/WaitPlugin.js +0 -47
  67. package/lib/webpack/utils.d.ts +0 -33
  68. package/lib/webpack/utils.js +0 -215
  69. /package/lib/{templates/ssr.html.template.d.ts → ssg/ssgTemplate.html.d.ts} +0 -0
  70. /package/lib/{templates/ssr.html.template.js → ssg/ssgTemplate.html.js} +0 -0
  71. /package/lib/{templates → webpack/templates}/dev.html.template.ejs +0 -0
@@ -6,33 +6,12 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.globSourceCodeFilePaths = globSourceCodeFilePaths;
10
9
  exports.extractSiteSourceCodeTranslations = extractSiteSourceCodeTranslations;
11
- exports.extractAllSourceCodeFileTranslations = extractAllSourceCodeFileTranslations;
12
- exports.extractSourceCodeFileTranslations = extractSourceCodeFileTranslations;
13
10
  const tslib_1 = require("tslib");
14
11
  const path_1 = tslib_1.__importDefault(require("path"));
15
- const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
16
12
  const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
17
- const traverse_1 = tslib_1.__importDefault(require("@babel/traverse"));
18
- const generator_1 = tslib_1.__importDefault(require("@babel/generator"));
19
- const core_1 = require("@babel/core");
20
13
  const utils_1 = require("@docusaurus/utils");
21
- const utils_2 = require("../utils");
22
- // We only support extracting source code translations from these kind of files
23
- const TranslatableSourceCodeExtension = new Set([
24
- '.js',
25
- '.jsx',
26
- '.ts',
27
- '.tsx',
28
- // TODO support md/mdx too? (may be overkill)
29
- // need to compile the MDX to JSX first and remove front matter
30
- // '.md',
31
- // '.mdx',
32
- ]);
33
- function isTranslatableSourceCodePath(filePath) {
34
- return TranslatableSourceCodeExtension.has(path_1.default.extname(filePath));
35
- }
14
+ const babel_1 = require("@docusaurus/babel");
36
15
  function getSiteSourceCodeFilePaths(siteDir) {
37
16
  return [path_1.default.join(siteDir, utils_1.SRC_DIR_NAME)];
38
17
  }
@@ -49,10 +28,6 @@ function getPluginSourceCodeFilePaths(plugin) {
49
28
  }
50
29
  return codePaths.map((p) => path_1.default.resolve(plugin.path, p));
51
30
  }
52
- async function globSourceCodeFilePaths(dirPaths) {
53
- const filePaths = await (0, utils_2.safeGlobby)(dirPaths);
54
- return filePaths.filter(isTranslatableSourceCodePath);
55
- }
56
31
  async function getSourceCodeFilePaths(siteDir, plugins) {
57
32
  const sitePaths = getSiteSourceCodeFilePaths(siteDir);
58
33
  // The getPathsToWatch() generally returns the js/jsx/ts/tsx/md/mdx file paths
@@ -61,9 +36,13 @@ async function getSourceCodeFilePaths(siteDir, plugins) {
61
36
  // new lifecycle method for that???
62
37
  const pluginsPaths = plugins.flatMap(getPluginSourceCodeFilePaths);
63
38
  const allPaths = [...sitePaths, ...pluginsPaths];
64
- return globSourceCodeFilePaths(allPaths);
39
+ return (0, utils_1.globTranslatableSourceFiles)(allPaths);
65
40
  }
66
- async function extractSiteSourceCodeTranslations(siteDir, plugins, babelOptions, extraSourceCodeFilePaths = []) {
41
+ async function extractSiteSourceCodeTranslations({ siteDir, plugins, extraSourceCodeFilePaths = [], }) {
42
+ const babelOptions = (0, babel_1.getBabelOptions)({
43
+ isServer: true,
44
+ babelOptions: await (0, babel_1.getCustomBabelConfigFilePath)(siteDir),
45
+ });
67
46
  // Should we warn here if the same translation "key" is found in multiple
68
47
  // source code files?
69
48
  function toTranslationFileContent(sourceCodeFileTranslations) {
@@ -74,7 +53,7 @@ async function extractSiteSourceCodeTranslations(siteDir, plugins, babelOptions,
74
53
  ...sourceCodeFilePaths,
75
54
  ...extraSourceCodeFilePaths,
76
55
  ];
77
- const sourceCodeFilesTranslations = await extractAllSourceCodeFileTranslations(allSourceCodeFilePaths, babelOptions);
56
+ const sourceCodeFilesTranslations = await (0, babel_1.extractAllSourceCodeFileTranslations)(allSourceCodeFilePaths, babelOptions);
78
57
  logSourceCodeFileTranslationsWarnings(sourceCodeFilesTranslations);
79
58
  return toTranslationFileContent(sourceCodeFilesTranslations);
80
59
  }
@@ -85,170 +64,3 @@ function logSourceCodeFileTranslationsWarnings(sourceCodeFilesTranslations) {
85
64
  }
86
65
  });
87
66
  }
88
- async function extractAllSourceCodeFileTranslations(sourceCodeFilePaths, babelOptions) {
89
- return Promise.all(sourceCodeFilePaths.flatMap((sourceFilePath) => extractSourceCodeFileTranslations(sourceFilePath, babelOptions)));
90
- }
91
- async function extractSourceCodeFileTranslations(sourceCodeFilePath, babelOptions) {
92
- try {
93
- const code = await fs_extra_1.default.readFile(sourceCodeFilePath, 'utf8');
94
- const ast = (0, core_1.parse)(code, {
95
- ...babelOptions,
96
- ast: true,
97
- // filename is important, because babel does not process the same files
98
- // according to their js/ts extensions.
99
- // See https://twitter.com/NicoloRibaudo/status/1321130735605002243
100
- filename: sourceCodeFilePath,
101
- });
102
- const translations = extractSourceCodeAstTranslations(ast, sourceCodeFilePath);
103
- return translations;
104
- }
105
- catch (err) {
106
- logger_1.default.error `Error while attempting to extract Docusaurus translations from source code file at path=${sourceCodeFilePath}.`;
107
- throw err;
108
- }
109
- }
110
- /*
111
- Need help understanding this?
112
-
113
- Useful resources:
114
- https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
115
- https://github.com/formatjs/formatjs/blob/main/packages/babel-plugin-formatjs/index.ts
116
- https://github.com/pugjs/babel-walk
117
- */
118
- function extractSourceCodeAstTranslations(ast, sourceCodeFilePath) {
119
- function sourceWarningPart(node) {
120
- return `File: ${sourceCodeFilePath} at line ${node.loc?.start.line ?? '?'}
121
- Full code: ${(0, generator_1.default)(node).code}`;
122
- }
123
- const translations = {};
124
- const warnings = [];
125
- let translateComponentName;
126
- let translateFunctionName;
127
- // First pass: find import declarations of Translate / translate.
128
- // If not found, don't process the rest to avoid false positives
129
- (0, traverse_1.default)(ast, {
130
- ImportDeclaration(path) {
131
- if (path.node.importKind === 'type' ||
132
- path.get('source').node.value !== '@docusaurus/Translate') {
133
- return;
134
- }
135
- const importSpecifiers = path.get('specifiers');
136
- const defaultImport = importSpecifiers.find((specifier) => specifier.node.type === 'ImportDefaultSpecifier');
137
- const callbackImport = importSpecifiers.find((specifier) => specifier.node.type === 'ImportSpecifier' &&
138
- (specifier.get('imported')
139
- .node.name === 'translate' ||
140
- specifier.get('imported')
141
- .node.value === 'translate'));
142
- translateComponentName = defaultImport?.get('local').node.name;
143
- translateFunctionName = callbackImport?.get('local').node.name;
144
- },
145
- });
146
- (0, traverse_1.default)(ast, {
147
- ...(translateComponentName && {
148
- JSXElement(path) {
149
- if (!path
150
- .get('openingElement')
151
- .get('name')
152
- .isJSXIdentifier({ name: translateComponentName })) {
153
- return;
154
- }
155
- function evaluateJSXProp(propName) {
156
- const attributePath = path
157
- .get('openingElement.attributes')
158
- .find((attr) => attr.isJSXAttribute() &&
159
- attr.get('name').isJSXIdentifier({ name: propName }));
160
- if (attributePath) {
161
- const attributeValue = attributePath.get('value');
162
- const attributeValueEvaluated = attributeValue.isJSXExpressionContainer()
163
- ? attributeValue.get('expression').evaluate()
164
- : attributeValue.evaluate();
165
- if (attributeValueEvaluated.confident &&
166
- typeof attributeValueEvaluated.value === 'string') {
167
- return attributeValueEvaluated.value;
168
- }
169
- warnings.push(`<Translate> prop=${propName} should be a statically evaluable object.
170
- Example: <Translate id="optional id" description="optional description">Message</Translate>
171
- Dynamically constructed values are not allowed, because they prevent translations to be extracted.
172
- ${sourceWarningPart(path.node)}`);
173
- }
174
- return undefined;
175
- }
176
- const id = evaluateJSXProp('id');
177
- const description = evaluateJSXProp('description');
178
- let message;
179
- const childrenPath = path.get('children');
180
- // Handle empty content
181
- if (!childrenPath.length) {
182
- if (!id) {
183
- warnings.push(`<Translate> without children must have id prop.
184
- Example: <Translate id="my-id" />
185
- ${sourceWarningPart(path.node)}`);
186
- }
187
- else {
188
- translations[id] = {
189
- message: id,
190
- ...(description && { description }),
191
- };
192
- }
193
- return;
194
- }
195
- // Handle single non-empty content
196
- const singleChildren = childrenPath
197
- // Remove empty/useless text nodes that might be around our
198
- // translation! Makes the translation system more reliable to JSX
199
- // formatting issues
200
- .filter((children) => !(children.isJSXText() &&
201
- children.node.value.replace('\n', '').trim() === ''))
202
- .pop();
203
- const isJSXText = singleChildren?.isJSXText();
204
- const isJSXExpressionContainer = singleChildren?.isJSXExpressionContainer() &&
205
- singleChildren.get('expression').evaluate().confident;
206
- if (isJSXText || isJSXExpressionContainer) {
207
- message = isJSXText
208
- ? singleChildren.node.value.trim().replace(/\s+/g, ' ')
209
- : String(singleChildren.get('expression').evaluate().value);
210
- translations[id ?? message] = {
211
- message,
212
- ...(description && { description }),
213
- };
214
- }
215
- else {
216
- warnings.push(`Translate content could not be extracted. It has to be a static string and use optional but static props, like <Translate id="my-id" description="my-description">text</Translate>.
217
- ${sourceWarningPart(path.node)}`);
218
- }
219
- },
220
- }),
221
- ...(translateFunctionName && {
222
- CallExpression(path) {
223
- if (!path.get('callee').isIdentifier({ name: translateFunctionName })) {
224
- return;
225
- }
226
- const args = path.get('arguments');
227
- if (args.length === 1 || args.length === 2) {
228
- const firstArgPath = args[0];
229
- // translate("x" + "y"); => translate("xy");
230
- const firstArgEvaluated = firstArgPath.evaluate();
231
- if (firstArgEvaluated.confident &&
232
- typeof firstArgEvaluated.value === 'object') {
233
- const { message, id, description } = firstArgEvaluated.value;
234
- translations[String(id ?? message)] = {
235
- message: String(message ?? id),
236
- ...(Boolean(description) && { description: String(description) }),
237
- };
238
- }
239
- else {
240
- warnings.push(`translate() first arg should be a statically evaluable object.
241
- Example: translate({message: "text",id: "optional.id",description: "optional description"}
242
- Dynamically constructed values are not allowed, because they prevent translations to be extracted.
243
- ${sourceWarningPart(path.node)}`);
244
- }
245
- }
246
- else {
247
- warnings.push(`translate() function only takes 1 or 2 args
248
- ${sourceWarningPart(path.node)}`);
249
- }
250
- },
251
- }),
252
- });
253
- return { sourceCodeFilePath, translations, warnings };
254
- }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import type { SSGParams } from './ssgParams';
8
+ import type { AppRenderer, SiteCollectedData } from '../common';
9
+ export declare function loadAppRenderer({ serverBundlePath, }: {
10
+ serverBundlePath: string;
11
+ }): Promise<AppRenderer>;
12
+ export declare function printSSGWarnings(results: {
13
+ pathname: string;
14
+ warnings: string[];
15
+ }[]): void;
16
+ export declare function generateStaticFiles({ pathnames, params, }: {
17
+ pathnames: string[];
18
+ params: SSGParams;
19
+ }): Promise<{
20
+ collectedData: SiteCollectedData;
21
+ }>;
@@ -7,31 +7,26 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.loadAppRenderer = loadAppRenderer;
10
+ exports.printSSGWarnings = printSSGWarnings;
10
11
  exports.generateStaticFiles = generateStaticFiles;
11
- exports.generateHashRouterEntrypoint = generateHashRouterEntrypoint;
12
12
  const tslib_1 = require("tslib");
13
13
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
14
- const module_1 = require("module");
15
14
  const path_1 = tslib_1.__importDefault(require("path"));
16
15
  const lodash_1 = tslib_1.__importDefault(require("lodash"));
16
+ // TODO eval is archived / unmaintained: https://github.com/pierrec/node-eval
17
+ // We should internalize/modernize it
17
18
  const eval_1 = tslib_1.__importDefault(require("eval"));
18
19
  const p_map_1 = tslib_1.__importDefault(require("p-map"));
19
- const html_minifier_terser_1 = require("html-minifier-terser");
20
- const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
21
- const utils_1 = require("./utils");
22
- const templates_1 = require("./templates/templates");
23
- // Secret way to set SSR plugin concurrency option
24
- // Waiting for feedback before documenting this officially?
25
- const Concurrency = process.env.DOCUSAURUS_SSR_CONCURRENCY
26
- ? parseInt(process.env.DOCUSAURUS_SSR_CONCURRENCY, 10)
27
- : // Not easy to define a reasonable option default
28
- // Will still be better than Infinity
29
- // See also https://github.com/sindresorhus/p-map/issues/24
30
- 32;
20
+ const logger_1 = tslib_1.__importStar(require("@docusaurus/logger"));
21
+ const bundler_1 = require("@docusaurus/bundler");
22
+ const ssgTemplate_1 = require("./ssgTemplate");
23
+ const ssgUtils_1 = require("./ssgUtils");
24
+ const ssgNodeRequire_1 = require("./ssgNodeRequire");
31
25
  async function loadAppRenderer({ serverBundlePath, }) {
32
- const source = await utils_1.PerfLogger.async(`Load server bundle`, () => fs_extra_1.default.readFile(serverBundlePath));
33
- utils_1.PerfLogger.log(`Server bundle size = ${(source.length / 1024000).toFixed(3)} MB`);
26
+ const source = await logger_1.PerfLogger.async(`Load server bundle`, () => fs_extra_1.default.readFile(serverBundlePath));
27
+ logger_1.PerfLogger.log(`Server bundle size = ${(source.length / 1024000).toFixed(3)} MB`);
34
28
  const filename = path_1.default.basename(serverBundlePath);
29
+ const ssgRequire = (0, ssgNodeRequire_1.createSSGRequire)(serverBundlePath);
35
30
  const globals = {
36
31
  // When using "new URL('file.js', import.meta.url)", Webpack will emit
37
32
  // __filename, and this plugin will throw. not sure the __filename value
@@ -40,43 +35,91 @@ async function loadAppRenderer({ serverBundlePath, }) {
40
35
  __filename: '',
41
36
  // This uses module.createRequire() instead of very old "require-like" lib
42
37
  // See also: https://github.com/pierrec/node-eval/issues/33
43
- require: (0, module_1.createRequire)(serverBundlePath),
38
+ require: ssgRequire.require,
44
39
  };
45
- const serverEntry = await utils_1.PerfLogger.async(`Evaluate server bundle`, () => (0, eval_1.default)(source,
40
+ const serverEntry = await logger_1.PerfLogger.async(`Evaluate server bundle`, () => (0, eval_1.default)(source,
46
41
  /* filename: */ filename,
47
42
  /* scope: */ globals,
48
43
  /* includeGlobals: */ true));
49
44
  if (!serverEntry?.default || typeof serverEntry.default !== 'function') {
50
45
  throw new Error(`Server bundle export from "${filename}" must be a function that renders the Docusaurus React app.`);
51
46
  }
52
- return serverEntry.default;
53
- }
54
- function pathnameToFilename({ pathname, trailingSlash, }) {
55
- const outputFileName = pathname.replace(/^[/\\]/, ''); // Remove leading slashes for webpack-dev-server
56
- // Paths ending with .html are left untouched
57
- if (/\.html?$/i.test(outputFileName)) {
58
- return outputFileName;
47
+ async function shutdown() {
48
+ ssgRequire.cleanup();
59
49
  }
60
- // Legacy retro-compatible behavior
61
- if (typeof trailingSlash === 'undefined') {
62
- return path_1.default.join(outputFileName, 'index.html');
50
+ return {
51
+ render: serverEntry.default,
52
+ shutdown,
53
+ };
54
+ }
55
+ function printSSGWarnings(results) {
56
+ // Escape hatch because SWC is quite aggressive to report errors
57
+ // See https://github.com/facebook/docusaurus/pull/10554
58
+ // See https://github.com/swc-project/swc/discussions/9616#discussioncomment-10846201
59
+ if (process.env.DOCUSAURUS_IGNORE_SSG_WARNINGS === 'true') {
60
+ return;
63
61
  }
64
- // New behavior: we can say if we prefer file/folder output
65
- // Useful resource: https://github.com/slorber/trailing-slash-guide
66
- if (pathname === '' || pathname.endsWith('/') || trailingSlash) {
67
- return path_1.default.join(outputFileName, 'index.html');
62
+ const ignoredWarnings = [
63
+ // TODO React/Docusaurus emit NULL chars, and minifier detects it
64
+ // see https://github.com/facebook/docusaurus/issues/9985
65
+ 'Unexpected null character',
66
+ ];
67
+ const keepWarning = (warning) => {
68
+ return !ignoredWarnings.some((iw) => warning.includes(iw));
69
+ };
70
+ const resultsWithWarnings = results
71
+ .map((result) => {
72
+ return {
73
+ ...result,
74
+ warnings: result.warnings.filter(keepWarning),
75
+ };
76
+ })
77
+ .filter((result) => result.warnings.length > 0);
78
+ if (resultsWithWarnings.length) {
79
+ const message = `Docusaurus static site generation process emitted warnings for ${resultsWithWarnings.length} path${resultsWithWarnings.length ? 's' : ''}
80
+ This is non-critical and can be disabled with DOCUSAURUS_IGNORE_SSG_WARNINGS=true
81
+ Troubleshooting guide: https://github.com/facebook/docusaurus/discussions/10580
82
+
83
+ - ${resultsWithWarnings
84
+ .map((result) => `${logger_1.default.path(result.pathname)}:
85
+ - ${result.warnings.join('\n - ')}
86
+ `)
87
+ .join('\n- ')}`;
88
+ logger_1.default.warn(message);
68
89
  }
69
- return `${outputFileName}.html`;
70
90
  }
71
- async function generateStaticFiles({ pathnames, renderer, params, }) {
91
+ async function generateStaticFiles({ pathnames, params, }) {
92
+ const [renderer, htmlMinifier, ssgTemplate] = await Promise.all([
93
+ logger_1.PerfLogger.async('Load App renderer', () => loadAppRenderer({
94
+ serverBundlePath: params.serverBundlePath,
95
+ })),
96
+ logger_1.PerfLogger.async('Load HTML minifier', () => (0, bundler_1.getHtmlMinifier)({
97
+ type: params.htmlMinifierType,
98
+ })),
99
+ logger_1.PerfLogger.async('Compile SSG template', () => (0, ssgTemplate_1.compileSSGTemplate)(params.ssgTemplateContent)),
100
+ ]);
72
101
  // Note that we catch all async errors on purpose
73
102
  // Docusaurus presents all the SSG errors to the user, not just the first one
74
103
  const results = await (0, p_map_1.default)(pathnames, async (pathname) => generateStaticFile({
75
104
  pathname,
76
105
  renderer,
77
106
  params,
78
- }).then((result) => ({ pathname, result, error: null }), (error) => ({ pathname, result: null, error: error })), { concurrency: Concurrency });
79
- const [allSSGErrors, allSSGSuccesses] = lodash_1.default.partition(results, (r) => !!r.error);
107
+ htmlMinifier,
108
+ ssgTemplate,
109
+ }).then((result) => ({
110
+ pathname,
111
+ result,
112
+ error: null,
113
+ warnings: result.warnings,
114
+ }), (error) => ({
115
+ pathname,
116
+ result: null,
117
+ error: error,
118
+ warnings: [],
119
+ })), { concurrency: ssgUtils_1.SSGConcurrency });
120
+ await renderer.shutdown();
121
+ printSSGWarnings(results);
122
+ const [allSSGErrors, allSSGSuccesses] = lodash_1.default.partition(results, (result) => !!result.error);
80
123
  if (allSSGErrors.length > 0) {
81
124
  const message = `Docusaurus static site generation failed for ${allSSGErrors.length} path${allSSGErrors.length ? 's' : ''}:\n- ${allSSGErrors
82
125
  .map((ssgError) => logger_1.default.path(ssgError.pathname))
@@ -93,24 +136,29 @@ async function generateStaticFiles({ pathnames, renderer, params, }) {
93
136
  .value();
94
137
  return { collectedData };
95
138
  }
96
- async function generateStaticFile({ pathname, renderer, params, }) {
139
+ async function generateStaticFile({ pathname, renderer, params, htmlMinifier, ssgTemplate, }) {
97
140
  try {
98
141
  // This only renders the app HTML
99
- const result = await renderer({
142
+ const result = await renderer.render({
100
143
  pathname,
101
144
  });
102
145
  // This renders the full page HTML, including head tags...
103
- const fullPageHtml = (0, templates_1.renderSSRTemplate)({
146
+ const fullPageHtml = (0, ssgTemplate_1.renderSSGTemplate)({
104
147
  params,
105
148
  result,
149
+ ssgTemplate,
106
150
  });
107
- const content = await minifyHtml(fullPageHtml);
108
- await writeStaticFile({
151
+ const minifierResult = await htmlMinifier.minify(fullPageHtml);
152
+ await (0, ssgUtils_1.writeStaticFile)({
109
153
  pathname,
110
- content,
154
+ content: minifierResult.code,
111
155
  params,
112
156
  });
113
- return result;
157
+ return {
158
+ collectedData: result.collectedData,
159
+ // As of today, only the html minifier can emit SSG warnings
160
+ warnings: minifierResult.warnings,
161
+ };
114
162
  }
115
163
  catch (errorUnknown) {
116
164
  const error = errorUnknown;
@@ -133,42 +181,3 @@ It might also require to wrap your client code in ${logger_1.default.code('useEf
133
181
  }
134
182
  return parts.join('\n');
135
183
  }
136
- async function generateHashRouterEntrypoint({ content, params, }) {
137
- await writeStaticFile({
138
- pathname: '/',
139
- content,
140
- params,
141
- });
142
- }
143
- async function writeStaticFile({ content, pathname, params, }) {
144
- function removeBaseUrl(p, baseUrl) {
145
- return baseUrl === '/' ? p : p.replace(new RegExp(`^${baseUrl}`), '/');
146
- }
147
- const filename = pathnameToFilename({
148
- pathname: removeBaseUrl(pathname, params.baseUrl),
149
- trailingSlash: params.trailingSlash,
150
- });
151
- const filePath = path_1.default.join(params.outDir, filename);
152
- await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
153
- await fs_extra_1.default.writeFile(filePath, content);
154
- }
155
- async function minifyHtml(html) {
156
- try {
157
- if (process.env.SKIP_HTML_MINIFICATION === 'true') {
158
- return html;
159
- }
160
- // Minify html with https://github.com/DanielRuf/html-minifier-terser
161
- return await (0, html_minifier_terser_1.minify)(html, {
162
- removeComments: false,
163
- removeRedundantAttributes: true,
164
- removeEmptyAttributes: true,
165
- removeScriptTypeAttributes: true,
166
- removeStyleLinkTypeAttributes: true,
167
- useShortDoctype: true,
168
- minifyJS: true,
169
- });
170
- }
171
- catch (err) {
172
- throw new Error('HTML minification failed', { cause: err });
173
- }
174
- }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import type { Props, RouterType } from '@docusaurus/types';
8
+ import type { SiteCollectedData } from '../common';
9
+ export declare function executeSSG({ props, serverBundlePath, clientManifestPath, router, }: {
10
+ props: Props;
11
+ serverBundlePath: string;
12
+ clientManifestPath: string;
13
+ router: RouterType;
14
+ }): Promise<{
15
+ collectedData: SiteCollectedData;
16
+ }>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Facebook, Inc. and its affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.executeSSG = executeSSG;
10
+ const logger_1 = require("@docusaurus/logger");
11
+ const ssgParams_1 = require("./ssgParams");
12
+ const ssg_1 = require("./ssg");
13
+ const ssgTemplate_1 = require("./ssgTemplate");
14
+ const ssgUtils_1 = require("./ssgUtils");
15
+ // TODO Docusaurus v4 - introduce SSG worker threads
16
+ async function executeSSG({ props, serverBundlePath, clientManifestPath, router, }) {
17
+ const params = await (0, ssgParams_1.createSSGParams)({
18
+ serverBundlePath,
19
+ clientManifestPath,
20
+ props,
21
+ });
22
+ if (router === 'hash') {
23
+ logger_1.PerfLogger.start('Generate Hash Router entry point');
24
+ const content = await (0, ssgTemplate_1.renderHashRouterTemplate)({ params });
25
+ await (0, ssgUtils_1.generateHashRouterEntrypoint)({ content, params });
26
+ logger_1.PerfLogger.end('Generate Hash Router entry point');
27
+ return { collectedData: {} };
28
+ }
29
+ const ssgResult = await logger_1.PerfLogger.async('Generate static files', () => (0, ssg_1.generateStaticFiles)({
30
+ pathnames: props.routesPaths,
31
+ params,
32
+ }));
33
+ return ssgResult;
34
+ }
@@ -4,5 +4,8 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
- import { Globby } from '@docusaurus/utils';
8
- export declare function safeGlobby(patterns: string[], options?: Globby.GlobbyOptions): Promise<string[]>;
7
+ export type SSGNodeRequire = {
8
+ require: NodeJS.Require;
9
+ cleanup: () => void;
10
+ };
11
+ export declare function createSSGRequire(serverBundlePath: string): SSGNodeRequire;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Facebook, Inc. and its affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createSSGRequire = createSSGRequire;
10
+ const module_1 = require("module");
11
+ // The eval/vm.Script used for running the server bundle need a require() impl
12
+ // This impl has to be relative to the server bundler path
13
+ // This enables the server bundle to resolve relative paths such as:
14
+ // - require('./assets/js/some-chunk.123456.js')
15
+ //
16
+ // Unfortunately, Node.js vm.Script doesn't isolate memory / require.cache
17
+ // This means that if we build multiple Docusaurus localized sites in a row
18
+ // The Node.js require cache will keep growing and retain in memory the JS
19
+ // assets of the former SSG builds
20
+ // We have to clean up the node require cache manually to avoid leaking memory!
21
+ // See also https://x.com/sebastienlorber/status/1848399310116831702
22
+ function createSSGRequire(serverBundlePath) {
23
+ const realRequire = (0, module_1.createRequire)(serverBundlePath);
24
+ const allRequiredIds = [];
25
+ const ssgRequireFunction = (id) => {
26
+ const module = realRequire(id);
27
+ allRequiredIds.push(id);
28
+ return module;
29
+ };
30
+ const cleanup = () => {
31
+ allRequiredIds.forEach((id) => {
32
+ delete realRequire.cache[realRequire.resolve(id)];
33
+ });
34
+ };
35
+ ssgRequireFunction.resolve = realRequire.resolve;
36
+ ssgRequireFunction.cache = realRequire.cache;
37
+ ssgRequireFunction.extensions = realRequire.extensions;
38
+ ssgRequireFunction.main = realRequire.main;
39
+ return { require: ssgRequireFunction, cleanup };
40
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import type { Manifest } from 'react-loadable-ssr-addon-v5-slorber';
8
+ import type { Props } from '@docusaurus/types';
9
+ import type { HtmlMinifierType } from '@docusaurus/bundler';
10
+ export type SSGParams = {
11
+ trailingSlash: boolean | undefined;
12
+ manifest: Manifest;
13
+ headTags: string;
14
+ preBodyTags: string;
15
+ postBodyTags: string;
16
+ outDir: string;
17
+ baseUrl: string;
18
+ noIndex: boolean;
19
+ DOCUSAURUS_VERSION: string;
20
+ htmlMinifierType: HtmlMinifierType;
21
+ serverBundlePath: string;
22
+ ssgTemplateContent: string;
23
+ };
24
+ export declare function createSSGParams({ props, serverBundlePath, clientManifestPath, }: {
25
+ props: Props;
26
+ serverBundlePath: string;
27
+ clientManifestPath: string;
28
+ }): Promise<SSGParams>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) Facebook, Inc. and its affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createSSGParams = createSSGParams;
10
+ const tslib_1 = require("tslib");
11
+ const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
12
+ const utils_1 = require("@docusaurus/utils");
13
+ const logger_1 = require("@docusaurus/logger");
14
+ const ssgTemplate_html_1 = tslib_1.__importDefault(require("./ssgTemplate.html"));
15
+ async function createSSGParams({ props, serverBundlePath, clientManifestPath, }) {
16
+ const manifest = await logger_1.PerfLogger.async('Read client manifest', () => fs_extra_1.default.readJSON(clientManifestPath, 'utf-8'));
17
+ const params = {
18
+ trailingSlash: props.siteConfig.trailingSlash,
19
+ outDir: props.outDir,
20
+ baseUrl: props.baseUrl,
21
+ manifest,
22
+ headTags: props.headTags,
23
+ preBodyTags: props.preBodyTags,
24
+ postBodyTags: props.postBodyTags,
25
+ ssgTemplateContent: props.siteConfig.ssrTemplate ?? ssgTemplate_html_1.default,
26
+ noIndex: props.siteConfig.noIndex,
27
+ DOCUSAURUS_VERSION: utils_1.DOCUSAURUS_VERSION,
28
+ serverBundlePath,
29
+ htmlMinifierType: props.siteConfig.future.experimental_faster
30
+ .swcHtmlMinimizer
31
+ ? 'swc'
32
+ : 'terser',
33
+ };
34
+ // Useless but ensures that SSG params remain serializable
35
+ return structuredClone(params);
36
+ }