@doc-kit/core 1.0.0 → 1.1.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 (37) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +21 -8
  3. package/src/generators/ast/generate.mjs +5 -4
  4. package/src/generators/ast-js/generate.mjs +1 -1
  5. package/src/generators/index.mjs +1 -0
  6. package/src/generators/json-simple/generate.mjs +3 -3
  7. package/src/generators/loader.mjs +8 -3
  8. package/src/generators/metadata/generate.mjs +3 -2
  9. package/src/generators/metadata/types.d.ts +19 -0
  10. package/src/generators/metadata/utils/parse.mjs +10 -9
  11. package/src/generators/metadata/utils/resolveTypes.mjs +3 -2
  12. package/src/generators/metadata/utils/slugger.mjs +1 -1
  13. package/src/generators/metadata/utils/transformers.mjs +5 -4
  14. package/src/generators/metadata/utils/visitors.mjs +4 -3
  15. package/src/generators/metadata/utils/yaml.mjs +1 -1
  16. package/src/generators/pipeline.mjs +169 -0
  17. package/src/generators/types.d.ts +20 -0
  18. package/src/generators.mjs +19 -12
  19. package/src/logger/transports/console.mjs +2 -2
  20. package/src/logger/transports/github.mjs +3 -3
  21. package/src/logger/utils/colors.mjs +1 -1
  22. package/src/parsers/markdown.mjs +6 -7
  23. package/src/threading/chunk-worker.mjs +2 -2
  24. package/src/threading/index.mjs +1 -1
  25. package/src/threading/parallel.mjs +1 -1
  26. package/src/utils/configuration/constants.mjs +13 -0
  27. package/src/utils/configuration/index.mjs +13 -8
  28. package/src/utils/configuration/types.d.ts +1 -1
  29. package/src/utils/generators.mjs +30 -0
  30. package/src/utils/inline.mjs +82 -0
  31. package/src/utils/queries/index.mjs +9 -2
  32. package/src/utils/queries/utils.mjs +35 -11
  33. package/src/utils/remark-shiki.mjs +29 -0
  34. package/src/utils/remark.mjs +42 -49
  35. package/src/utils/signature/parseList.mjs +4 -3
  36. package/src/utils/type-annotations/hast.mjs +0 -58
  37. package/src/utils/type-annotations/highlighted.mjs +64 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @doc-kit/core
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1090](https://github.com/nodejs/doc-kit/pull/1090) [`0a9dc37`](https://github.com/nodejs/doc-kit/commit/0a9dc37dc96f8044c76c3ca9b35e97895a87d1fd) Thanks [@avivkeller](https://github.com/avivkeller)! - perf: build the site one page at a time, and `all.html` from the module pages
8
+
9
+ - [#1057](https://github.com/nodejs/doc-kit/pull/1057) [`da5d8e6`](https://github.com/nodejs/doc-kit/commit/da5d8e63d0d33fe7a95c46afb8d5140d576827b4) Thanks [@avivkeller](https://github.com/avivkeller)! - feat: `dependent` generators and the `section-pages` generator
10
+
11
+ ## 1.0.1
12
+
13
+ ### Patch Changes
14
+
15
+ - [#1023](https://github.com/nodejs/doc-kit/pull/1023) [`27a412a`](https://github.com/nodejs/doc-kit/commit/27a412ac35ab06e977b03b559e6525c0c5c5356c) Thanks [@btea](https://github.com/btea)! - Fix empty paragraphs in API docs when a typed parameter list contains trailing non-parameter items
16
+
17
+ When a loose markdown list in the API docs starts with typed parameters (e.g. `actual`, `expected`, `Returns`) but also contains plain prose bullets (e.g. algorithm complexity notes), the entire list was previously treated as a parameter signature table. The non-parameter items had no name or type, causing them to render as empty `<section>` blocks on the built site.
18
+
19
+ The fix splits the list at the first non-parameter item: typed items become the `FunctionSignature` table, and the remaining items render as regular markdown content.
20
+
21
+ - [#1045](https://github.com/nodejs/doc-kit/pull/1045) [`4a22c85`](https://github.com/nodejs/doc-kit/commit/4a22c85dca917f52c773aba084191da7c622687e) Thanks [@avivkeller](https://github.com/avivkeller)! - MDX nodes in the HTML-string pipelines are now dropped, and do not crash.
22
+
23
+ - [#1049](https://github.com/nodejs/doc-kit/pull/1049) [`8cc1fd0`](https://github.com/nodejs/doc-kit/commit/8cc1fd010ac38b16dd3c9bcd422c0ce0e5eb483d) Thanks [@avivkeller](https://github.com/avivkeller)! - Render markdown summaries as markup instead of as their source
24
+
3
25
  ## 1.0.0
4
26
 
5
27
  ### Major Changes
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@doc-kit/core",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
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",
8
8
  "url": "git+https://github.com/nodejs/doc-kit.git",
9
9
  "directory": "packages/core"
10
10
  },
11
+ "license": "MIT",
11
12
  "main": "./src/generators.mjs",
12
13
  "exports": {
13
14
  ".": "./src/generators.mjs",
@@ -18,7 +19,19 @@
18
19
  "./package.json": "./package.json",
19
20
  "./shiki.config.mjs": "./shiki.config.mjs",
20
21
  "./src/*": "./src/*",
21
- "./*": "./src/*"
22
+ "./*": [
23
+ "./src/*",
24
+ "./src/*.d.ts"
25
+ ]
26
+ },
27
+ "imports": {
28
+ "#generators/*": "./src/generators/*",
29
+ "#logger/*": "./src/logger/*",
30
+ "#parsers/*": [
31
+ "./src/parsers/*",
32
+ "./src/parsers/*.d.ts"
33
+ ],
34
+ "#utils/*": "./src/utils/*"
22
35
  },
23
36
  "files": [
24
37
  "src",
@@ -31,7 +44,7 @@
31
44
  ],
32
45
  "dependencies": {
33
46
  "@actions/core": "^3.0.0",
34
- "@node-core/rehype-shiki": "^1.4.3",
47
+ "@node-core/rehype-shiki": "^1.5.0",
35
48
  "@swc/html-wasm": "^1.15.46",
36
49
  "@swc/wasm": "^1.15.46",
37
50
  "acorn": "^8.17.0",
@@ -40,7 +53,7 @@
40
53
  "github-slugger": "^2.0.0",
41
54
  "glob-parent": "^6.0.2",
42
55
  "hastscript": "^9.0.1",
43
- "piscina": "^5.3.0",
56
+ "piscina": "^5.3.1",
44
57
  "rehype-stringify": "^10.0.1",
45
58
  "remark-gfm": "^4.0.1",
46
59
  "remark-mdx": "^3.1.1",
@@ -48,7 +61,7 @@
48
61
  "remark-rehype": "^11.1.2",
49
62
  "remark-stringify": "^11.0.0",
50
63
  "semver": "^7.8.5",
51
- "shiki": "^4.3.0",
64
+ "shiki": "^4.4.3",
52
65
  "tinyglobby": "^0.2.17",
53
66
  "unified": "^11.0.5",
54
67
  "unist-builder": "^4.0.0",
@@ -64,9 +77,9 @@
64
77
  "hast-util-to-string": "^3.0.1"
65
78
  },
66
79
  "peerDependencies": {
67
- "@node-core/doc-kit": "2.0.0",
68
- "@node-core/doc-kit-legacy": "1.0.0",
69
- "@doc-kit/generator-react": "0.1.0"
80
+ "@node-core/doc-kit": ">=2.0.0",
81
+ "@node-core/doc-kit-legacy": ">=1.0.0",
82
+ "@doc-kit/generator-react": ">=0.1.0"
70
83
  },
71
84
  "peerDependenciesMeta": {
72
85
  "@node-core/doc-kit": {
@@ -7,11 +7,12 @@ import globParent from 'glob-parent';
7
7
  import { globSync } from 'tinyglobby';
8
8
  import { parse as parseYaml } from 'yaml';
9
9
 
10
+ import getConfig from '#utils/configuration/index.mjs';
11
+ import { withExt } from '#utils/file.mjs';
12
+ import { QUERIES } from '#utils/queries/index.mjs';
13
+ import { getRemark as remark, getRemarkMdx } from '#utils/remark.mjs';
14
+
10
15
  import { STABILITY_INDEX_URL } from './constants.mjs';
11
- import getConfig from '../../utils/configuration/index.mjs';
12
- import { withExt } from '../../utils/file.mjs';
13
- import { QUERIES } from '../../utils/queries/index.mjs';
14
- import { getRemark as remark, getRemarkMdx } from '../../utils/remark.mjs';
15
16
 
16
17
  /**
17
18
  * Determines whether a file should be parsed as MDX. A `.mdx` extension opts in
@@ -6,7 +6,7 @@ import { extname } from 'node:path';
6
6
  import { parse } from 'acorn';
7
7
  import { globSync } from 'tinyglobby';
8
8
 
9
- import getConfig from '../../utils/configuration/index.mjs';
9
+ import getConfig from '#utils/configuration/index.mjs';
10
10
 
11
11
  /**
12
12
  * Process a chunk of JavaScript files in a worker thread.
@@ -22,6 +22,7 @@ export const publicGenerators = {
22
22
  'llms-txt': '@doc-kit/generator-react/llms-txt',
23
23
  sitemap: '@doc-kit/generator-react/sitemap',
24
24
  html: '@doc-kit/generator-react/html',
25
+ 'section-pages': '@doc-kit/generator-react/section-pages',
25
26
  };
26
27
 
27
28
  // These ones are special since they don't produce standard output,
@@ -4,9 +4,9 @@ import { join } from 'node:path';
4
4
 
5
5
  import { remove } from 'unist-util-remove';
6
6
 
7
- import getConfig from '../../utils/configuration/index.mjs';
8
- import { writeFile } from '../../utils/file.mjs';
9
- import { UNIST } from '../../utils/queries/index.mjs';
7
+ import getConfig from '#utils/configuration/index.mjs';
8
+ import { writeFile } from '#utils/file.mjs';
9
+ import { UNIST } from '#utils/queries/index.mjs';
10
10
 
11
11
  /**
12
12
  * Generates the simplified JSON version of the API docs
@@ -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
 
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ import getConfig from '#utils/configuration/index.mjs';
4
+ import { loadFromURL } from '#utils/loaders.mjs';
5
+
3
6
  import { parseApiDoc } from './utils/parse.mjs';
4
- import getConfig from '../../utils/configuration/index.mjs';
5
- import { loadFromURL } from '../../utils/loaders.mjs';
6
7
 
7
8
  /**
8
9
  * Process a chunk of API doc files in a worker thread.
@@ -120,6 +120,23 @@ export interface StabilityData extends Data {
120
120
  */
121
121
  export type StabilityNode = NodeWithData<Blockquote, StabilityData>;
122
122
 
123
+ /**
124
+ * Describes a chunk page: a section of a module rendered as a page of its own
125
+ * (see the `section-pages` generator). Present on every entry of a chunk page.
126
+ */
127
+ export interface ChunkInfo {
128
+ /** The `api` of the module the chunk was split from */
129
+ api: string;
130
+ /** The `path` of the module the chunk was split from */
131
+ path: string;
132
+ /** The section's anchor on the full module page */
133
+ slug: string;
134
+ /** The chunk's position within the module, in document order */
135
+ index: number;
136
+ /** The original depth of the chunk's heading */
137
+ depth: number;
138
+ }
139
+
123
140
  /**
124
141
  * Complete metadata entry for a single API documentation element.
125
142
  *
@@ -136,6 +153,8 @@ export interface MetadataEntry extends YAMLProperties {
136
153
  synthetic?: boolean;
137
154
  /** Whether this page was authored as MDX (JSX-in-Markdown). */
138
155
  mdx?: boolean;
156
+ /** Set when this entry belongs to a chunk page rather than a full page. */
157
+ chunk?: ChunkInfo;
139
158
  /** Processed heading with metadata */
140
159
  heading: HeadingNode;
141
160
  /** Stability classification information */
@@ -9,6 +9,15 @@ import { remove } from 'unist-util-remove';
9
9
  import { selectAll } from 'unist-util-select';
10
10
  import { SKIP, visit } from 'unist-util-visit';
11
11
 
12
+ import {
13
+ DEPRECATION_HEADING_REGEX,
14
+ IGNORE_STABILITY_STEMS,
15
+ } from '#generators/metadata/constants.mjs';
16
+ import { UNIST } from '#utils/queries/index.mjs';
17
+ import { getRemark as remark } from '#utils/remark.mjs';
18
+ import { relative } from '#utils/url.mjs';
19
+
20
+ import { resolveTypeAnnotations } from './resolveTypes.mjs';
12
21
  import createNodeSlugger from './slugger.mjs';
13
22
  import { transformNodeToHeading } from './transformers.mjs';
14
23
  import {
@@ -18,14 +27,6 @@ import {
18
27
  visitTextWithUnixManualNode,
19
28
  visitYAML,
20
29
  } from './visitors.mjs';
21
- import { UNIST } from '../../../utils/queries/index.mjs';
22
- import { getRemark as remark } from '../../../utils/remark.mjs';
23
- import { relative } from '../../../utils/url.mjs';
24
- import {
25
- DEPRECATION_HEADING_REGEX,
26
- IGNORE_STABILITY_STEMS,
27
- } from '../constants.mjs';
28
- import { resolveTypeAnnotations } from './resolveTypes.mjs';
29
30
 
30
31
  /**
31
32
  * This generator generates a flattened list of metadata entries from a API doc
@@ -87,7 +88,7 @@ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => {
87
88
  // Handles the normalisation URLs that reference to API doc files with .md extension
88
89
  visit(tree, UNIST.isMarkdownUrl, node => visitMarkdownLink(node));
89
90
 
90
- // If the document has no headings but it has content, we add a fake heading to the top
91
+ // If the document has no headings but it has content, we add a fake heading to the top, it will not be rendered
91
92
  if (headingNodes.length === 0 && tree.children.length > 0) {
92
93
  tree.children.unshift(createTree('heading', { depth: 1 }, []));
93
94
  }
@@ -1,9 +1,10 @@
1
1
  import { parseSync } from '@swc/wasm';
2
2
  import { visit } from 'unist-util-visit';
3
3
 
4
+ import logger from '#logger/index.mjs';
5
+ import { walk } from '#utils/swc.mjs';
6
+
4
7
  import { lookupTypeName, resolveTypeReference } from './transformers.mjs';
5
- import logger from '../../../logger/index.mjs';
6
- import { walk } from '../../../utils/swc.mjs';
7
8
 
8
9
  // `null` is a keyword kind in SWC, unlike most parsers which treat it as a literal type
9
10
  const KEYWORDS = new Set([
@@ -2,7 +2,7 @@
2
2
 
3
3
  import GitHubSlugger, { slug as defaultSlugFn } from 'github-slugger';
4
4
 
5
- import { DOC_API_SLUGS_REPLACEMENTS } from '../constants.mjs';
5
+ import { DOC_API_SLUGS_REPLACEMENTS } from '#generators/metadata/constants.mjs';
6
6
 
7
7
  /**
8
8
  * Creates a modified version of the GitHub Slugger
@@ -2,11 +2,12 @@ import {
2
2
  DOC_MAN_BASE_URL,
3
3
  DOC_API_HEADING_TYPES,
4
4
  MODULE_QUALIFIED_NAME,
5
- } from '../constants.mjs';
5
+ } from '#generators/metadata/constants.mjs';
6
+ import BUILTIN_TYPE_MAP from '#generators/metadata/maps/builtin.json' with { type: 'json' };
7
+ import MDN_TYPE_MAP from '#generators/metadata/maps/mdn.json' with { type: 'json' };
8
+ import { transformNodesToString } from '#utils/unist.mjs';
9
+
6
10
  import { slug } from './slugger.mjs';
7
- import { transformNodesToString } from '../../../utils/unist.mjs';
8
- import BUILTIN_TYPE_MAP from '../maps/builtin.json' with { type: 'json' };
9
- import MDN_TYPE_MAP from '../maps/mdn.json' with { type: 'json' };
10
11
 
11
12
  /**
12
13
  * @param {string} text The inner text
@@ -2,11 +2,12 @@
2
2
 
3
3
  import { SKIP } from 'unist-util-visit';
4
4
 
5
+ import { QUERIES } from '#utils/queries/index.mjs';
6
+ import { getRemark as remark } from '#utils/remark.mjs';
7
+ import { transformNodesToString } from '#utils/unist.mjs';
8
+
5
9
  import { transformUnixManualToLink } from './transformers.mjs';
6
10
  import { extractYamlContent, parseYAMLIntoMetadata } from './yaml.mjs';
7
- import { QUERIES } from '../../../utils/queries/index.mjs';
8
- import { getRemark as remark } from '../../../utils/remark.mjs';
9
- import { transformNodesToString } from '../../../utils/unist.mjs';
10
11
 
11
12
  /**
12
13
  * Updates a Markdown link into a HTML link for API docs
@@ -2,7 +2,7 @@
2
2
 
3
3
  import yaml from 'yaml';
4
4
 
5
- import { QUERIES } from '../../../utils/queries/index.mjs';
5
+ import { QUERIES } from '#utils/queries/index.mjs';
6
6
 
7
7
  /**
8
8
  * Extracts raw YAML content from a node
@@ -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
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { styleText } from 'node:util';
4
4
 
5
- import { prettifyLevel } from '../utils/colors.mjs';
6
- import { prettifyTimestamp } from '../utils/time.mjs';
5
+ import { prettifyLevel } from '#logger/utils/colors.mjs';
6
+ import { prettifyTimestamp } from '#logger/utils/time.mjs';
7
7
 
8
8
  /**
9
9
  * Logs a formatted message to stdout for human-friendly CLI output.
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { debug, notice, warning, error } from '@actions/core';
4
4
 
5
- import { LogLevel } from '../constants.mjs';
6
- import { prettifyLevel } from '../utils/colors.mjs';
7
- import { prettifyTimestamp } from '../utils/time.mjs';
5
+ import { LogLevel } from '#logger/constants.mjs';
6
+ import { prettifyLevel } from '#logger/utils/colors.mjs';
7
+ import { prettifyTimestamp } from '#logger/utils/time.mjs';
8
8
 
9
9
  const actions = {
10
10
  [LogLevel.debug]: debug,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { styleText } from 'node:util';
4
4
 
5
- import { levelTags, levelToColorMap } from '../constants.mjs';
5
+ import { levelTags, levelToColorMap } from '#logger/constants.mjs';
6
6
 
7
7
  /**
8
8
  * Returns a styled, uppercase log level tag for CLI output with color mapping
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { coerce } from 'semver';
4
4
 
5
- import { loadFromURL } from '../utils/loaders.mjs';
5
+ import { loadFromURL } from '#utils/loaders.mjs';
6
6
 
7
7
  // A ReGeX for retrieving Node.js version headers from the CHANGELOG.md
8
8
  const NODE_VERSIONS_REGEX = /\* \[Node\.js ([0-9.]+)\]\S+ (.*)\r?\n/g;
@@ -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,5 +1,5 @@
1
- import { loadGenerator } from '../generators/loader.mjs';
2
- import { setConfig } from '../utils/configuration/index.mjs';
1
+ import { loadGenerator } from '#generators/loader.mjs';
2
+ import { setConfig } from '#utils/configuration/index.mjs';
3
3
 
4
4
  /**
5
5
  * Processes a chunk of items using the specified generator's processChunk method.
@@ -1,6 +1,6 @@
1
1
  import Piscina from 'piscina';
2
2
 
3
- import logger from '../logger/index.mjs';
3
+ import logger from '#logger/index.mjs';
4
4
 
5
5
  const poolLogger = logger.child('WorkerPool');
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- import logger from '../logger/index.mjs';
3
+ import logger from '#logger/index.mjs';
4
4
 
5
5
  const parallelLogger = logger.child('parallel');
6
6
 
@@ -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;
@@ -11,12 +11,14 @@ import { coerce } from 'semver';
11
11
  import {
12
12
  loadGenerators,
13
13
  resolveGeneratorSpecifier,
14
- } from '../../generators/loader.mjs';
15
- import logger from '../../logger/index.mjs';
16
- import { parseChangelog, parseIndex } from '../../parsers/markdown.mjs';
17
- import { enforceArray } from '../array.mjs';
18
- import { leftHandAssign } from '../generators.mjs';
19
- import { deepMerge } from '../misc.mjs';
14
+ } from '#generators/loader.mjs';
15
+ import logger from '#logger/index.mjs';
16
+ import { parseChangelog, parseIndex } from '#parsers/markdown.mjs';
17
+ import { enforceArray } from '#utils/array.mjs';
18
+ import { leftHandAssign } from '#utils/generators.mjs';
19
+ import { deepMerge } from '#utils/misc.mjs';
20
+
21
+ import { DEFAULT_CHUNK_SIZE, DEFAULT_MAX_THREADS } from './constants.mjs';
20
22
 
21
23
  const configExplorer = cosmiconfig('doc-kit');
22
24
 
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  import type { SemVer } from 'semver';
2
- import type { ReleaseEntry } from '../../parsers/types';
2
+ import type { ReleaseEntry } from '#parsers/types';
3
3
 
4
4
  export type Configuration = {
5
5
  global: GlobalConfiguration;
@@ -2,6 +2,36 @@
2
2
 
3
3
  import { coerce, major } from 'semver';
4
4
 
5
+ import { transformNodeToString } from './unist.mjs';
6
+
7
+ /**
8
+ * Retrieves the description of a given API doc entry. It first checks whether
9
+ * the entry has a llm_description property. If not, it extracts the first
10
+ * paragraph from the entry's content.
11
+ *
12
+ * @param {import('../generators/metadata/types').MetadataEntry} entry
13
+ * @returns {string}
14
+ */
15
+ export const getEntryDescription = entry => {
16
+ if (entry.llm_description) {
17
+ return entry.llm_description.trim();
18
+ }
19
+
20
+ const descriptionNode = entry.content.children.find(
21
+ child => child.type === 'paragraph'
22
+ );
23
+
24
+ if (!descriptionNode) {
25
+ return '';
26
+ }
27
+
28
+ return (
29
+ transformNodeToString(descriptionNode)
30
+ // Remove newlines and extra spaces
31
+ .replace(/[\r\n]+/g, '')
32
+ );
33
+ };
34
+
5
35
  /**
6
36
  * Groups all the API metadata nodes by module (`api` property) so that we can process each different file
7
37
  * based on the module it belongs to.
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ import rehypeStringify from 'rehype-stringify';
4
+ import remarkParse from 'remark-parse';
5
+ import remarkRehype from 'remark-rehype';
6
+ import { unified } from 'unified';
7
+ import { u as createTree } from 'unist-builder';
8
+ import { SKIP, visit } from 'unist-util-visit';
9
+
10
+ import { lazy } from './misc.mjs';
11
+
12
+ /**
13
+ * Renders a `root` as just its children, since content placed directly on one
14
+ * is otherwise rendered with a line break between each node, which surfaces as
15
+ * stray whitespace.
16
+ *
17
+ * @param {import('mdast-util-to-hast').State} state
18
+ * @param {import('unist').Parent} node
19
+ */
20
+ const inlineRoot = (state, node) => ({
21
+ type: 'root',
22
+ children: state.all(node),
23
+ });
24
+
25
+ /**
26
+ * Retrieves an instance of Remark configured to parse plain markdown, without
27
+ * the extensions that only apply to whole documents.
28
+ */
29
+ const getInlineParser = lazy(() => unified().use(remarkParse));
30
+
31
+ /**
32
+ * Retrieves an instance of Remark configured to render inline nodes as an HTML
33
+ * string. Raw HTML is dropped rather than passed through, since the result is
34
+ * inserted into the page as-is.
35
+ */
36
+ const getInlineRenderer = lazy(() =>
37
+ unified()
38
+ .use(remarkRehype, { handlers: { root: inlineRoot } })
39
+ .use(rehypeStringify)
40
+ );
41
+
42
+ /**
43
+ * Parses a single line of markdown (a change description, a summary, ...) into
44
+ * the inline nodes it is made of.
45
+ *
46
+ * @param {string} markdown - The markdown to parse.
47
+ * @param {boolean} [dropLinks] - Replace links with their text? Anchors cannot
48
+ * nest, so content rendered inside a link must not contain one.
49
+ * @returns {Array<import('mdast').PhrasingContent>} The parsed nodes.
50
+ */
51
+ export const parseInline = (markdown, dropLinks = false) => {
52
+ const tree = getInlineParser().parse(markdown);
53
+
54
+ if (dropLinks) {
55
+ visit(tree, 'link', (node, index, parent) => {
56
+ parent.children.splice(index, 1, ...node.children);
57
+
58
+ return [SKIP, index];
59
+ });
60
+ }
61
+
62
+ const [first] = tree.children;
63
+
64
+ // A single line of prose parses into one paragraph, which is dropped so the
65
+ // nodes can be rendered inline
66
+ return tree.children.length === 1 && first.type === 'paragraph'
67
+ ? first.children
68
+ : tree.children;
69
+ };
70
+
71
+ /**
72
+ * Renders inline nodes as an HTML string, for the places that hand rendered
73
+ * markup to a component through a data channel rather than as an AST.
74
+ *
75
+ * @param {Array<import('mdast').PhrasingContent>} nodes - The nodes to render.
76
+ * @returns {string} The rendered HTML.
77
+ */
78
+ export const renderAsHTML = nodes => {
79
+ const renderer = getInlineRenderer();
80
+
81
+ return renderer.stringify(renderer.runSync(createTree('root', nodes)));
82
+ };
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
- import { transformNodesToString } from '../unist.mjs';
4
- import { isTypedList } from './utils.mjs';
3
+ import { transformNodesToString } from '#utils/unist.mjs';
4
+
5
+ import { isTypedListItem, isTypedList } from './utils.mjs';
5
6
 
6
7
  // This defines the actual REGEX Queries
7
8
  export const QUERIES = {
@@ -71,6 +72,12 @@ export const UNIST = {
71
72
  */
72
73
  isLooselyTypedList: list => Boolean(isTypedList(list)),
73
74
 
75
+ /**
76
+ * @param {import('@types/mdast').ListItem} item
77
+ * @returns {boolean}
78
+ */
79
+ isTypedListItem,
80
+
74
81
  /**
75
82
  * @param {import('@types/mdast').List} list
76
83
  * @returns {boolean}
@@ -2,20 +2,17 @@ import { VALID_JAVASCRIPT_PROPERTY } from './constants.mjs';
2
2
  import { QUERIES } from './index.mjs';
3
3
 
4
4
  /**
5
- * @param {import('@types/mdast').List} list
5
+ * Inspects the first phrasing node of a paragraph and returns how confidently
6
+ * it looks like the start of a typed parameter.
7
+ *
8
+ * @param {import('@types/mdast').PhrasingContent | undefined} firstNode
6
9
  * @returns {0 | 1 | 2} confidence
7
10
  *
8
- * 0: This is not a typed list
9
- * 1: This is a loosely typed list
10
- * 2: This is a strongly typed list
11
+ * 0: Not a typed parameter
12
+ * 1: Loosely typed (inlineCode + valid property name)
13
+ * 2: Strongly typed (typed list starter or direct type annotation)
11
14
  */
12
- export const isTypedList = list => {
13
- if (!list || list.type !== 'list') {
14
- return 0;
15
- }
16
-
17
- const firstNode = list.children?.[0]?.children?.[0]?.children[0];
18
-
15
+ const getTypedConfidence = firstNode => {
19
16
  if (!firstNode) {
20
17
  return 0;
21
18
  }
@@ -43,3 +40,30 @@ export const isTypedList = list => {
43
40
 
44
41
  return 0;
45
42
  };
43
+
44
+ /**
45
+ * Checks whether a single list item looks like a typed parameter — i.e. it
46
+ * starts with a property name (`inlineCode`), a Returns/Extends/Type prefix,
47
+ * or a direct type annotation.
48
+ *
49
+ * @param {import('@types/mdast').ListItem} item
50
+ * @returns {boolean}
51
+ */
52
+ export const isTypedListItem = item =>
53
+ Boolean(getTypedConfidence(item?.children?.[0]?.children?.[0]));
54
+
55
+ /**
56
+ * @param {import('@types/mdast').List} list
57
+ * @returns {0 | 1 | 2} confidence
58
+ *
59
+ * 0: This is not a typed list
60
+ * 1: This is a loosely typed list
61
+ * 2: This is a strongly typed list
62
+ */
63
+ export const isTypedList = list => {
64
+ if (!list || list.type !== 'list') {
65
+ return 0;
66
+ }
67
+
68
+ return getTypedConfidence(list.children?.[0]?.children?.[0]?.children?.[0]);
69
+ };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ import rehypeStringify from 'rehype-stringify';
4
+ import remarkParse from 'remark-parse';
5
+ import remarkRehype from 'remark-rehype';
6
+ import { unified } from 'unified';
7
+
8
+ import syntaxHighlighter from './highlighter.mjs';
9
+ import { lazy } from './misc.mjs';
10
+ import { rehypeOptions } from './remark.mjs';
11
+
12
+ /**
13
+ * Retrieves an instance of Remark configured to output stringified HTML code
14
+ * including parsing Code Boxes with syntax highlighting.
15
+ *
16
+ * This lives apart from `./remark.mjs` because importing it loads Shiki (every
17
+ * grammar plus the regex engine, ~250MB per process). Only the generators that
18
+ * highlight code — `legacy-html` here — should pay for that.
19
+ */
20
+ export const getRemarkRehypeWithShiki = lazy(() =>
21
+ unified()
22
+ .use(remarkParse)
23
+ // legacy-html gets the minimal (unhighlighted) type rendering
24
+ .use(remarkRehype, rehypeOptions)
25
+ // This is a custom ad-hoc within the Shiki Rehype plugin, used to highlight code
26
+ // and transform them into HAST nodes
27
+ .use(syntaxHighlighter)
28
+ .use(rehypeStringify, { allowDangerousHtml: true })
29
+ );
@@ -8,23 +8,51 @@ import remarkRehype from 'remark-rehype';
8
8
  import remarkStringify from 'remark-stringify';
9
9
  import { unified } from 'unified';
10
10
 
11
- import syntaxHighlighter from './highlighter.mjs';
12
11
  import { lazy } from './misc.mjs';
13
12
  import { typeAnnotationToHast } from './type-annotations/hast.mjs';
14
13
  import remarkTypeAnnotations from './type-annotations/remark.mjs';
15
14
 
16
- // MDX node types that may appear in trees parsed by `getRemarkMdx`; the
17
- // rehype pipelines pass them through untouched.
18
- const passThrough = [
19
- 'element',
20
- 'mdxJsxTextElement',
21
- 'mdxJsxFlowElement',
22
- 'mdxJsxAttribute',
23
- 'mdxJsxAttributeValueExpression',
24
- 'mdxFlowExpression',
25
- 'mdxTextExpression',
26
- 'mdxjsEsm',
27
- ];
15
+ // Nothing in this module loads Shiki: the `ast` and `metadata` stages (and
16
+ // every worker that runs them) import it, and none of them highlight code.
17
+ // The highlighting pipeline lives in `./remark-shiki.mjs`.
18
+
19
+ /**
20
+ * Renders an MDX JSX element as just its children, so the surrounding prose
21
+ * still renders in HTML-string output.
22
+ *
23
+ * @param {import('mdast-util-to-hast').State} state
24
+ * @param {import('unist').Parent} node
25
+ */
26
+ const mdxElementToChildren = (state, node) => state.all(node);
27
+
28
+ /**
29
+ * Drops a node from HTML-string output.
30
+ */
31
+ const dropNode = () => undefined;
32
+
33
+ /**
34
+ * The `remark-rehype` options shared by the HTML-string pipelines.
35
+ *
36
+ * Existing HTML nodes pass through untouched (they were created during the
37
+ * rehype process), and dangerous HTML is allowed since the Markdown sources
38
+ * are trusted. The MDX node types cannot be rendered to an HTML string (that
39
+ * is the React generators' job): JSX elements degrade to their children so the
40
+ * surrounding prose still renders, and expressions/ESM are dropped.
41
+ *
42
+ * @type {import('remark-rehype').Options}
43
+ */
44
+ export const rehypeOptions = {
45
+ allowDangerousHtml: true,
46
+ passThrough: ['element'],
47
+ handlers: {
48
+ typeAnnotation: typeAnnotationToHast,
49
+ mdxJsxTextElement: mdxElementToChildren,
50
+ mdxJsxFlowElement: mdxElementToChildren,
51
+ mdxFlowExpression: dropNode,
52
+ mdxTextExpression: dropNode,
53
+ mdxjsEsm: dropNode,
54
+ },
55
+ };
28
56
 
29
57
  /**
30
58
  * Retrieves an instance of Remark configured to parse GFM (GitHub Flavored Markdown)
@@ -57,41 +85,6 @@ export const getRemarkMdx = lazy(() =>
57
85
  export const getRemarkRehype = lazy(() =>
58
86
  unified()
59
87
  .use(remarkParse)
60
- // We make Rehype ignore existing HTML nodes (just the node itself, not its children)
61
- // as these are nodes we manually created during the rehype process
62
- // We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
63
- // and we trust the sources of the Markdown files
64
- .use(remarkRehype, {
65
- allowDangerousHtml: true,
66
- passThrough,
67
- handlers: { typeAnnotation: typeAnnotationToHast },
68
- })
69
- // We allow dangerous HTML to be passed through, since we have HTML within our Markdown
70
- // and we trust the sources of the Markdown files
71
- .use(rehypeStringify, { allowDangerousHtml: true })
72
- );
73
-
74
- /**
75
- * Retrieves an instance of Remark configured to output stringified HTML code
76
- * including parsing Code Boxes with syntax highlighting
77
- */
78
- export const getRemarkRehypeWithShiki = lazy(() =>
79
- unified()
80
- .use(remarkParse)
81
- // We make Rehype ignore existing HTML nodes (just the node itself, not its children)
82
- // as these are nodes we manually created during the rehype process
83
- // We also allow dangerous HTML to be passed through, since we have HTML within our Markdown
84
- // and we trust the sources of the Markdown files
85
- .use(remarkRehype, {
86
- allowDangerousHtml: true,
87
- passThrough,
88
- // legacy-html gets the minimal (unhighlighted) type rendering
89
- handlers: { typeAnnotation: typeAnnotationToHast },
90
- })
91
- // This is a custom ad-hoc within the Shiki Rehype plugin, used to highlight code
92
- // and transform them into HAST nodes
93
- .use(syntaxHighlighter)
94
- // We allow dangerous HTML to be passed through, since we have HTML within our Markdown
95
- // and we trust the sources of the Markdown files
88
+ .use(remarkRehype, rehypeOptions)
96
89
  .use(rehypeStringify, { allowDangerousHtml: true })
97
90
  );
@@ -1,12 +1,13 @@
1
+ import { leftHandAssign } from '#utils/generators.mjs';
2
+ import { QUERIES, UNIST } from '#utils/queries/index.mjs';
3
+ import { transformNodesToString } from '#utils/unist.mjs';
4
+
1
5
  import {
2
6
  DEFAULT_EXPRESSION,
3
7
  LEADING_HYPHEN,
4
8
  NAME_EXPRESSION,
5
9
  } from './constants.mjs';
6
10
  import parseSignature from './parseSignature.mjs';
7
- import { leftHandAssign } from '../generators.mjs';
8
- import { QUERIES, UNIST } from '../queries/index.mjs';
9
- import { transformNodesToString } from '../unist.mjs';
10
11
 
11
12
  /**
12
13
  * Extracts and removes a specific pattern from a text string while storing the result in a key of the `current` object.
@@ -1,9 +1,5 @@
1
1
  'use strict';
2
2
 
3
- import { highlighter } from '../highlighter.mjs';
4
-
5
- const [lightTheme, darkTheme] = highlighter.shiki.getLoadedThemes();
6
-
7
3
  /**
8
4
  * Slices a type's text by its resolved link ranges into hast children —
9
5
  * plain text segments interleaved with `<a class="type-link">` anchors.
@@ -59,57 +55,3 @@ export const typeAnnotationToHast = (state, node) => {
59
55
 
60
56
  return state.applyData(node, result);
61
57
  };
62
-
63
- /**
64
- * Syntax-highlighted mdast→hast handler for `typeAnnotation` nodes, used by
65
- * the web (JSX) pipeline. The whole type is highlighted as one inline
66
- * fragment, and each resolved identifier's exact character range is wrapped
67
- * in an `<a>` via Shiki decorations. Values that are not TypeScript (display
68
- * names such as `HTTP/2 Headers Object`) are highlighted as plain text, so
69
- * their prose is not coloured as operators and numeric literals.
70
- *
71
- * Falls back to the minimal handler when the type failed to parse or nothing
72
- * resolved (no point paying for highlighting then).
73
- *
74
- * @param {import('mdast-util-to-hast').State} state
75
- * @param {import('mdast').Node} node
76
- * @returns {import('hast').Element}
77
- */
78
- export const typeAnnotationToHighlightedHast = (state, node) => {
79
- const links = node.data?.links ?? [];
80
-
81
- if (node.data?.parseError || links.length === 0) {
82
- return typeAnnotationToHast(state, node);
83
- }
84
-
85
- const root = highlighter.shiki.codeToHast(node.value, {
86
- lang: node.data?.typescript ? 'typescript' : 'text',
87
- themes: { light: lightTheme, dark: darkTheme },
88
- decorations: links.map(({ start, end, href }) => ({
89
- start,
90
- end,
91
- tagName: 'a',
92
- properties: { href, class: 'type-link' },
93
- alwaysWrap: true,
94
- })),
95
- });
96
-
97
- // codeToHast wraps the highlighted line in <pre><code>; re-shape that into
98
- // a single inline <code> element ("only the outermost type opens/closes
99
- // the code fragment")
100
- const [preElement] = root.children;
101
- const [codeElement] = preElement.children;
102
-
103
- const result = {
104
- type: 'element',
105
- tagName: 'code',
106
- properties: {
107
- class: `${preElement.properties.class} type`,
108
- },
109
- children: codeElement.children,
110
- };
111
-
112
- state.patch(node, result);
113
-
114
- return state.applyData(node, result);
115
- };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ import { highlighter } from '#utils/highlighter.mjs';
4
+
5
+ import { typeAnnotationToHast } from './hast.mjs';
6
+
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
+ /**
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.
19
+ *
20
+ * Falls back to the minimal handler when the type failed to parse or nothing
21
+ * resolved (no point paying for highlighting then).
22
+ *
23
+ * @param {import('mdast-util-to-hast').State} state
24
+ * @param {import('mdast').Node} node
25
+ * @returns {import('hast').Element}
26
+ */
27
+ export const typeAnnotationToHighlightedHast = (state, node) => {
28
+ const links = node.data?.links ?? [];
29
+
30
+ if (node.data?.parseError || links.length === 0) {
31
+ return typeAnnotationToHast(state, node);
32
+ }
33
+
34
+ const root = highlighter.shiki.codeToHast(node.value, {
35
+ lang: node.data?.typescript ? 'typescript' : 'text',
36
+ themes: { light: lightTheme, dark: darkTheme },
37
+ decorations: links.map(({ start, end, href }) => ({
38
+ start,
39
+ end,
40
+ tagName: 'a',
41
+ properties: { href, class: 'type-link' },
42
+ alwaysWrap: true,
43
+ })),
44
+ });
45
+
46
+ // codeToHast wraps the highlighted line in <pre><code>; re-shape that into
47
+ // a single inline <code> element ("only the outermost type opens/closes
48
+ // the code fragment")
49
+ const [preElement] = root.children;
50
+ const [codeElement] = preElement.children;
51
+
52
+ const result = {
53
+ type: 'element',
54
+ tagName: 'code',
55
+ properties: {
56
+ class: `${preElement.properties.class} type`,
57
+ },
58
+ children: codeElement.children,
59
+ };
60
+
61
+ state.patch(node, result);
62
+
63
+ return state.applyData(node, result);
64
+ };