@docusaurus/core 3.7.0 → 3.8.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 (47) hide show
  1. package/bin/beforeCli.mjs +3 -3
  2. package/lib/client/App.js +7 -4
  3. package/lib/client/serverEntry.js +10 -4
  4. package/lib/client/serverHelmetUtils.d.ts +11 -0
  5. package/lib/client/serverHelmetUtils.js +39 -0
  6. package/lib/client/theme-fallback/ThemeProvider/index.d.ts +9 -0
  7. package/lib/client/theme-fallback/ThemeProvider/index.js +17 -0
  8. package/lib/commands/build/buildLocale.js +33 -10
  9. package/lib/commands/clear.js +4 -3
  10. package/lib/commands/deploy.js +70 -28
  11. package/lib/commands/serve.js +2 -2
  12. package/lib/commands/start/start.js +7 -4
  13. package/lib/commands/start/utils.js +17 -3
  14. package/lib/commands/start/webpack.js +1 -1
  15. package/lib/commands/utils/clearPath.d.ts +10 -0
  16. package/lib/commands/utils/clearPath.js +21 -0
  17. package/lib/commands/utils/legacy/evalSourceMapMiddleware.d.ts +2 -0
  18. package/lib/commands/utils/legacy/evalSourceMapMiddleware.js +57 -0
  19. package/lib/commands/utils/openBrowser/openBrowser.d.ts +10 -0
  20. package/lib/commands/utils/openBrowser/openBrowser.js +124 -0
  21. package/lib/commands/utils/openBrowser/openChrome.applescript +94 -0
  22. package/lib/server/configValidation.d.ts +3 -1
  23. package/lib/server/configValidation.js +45 -4
  24. package/lib/ssg/ssgEnv.d.ts +10 -0
  25. package/lib/ssg/ssgEnv.js +38 -0
  26. package/lib/ssg/ssgExecutor.js +121 -8
  27. package/lib/ssg/ssgGlobalResult.d.ts +12 -0
  28. package/lib/ssg/ssgGlobalResult.js +74 -0
  29. package/lib/ssg/ssgParams.d.ts +1 -0
  30. package/lib/ssg/ssgParams.js +1 -0
  31. package/lib/ssg/ssgRenderer.d.ts +29 -0
  32. package/lib/ssg/{ssg.js → ssgRenderer.js} +55 -90
  33. package/lib/ssg/ssgTemplate.js +6 -7
  34. package/lib/ssg/ssgUtils.d.ts +0 -1
  35. package/lib/ssg/ssgUtils.js +0 -9
  36. package/lib/ssg/ssgWorkerInline.d.ts +12 -0
  37. package/lib/ssg/ssgWorkerInline.js +17 -0
  38. package/lib/ssg/ssgWorkerThread.d.ts +13 -0
  39. package/lib/ssg/ssgWorkerThread.js +36 -0
  40. package/lib/webpack/base.js +76 -28
  41. package/lib/webpack/client.js +0 -3
  42. package/lib/webpack/plugins/BundlerCPUProfilerPlugin.d.ts +12 -0
  43. package/lib/webpack/plugins/BundlerCPUProfilerPlugin.js +41 -0
  44. package/package.json +14 -14
  45. package/lib/ssg/ssg.d.ts +0 -21
  46. package/lib/webpack/plugins/CleanWebpackPlugin.d.ts +0 -59
  47. package/lib/webpack/plugins/CleanWebpackPlugin.js +0 -177
@@ -0,0 +1,124 @@
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.default = openBrowser;
10
+ const tslib_1 = require("tslib");
11
+ // This code was initially in CRA/react-dev-utils (deprecated in 2025)
12
+ // We copied and refactored it
13
+ // See https://github.com/facebook/docusaurus/pull/10956
14
+ // See https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openBrowser.js
15
+ /* eslint-disable */
16
+ const child_process_1 = require("child_process");
17
+ const util_1 = require("util");
18
+ const open_1 = tslib_1.__importDefault(require("open"));
19
+ const logger_1 = require("@docusaurus/logger");
20
+ const execPromise = (0, util_1.promisify)(child_process_1.exec);
21
+ // Not sure if we need this, but let's keep a secret escape hatch
22
+ // CRA/react-dev-utils supported BROWSER/BROWSER_ARGS
23
+ const BrowserEnv = process.env.DOCUSAURUS_BROWSER;
24
+ const BrowserEnvArgs = process.env.DOCUSAURUS_BROWSER_ARGS
25
+ ? process.env.DOCUSAURUS_BROWSER_ARGS.split(' ')
26
+ : [];
27
+ // If we're on OS X, the user hasn't specifically
28
+ // requested a different browser, we can try opening
29
+ // Chrome with AppleScript. This lets us reuse an
30
+ // existing tab when possible instead of creating a new one.
31
+ // Copied from https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openBrowser.js
32
+ async function tryOpenWithAppleScript({ url, browser, }) {
33
+ const shouldTryOpenChromiumWithAppleScript = process.platform === 'darwin' &&
34
+ (typeof browser !== 'string' || browser === 'google chrome');
35
+ if (!shouldTryOpenChromiumWithAppleScript) {
36
+ return false;
37
+ }
38
+ if (shouldTryOpenChromiumWithAppleScript) {
39
+ async function getBrowsersToTry() {
40
+ // Will use the first open browser found from list
41
+ const supportedChromiumBrowsers = [
42
+ 'Google Chrome Canary',
43
+ 'Google Chrome Dev',
44
+ 'Google Chrome Beta',
45
+ 'Google Chrome',
46
+ 'Microsoft Edge',
47
+ 'Brave Browser',
48
+ 'Vivaldi',
49
+ 'Chromium',
50
+ ];
51
+ // Among all the supported browsers, retrieves to stdout the active ones
52
+ const command = `ps cax -o command | grep -E "^(${supportedChromiumBrowsers.join('|')})$"`;
53
+ const result = await execPromise(command);
54
+ const activeBrowsers = result.stdout.toString().trim().split('\n');
55
+ // This preserves the initial browser order
56
+ // We open Google Chrome Canary in priority over Google Chrome
57
+ return supportedChromiumBrowsers.filter((b) => activeBrowsers.includes(b));
58
+ }
59
+ async function tryBrowser(browserName) {
60
+ try {
61
+ // This command runs the openChrome.applescript (copied from CRA)
62
+ const command = `osascript openChrome.applescript "${encodeURI(url)}" "${browserName}"`;
63
+ await execPromise(command, {
64
+ cwd: __dirname,
65
+ });
66
+ return true;
67
+ }
68
+ catch (err) {
69
+ console.error(`Failed to open browser ${browserName} with AppleScript`, err);
70
+ return false;
71
+ }
72
+ }
73
+ const browsers = await logger_1.PerfLogger.async('getBrowsersToTry', () => getBrowsersToTry());
74
+ for (let browser of browsers) {
75
+ const success = await logger_1.PerfLogger.async(browser, () => tryBrowser(browser));
76
+ if (success) {
77
+ return true;
78
+ }
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ function toOpenApp(params) {
84
+ if (!params.browser) {
85
+ return undefined;
86
+ }
87
+ // Handles "cross-platform" shortcuts like "chrome", "firefox", "edge"
88
+ if (open_1.default.apps[params.browser]) {
89
+ return {
90
+ name: open_1.default.apps[params.browser],
91
+ arguments: params.browserArgs,
92
+ };
93
+ }
94
+ // Fallback to platform-specific app name
95
+ return {
96
+ name: params.browser,
97
+ arguments: params.browserArgs,
98
+ };
99
+ }
100
+ async function startBrowserProcess(params) {
101
+ if (await logger_1.PerfLogger.async('tryOpenWithAppleScript', () => tryOpenWithAppleScript(params))) {
102
+ return true;
103
+ }
104
+ try {
105
+ await (0, open_1.default)(params.url, {
106
+ app: toOpenApp(params),
107
+ wait: false,
108
+ });
109
+ return true;
110
+ }
111
+ catch (err) {
112
+ return false;
113
+ }
114
+ }
115
+ /**
116
+ * Returns true if it opened a browser
117
+ */
118
+ async function openBrowser(url) {
119
+ return startBrowserProcess({
120
+ url,
121
+ browser: BrowserEnv,
122
+ browserArgs: BrowserEnvArgs,
123
+ });
124
+ }
@@ -0,0 +1,94 @@
1
+ (*
2
+ Copyright (c) 2015-present, Facebook, Inc.
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
+
8
+ property targetTab: null
9
+ property targetTabIndex: -1
10
+ property targetWindow: null
11
+ property theProgram: "Google Chrome"
12
+
13
+ on run argv
14
+ set theURL to item 1 of argv
15
+
16
+ -- Allow requested program to be optional,
17
+ -- default to Google Chrome
18
+ if (count of argv) > 1 then
19
+ set theProgram to item 2 of argv
20
+ end if
21
+
22
+ using terms from application "Google Chrome"
23
+ tell application theProgram
24
+
25
+ if (count every window) = 0 then
26
+ make new window
27
+ end if
28
+
29
+ -- 1: Looking for tab running debugger
30
+ -- then, Reload debugging tab if found
31
+ -- then return
32
+ set found to my lookupTabWithUrl(theURL)
33
+ if found then
34
+ set targetWindow's active tab index to targetTabIndex
35
+ tell targetTab to reload
36
+ tell targetWindow to activate
37
+ set index of targetWindow to 1
38
+ return
39
+ end if
40
+
41
+ -- 2: Looking for Empty tab
42
+ -- In case debugging tab was not found
43
+ -- We try to find an empty tab instead
44
+ set found to my lookupTabWithUrl("chrome://newtab/")
45
+ if found then
46
+ set targetWindow's active tab index to targetTabIndex
47
+ set URL of targetTab to theURL
48
+ tell targetWindow to activate
49
+ return
50
+ end if
51
+
52
+ -- 3: Create new tab
53
+ -- both debugging and empty tab were not found
54
+ -- make a new tab with url
55
+ tell window 1
56
+ activate
57
+ make new tab with properties {URL:theURL}
58
+ end tell
59
+ end tell
60
+ end using terms from
61
+ end run
62
+
63
+ -- Function:
64
+ -- Lookup tab with given url
65
+ -- if found, store tab, index, and window in properties
66
+ -- (properties were declared on top of file)
67
+ on lookupTabWithUrl(lookupUrl)
68
+ using terms from application "Google Chrome"
69
+ tell application theProgram
70
+ -- Find a tab with the given url
71
+ set found to false
72
+ set theTabIndex to -1
73
+ repeat with theWindow in every window
74
+ set theTabIndex to 0
75
+ repeat with theTab in every tab of theWindow
76
+ set theTabIndex to theTabIndex + 1
77
+ if (theTab's URL as string) contains lookupUrl then
78
+ -- assign tab, tab index, and window to properties
79
+ set targetTab to theTab
80
+ set targetTabIndex to theTabIndex
81
+ set targetWindow to theWindow
82
+ set found to true
83
+ exit repeat
84
+ end if
85
+ end repeat
86
+
87
+ if found then
88
+ exit repeat
89
+ end if
90
+ end repeat
91
+ end tell
92
+ end using terms from
93
+ return found
94
+ end lookupTabWithUrl
@@ -5,12 +5,14 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import { Joi } from '@docusaurus/utils-validation';
8
- import type { FasterConfig, FutureConfig, StorageConfig } from '@docusaurus/types/src/config';
8
+ import type { FasterConfig, FutureConfig, FutureV4Config, StorageConfig } from '@docusaurus/types/src/config';
9
9
  import type { DocusaurusConfig, I18nConfig, MarkdownConfig } from '@docusaurus/types';
10
10
  export declare const DEFAULT_I18N_CONFIG: I18nConfig;
11
11
  export declare const DEFAULT_STORAGE_CONFIG: StorageConfig;
12
12
  export declare const DEFAULT_FASTER_CONFIG: FasterConfig;
13
13
  export declare const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig;
14
+ export declare const DEFAULT_FUTURE_V4_CONFIG: FutureV4Config;
15
+ export declare const DEFAULT_FUTURE_V4_CONFIG_TRUE: FutureV4Config;
14
16
  export declare const DEFAULT_FUTURE_CONFIG: FutureConfig;
15
17
  export declare const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig;
16
18
  export declare const DEFAULT_CONFIG: Pick<DocusaurusConfig, 'i18n' | 'future' | 'onBrokenLinks' | 'onBrokenAnchors' | 'onBrokenMarkdownLinks' | 'onDuplicateRoutes' | 'plugins' | 'themes' | 'presets' | 'headTags' | 'stylesheets' | 'scripts' | 'clientModules' | 'customFields' | 'themeConfig' | 'titleDelimiter' | 'noIndex' | 'tagline' | 'baseUrlIssueBanner' | 'staticDirectories' | 'markdown'>;
@@ -6,11 +6,13 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ConfigSchema = exports.DEFAULT_CONFIG = exports.DEFAULT_MARKDOWN_CONFIG = exports.DEFAULT_FUTURE_CONFIG = exports.DEFAULT_FASTER_CONFIG_TRUE = exports.DEFAULT_FASTER_CONFIG = exports.DEFAULT_STORAGE_CONFIG = exports.DEFAULT_I18N_CONFIG = void 0;
9
+ exports.ConfigSchema = exports.DEFAULT_CONFIG = exports.DEFAULT_MARKDOWN_CONFIG = exports.DEFAULT_FUTURE_CONFIG = exports.DEFAULT_FUTURE_V4_CONFIG_TRUE = exports.DEFAULT_FUTURE_V4_CONFIG = exports.DEFAULT_FASTER_CONFIG_TRUE = exports.DEFAULT_FASTER_CONFIG = exports.DEFAULT_STORAGE_CONFIG = exports.DEFAULT_I18N_CONFIG = void 0;
10
10
  exports.validateConfig = validateConfig;
11
+ const tslib_1 = require("tslib");
11
12
  const utils_1 = require("@docusaurus/utils");
12
13
  const utils_validation_1 = require("@docusaurus/utils-validation");
13
14
  const utils_common_1 = require("@docusaurus/utils-common");
15
+ const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
14
16
  const DEFAULT_I18N_LOCALE = 'en';
15
17
  exports.DEFAULT_I18N_CONFIG = {
16
18
  defaultLocale: DEFAULT_I18N_LOCALE,
@@ -29,6 +31,8 @@ exports.DEFAULT_FASTER_CONFIG = {
29
31
  lightningCssMinimizer: false,
30
32
  mdxCrossCompilerCache: false,
31
33
  rspackBundler: false,
34
+ rspackPersistentCache: false,
35
+ ssgWorkerThreads: false,
32
36
  };
33
37
  // When using the "faster: true" shortcut
34
38
  exports.DEFAULT_FASTER_CONFIG_TRUE = {
@@ -38,8 +42,20 @@ exports.DEFAULT_FASTER_CONFIG_TRUE = {
38
42
  lightningCssMinimizer: true,
39
43
  mdxCrossCompilerCache: true,
40
44
  rspackBundler: true,
45
+ rspackPersistentCache: true,
46
+ ssgWorkerThreads: true,
47
+ };
48
+ exports.DEFAULT_FUTURE_V4_CONFIG = {
49
+ removeLegacyPostBuildHeadAttribute: false,
50
+ useCssCascadeLayers: false,
51
+ };
52
+ // When using the "v4: true" shortcut
53
+ exports.DEFAULT_FUTURE_V4_CONFIG_TRUE = {
54
+ removeLegacyPostBuildHeadAttribute: true,
55
+ useCssCascadeLayers: true,
41
56
  };
42
57
  exports.DEFAULT_FUTURE_CONFIG = {
58
+ v4: exports.DEFAULT_FUTURE_V4_CONFIG,
43
59
  experimental_faster: exports.DEFAULT_FASTER_CONFIG,
44
60
  experimental_storage: exports.DEFAULT_STORAGE_CONFIG,
45
61
  experimental_router: 'browser',
@@ -155,11 +171,22 @@ const FASTER_CONFIG_SCHEMA = utils_validation_1.Joi.alternatives()
155
171
  lightningCssMinimizer: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FASTER_CONFIG.lightningCssMinimizer),
156
172
  mdxCrossCompilerCache: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FASTER_CONFIG.mdxCrossCompilerCache),
157
173
  rspackBundler: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FASTER_CONFIG.rspackBundler),
174
+ rspackPersistentCache: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FASTER_CONFIG.rspackPersistentCache),
175
+ ssgWorkerThreads: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FASTER_CONFIG.ssgWorkerThreads),
158
176
  }), utils_validation_1.Joi.boolean()
159
177
  .required()
160
178
  .custom((bool) => bool ? exports.DEFAULT_FASTER_CONFIG_TRUE : exports.DEFAULT_FASTER_CONFIG))
161
179
  .optional()
162
180
  .default(exports.DEFAULT_FASTER_CONFIG);
181
+ const FUTURE_V4_SCHEMA = utils_validation_1.Joi.alternatives()
182
+ .try(utils_validation_1.Joi.object({
183
+ removeLegacyPostBuildHeadAttribute: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FUTURE_V4_CONFIG.removeLegacyPostBuildHeadAttribute),
184
+ useCssCascadeLayers: utils_validation_1.Joi.boolean().default(exports.DEFAULT_FUTURE_V4_CONFIG.useCssCascadeLayers),
185
+ }), utils_validation_1.Joi.boolean()
186
+ .required()
187
+ .custom((bool) => bool ? exports.DEFAULT_FUTURE_V4_CONFIG_TRUE : exports.DEFAULT_FUTURE_V4_CONFIG))
188
+ .optional()
189
+ .default(exports.DEFAULT_FUTURE_V4_CONFIG);
163
190
  const STORAGE_CONFIG_SCHEMA = utils_validation_1.Joi.object({
164
191
  type: utils_validation_1.Joi.string()
165
192
  .equal('localStorage', 'sessionStorage')
@@ -171,6 +198,7 @@ const STORAGE_CONFIG_SCHEMA = utils_validation_1.Joi.object({
171
198
  .optional()
172
199
  .default(exports.DEFAULT_STORAGE_CONFIG);
173
200
  const FUTURE_CONFIG_SCHEMA = utils_validation_1.Joi.object({
201
+ v4: FUTURE_V4_SCHEMA,
174
202
  experimental_faster: FASTER_CONFIG_SCHEMA,
175
203
  experimental_storage: STORAGE_CONFIG_SCHEMA,
176
204
  experimental_router: utils_validation_1.Joi.string()
@@ -308,6 +336,20 @@ exports.ConfigSchema = utils_validation_1.Joi.object({
308
336
  }).messages({
309
337
  'docusaurus.configValidationWarning': 'Docusaurus config validation warning. Field {#label}: {#warningMessage}',
310
338
  });
339
+ // Expressing this kind of logic in Joi is a pain
340
+ // We also want to decouple logic from Joi: easier to remove it later!
341
+ function ensureDocusaurusConfigConsistency(config) {
342
+ if (config.future.experimental_faster.ssgWorkerThreads &&
343
+ !config.future.v4.removeLegacyPostBuildHeadAttribute) {
344
+ throw new Error(`Docusaurus config ${logger_1.default.code('future.experimental_faster.ssgWorkerThreads')} requires the future flag ${logger_1.default.code('future.v4.removeLegacyPostBuildHeadAttribute')} to be turned on.
345
+ If you use Docusaurus Faster, we recommend that you also activate Docusaurus v4 future flags: ${logger_1.default.code('{future: {v4: true}}')}
346
+ All the v4 future flags are documented here: https://docusaurus.io/docs/api/docusaurus-config#future`);
347
+ }
348
+ if (config.future.experimental_faster.rspackPersistentCache &&
349
+ !config.future.experimental_faster.rspackBundler) {
350
+ throw new Error(`Docusaurus config flag ${logger_1.default.code('future.experimental_faster.rspackPersistentCache')} requires the flag ${logger_1.default.code('future.experimental_faster.rspackBundler')} to be turned on.`);
351
+ }
352
+ }
311
353
  // TODO move to @docusaurus/utils-validation
312
354
  function validateConfig(config, siteConfigPath) {
313
355
  const { error, warning, value } = exports.ConfigSchema.validate(config, {
@@ -329,7 +371,6 @@ function validateConfig(config, siteConfigPath) {
329
371
  : formattedError;
330
372
  throw new Error(formattedError);
331
373
  }
332
- else {
333
- return value;
334
- }
374
+ ensureDocusaurusConfigConsistency(value);
375
+ return value;
335
376
  }
@@ -0,0 +1,10 @@
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
+ export declare const SSGConcurrency: number;
8
+ export declare const SSGWorkerThreadCount: number | undefined;
9
+ export declare const SSGWorkerThreadTaskSize: number;
10
+ export declare const SSGWorkerThreadRecyclerMaxMemory: number | undefined;
@@ -0,0 +1,38 @@
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.SSGWorkerThreadRecyclerMaxMemory = exports.SSGWorkerThreadTaskSize = exports.SSGWorkerThreadCount = exports.SSGConcurrency = void 0;
10
+ // Secret way to set SSR plugin async concurrency option
11
+ // Waiting for feedback before documenting this officially?
12
+ // TODO Docusaurus v4, rename SSR => SSG
13
+ exports.SSGConcurrency = process.env.DOCUSAURUS_SSR_CONCURRENCY
14
+ ? parseInt(process.env.DOCUSAURUS_SSR_CONCURRENCY, 10)
15
+ : // Not easy to define a reasonable option default
16
+ // Will still be better than Infinity
17
+ // See also https://github.com/sindresorhus/p-map/issues/24
18
+ 32;
19
+ // Secret way to set SSR plugin async concurrency option
20
+ // Waiting for feedback before documenting this officially?
21
+ exports.SSGWorkerThreadCount = process.env
22
+ .DOCUSAURUS_SSG_WORKER_THREAD_COUNT
23
+ ? parseInt(process.env.DOCUSAURUS_SSG_WORKER_THREAD_COUNT, 10)
24
+ : undefined;
25
+ // Number of pathnames to SSG per worker task
26
+ exports.SSGWorkerThreadTaskSize = process.env
27
+ .DOCUSAURUS_SSG_WORKER_THREAD_TASK_SIZE
28
+ ? parseInt(process.env.DOCUSAURUS_SSG_WORKER_THREAD_TASK_SIZE, 10)
29
+ : 10; // TODO need fine-tuning
30
+ // Controls worker thread recycling behavior (maxMemoryLimitBeforeRecycle)
31
+ // See https://github.com/facebook/docusaurus/pull/11166
32
+ // See https://github.com/facebook/docusaurus/issues/11161
33
+ exports.SSGWorkerThreadRecyclerMaxMemory = process.env
34
+ .DOCUSAURUS_SSG_WORKER_THREAD_RECYCLER_MAX_MEMORY
35
+ ? parseInt(process.env.DOCUSAURUS_SSG_WORKER_THREAD_RECYCLER_MAX_MEMORY, 10)
36
+ : // 1 GB is a quite reasonable max value
37
+ // It should work well even for large sites
38
+ 1000000000;
@@ -7,18 +7,128 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.executeSSG = executeSSG;
10
- const logger_1 = require("@docusaurus/logger");
10
+ const tslib_1 = require("tslib");
11
+ const path = tslib_1.__importStar(require("path"));
12
+ const node_url_1 = require("node:url");
13
+ const os_1 = tslib_1.__importDefault(require("os"));
14
+ const lodash_1 = tslib_1.__importDefault(require("lodash"));
15
+ const logger_1 = tslib_1.__importStar(require("@docusaurus/logger"));
11
16
  const ssgParams_1 = require("./ssgParams");
12
- const ssg_1 = require("./ssg");
13
17
  const ssgTemplate_1 = require("./ssgTemplate");
18
+ const ssgEnv_1 = require("./ssgEnv");
14
19
  const ssgUtils_1 = require("./ssgUtils");
15
- // TODO Docusaurus v4 - introduce SSG worker threads
20
+ const ssgGlobalResult_1 = require("./ssgGlobalResult");
21
+ const ssgWorkerInline_1 = require("./ssgWorkerInline");
22
+ const createSimpleSSGExecutor = async ({ params, pathnames, }) => {
23
+ return {
24
+ run: () => {
25
+ return logger_1.PerfLogger.async('SSG (current thread)', async () => {
26
+ const ssgResults = await (0, ssgWorkerInline_1.executeSSGInlineTask)({
27
+ pathnames,
28
+ params,
29
+ });
30
+ return (0, ssgGlobalResult_1.createGlobalSSGResult)(ssgResults);
31
+ });
32
+ },
33
+ destroy: async () => {
34
+ // nothing to do
35
+ },
36
+ };
37
+ };
38
+ // Sensible default that gives decent performances
39
+ // It's hard to have a perfect formula that works for all hosts
40
+ // Each thread has some creation overhead
41
+ // Having 1 thread per cpu doesn't necessarily improve perf on small sites
42
+ // We want to ensure that we don't create a worker thread for less than x paths
43
+ function inferNumberOfThreads({ pageCount, cpuCount, minPagesPerCpu, }) {
44
+ // Calculate "ideal" amount of threads based on the number of pages to render
45
+ const threadsByWorkload = Math.ceil(pageCount / minPagesPerCpu);
46
+ // Use the smallest of threadsByWorkload or cpuCount, ensuring min=1 thread
47
+ return Math.max(1, Math.min(threadsByWorkload, cpuCount));
48
+ }
49
+ function getNumberOfThreads(pathnames) {
50
+ if (typeof ssgEnv_1.SSGWorkerThreadCount !== 'undefined') {
51
+ return ssgEnv_1.SSGWorkerThreadCount;
52
+ }
53
+ // See also https://github.com/tinylibs/tinypool/pull/108
54
+ const cpuCount =
55
+ // TODO Docusaurus v4: bump node, availableParallelism() now always exists
56
+ typeof os_1.default.availableParallelism === 'function'
57
+ ? os_1.default.availableParallelism()
58
+ : os_1.default.cpus().length;
59
+ return inferNumberOfThreads({
60
+ pageCount: pathnames.length,
61
+ cpuCount,
62
+ // These are "magic value" that we should refine based on user feedback
63
+ // Local tests show that it's not worth spawning new workers for few pages
64
+ minPagesPerCpu: 100,
65
+ });
66
+ }
67
+ const createPooledSSGExecutor = async ({ params, pathnames, }) => {
68
+ const numberOfThreads = getNumberOfThreads(pathnames);
69
+ // When the inferred or provided number of threads is just 1
70
+ // It's not worth it to use a thread pool
71
+ // This also allows users to disable the thread pool with the env variable
72
+ // DOCUSAURUS_SSG_WORKER_THREADS=1
73
+ if (numberOfThreads === 1) {
74
+ return createSimpleSSGExecutor({ params, pathnames });
75
+ }
76
+ const pool = await logger_1.PerfLogger.async(`Create SSG thread pool - ${logger_1.default.cyan(numberOfThreads)} threads`, async () => {
77
+ const Tinypool = await import('tinypool').then((m) => m.default);
78
+ const workerURL = (0, node_url_1.pathToFileURL)(path.resolve(__dirname, 'ssgWorkerThread.js'));
79
+ return new Tinypool({
80
+ filename: workerURL.pathname,
81
+ minThreads: numberOfThreads,
82
+ maxThreads: numberOfThreads,
83
+ concurrentTasksPerWorker: 1,
84
+ runtime: 'worker_threads',
85
+ isolateWorkers: false,
86
+ workerData: { params },
87
+ // WORKER MEMORY MANAGEMENT
88
+ // Allows containing SSG memory leaks with a thread recycling workaround
89
+ // See https://github.com/facebook/docusaurus/pull/11166
90
+ // See https://github.com/facebook/docusaurus/issues/11161
91
+ maxMemoryLimitBeforeRecycle: ssgEnv_1.SSGWorkerThreadRecyclerMaxMemory,
92
+ resourceLimits: {
93
+ // For some reason I can't figure out how to limit memory on a worker
94
+ // See https://x.com/sebastienlorber/status/1920781195618513143
95
+ },
96
+ });
97
+ });
98
+ const pathnamesChunks = lodash_1.default.chunk(pathnames, ssgEnv_1.SSGWorkerThreadTaskSize);
99
+ // Tiny wrapper for type-safety
100
+ const submitTask = async (task) => {
101
+ const result = await pool.run(task);
102
+ // Note, we don't use PerfLogger.async() because all tasks are submitted
103
+ // immediately at once and queued, while results are received progressively
104
+ logger_1.PerfLogger.log(`Result for task ${logger_1.default.name(task.id)}`);
105
+ return result;
106
+ };
107
+ return {
108
+ run: async () => {
109
+ const results = await logger_1.PerfLogger.async(`Thread pool`, async () => {
110
+ return Promise.all(pathnamesChunks.map((taskPathnames, taskIndex) => {
111
+ return submitTask({
112
+ id: taskIndex + 1,
113
+ pathnames: taskPathnames,
114
+ });
115
+ }));
116
+ });
117
+ const allResults = results.flat();
118
+ return (0, ssgGlobalResult_1.createGlobalSSGResult)(allResults);
119
+ },
120
+ destroy: async () => {
121
+ await pool.destroy();
122
+ },
123
+ };
124
+ };
16
125
  async function executeSSG({ props, serverBundlePath, clientManifestPath, router, }) {
17
126
  const params = await (0, ssgParams_1.createSSGParams)({
18
127
  serverBundlePath,
19
128
  clientManifestPath,
20
129
  props,
21
130
  });
131
+ // TODO doesn't look like the appropriate place for hash router entry
22
132
  if (router === 'hash') {
23
133
  logger_1.PerfLogger.start('Generate Hash Router entry point');
24
134
  const content = await (0, ssgTemplate_1.renderHashRouterTemplate)({ params });
@@ -26,9 +136,12 @@ async function executeSSG({ props, serverBundlePath, clientManifestPath, router,
26
136
  logger_1.PerfLogger.end('Generate Hash Router entry point');
27
137
  return { collectedData: {} };
28
138
  }
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;
139
+ const createExecutor = props.siteConfig.future.experimental_faster
140
+ .ssgWorkerThreads
141
+ ? createPooledSSGExecutor
142
+ : createSimpleSSGExecutor;
143
+ const executor = await createExecutor({ params, pathnames: props.routesPaths });
144
+ const result = await executor.run();
145
+ await executor.destroy();
146
+ return result;
34
147
  }
@@ -0,0 +1,12 @@
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 { SSGResult } from './ssgRenderer';
8
+ import type { SiteCollectedData } from '../common';
9
+ export type SSGGlobalResult = {
10
+ collectedData: SiteCollectedData;
11
+ };
12
+ export declare function createGlobalSSGResult(ssgResults: SSGResult[]): Promise<SSGGlobalResult>;
@@ -0,0 +1,74 @@
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.createGlobalSSGResult = createGlobalSSGResult;
10
+ const tslib_1 = require("tslib");
11
+ const lodash_1 = tslib_1.__importDefault(require("lodash"));
12
+ const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
13
+ function printSSGWarnings(results) {
14
+ // Escape hatch because SWC is quite aggressive to report errors
15
+ // See https://github.com/facebook/docusaurus/pull/10554
16
+ // See https://github.com/swc-project/swc/discussions/9616#discussioncomment-10846201
17
+ if (process.env.DOCUSAURUS_IGNORE_SSG_WARNINGS === 'true') {
18
+ return;
19
+ }
20
+ const ignoredWarnings = [
21
+ // TODO Docusaurus v4: remove with React 19 upgrade
22
+ // React 18 emit NULL chars, and minifier detects it
23
+ // see https://github.com/facebook/docusaurus/issues/9985
24
+ 'Unexpected null character',
25
+ ];
26
+ const keepWarning = (warning) => {
27
+ return !ignoredWarnings.some((iw) => warning.includes(iw));
28
+ };
29
+ const resultsWithWarnings = results
30
+ .map((success) => {
31
+ return {
32
+ ...success,
33
+ warnings: success.result.warnings.filter(keepWarning),
34
+ };
35
+ })
36
+ .filter((result) => result.warnings.length > 0);
37
+ if (resultsWithWarnings.length) {
38
+ const message = `Docusaurus static site generation process emitted warnings for ${resultsWithWarnings.length} path${resultsWithWarnings.length ? 's' : ''}
39
+ This is non-critical and can be disabled with DOCUSAURUS_IGNORE_SSG_WARNINGS=true
40
+ Troubleshooting guide: https://github.com/facebook/docusaurus/discussions/10580
41
+
42
+ - ${resultsWithWarnings
43
+ .map((result) => `${logger_1.default.path(result.pathname)}:
44
+ - ${result.warnings.join('\n - ')}
45
+ `)
46
+ .join('\n- ')}`;
47
+ logger_1.default.warn(message);
48
+ }
49
+ }
50
+ function throwSSGError(ssgErrors) {
51
+ const message = `Docusaurus static site generation failed for ${ssgErrors.length} path${ssgErrors.length ? 's' : ''}:\n- ${ssgErrors
52
+ .map((ssgError) => logger_1.default.path(ssgError.pathname))
53
+ .join('\n- ')}`;
54
+ // Note logging this error properly require using inspect(error,{depth})
55
+ // See https://github.com/nodejs/node/issues/51637
56
+ throw new Error(message, {
57
+ cause: new AggregateError(ssgErrors.map((ssgError) => ssgError.error)),
58
+ });
59
+ }
60
+ async function createGlobalSSGResult(ssgResults) {
61
+ const [ssgSuccesses, ssgErrors] = lodash_1.default.partition(ssgResults, (result) => result.success);
62
+ // For now, only success results emit warnings
63
+ // For errors, we throw without warnings
64
+ printSSGWarnings(ssgSuccesses);
65
+ if (ssgErrors.length > 0) {
66
+ throwSSGError(ssgErrors);
67
+ }
68
+ // If we only have SSG successes, we can consolidate those in a single result
69
+ const collectedData = lodash_1.default.chain(ssgSuccesses)
70
+ .keyBy((success) => success.pathname)
71
+ .mapValues((ssgSuccess) => ssgSuccess.result.collectedData)
72
+ .value();
73
+ return { collectedData };
74
+ }
@@ -20,6 +20,7 @@ export type SSGParams = {
20
20
  htmlMinifierType: HtmlMinifierType;
21
21
  serverBundlePath: string;
22
22
  ssgTemplateContent: string;
23
+ v4RemoveLegacyPostBuildHeadAttribute: boolean;
23
24
  };
24
25
  export declare function createSSGParams({ props, serverBundlePath, clientManifestPath, }: {
25
26
  props: Props;
@@ -30,6 +30,7 @@ async function createSSGParams({ props, serverBundlePath, clientManifestPath, })
30
30
  .swcHtmlMinimizer
31
31
  ? 'swc'
32
32
  : 'terser',
33
+ v4RemoveLegacyPostBuildHeadAttribute: props.siteConfig.future.v4.removeLegacyPostBuildHeadAttribute,
33
34
  };
34
35
  // Useless but ensures that SSG params remain serializable
35
36
  return structuredClone(params);