@doc-kit/core 1.0.1 → 1.2.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 (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -1
  3. package/package.json +11 -6
  4. package/src/generators/index.mjs +3 -0
  5. package/src/generators/json/README.md +151 -0
  6. package/src/generators/json/constants.mjs +57 -0
  7. package/src/generators/json/generate.mjs +65 -0
  8. package/src/generators/json/generated/schema.d.ts +351 -0
  9. package/src/generators/json/index.mjs +27 -0
  10. package/src/generators/json/schema.json +529 -0
  11. package/src/generators/json/types.d.ts +29 -0
  12. package/src/generators/json/utils/document.mjs +58 -0
  13. package/src/generators/json/utils/entry.mjs +87 -0
  14. package/src/generators/json/utils/lifecycle.mjs +61 -0
  15. package/src/generators/json/utils/markdown.mjs +57 -0
  16. package/src/generators/json/utils/node.mjs +153 -0
  17. package/src/generators/json/utils/schema.mjs +15 -0
  18. package/src/generators/json/utils/signature.mjs +185 -0
  19. package/src/generators/json/utils/types.mjs +28 -0
  20. package/src/generators/json-all/README.md +31 -0
  21. package/src/generators/json-all/constants.mjs +5 -0
  22. package/src/generators/json-all/generate.mjs +35 -0
  23. package/src/generators/json-all/index.mjs +25 -0
  24. package/src/generators/json-all/schema.json +22 -0
  25. package/src/generators/json-all/types.d.ts +20 -0
  26. package/src/generators/json-simple/generate.mjs +2 -7
  27. package/src/generators/loader.mjs +8 -3
  28. package/src/generators/metadata/maps/mdn.json +9 -2
  29. package/src/generators/metadata/types.d.ts +24 -0
  30. package/src/generators/pipeline.mjs +169 -0
  31. package/src/generators/types.d.ts +20 -0
  32. package/src/generators.mjs +19 -12
  33. package/src/logger/logger.mjs +8 -0
  34. package/src/parsers/markdown.mjs +5 -6
  35. package/src/threading/chunk-worker.mjs +7 -0
  36. package/src/threading/index.mjs +1 -0
  37. package/src/utils/configuration/constants.mjs +13 -0
  38. package/src/utils/configuration/index.mjs +7 -2
  39. package/src/utils/file.mjs +10 -0
  40. package/src/utils/hierarchy.mjs +50 -0
  41. package/src/utils/overloads.mjs +54 -0
  42. package/src/utils/queries/index.mjs +2 -0
  43. package/src/utils/queries/utils.mjs +15 -0
  44. package/src/utils/remark-shiki.mjs +29 -0
  45. package/src/utils/remark.mjs +27 -49
  46. package/src/utils/signature/constants.mjs +3 -0
  47. package/src/utils/signature/extractListItem.mjs +140 -0
  48. package/src/utils/stability.mjs +24 -0
  49. package/src/utils/type-annotations/hast.mjs +0 -58
  50. package/src/utils/type-annotations/highlighted.mjs +64 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://doc-kit.nodejs.org/schemas/api-doc-all/1.0.1.json",
4
+ "title": "Bundle",
5
+ "description": "Every document of a documentation set, as emitted by the doc-kit `json-all` generator",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "The URL of the schema this bundle conforms to. Its last path segment is the schema version."
11
+ },
12
+ "documents": {
13
+ "type": "array",
14
+ "items": {
15
+ "$ref": "https://doc-kit.nodejs.org/schemas/api-doc/1.0.1.json"
16
+ },
17
+ "description": "The documents, in the order of the configured index, then by id."
18
+ }
19
+ },
20
+ "required": ["$schema", "documents"],
21
+ "additionalProperties": false
22
+ }
@@ -0,0 +1,20 @@
1
+ import type { Document } from '../json/generated/schema';
2
+
3
+ /**
4
+ * Every document of a documentation set
5
+ */
6
+ export interface Bundle {
7
+ /** The URL of the schema the bundle conforms to */
8
+ $schema: string;
9
+ documents: Array<Document>;
10
+ }
11
+
12
+ export interface Configuration {
13
+ /** Where the schema is published; `{schemaVersion}` is filled in */
14
+ schemaURL: string;
15
+ }
16
+
17
+ export type Generator = GeneratorMetadata<
18
+ Configuration,
19
+ Generate<Array<Document>, Promise<Bundle>>
20
+ >;
@@ -5,7 +5,7 @@ import { join } from 'node:path';
5
5
  import { remove } from 'unist-util-remove';
6
6
 
7
7
  import getConfig from '#utils/configuration/index.mjs';
8
- import { writeFile } from '#utils/file.mjs';
8
+ import { writeJSON } from '#utils/file.mjs';
9
9
  import { UNIST } from '#utils/queries/index.mjs';
10
10
 
11
11
  /**
@@ -21,12 +21,7 @@ export async function generate(input) {
21
21
  );
22
22
 
23
23
  if (config.output) {
24
- // Writes all the API docs stringified content into one file
25
- // Note: The full JSON generator in the future will create one JSON file per top-level API doc file
26
- await writeFile(
27
- join(config.output, 'api-docs.json'),
28
- config.minify ? JSON.stringify(input) : JSON.stringify(input, null, 2)
29
- );
24
+ await writeJSON(join(config.output, 'api-docs.json'), input, config.minify);
30
25
  }
31
26
 
32
27
  return input;
@@ -3,6 +3,8 @@
3
3
  import { isAbsolute } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
 
6
+ import { enforceArray } from '#utils/array.mjs';
7
+
6
8
  import { allGenerators } from './index.mjs';
7
9
 
8
10
  /**
@@ -67,7 +69,7 @@ export const loadGenerator = async specifier => {
67
69
 
68
70
  /**
69
71
  * Loads the given generators plus the transitive closure of their
70
- * dependencies (via `dependsOn`).
72
+ * dependencies (via `dependsOn`) and dependents (via `dependent`).
71
73
  *
72
74
  * @param {string[]} targets - Shorthand names, paths, or import specifiers
73
75
  * @returns {Promise<Map<string, GeneratorMetadata>>} Loaded generators keyed
@@ -87,8 +89,11 @@ export const loadGenerators = async targets => {
87
89
  const generator = await loadGenerator(specifier);
88
90
  generators.set(specifier, generator);
89
91
 
90
- if (generator.dependsOn) {
91
- queue.push(resolveGeneratorSpecifier(generator.dependsOn));
92
+ for (const related of [
93
+ ...enforceArray(generator.dependsOn ?? []),
94
+ ...enforceArray(generator.dependent ?? []),
95
+ ]) {
96
+ queue.push(resolveGeneratorSpecifier(related));
92
97
  }
93
98
  }
94
99
 
@@ -421,6 +421,7 @@
421
421
  "idbkeyrange": "https://developer.mozilla.org/docs/Web/API/IDBKeyRange",
422
422
  "idbobjectstore": "https://developer.mozilla.org/docs/Web/API/IDBObjectStore",
423
423
  "idbopendbrequest": "https://developer.mozilla.org/docs/Web/API/IDBOpenDBRequest",
424
+ "idbrecord": "https://developer.mozilla.org/docs/Web/API/IDBRecord",
424
425
  "idbrequest": "https://developer.mozilla.org/docs/Web/API/IDBRequest",
425
426
  "idbtransaction": "https://developer.mozilla.org/docs/Web/API/IDBTransaction",
426
427
  "idbversionchangeevent": "https://developer.mozilla.org/docs/Web/API/IDBVersionChangeEvent",
@@ -442,6 +443,7 @@
442
443
  "inputdeviceinfo": "https://developer.mozilla.org/docs/Web/API/InputDeviceInfo",
443
444
  "inputevent": "https://developer.mozilla.org/docs/Web/API/InputEvent",
444
445
  "installevent": "https://developer.mozilla.org/docs/Web/API/InstallEvent",
446
+ "interactioncontentfulpaint": "https://developer.mozilla.org/docs/Web/API/InteractionContentfulPaint",
445
447
  "interestevent": "https://developer.mozilla.org/docs/Web/API/InterestEvent",
446
448
  "intersectionobserver": "https://developer.mozilla.org/docs/Web/API/IntersectionObserver",
447
449
  "intersectionobserverentry": "https://developer.mozilla.org/docs/Web/API/IntersectionObserverEntry",
@@ -452,6 +454,7 @@
452
454
  "keyboardlayoutmap": "https://developer.mozilla.org/docs/Web/API/KeyboardLayoutMap",
453
455
  "keyframeeffect": "https://developer.mozilla.org/docs/Web/API/KeyframeEffect",
454
456
  "languagedetector": "https://developer.mozilla.org/docs/Web/API/LanguageDetector",
457
+ "languagemodel": "https://developer.mozilla.org/docs/Web/API/LanguageModel",
455
458
  "largestcontentfulpaint": "https://developer.mozilla.org/docs/Web/API/LargestContentfulPaint",
456
459
  "launchparams": "https://developer.mozilla.org/docs/Web/API/LaunchParams",
457
460
  "launchqueue": "https://developer.mozilla.org/docs/Web/API/LaunchQueue",
@@ -589,7 +592,9 @@
589
592
  "performanceresourcetiming": "https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming",
590
593
  "performancescripttiming": "https://developer.mozilla.org/docs/Web/API/PerformanceScriptTiming",
591
594
  "performanceservertiming": "https://developer.mozilla.org/docs/Web/API/PerformanceServerTiming",
595
+ "performancesoftnavigation": "https://developer.mozilla.org/docs/Web/API/PerformanceSoftNavigation",
592
596
  "performancetiming": "https://developer.mozilla.org/docs/Web/API/PerformanceTiming",
597
+ "performancetimingconfidence": "https://developer.mozilla.org/docs/Web/API/PerformanceTimingConfidence",
593
598
  "periodicsyncevent": "https://developer.mozilla.org/docs/Web/API/PeriodicSyncEvent",
594
599
  "periodicsyncmanager": "https://developer.mozilla.org/docs/Web/API/PeriodicSyncManager",
595
600
  "periodicwave": "https://developer.mozilla.org/docs/Web/API/PeriodicWave",
@@ -636,6 +641,7 @@
636
641
  "rtcerror": "https://developer.mozilla.org/docs/Web/API/RTCError",
637
642
  "rtcerrorevent": "https://developer.mozilla.org/docs/Web/API/RTCErrorEvent",
638
643
  "rtcicecandidate": "https://developer.mozilla.org/docs/Web/API/RTCIceCandidate",
644
+ "rtcicecandidatepair": "https://developer.mozilla.org/docs/Web/API/RTCIceCandidatePair",
639
645
  "rtcicetransport": "https://developer.mozilla.org/docs/Web/API/RTCIceTransport",
640
646
  "rtcidentityassertion": "https://developer.mozilla.org/docs/Web/API/RTCIdentityAssertion",
641
647
  "rtcpeerconnection": "https://developer.mozilla.org/docs/Web/API/RTCPeerConnection",
@@ -672,7 +678,6 @@
672
678
  "restrictiontarget": "https://developer.mozilla.org/docs/Web/API/RestrictionTarget",
673
679
  "svgaelement": "https://developer.mozilla.org/docs/Web/API/SVGAElement",
674
680
  "svgangle": "https://developer.mozilla.org/docs/Web/API/SVGAngle",
675
- "svganimatecolorelement": "https://developer.mozilla.org/docs/Web/API/SVGAnimateColorElement",
676
681
  "svganimateelement": "https://developer.mozilla.org/docs/Web/API/SVGAnimateElement",
677
682
  "svganimatemotionelement": "https://developer.mozilla.org/docs/Web/API/SVGAnimateMotionElement",
678
683
  "svganimatetransformelement": "https://developer.mozilla.org/docs/Web/API/SVGAnimateTransformElement",
@@ -749,7 +754,6 @@
749
754
  "svgradialgradientelement": "https://developer.mozilla.org/docs/Web/API/SVGRadialGradientElement",
750
755
  "svgrect": "https://developer.mozilla.org/docs/Web/API/SVGRect",
751
756
  "svgrectelement": "https://developer.mozilla.org/docs/Web/API/SVGRectElement",
752
- "svgrenderingintent": "https://developer.mozilla.org/docs/Web/API/SVGRenderingIntent",
753
757
  "svgsvgelement": "https://developer.mozilla.org/docs/Web/API/SVGSVGElement",
754
758
  "svgscriptelement": "https://developer.mozilla.org/docs/Web/API/SVGScriptElement",
755
759
  "svgsetelement": "https://developer.mozilla.org/docs/Web/API/SVGSetElement",
@@ -947,8 +951,10 @@
947
951
  "webtransport": "https://developer.mozilla.org/docs/Web/API/WebTransport",
948
952
  "webtransportbidirectionalstream": "https://developer.mozilla.org/docs/Web/API/WebTransportBidirectionalStream",
949
953
  "webtransportdatagramduplexstream": "https://developer.mozilla.org/docs/Web/API/WebTransportDatagramDuplexStream",
954
+ "webtransportdatagramswritable": "https://developer.mozilla.org/docs/Web/API/WebTransportDatagramsWritable",
950
955
  "webtransporterror": "https://developer.mozilla.org/docs/Web/API/WebTransportError",
951
956
  "webtransportreceivestream": "https://developer.mozilla.org/docs/Web/API/WebTransportReceiveStream",
957
+ "webtransportsendgroup": "https://developer.mozilla.org/docs/Web/API/WebTransportSendGroup",
952
958
  "webtransportsendstream": "https://developer.mozilla.org/docs/Web/API/WebTransportSendStream",
953
959
  "wheelevent": "https://developer.mozilla.org/docs/Web/API/WheelEvent",
954
960
  "window": "https://developer.mozilla.org/docs/Web/API/Window",
@@ -1014,6 +1020,7 @@
1014
1020
  "xrview": "https://developer.mozilla.org/docs/Web/API/XRView",
1015
1021
  "xrviewerpose": "https://developer.mozilla.org/docs/Web/API/XRViewerPose",
1016
1022
  "xrviewport": "https://developer.mozilla.org/docs/Web/API/XRViewport",
1023
+ "xrvisibilitymaskchangeevent": "https://developer.mozilla.org/docs/Web/API/XRVisibilityMaskChangeEvent",
1017
1024
  "xrwebglbinding": "https://developer.mozilla.org/docs/Web/API/XRWebGLBinding",
1018
1025
  "xrwebgldepthinformation": "https://developer.mozilla.org/docs/Web/API/XRWebGLDepthInformation",
1019
1026
  "xrwebgllayer": "https://developer.mozilla.org/docs/Web/API/XRWebGLLayer",
@@ -97,6 +97,11 @@ export interface HeadingData extends Data {
97
97
  * from the ToC while still rendering on the page.
98
98
  */
99
99
  isOverload?: boolean;
100
+ /**
101
+ * The slug of the first heading of the overloaded function this heading is
102
+ * an overload of. Set alongside `isOverload`.
103
+ */
104
+ overloadOf?: string;
100
105
  }
101
106
 
102
107
  /**
@@ -120,6 +125,23 @@ export interface StabilityData extends Data {
120
125
  */
121
126
  export type StabilityNode = NodeWithData<Blockquote, StabilityData>;
122
127
 
128
+ /**
129
+ * Describes a chunk page: a section of a module rendered as a page of its own
130
+ * (see the `section-pages` generator). Present on every entry of a chunk page.
131
+ */
132
+ export interface ChunkInfo {
133
+ /** The `api` of the module the chunk was split from */
134
+ api: string;
135
+ /** The `path` of the module the chunk was split from */
136
+ path: string;
137
+ /** The section's anchor on the full module page */
138
+ slug: string;
139
+ /** The chunk's position within the module, in document order */
140
+ index: number;
141
+ /** The original depth of the chunk's heading */
142
+ depth: number;
143
+ }
144
+
123
145
  /**
124
146
  * Complete metadata entry for a single API documentation element.
125
147
  *
@@ -136,6 +158,8 @@ export interface MetadataEntry extends YAMLProperties {
136
158
  synthetic?: boolean;
137
159
  /** Whether this page was authored as MDX (JSX-in-Markdown). */
138
160
  mdx?: boolean;
161
+ /** Set when this entry belongs to a chunk page rather than a full page. */
162
+ chunk?: ChunkInfo;
139
163
  /** Processed heading with metadata */
140
164
  heading: HeadingNode;
141
165
  /** Stability classification information */
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ import { enforceArray } from '#utils/array.mjs';
4
+
5
+ import { resolveGeneratorSpecifier } from './loader.mjs';
6
+
7
+ /**
8
+ * The resolved specifiers of the generators a generator is delivered through.
9
+ *
10
+ * @param {GeneratorMetadata | undefined} generator
11
+ * @returns {string[]}
12
+ */
13
+ const dependentsOf = generator =>
14
+ enforceArray(generator?.dependent ?? []).map(resolveGeneratorSpecifier);
15
+
16
+ /**
17
+ * Collects the generators reached from the given ones through `dependsOn`.
18
+ *
19
+ * @param {string[]} specifiers - Resolved generator specifiers
20
+ * @param {Map<string, GeneratorMetadata>} generators - Loaded generators
21
+ * @returns {Set<string>}
22
+ */
23
+ const dependencyClosure = (specifiers, generators) => {
24
+ const closure = new Set();
25
+ const stack = [...specifiers];
26
+
27
+ while (stack.length > 0) {
28
+ const specifier = stack.pop();
29
+
30
+ if (closure.has(specifier)) {
31
+ continue;
32
+ }
33
+
34
+ closure.add(specifier);
35
+
36
+ const { dependsOn } = generators.get(specifier) ?? {};
37
+
38
+ if (dependsOn) {
39
+ stack.push(resolveGeneratorSpecifier(dependsOn));
40
+ }
41
+ }
42
+
43
+ return closure;
44
+ };
45
+
46
+ /**
47
+ * Splices a generator into a dependent's pipeline: walking from the dependent
48
+ * up its dependency chain, the first generator that consumes the splicing
49
+ * generator's own input is rewired to consume the splicing generator instead.
50
+ *
51
+ * @param {string} specifier - The generator to splice in
52
+ * @param {string} dependent - The pipeline to splice it into
53
+ * @param {Map<string, string | undefined>} inputOf - Effective inputs, mutated
54
+ * @param {string} name - The splicing generator's name, for errors
55
+ */
56
+ const splice = (specifier, dependent, inputOf, name) => {
57
+ const upstream = inputOf.get(specifier);
58
+ const visited = new Set();
59
+
60
+ let current = dependent;
61
+
62
+ while (current !== undefined && current !== specifier) {
63
+ if (visited.has(current)) {
64
+ break;
65
+ }
66
+
67
+ visited.add(current);
68
+
69
+ if (inputOf.get(current) === upstream) {
70
+ inputOf.set(current, specifier);
71
+ return;
72
+ }
73
+
74
+ current = inputOf.get(current);
75
+ }
76
+
77
+ throw new Error(
78
+ `Generator "${name}" declares "${dependent}" as its dependent, but ` +
79
+ `nothing in that pipeline consumes "${upstream ?? 'no input'}", so ` +
80
+ 'there is nowhere to splice it in.'
81
+ );
82
+ };
83
+
84
+ /**
85
+ * Follows a generator's `dependent` links to the generators that finally
86
+ * deliver its output. When some of its dependents are already part of the
87
+ * run, only those deliver it; otherwise all of them do.
88
+ *
89
+ * @param {string} specifier - Resolved generator specifier
90
+ * @param {Map<string, GeneratorMetadata>} generators - Loaded generators
91
+ * @param {Set<string>} requested - Generators already part of the run
92
+ * @param {Set<string>} [seen] - Guards against dependent cycles
93
+ * @returns {string[]} Resolved specifiers of the terminal generators
94
+ */
95
+ const resolveTerminals = (
96
+ specifier,
97
+ generators,
98
+ requested,
99
+ seen = new Set()
100
+ ) => {
101
+ if (seen.has(specifier)) {
102
+ return [];
103
+ }
104
+
105
+ seen.add(specifier);
106
+
107
+ const dependents = dependentsOf(generators.get(specifier));
108
+
109
+ if (dependents.length === 0) {
110
+ return [specifier];
111
+ }
112
+
113
+ const active = dependents.filter(dependent => requested.has(dependent));
114
+
115
+ return (active.length > 0 ? active : dependents).flatMap(dependent =>
116
+ resolveTerminals(dependent, generators, requested, seen)
117
+ );
118
+ };
119
+
120
+ /**
121
+ * Resolves the effective pipeline for a run: which generator each generator
122
+ * reads its input from, and which generators the run finally collects.
123
+ *
124
+ * By default a generator reads from its `dependsOn`. A generator that declares
125
+ * one or more `dependent`s is spliced into each dependent's pipeline (see
126
+ * {@link splice}). Several generators may splice at the same point; each later
127
+ * one is inserted upstream of the earlier ones, forming a chain.
128
+ *
129
+ * A requested generator that declares dependents is delivered through them,
130
+ * so the run collects their output rather than the generator's own: through
131
+ * the dependents that are already part of the run when there are any, and
132
+ * through all of them otherwise.
133
+ *
134
+ * @param {string[]} targets - Requested targets, as resolved specifiers
135
+ * @param {Map<string, GeneratorMetadata>} generators - Loaded generators,
136
+ * including every dependency and dependent of the targets
137
+ * @returns {{ targets: string[], inputOf: Map<string, string | undefined> }}
138
+ * The generators to collect, and each generator's effective input generator
139
+ */
140
+ export const resolvePipeline = (targets, generators) => {
141
+ /** @type {Map<string, string | undefined>} */
142
+ const inputOf = new Map();
143
+
144
+ for (const [specifier, { dependsOn }] of generators) {
145
+ inputOf.set(
146
+ specifier,
147
+ dependsOn ? resolveGeneratorSpecifier(dependsOn) : undefined
148
+ );
149
+ }
150
+
151
+ for (const [specifier, generator] of generators) {
152
+ for (const dependent of dependentsOf(generator)) {
153
+ splice(specifier, dependent, inputOf, generator.name);
154
+ }
155
+ }
156
+
157
+ const requested = dependencyClosure(targets, generators);
158
+ const collected = [];
159
+
160
+ for (const target of targets) {
161
+ for (const terminal of resolveTerminals(target, generators, requested)) {
162
+ if (!collected.includes(terminal)) {
163
+ collected.push(terminal);
164
+ }
165
+ }
166
+ }
167
+
168
+ return { targets: collected, inputOf };
169
+ };
@@ -97,6 +97,26 @@ declare global {
97
97
  */
98
98
  dependsOn: string | undefined;
99
99
 
100
+ /**
101
+ * The generator(s) this generator's output is delivered through, declared
102
+ * as import specifiers. This is the inverse of `dependsOn`: rather than
103
+ * pulling a dependency in, it pushes this generator into another
104
+ * generator's pipeline.
105
+ *
106
+ * The orchestrator splices this generator in front of the first generator
107
+ * in each dependent's dependency chain that consumes the same `dependsOn`
108
+ * as this one. For example, a generator with
109
+ * `dependsOn: '@doc-kit/core/metadata'` and
110
+ * `dependent: '@doc-kit/generator-react/html'` is inserted between
111
+ * `metadata` and `jsx-ast` (the consumer of `metadata` on the way to
112
+ * `html`), so its output is what `jsx-ast` — and therefore `html` — sees.
113
+ *
114
+ * Requesting a generator that declares dependents runs them as well, and
115
+ * the run's result is their output: when some of the dependents are
116
+ * already part of the run, only those; otherwise all of them.
117
+ */
118
+ dependent?: string | string[];
119
+
100
120
  /**
101
121
  * Generators are abstract and the different generators have different sort of inputs and outputs.
102
122
  * For example, a MDX generator would take the raw AST and output MDX with React Components;
@@ -5,6 +5,7 @@ import {
5
5
  loadGenerators,
6
6
  resolveGeneratorSpecifier,
7
7
  } from './generators/loader.mjs';
8
+ import { resolvePipeline } from './generators/pipeline.mjs';
8
9
  import logger from './logger/index.mjs';
9
10
  import createWorkerPool from './threading/index.mjs';
10
11
  import createParallelWorker from './threading/parallel.mjs';
@@ -44,9 +45,12 @@ const createGenerator = () => {
44
45
  *
45
46
  * @param {string} specifier - Resolved generator specifier to schedule
46
47
  * @param {Map<string, GeneratorMetadata>} generators - Loaded generators
48
+ * @param {Map<string, string | undefined>} inputOf - Each generator's
49
+ * effective input generator (its dependency, unless another generator was
50
+ * spliced in front of it via `dependent`)
47
51
  * @param {import('./utils/configuration/types').Configuration} configuration - Runtime options
48
52
  */
49
- const scheduleGenerator = (specifier, generators, configuration) => {
53
+ const scheduleGenerator = (specifier, generators, inputOf, configuration) => {
50
54
  if (cache.has(specifier)) {
51
55
  return;
52
56
  }
@@ -54,12 +58,11 @@ const createGenerator = () => {
54
58
  const generator = generators.get(specifier);
55
59
  const { name, generate, hasParallelProcessor } = generator;
56
60
 
57
- const dependsOn =
58
- generator.dependsOn && resolveGeneratorSpecifier(generator.dependsOn);
61
+ const dependsOn = inputOf.get(specifier);
59
62
 
60
63
  // Schedule dependency first
61
64
  if (dependsOn && !cache.has(dependsOn)) {
62
- scheduleGenerator(dependsOn, generators, configuration);
65
+ scheduleGenerator(dependsOn, generators, inputOf, configuration);
63
66
  }
64
67
 
65
68
  generatorsLogger.debug(`Scheduling "${name}"`, {
@@ -104,8 +107,16 @@ const createGenerator = () => {
104
107
 
105
108
  // Resolve shorthand names and load the full dependency closure up front,
106
109
  // so scheduling below is fully synchronous.
107
- const targets = target.map(resolveGeneratorSpecifier);
108
- const generators = await loadGenerators(targets);
110
+ const generators = await loadGenerators(
111
+ target.map(resolveGeneratorSpecifier)
112
+ );
113
+
114
+ // Work out who reads from whom once generators declaring a `dependent`
115
+ // have been spliced in, and which generators the run finally collects.
116
+ const { targets, inputOf } = resolvePipeline(
117
+ target.map(resolveGeneratorSpecifier),
118
+ generators
119
+ );
109
120
 
110
121
  generatorsLogger.debug(`Starting pipeline`, {
111
122
  generators: targets.join(', '),
@@ -114,18 +125,14 @@ const createGenerator = () => {
114
125
 
115
126
  // Compute consumer counts up front so dependencies can be evicted as soon
116
127
  // as their last consumer runs (must be ready before any generator starts).
117
- cache.populateConsumerCounts(targets, specifier => {
118
- const { dependsOn } = generators.get(specifier);
119
-
120
- return dependsOn && resolveGeneratorSpecifier(dependsOn);
121
- });
128
+ cache.populateConsumerCounts(targets, specifier => inputOf.get(specifier));
122
129
 
123
130
  // Create worker pool
124
131
  pool = createWorkerPool(threads);
125
132
 
126
133
  // Schedule all generators
127
134
  for (const specifier of targets) {
128
- scheduleGenerator(specifier, generators, configuration);
135
+ scheduleGenerator(specifier, generators, inputOf, configuration);
129
136
  }
130
137
 
131
138
  // Start all collections in parallel (don't await sequentially). Consuming
@@ -165,6 +165,13 @@ export const createLogger = (
165
165
  }
166
166
  };
167
167
 
168
+ /**
169
+ * Gets the current log level for this logger instance.
170
+ *
171
+ * @returns {number} The current numeric log level
172
+ */
173
+ const getLogLevel = () => currentLevel;
174
+
168
175
  return {
169
176
  info,
170
177
  warn,
@@ -173,5 +180,6 @@ export const createLogger = (
173
180
  debug,
174
181
  child,
175
182
  setLogLevel,
183
+ getLogLevel,
176
184
  };
177
185
  };
@@ -22,9 +22,7 @@ const NODE_LTS_VERSION_REGEX = /Long Term Support/i;
22
22
  export const parseChangelog = async path => {
23
23
  const changelog = await loadFromURL(path);
24
24
 
25
- const nodeMajors = Array.from(changelog.matchAll(NODE_VERSIONS_REGEX));
26
-
27
- return nodeMajors.map(match => ({
25
+ return Array.from(changelog.matchAll(NODE_VERSIONS_REGEX), match => ({
28
26
  version: coerce(match[1]),
29
27
  isLts: NODE_LTS_VERSION_REGEX.test(match[2]),
30
28
  }));
@@ -43,7 +41,8 @@ export const parseIndex = async path => {
43
41
 
44
42
  const index = await loadFromURL(path);
45
43
 
46
- const items = Array.from(index.matchAll(LIST_ITEM_REGEX));
47
-
48
- return items.map(([, section, api]) => ({ section, api }));
44
+ return Array.from(index.matchAll(LIST_ITEM_REGEX), ([, section, api]) => ({
45
+ section,
46
+ api,
47
+ }));
49
48
  };
@@ -1,6 +1,13 @@
1
+ import { workerData } from 'node:worker_threads';
2
+
1
3
  import { loadGenerator } from '#generators/loader.mjs';
4
+ import logger from '#logger/index.mjs';
2
5
  import { setConfig } from '#utils/configuration/index.mjs';
3
6
 
7
+ if (workerData?.logLevel !== undefined) {
8
+ logger.setLogLevel(workerData.logLevel);
9
+ }
10
+
4
11
  /**
5
12
  * Processes a chunk of items using the specified generator's processChunk method.
6
13
  * This is the worker entry point for Piscina.
@@ -23,5 +23,6 @@ export default function createWorkerPool(threads) {
23
23
  minThreads: 0,
24
24
  maxThreads: threads,
25
25
  idleTimeout: 1_000,
26
+ workerData: { logLevel: logger.getLogLevel() },
26
27
  });
27
28
  }
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
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.
7
+ */
8
+ export const DEFAULT_MAX_THREADS = 4;
9
+
10
+ /**
11
+ * The default number of items each worker task processes.
12
+ */
13
+ export const DEFAULT_CHUNK_SIZE = 10;
@@ -18,6 +18,8 @@ import { enforceArray } from '#utils/array.mjs';
18
18
  import { leftHandAssign } from '#utils/generators.mjs';
19
19
  import { deepMerge } from '#utils/misc.mjs';
20
20
 
21
+ import { DEFAULT_CHUNK_SIZE, DEFAULT_MAX_THREADS } from './constants.mjs';
22
+
21
23
  const configExplorer = cosmiconfig('doc-kit');
22
24
 
23
25
  /**
@@ -68,8 +70,11 @@ export const getDefaultConfig = (generators, config) =>
68
70
  // riscv64 with sv39. Running multiple generators that use wasm in
69
71
  // parallel could cause failures to allocate new wasm instance.
70
72
  // See also https://github.com/nodejs/node/pull/60591
71
- threads: process.arch === 'riscv64' ? 1 : cpus().length,
72
- chunkSize: 10,
73
+ threads:
74
+ process.arch === 'riscv64'
75
+ ? 1
76
+ : Math.min(cpus().length, DEFAULT_MAX_THREADS),
77
+ chunkSize: DEFAULT_CHUNK_SIZE,
73
78
  })
74
79
  );
75
80
 
@@ -18,3 +18,13 @@ export const writeFile = (file, ...args) =>
18
18
  fs
19
19
  .mkdir(dirname(file), { recursive: true })
20
20
  .then(() => fs.writeFile(file, ...args));
21
+
22
+ /**
23
+ * Writes a value as JSON
24
+ *
25
+ * @param {string} file
26
+ * @param {unknown} value
27
+ * @param {boolean} [minify]
28
+ */
29
+ export const writeJSON = (file, value, minify = false) =>
30
+ writeFile(file, JSON.stringify(value, null, minify ? 0 : 2));
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A node in the entry hierarchy.
5
+ *
6
+ * @typedef {object} HierarchizedEntry
7
+ * @property {import('../generators/metadata/types').MetadataEntry} entry The metadata entry this node wraps
8
+ * @property {Array<HierarchizedEntry>} children Entries nested under this one, by heading depth
9
+ */
10
+
11
+ /**
12
+ * Finds the closest preceding node whose heading is shallower than the
13
+ * entry's, which is the entry's parent.
14
+ *
15
+ * @param {import('../generators/metadata/types').MetadataEntry} entry The entry to find a parent for
16
+ * @param {Array<HierarchizedEntry>} nodes Wrapper nodes, index-aligned with the entries
17
+ * @param {number} startIdx The index to search backwards from
18
+ * @returns {HierarchizedEntry | undefined} The parent, or `undefined` when no shallower entry precedes it
19
+ */
20
+ export const findParent = (entry, nodes, startIdx) => {
21
+ for (let i = startIdx; i >= 0; i--) {
22
+ if (nodes[i].entry.heading.depth < entry.heading.depth) {
23
+ return nodes[i];
24
+ }
25
+ }
26
+
27
+ return undefined;
28
+ };
29
+
30
+ /**
31
+ * @param {Array<import('../generators/metadata/types').MetadataEntry>} entries Entries in document order
32
+ * @returns {Array<HierarchizedEntry>} The root nodes
33
+ */
34
+ export const buildHierarchy = entries => {
35
+ const roots = [];
36
+
37
+ // Wrapper nodes, index-aligned with `entries`
38
+ const nodes = entries.map(entry => ({ entry, children: [] }));
39
+
40
+ nodes.forEach((node, i) => {
41
+ const parent =
42
+ node.entry.heading.depth <= 1
43
+ ? undefined
44
+ : findParent(node.entry, nodes, i - 1);
45
+
46
+ (parent?.children ?? roots).push(node);
47
+ });
48
+
49
+ return roots;
50
+ };