@doc-kit/core 1.2.1 → 1.3.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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/package.json +6 -7
  4. package/src/generators/ast/generate.mjs +7 -3
  5. package/src/generators/ast/index.mjs +9 -0
  6. package/src/generators/index.mjs +1 -0
  7. package/src/generators/json/README.md +1 -1
  8. package/src/generators/json/generated/schema.d.ts +1 -1
  9. package/src/generators/json/index.mjs +9 -0
  10. package/src/generators/json/utils/markdown.mjs +2 -2
  11. package/src/generators/json-all/README.md +1 -1
  12. package/src/generators/json-simple/README.md +1 -1
  13. package/src/generators/loader.mjs +14 -0
  14. package/src/generators/metadata/generate.mjs +4 -2
  15. package/src/generators/metadata/index.mjs +9 -0
  16. package/src/generators/metadata/maps/mdn.json +2 -0
  17. package/src/generators/metadata/types.d.ts +1 -1
  18. package/src/generators/metadata/utils/parse.mjs +2 -4
  19. package/src/generators/metadata/utils/visitors.mjs +2 -2
  20. package/src/generators/types.d.ts +8 -0
  21. package/src/generators.mjs +4 -0
  22. package/src/logger/constants.mjs +35 -0
  23. package/src/logger/transports/github.mjs +48 -15
  24. package/src/plugins/shiki/highlighter.mjs +373 -0
  25. package/src/plugins/shiki/rehype.mjs +38 -0
  26. package/src/{utils/type-annotations/highlighted.mjs → plugins/type-annotations/highlighter.mjs} +14 -19
  27. package/src/{utils → plugins}/type-annotations/remark.mjs +6 -2
  28. package/src/threading/chunk-worker.mjs +4 -0
  29. package/src/threading/constants.mjs +10 -0
  30. package/src/threading/index.mjs +6 -1
  31. package/src/utils/configuration/constants.mjs +3 -3
  32. package/src/utils/configuration/index.mjs +86 -20
  33. package/src/utils/configuration/types.d.ts +27 -0
  34. package/src/utils/loaders.mjs +23 -1
  35. package/src/utils/markdown/constants.mjs +7 -0
  36. package/src/utils/markdown/plugins.mjs +232 -0
  37. package/src/utils/markdown/processor.mjs +60 -0
  38. package/src/utils/highlighter.mjs +0 -198
  39. package/src/utils/remark-shiki.mjs +0 -29
  40. package/src/utils/remark.mjs +0 -90
  41. /package/src/{utils → plugins}/type-annotations/hast.mjs +0 -0
  42. /package/src/{utils → plugins}/type-annotations/mdast.mjs +0 -0
  43. /package/src/{utils → plugins}/type-annotations/syntax.mjs +0 -0
@@ -0,0 +1,373 @@
1
+ 'use strict';
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { endianness } from 'node:os';
5
+
6
+ import createSyntaxHighlighter from '@node-core/rehype-shiki/highlighter';
7
+ import { isSpecialLang } from 'shiki/core';
8
+ import { bundledLanguagesInfo } from 'shiki/langs';
9
+ import { bundledThemes } from 'shiki/themes';
10
+
11
+ import { importFromURL } from '#utils/loaders.mjs';
12
+ import { getMarkdownPlugins } from '#utils/markdown/plugins.mjs';
13
+ import { lazy } from '#utils/misc.mjs';
14
+
15
+ const require = createRequire(import.meta.url);
16
+
17
+ /**
18
+ * A language a highlighter highlights.
19
+ *
20
+ * @typedef {Pick<import('shiki').LanguageRegistration, 'name' | 'displayName' | 'aliases'>} Language
21
+ */
22
+
23
+ /**
24
+ * A syntax highlighter, creating its Shiki instance on first use.
25
+ *
26
+ * @typedef {Object} SyntaxHighlighter
27
+ * @property {import('shiki').HighlighterCore} shiki - The Shiki instance
28
+ * @property {(languageId?: string) => string} resolveLanguage - Resolves a language, falling back to plain text for unknown ones
29
+ * @property {(code: string, lang: string, meta?: Record<string, unknown>) => string} highlightToHtml - Highlights code, returning the inner HTML of its `<code>` element
30
+ * @property {(code: string, lang: string, meta?: Record<string, unknown>) => ReturnType<import('shiki').HighlighterCore['codeToHast']>} highlightToHast - Highlights code, returning a HAST tree
31
+ * @property {Array<Language>} langs - The languages it highlights
32
+ */
33
+
34
+ /**
35
+ * Creates Shiki's regular expression engine: the wasm (Oniguruma) one where
36
+ * it can, the JavaScript one otherwise.
37
+ *
38
+ * @returns {Promise<import('shiki').RegexEngine>}
39
+ */
40
+ const createEngine = async () => {
41
+ // riscv64 with sv39 has limited virtual memory space, where creating
42
+ // too many (>20) wasm memory instances fails.
43
+ // https://github.com/nodejs/node/pull/60591
44
+ //
45
+ // The wasm highlighter is currently not compatible with big endian.
46
+ // https://github.com/nodejs/node/pull/62512#issuecomment-4243469950
47
+ if (process.arch !== 'riscv64' && endianness() === 'LE') {
48
+ const { createOnigurumaEngine } = await import('shiki/engine/oniguruma');
49
+
50
+ return createOnigurumaEngine(import('shiki/wasm'));
51
+ }
52
+
53
+ const { createJavaScriptRegexEngine } =
54
+ await import('shiki/engine/javascript');
55
+
56
+ // Not every bundled grammar compiles under the JavaScript engine,
57
+ // so skip the patterns it cannot handle instead of throwing.
58
+ return createJavaScriptRegexEngine({ forgiving: true });
59
+ };
60
+
61
+ // The regular expression engine of this thread, for all its highlighters
62
+ let engine;
63
+
64
+ // The highlighters of the options given, by their JSON
65
+ const highlighters = new Map();
66
+
67
+ // What code in an unknown language is highlighted as
68
+ const FALLBACK_LANGUAGE = 'text';
69
+
70
+ // The languages Shiki bundles. Registering a grammar takes time and memory,
71
+ // and slows down every highlight after it, so each one is only imported and
72
+ // registered once code in it is highlighted (see `resolveLanguage`).
73
+ const BUNDLED_LANGUAGES = bundledLanguagesInfo.map(({ id, name, aliases }) => ({
74
+ name: id,
75
+ displayName: name,
76
+ aliases,
77
+ }));
78
+
79
+ // The bundled language each of their names and aliases stands for, as code
80
+ // names its language by either
81
+ const BUNDLED_NAMES = new Map(
82
+ BUNDLED_LANGUAGES.flatMap(({ name, aliases = [] }) =>
83
+ [name, ...aliases].map(alias => [alias, name])
84
+ )
85
+ );
86
+
87
+ /**
88
+ * Imports a bundled language: its grammar, and those of the languages it
89
+ * embeds, lazily too. Shiki only highlights a language embedded lazily (a
90
+ * fenced code block in Markdown) once it is registered, so registering it
91
+ * later would make code highlight differently depending on what was
92
+ * highlighted before it.
93
+ *
94
+ * For the same reason, a grammar injecting into others' (`ts-tags`', into
95
+ * JavaScript's tagged templates) only injects into the language it was
96
+ * imported for: Shiki only applies an injection to the grammars it creates
97
+ * after it. A language given in `langs` keeps its injections.
98
+ *
99
+ * @param {string} name - A name or alias of the language
100
+ * @returns {Array<import('shiki').LanguageRegistration>}
101
+ */
102
+ const importBundledLanguage = name => {
103
+ const id = BUNDLED_NAMES.get(name);
104
+ const langs = new Map();
105
+ const pending = [id];
106
+
107
+ while (pending.length > 0) {
108
+ const grammars = require(`shiki/langs/${pending.pop()}.mjs`).default;
109
+
110
+ for (const lang of grammars) {
111
+ if (langs.has(lang.name)) {
112
+ continue;
113
+ }
114
+
115
+ langs.set(lang.name, lang);
116
+
117
+ const { embeddedLangs = [], embeddedLangsLazy = [] } = lang;
118
+
119
+ for (const embedded of [...embeddedLangs, ...embeddedLangsLazy]) {
120
+ if (BUNDLED_NAMES.has(embedded)) {
121
+ pending.push(BUNDLED_NAMES.get(embedded));
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ const { scopeName } = langs.get(id);
128
+
129
+ return [...langs.values()].map(lang =>
130
+ lang.injectTo ? { ...lang, injectTo: [scopeName] } : lang
131
+ );
132
+ };
133
+
134
+ /**
135
+ * Adds the bundled languages that languages embed, which Shiki registers
136
+ * along with them.
137
+ *
138
+ * @param {Array<import('shiki').LanguageRegistration>} langs - The languages
139
+ * @returns {Array<import('shiki').LanguageRegistration>}
140
+ */
141
+ const withEmbeddedLanguages = langs => {
142
+ const names = new Set(langs.map(({ name }) => name));
143
+
144
+ const embedded = langs
145
+ .flatMap(({ embeddedLangs = [], embeddedLangsLazy = [] }) => [
146
+ ...embeddedLangs,
147
+ ...embeddedLangsLazy,
148
+ ])
149
+ .filter(name => !names.has(name) && BUNDLED_NAMES.has(name))
150
+ .flatMap(importBundledLanguage);
151
+
152
+ return [...embedded, ...langs];
153
+ };
154
+
155
+ /**
156
+ * Imports a list of options, each given as it is, or as a module (a path
157
+ * relative to the working directory, or a URL) default-exporting it, or a list
158
+ * of them.
159
+ *
160
+ * @template T
161
+ * @param {Array<string | T>} options - The options
162
+ * @returns {Promise<Array<T>>}
163
+ */
164
+ const importList = async options => {
165
+ const imports = options.map(option =>
166
+ typeof option === 'string' ? importFromURL(option) : option
167
+ );
168
+
169
+ const imported = await Promise.all(imports);
170
+
171
+ return imported.flat();
172
+ };
173
+
174
+ /**
175
+ * Imports a theme: the name of one Shiki bundles, a module default-exporting
176
+ * one, or the theme itself. Shiki tells themes apart by their name, so a theme
177
+ * without one is named after its color scheme.
178
+ *
179
+ * @param {string | import('shiki').ThemeRegistration} theme - The theme
180
+ * @param {'light' | 'dark'} scheme - Its color scheme
181
+ * @returns {Promise<import('shiki').ThemeRegistration>}
182
+ */
183
+ const importTheme = async (theme, scheme) => {
184
+ let imported = theme;
185
+
186
+ if (typeof theme === 'string') {
187
+ if (theme in bundledThemes) {
188
+ const { default: bundledTheme } = await bundledThemes[theme]();
189
+
190
+ imported = bundledTheme;
191
+ } else {
192
+ imported = await importFromURL(theme);
193
+ }
194
+ }
195
+
196
+ return { name: `custom-${scheme}`, ...imported };
197
+ };
198
+
199
+ /**
200
+ * Imports the options of a highlighter, and creates it.
201
+ *
202
+ * @param {import('./rehype.mjs').ShikiOptions} options - The options
203
+ * @returns {Promise<SyntaxHighlighter>}
204
+ */
205
+ const importHighlighter = async ({
206
+ langs = [],
207
+ langAlias = {},
208
+ themes,
209
+ transformers = [],
210
+ }) => {
211
+ engine ??= createEngine();
212
+
213
+ const [regexEngine, importedLangs, importedTransformers] = await Promise.all([
214
+ engine,
215
+ importList(langs),
216
+ importList(transformers),
217
+ ]);
218
+
219
+ const coreOptions = {
220
+ engine: regexEngine,
221
+ // The bundled languages are registered as code uses them
222
+ langs: withEmbeddedLanguages(importedLangs),
223
+ // A copy, as Shiki adds the aliases of the languages it registers to it
224
+ langAlias: { ...langAlias },
225
+ };
226
+
227
+ // Without themes of its own, the highlighter has a default light and dark one
228
+ if (themes) {
229
+ const [light, dark] = await Promise.all([
230
+ importTheme(themes.light, 'light'),
231
+ importTheme(themes.dark, 'dark'),
232
+ ]);
233
+
234
+ coreOptions.themes = [light, dark];
235
+ }
236
+
237
+ /**
238
+ * Creates the Shiki instance, on first use, and the options it highlights
239
+ * with.
240
+ */
241
+ const load = lazy(() => {
242
+ const { shiki } = createSyntaxHighlighter({ coreOptions });
243
+
244
+ // The themes are given by name: Shiki keeps the themes it loaded parsed,
245
+ // but parses a theme object it's given again for every highlight
246
+ const [light, dark] =
247
+ coreOptions.themes?.map(theme => theme.name) ?? shiki.getLoadedThemes();
248
+
249
+ const highlightOptions = {
250
+ themes: { light, dark },
251
+ defaultColor: 'light',
252
+ transformers: importedTransformers,
253
+ };
254
+
255
+ return { shiki, highlightOptions };
256
+ });
257
+
258
+ /**
259
+ * Resolves a language, falling back to plain text for unknown ones. A
260
+ * bundled language is registered the first time it's resolved.
261
+ *
262
+ * @param {string} [languageId]
263
+ * @returns {string}
264
+ */
265
+ const resolveLanguage = languageId => {
266
+ if (!languageId) {
267
+ return FALLBACK_LANGUAGE;
268
+ }
269
+
270
+ const instance = load().shiki;
271
+ const name = instance.resolveLangAlias(languageId.toLowerCase());
272
+
273
+ if (isSpecialLang(name) || instance.getLoadedLanguages().includes(name)) {
274
+ return languageId;
275
+ }
276
+
277
+ if (!BUNDLED_NAMES.has(name)) {
278
+ return FALLBACK_LANGUAGE;
279
+ }
280
+
281
+ instance.loadLanguageSync(importBundledLanguage(name));
282
+
283
+ return languageId;
284
+ };
285
+
286
+ /**
287
+ * The options Shiki highlights code with.
288
+ *
289
+ * @param {string} lang - The language of the code
290
+ * @param {Record<string, unknown>} meta - Its metadata
291
+ */
292
+ const optionsFor = (lang, meta) => ({
293
+ lang: resolveLanguage(lang),
294
+ ...load().highlightOptions,
295
+ meta,
296
+ });
297
+
298
+ return {
299
+ langs: [...BUNDLED_LANGUAGES, ...importedLangs],
300
+
301
+ /**
302
+ * The Shiki instance.
303
+ */
304
+ get shiki() {
305
+ return load().shiki;
306
+ },
307
+
308
+ resolveLanguage,
309
+
310
+ /**
311
+ * Highlights code, returning the inner HTML of its `<code>` element.
312
+ *
313
+ * @param {string} code - The code
314
+ * @param {string} lang - Its language
315
+ * @param {Record<string, unknown>} [meta] - Its metadata
316
+ */
317
+ highlightToHtml: (code, lang, meta = {}) =>
318
+ load()
319
+ .shiki.codeToHtml(code, optionsFor(lang, meta))
320
+ // Shiki wraps the highlighted code in a <pre> and a <code>
321
+ .match(/<code>(.+?)<\/code>/s)[1],
322
+
323
+ /**
324
+ * Highlights code, returning a HAST tree.
325
+ *
326
+ * @param {string} code - The code
327
+ * @param {string} lang - Its language
328
+ * @param {Record<string, unknown>} [meta] - Its metadata
329
+ */
330
+ highlightToHast: (code, lang, meta = {}) =>
331
+ load().shiki.codeToHast(code, optionsFor(lang, meta)),
332
+ };
333
+ };
334
+
335
+ /**
336
+ * Creates a highlighter of every language Shiki bundles, with the given
337
+ * options (see `./rehype.mjs`). Its Shiki instance is created on first use,
338
+ * each bundled language is registered once code in it is highlighted, and the
339
+ * same options give the same highlighter.
340
+ *
341
+ * @param {import('./rehype.mjs').ShikiOptions} [options] - The options
342
+ * @returns {Promise<SyntaxHighlighter>}
343
+ */
344
+ export const createHighlighter = (options = {}) => {
345
+ const key = JSON.stringify(options);
346
+
347
+ if (!highlighters.has(key)) {
348
+ highlighters.set(key, importHighlighter(options));
349
+ }
350
+
351
+ return highlighters.get(key);
352
+ };
353
+
354
+ /**
355
+ * Gets the highlighter of the Shiki plugin of a generator's Markdown pipeline,
356
+ * as loaded on the current thread, to highlight code as the pipeline does.
357
+ *
358
+ * @param {string} generator - The name of the generator
359
+ * @returns {SyntaxHighlighter}
360
+ */
361
+ export const getHighlighter = generator => {
362
+ const shiki = getMarkdownPlugins(generator).rehypePlugins.find(
363
+ plugin => Array.isArray(plugin) && plugin[0].highlighter
364
+ );
365
+
366
+ if (!shiki) {
367
+ throw new Error(
368
+ `The Markdown pipeline of "${generator}" does not highlight code`
369
+ );
370
+ }
371
+
372
+ return shiki[0].highlighter;
373
+ };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ import rehypeShikiji from '@node-core/rehype-shiki/plugin';
4
+
5
+ import { createHighlighter } from './highlighter.mjs';
6
+
7
+ /**
8
+ * The options of the Shiki plugin. Its modules are paths relative to the
9
+ * working directory, or URLs.
10
+ *
11
+ * @typedef {Object} ShikiOptions
12
+ * @property {Array<string | import('shiki').LanguageRegistration>} [langs] - More languages: grammars, or modules default-exporting grammars
13
+ * @property {Record<string, string>} [langAlias] - Aliases of languages, e.g. `{ conf: 'ini' }`
14
+ * @property {{ light: string | import('shiki').ThemeRegistration, dark: string | import('shiki').ThemeRegistration }} [themes] - The light and dark themes: names of themes Shiki bundles, modules default-exporting themes, or themes
15
+ * @property {Array<string>} [transformers] - Modules default-exporting Shiki transformers, or lists of them
16
+ */
17
+
18
+ /**
19
+ * Loads the rehype plugin highlighting code blocks with Shiki, in every
20
+ * language it bundles. The plugin exposes its highlighter as `highlighter`
21
+ * (see `getHighlighter`).
22
+ *
23
+ * @param {ShikiOptions} [options] - The options
24
+ * @returns {Promise<import('unified').Plugin & { highlighter: import('./highlighter.mjs').SyntaxHighlighter }>}
25
+ */
26
+ export async function load(options) {
27
+ const highlighter = await createHighlighter(options);
28
+ const transformer = await rehypeShikiji({ highlighter });
29
+
30
+ /**
31
+ * Highlights the code blocks of a tree.
32
+ */
33
+ const shiki = () => transformer;
34
+
35
+ shiki.highlighter = highlighter;
36
+
37
+ return shiki;
38
+ }
@@ -1,38 +1,33 @@
1
1
  'use strict';
2
2
 
3
- import { highlighter } from '#utils/highlighter.mjs';
4
-
5
3
  import { typeAnnotationToHast } from './hast.mjs';
6
4
 
7
- // Kept apart from `./hast.mjs` on purpose: importing this module loads Shiki
8
- // (every grammar plus the regex engine), which only the pipelines that
9
- // highlight should pay for. The `ast` and `metadata` stages never do.
10
- const [lightTheme, darkTheme] = highlighter.shiki.getLoadedThemes();
11
-
12
5
  /**
13
- * Syntax-highlighted mdast→hast handler for `typeAnnotation` nodes, used by
14
- * the web (JSX) pipeline. The whole type is highlighted as one inline
15
- * fragment, and each resolved identifier's exact character range is wrapped
16
- * in an `<a>` via Shiki decorations. Values that are not TypeScript (display
17
- * names such as `HTTP/2 Headers Object`) are highlighted as plain text, so
18
- * their prose is not coloured as operators and numeric literals.
6
+ * Creates the syntax-highlighted mdast→hast handler for `typeAnnotation`
7
+ * nodes, used by the web (JSX) pipeline. The whole type is highlighted as one
8
+ * inline fragment, and each resolved identifier's exact character range is
9
+ * wrapped in an `<a>` via Shiki decorations. Values that are not TypeScript
10
+ * (display names such as `HTTP/2 Headers Object`) are highlighted as plain
11
+ * text, so their prose is not coloured as operators and numeric literals.
19
12
  *
20
13
  * Falls back to the minimal handler when the type failed to parse or nothing
21
14
  * resolved (no point paying for highlighting then).
22
15
  *
23
- * @param {import('mdast-util-to-hast').State} state
24
- * @param {import('mdast').Node} node
25
- * @returns {import('hast').Element}
16
+ * @param {() => import('#plugins/shiki/highlighter.mjs').SyntaxHighlighter} getHighlighter - Gives the highlighter, once a type is highlighted
17
+ * @returns {(state: import('mdast-util-to-hast').State, node: import('mdast').Node) => import('hast').Element}
26
18
  */
27
- export const typeAnnotationToHighlightedHast = (state, node) => {
19
+ export const createTypeAnnotationHandler = getHighlighter => (state, node) => {
28
20
  const links = node.data?.links ?? [];
29
21
 
30
22
  if (node.data?.parseError || links.length === 0) {
31
23
  return typeAnnotationToHast(state, node);
32
24
  }
33
25
 
34
- const root = highlighter.shiki.codeToHast(node.value, {
35
- lang: node.data?.typescript ? 'typescript' : 'text',
26
+ const { shiki, resolveLanguage } = getHighlighter();
27
+ const [lightTheme, darkTheme] = shiki.getLoadedThemes();
28
+
29
+ const root = shiki.codeToHast(node.value, {
30
+ lang: resolveLanguage(node.data?.typescript ? 'typescript' : 'text'),
36
31
  themes: { light: lightTheme, dark: darkTheme },
37
32
  decorations: links.map(({ start, end, href }) => ({
38
33
  start,
@@ -10,12 +10,16 @@ import { typeAnnotationSyntax } from './syntax.mjs';
10
10
  * Remark plugin that teaches the parser to treat any balanced `{...}` span in
11
11
  * text as a `typeAnnotation` node whose value is a TypeScript type expression.
12
12
  *
13
- * Only registered on the non-MDX pipeline — in MDX, `{...}` is a real
14
- * expression and is handled by remark-mdx instead.
13
+ * It does nothing in MDX, where `{...}` is a real expression, handled by
14
+ * remark-mdx instead.
15
15
  *
16
16
  * @this {import('unified').Processor}
17
17
  */
18
18
  export default function remarkTypeAnnotations() {
19
+ if (this.data('mdx')) {
20
+ return;
21
+ }
22
+
19
23
  const data = this.data();
20
24
 
21
25
  (data.micromarkExtensions ??= []).push(typeAnnotationSyntax());
@@ -3,6 +3,7 @@ import { workerData } from 'node:worker_threads';
3
3
  import { loadGenerator } from '#generators/loader.mjs';
4
4
  import logger from '#logger/index.mjs';
5
5
  import { setConfig } from '#utils/configuration/index.mjs';
6
+ import { loadMarkdownPlugins } from '#utils/markdown/plugins.mjs';
6
7
 
7
8
  if (workerData?.logLevel !== undefined) {
8
9
  logger.setLogLevel(workerData.logLevel);
@@ -26,5 +27,8 @@ export default async ({
26
27
 
27
28
  const generator = await loadGenerator(generatorSpecifier);
28
29
 
30
+ // Plugins can't be sent to workers, which import the pipeline themselves
31
+ await loadMarkdownPlugins(generator, configuration[generator.name]?.markdown);
32
+
29
33
  return generator.processChunk(input, itemIndices, extra);
30
34
  };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The heap size limit of each worker thread (V8's old space), in MB. V8 lets a
5
+ * heap grow to several times its live data before collecting it, the more the
6
+ * higher its limit, so every worker held several times what it was using. The
7
+ * biggest page of the Node.js docs, `all.html`, takes ~300MB. Node.js's
8
+ * `--max-old-space-size` replaces it.
9
+ */
10
+ export const WORKER_HEAP_SIZE = 512;
@@ -2,6 +2,8 @@ import Piscina from 'piscina';
2
2
 
3
3
  import logger from '#logger/index.mjs';
4
4
 
5
+ import { WORKER_HEAP_SIZE } from './constants.mjs';
6
+
5
7
  const poolLogger = logger.child('WorkerPool');
6
8
 
7
9
  const workerScript = import.meta.resolve('./chunk-worker.mjs');
@@ -22,7 +24,10 @@ export default function createWorkerPool(threads) {
22
24
  filename: workerScript,
23
25
  minThreads: 0,
24
26
  maxThreads: threads,
25
- idleTimeout: 1_000,
27
+ // A worker idle for 250ms ends, so its heap is gone during a long stretch
28
+ // of work on the main thread, such as bundling the site
29
+ idleTimeout: 250,
30
+ resourceLimits: { maxOldGenerationSizeMb: WORKER_HEAP_SIZE },
26
31
  workerData: { logLevel: logger.getLogLevel() },
27
32
  });
28
33
  }
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * The default `threads` ceiling. Each worker that highlights code holds Shiki's
5
- * grammars and regex engine (~300MB) on top of the pages it is building, so
6
- * past a few threads memory, not CPU, is what runs out. `--threads` raises it.
4
+ * The default `threads` ceiling. Each worker holds a heap of its own, with the
5
+ * libraries and the pages it is working on, so past a few threads memory, not
6
+ * CPU, is what runs out. `--threads` raises it.
7
7
  */
8
8
  export const DEFAULT_MAX_THREADS = 4;
9
9