@doc-kit/core 1.3.0 → 1.3.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @doc-kit/core
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1187](https://github.com/nodejs/doc-kit/pull/1187) [`e49eb52`](https://github.com/nodejs/doc-kit/commit/e49eb52472dfc2707a5966ee25c4dc7201360850) Thanks [@dxnnystyles](https://github.com/dxnnystyles)! - Keep json-simple output generation from mutating shared metadata entries.
8
+
9
+ - [#1179](https://github.com/nodejs/doc-kit/pull/1179) [`b8d7b77`](https://github.com/nodejs/doc-kit/commit/b8d7b7725f0dbe085d9f32af60fddd7459555af9) Thanks [@ovflowd](https://github.com/ovflowd)! - Builds use less memory, most of all with few threads: each worker's young generation is limited to 96MB, a single thread processes one chunk at a time and lets go of the pages' code before rendering them, and Vite's native bundler gets as many threads as doc-kit and returns the memory it frees. On the Node.js API docs, `--threads 1` now builds with Node.js's `--max-old-space-size=512`, and no longer crashes when `legacy-html` is built alongside other targets.
10
+
11
+ - [#1180](https://github.com/nodejs/doc-kit/pull/1180) [`92efbbe`](https://github.com/nodejs/doc-kit/commit/92efbbeb727a38b178c9ccb63a3a4f9cfbc110a4) Thanks [@ovflowd](https://github.com/ovflowd)! - Find input files with Node.js's `fs.globSync` instead of `tinyglobby` and `glob-parent`.
12
+
13
+ - [#1082](https://github.com/nodejs/doc-kit/pull/1082) [`9e21017`](https://github.com/nodejs/doc-kit/commit/9e21017d9597e3f8b5a18a41f3db8e5de216213a) Thanks [@btea](https://github.com/btea)! - Add a `doc-kit preview` command that serves the generated docs locally, building first when any `generate` option is passed. The default output directory (`./out`) is now defined in the shared configuration, so `generate` also writes there when no `--output` is given.
14
+
15
+ - [#1193](https://github.com/nodejs/doc-kit/pull/1193) [`854a944`](https://github.com/nodejs/doc-kit/commit/854a944699b3a996cb3a8544deaec3fe916fd449) Thanks [@OKAN-SOFTWARE](https://github.com/OKAN-SOFTWARE)! - Keep worker results and discovered source files in deterministic order so repeated documentation builds produce stable output.
16
+
17
+ - [#1178](https://github.com/nodejs/doc-kit/pull/1178) [`0321911`](https://github.com/nodejs/doc-kit/commit/03219116c78e7f4a721b4e39bc71ba54cb6f0ba8) Thanks [@ovflowd](https://github.com/ovflowd)! - Install fewer packages: `@doc-kit/core` updates `cosmiconfig` to v10 (TypeScript config files now load through Node's type stripping) and no longer installs `dedent`, and `@doc-kit/generator-react` estimates reading time with `Intl.Segmenter` instead of the `reading-time` package and updates `@node-core/ui-components` to 1.8.1, which no longer installs `@vcarl/remark-headings`.
18
+
3
19
  ## 1.3.0
4
20
 
5
21
  ### Minor Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@doc-kit/core",
3
3
  "type": "module",
4
- "version": "1.3.0",
4
+ "version": "1.3.1",
5
5
  "description": "The doc-kit engine: the pipeline and shared APIs that power the doc-kit generators and CLI",
6
6
  "repository": {
7
7
  "type": "git",
@@ -50,10 +50,8 @@
50
50
  "@swc/html-wasm": "^1.16.2",
51
51
  "@swc/wasm": "^1.15.46",
52
52
  "acorn": "^8.17.0",
53
- "cosmiconfig": "^9.0.2",
54
- "dedent": "^1.7.2",
53
+ "cosmiconfig": "^10.0.1",
55
54
  "github-slugger": "^2.0.0",
56
- "glob-parent": "^6.0.2",
57
55
  "mdast-util-slice-markdown": "^2.0.1",
58
56
  "mdast-util-to-string": "^4.0.0",
59
57
  "piscina": "^5.3.2",
@@ -65,7 +63,6 @@
65
63
  "remark-stringify": "^11.0.0",
66
64
  "semver": "^7.8.5",
67
65
  "shiki": "^4.4.3",
68
- "tinyglobby": "^0.2.17",
69
66
  "unified": "^11.0.5",
70
67
  "unist-builder": "^4.0.0",
71
68
  "unist-util-find-after": "^5.0.0",
@@ -77,6 +74,7 @@
77
74
  },
78
75
  "devDependencies": {
79
76
  "ajv": "^8.20.0",
77
+ "dedent": "^1.7.2",
80
78
  "hast-util-to-html": "^9.0.5",
81
79
  "hast-util-to-string": "^3.0.1"
82
80
  },
@@ -3,12 +3,11 @@
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { relative, sep } from 'node:path/posix';
5
5
 
6
- import globParent from 'glob-parent';
7
- import { globSync } from 'tinyglobby';
8
6
  import { parse as parseYaml } from 'yaml';
9
7
 
10
8
  import getConfig from '#utils/configuration/index.mjs';
11
9
  import { withExt } from '#utils/file.mjs';
10
+ import { findFiles, globParent } from '#utils/glob.mjs';
12
11
  import { getProcessor } from '#utils/markdown/processor.mjs';
13
12
  import { QUERIES } from '#utils/queries/index.mjs';
14
13
 
@@ -117,10 +116,9 @@ export async function* generate(_, worker) {
117
116
  const files = config.input.flatMap(input => {
118
117
  const parent = globParent(input);
119
118
 
120
- return globSync(input, { ignore: config.ignore }).map(child => [
121
- child,
122
- parent,
123
- ]);
119
+ return findFiles(input, { ignore: config.ignore })
120
+ .sort()
121
+ .map(child => [child, parent]);
124
122
  });
125
123
 
126
124
  // Parse markdown files in parallel using worker threads
@@ -4,9 +4,9 @@ import { readFile } from 'node:fs/promises';
4
4
  import { extname } from 'node:path';
5
5
 
6
6
  import { parse } from 'acorn';
7
- import { globSync } from 'tinyglobby';
8
7
 
9
8
  import getConfig from '#utils/configuration/index.mjs';
9
+ import { findFiles } from '#utils/glob.mjs';
10
10
 
11
11
  /**
12
12
  * Process a chunk of JavaScript files in a worker thread.
@@ -44,9 +44,9 @@ export async function processChunk(inputSlice, itemIndices) {
44
44
  export async function* generate(_, worker) {
45
45
  const config = getConfig('ast-js');
46
46
 
47
- const files = globSync(config.input, { ignore: config.ignore }).filter(
48
- p => extname(p) === '.js'
49
- );
47
+ const files = findFiles(config.input, { ignore: config.ignore })
48
+ .filter(p => extname(p) === '.js')
49
+ .sort();
50
50
 
51
51
  // Parse the Javascript sources into ASTs in parallel using worker threads
52
52
  // source is both the items list and the fullInput since we use sliceInput
@@ -16,13 +16,21 @@ import { UNIST } from '#utils/queries/index.mjs';
16
16
  export async function generate(input) {
17
17
  const config = getConfig('json-simple');
18
18
 
19
- input.forEach(node =>
20
- remove(node.content, [UNIST.isStabilityNode, UNIST.isHeading])
21
- );
19
+ const output = input.map(entry => {
20
+ const copy = { ...entry, content: structuredClone(entry.content) };
21
+
22
+ remove(copy.content, [UNIST.isStabilityNode, UNIST.isHeading]);
23
+
24
+ return copy;
25
+ });
22
26
 
23
27
  if (config.output) {
24
- await writeJSON(join(config.output, 'api-docs.json'), input, config.minify);
28
+ await writeJSON(
29
+ join(config.output, 'api-docs.json'),
30
+ output,
31
+ config.minify
32
+ );
25
33
  }
26
34
 
27
- return input;
35
+ return output;
28
36
  }
@@ -87,8 +87,10 @@ const createGenerator = () => {
87
87
  ? createParallelWorker(specifier, generator, pool, configuration)
88
88
  : Promise.resolve(null);
89
89
 
90
- const result = await generate(dependencyInput, await worker);
91
-
90
+ // Returned rather than awaited, so this function doesn't hold on to the
91
+ // input while the generator runs, and the generator can let it go
92
+ return generate(dependencyInput, await worker);
93
+ })().then(result => {
92
94
  // For streaming generators, "Completed" is logged when the cache
93
95
  // finishes collecting, not here when the generator returns
94
96
  if (!isAsyncIterable(result)) {
@@ -96,7 +98,7 @@ const createGenerator = () => {
96
98
  }
97
99
 
98
100
  return result;
99
- })()
101
+ })
100
102
  );
101
103
  };
102
104
 
@@ -1,5 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ /**
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
+ */
8
+ export const DEFAULT_MAX_THREADS = 4;
9
+
3
10
  /**
4
11
  * The heap size limit of each worker thread (V8's old space), in MB. V8 lets a
5
12
  * heap grow to several times its live data before collecting it, the more the
@@ -8,3 +15,10 @@
8
15
  * `--max-old-space-size` replaces it.
9
16
  */
10
17
  export const WORKER_HEAP_SIZE = 512;
18
+
19
+ /**
20
+ * The young generation size limit of each worker thread, in MB. V8 sizes it
21
+ * from the machine's memory, as if each worker were the only one, so it made up
22
+ * over half of a worker's heap. Node.js's `--max-semi-space-size` replaces it.
23
+ */
24
+ export const WORKER_GEN_SIZE = 96;
@@ -2,7 +2,7 @@ import Piscina from 'piscina';
2
2
 
3
3
  import logger from '#logger/index.mjs';
4
4
 
5
- import { WORKER_HEAP_SIZE } from './constants.mjs';
5
+ import { WORKER_HEAP_SIZE, WORKER_GEN_SIZE } from './constants.mjs';
6
6
 
7
7
  const poolLogger = logger.child('WorkerPool');
8
8
 
@@ -27,7 +27,11 @@ export default function createWorkerPool(threads) {
27
27
  // A worker idle for 250ms ends, so its heap is gone during a long stretch
28
28
  // of work on the main thread, such as bundling the site
29
29
  idleTimeout: 250,
30
- resourceLimits: { maxOldGenerationSizeMb: WORKER_HEAP_SIZE },
30
+ resourceLimits: {
31
+ maxOldGenerationSizeMb: WORKER_HEAP_SIZE,
32
+ maxYoungGenerationSizeMb: WORKER_GEN_SIZE,
33
+ },
34
+ recordTiming: false,
31
35
  workerData: { logLevel: logger.getLogLevel() },
32
36
  });
33
37
  }
@@ -77,12 +77,12 @@ export default function createParallelWorker(
77
77
 
78
78
  return {
79
79
  /**
80
- * Processes items in parallel, yielding results as chunks complete.
80
+ * Processes items in parallel, yielding results in submission order.
81
81
  *
82
82
  * @template T
83
83
  * @param {T[]} items - Items to process
84
84
  * @param {object} extra - Extra options
85
- * @yields {R[]} Chunk results as they complete
85
+ * @yields {R[]} Chunk results in input order
86
86
  */
87
87
  async *stream(items, extra) {
88
88
  if (items.length === 0) {
@@ -96,44 +96,41 @@ export default function createParallelWorker(
96
96
  { generator: name, chunks: chunks.length, chunkSize, threads }
97
97
  );
98
98
 
99
- const runInOneGo = threads <= 1 || items.length <= 2;
99
+ // On this thread, one chunk at a time, so their work isn't all held at once
100
+ if (threads <= 1 || items.length <= 2) {
101
+ for (const indices of chunks) {
102
+ yield await generator.processChunk(items, indices, extra);
103
+ }
100
104
 
101
- // Submit all tasks to Piscina - each promise resolves to itself for removal
102
- const pending = new Set(
103
- chunks.map(indices => {
104
- if (runInOneGo) {
105
- const promise = generator
106
- .processChunk(items, indices, extra)
107
- .then(result => ({ promise, result }));
108
-
109
- return promise;
110
- }
111
-
112
- const promise = pool
113
- .run(
114
- createTask(items, indices, extra, configuration, specifier, name)
115
- )
116
- .then(result => ({ promise, result }));
117
-
118
- return promise;
119
- })
120
- );
105
+ return;
106
+ }
121
107
 
122
- // Yield results as they complete (true parallel collection)
123
- let completed = 0;
108
+ // Start every chunk immediately, but emit results in submission order.
109
+ // Handle errors as each task settles so a later failure cannot become an
110
+ // unhandled rejection while earlier chunks are still running.
111
+ const tasks = chunks.map(indices => {
112
+ const task = pool.run(
113
+ createTask(items, indices, extra, configuration, specifier, name)
114
+ );
124
115
 
125
- while (pending.size > 0) {
126
- const { promise, result } = await Promise.race(pending);
116
+ return task.then(
117
+ result => ({ result }),
118
+ error => ({ error })
119
+ );
120
+ });
127
121
 
128
- pending.delete(promise);
122
+ for (const [index, task] of tasks.entries()) {
123
+ const outcome = await task;
129
124
 
130
- completed++;
125
+ if ('error' in outcome) {
126
+ throw outcome.error;
127
+ }
131
128
 
132
- parallelLogger.debug(`Chunk ${completed}/${chunks.length} completed`, {
129
+ parallelLogger.debug(`Chunk ${index + 1}/${chunks.length} completed`, {
133
130
  generator: name,
134
131
  });
135
132
 
136
- yield result;
133
+ yield outcome.result;
137
134
  }
138
135
  },
139
136
  };
@@ -1,13 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  /**
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.
4
+ * The default number of items each worker task processes.
7
5
  */
8
- export const DEFAULT_MAX_THREADS = 4;
6
+ export const DEFAULT_CHUNK_SIZE = 10;
9
7
 
10
8
  /**
11
- * The default number of items each worker task processes.
9
+ * The default directory generators write to when no `output` is configured.
12
10
  */
13
- export const DEFAULT_CHUNK_SIZE = 10;
11
+ export const DEFAULT_OUTPUT_DIRECTORY = './out';
@@ -22,7 +22,8 @@ import {
22
22
  import { resolveMarkdown } from '#utils/markdown/plugins.mjs';
23
23
  import { deepMerge } from '#utils/misc.mjs';
24
24
 
25
- import { DEFAULT_CHUNK_SIZE, DEFAULT_MAX_THREADS } from './constants.mjs';
25
+ import { DEFAULT_MAX_THREADS } from '../../threading/constants.mjs';
26
+ import { DEFAULT_CHUNK_SIZE, DEFAULT_OUTPUT_DIRECTORY } from './constants.mjs';
26
27
 
27
28
  const configExplorer = cosmiconfig('doc-kit');
28
29
 
@@ -63,6 +64,7 @@ export const getDefaultConfig = (generators, config) =>
63
64
  project: detectProject() ?? 'API Docs',
64
65
  version: process.version,
65
66
  minify: false,
67
+ output: DEFAULT_OUTPUT_DIRECTORY,
66
68
  ref: 'HEAD',
67
69
  // Without release history there is nothing to build a version picker
68
70
  // from, so generators render single-version output.
@@ -0,0 +1,42 @@
1
+ import { globSync } from 'node:fs';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // A path segment holding any glob syntax (wildcards, classes, braces or extglobs)
6
+ const GLOB_SEGMENT = /[*?[{]|[!+@]\(/;
7
+
8
+ /**
9
+ * Finds the files matching glob patterns, as POSIX paths relative to `cwd`
10
+ * (absolute patterns included). Directories are left out.
11
+ *
12
+ * @param {string | string[]} patterns - Glob patterns to match
13
+ * @param {{ cwd?: string | URL, ignore?: string[] }} [options]
14
+ * @returns {string[]}
15
+ */
16
+ export const findFiles = (patterns, { cwd = process.cwd(), ignore } = {}) => {
17
+ const root = cwd instanceof URL ? fileURLToPath(cwd) : cwd;
18
+
19
+ return globSync(patterns, { cwd: root, exclude: ignore, withFileTypes: true })
20
+ .filter(dirent => !dirent.isDirectory())
21
+ .map(dirent =>
22
+ relative(root, resolve(root, dirent.parentPath, dirent.name))
23
+ .split(sep)
24
+ .join('/')
25
+ );
26
+ };
27
+
28
+ /**
29
+ * Returns the directory part of a glob pattern before its first glob segment,
30
+ * e.g. `doc/api` for `doc/api/*.md`, or the parent directory of a plain path.
31
+ * Like `fs.globSync`, it reads backslashes as separators, never as escapes.
32
+ *
33
+ * @param {string} pattern - A glob pattern
34
+ * @returns {string}
35
+ */
36
+ export const globParent = pattern => {
37
+ const segments = pattern.replaceAll('\\', '/').split('/');
38
+ const index = segments.findIndex(segment => GLOB_SEGMENT.test(segment));
39
+ const parent = segments.slice(0, index === -1 ? -1 : index).join('/');
40
+
41
+ return parent || (segments[0] === '' ? '/' : '.');
42
+ };