Skip to content

Commit 702cb43

Browse files
authored
feat: create metadata entries generator (#272)
* fix: resolve generic interface extension error * refactor: create metadata generator * fix: remove thread pool loop * fix: lint * refactor: make metadata generator internal * refactor: inline return * refactor: move slugger to metadata generator dir * refactor: nits * refactor: nits * refactor: fix import sorting
1 parent 8e1ee40 commit 702cb43

File tree

21 files changed

+233
-174
lines changed

21 files changed

+233
-174
lines changed

bin/commands/generate.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,18 @@ export default {
135135
process.exit(1);
136136
}
137137

138-
const { runGenerators } = createGenerator(docs);
139138
const { getAllMajors } = createNodeReleases(opts.changelog);
140139

140+
const releases = await getAllMajors();
141+
142+
const { runGenerators } = createGenerator(docs);
143+
141144
await runGenerators({
142145
generators: opts.target,
143146
input: opts.input,
144147
output: opts.output && resolve(opts.output),
145148
version: coerce(opts.version),
146-
releases: await getAllMajors(),
149+
releases,
147150
gitRef: opts.gitRef,
148151
threads: parseInt(opts.threads, 10),
149152
});

bin/utils.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const parser = lazy(createMarkdownParser);
2424
* @param {string[]} input - Glob patterns for input files.
2525
* @param {string[]} [ignore] - Glob patterns to ignore.
2626
* @param {import('../src/linter/types').Linter} [linter] - Linter instance
27-
* @returns {Promise<ApiDocMetadataEntry[]>} - Parsed documentation objects.
27+
* @returns {Promise<Array<ParserOutput<import('mdast').Root>>>}
2828
*/
2929
export async function loadAndParse(input, ignore, linter) {
3030
const files = await loader().loadFiles(input, ignore);

src/generators.mjs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ import { allGenerators } from './generators/index.mjs';
44
import WorkerPool from './threading/index.mjs';
55

66
/**
7-
* @typedef {{ ast: GeneratorMetadata<ApiDocMetadataEntry, ApiDocMetadataEntry>}} AstGenerator The AST "generator" is a facade for the AST tree and it isn't really a generator
8-
* @typedef {AvailableGenerators & AstGenerator} AllGenerators A complete set of the available generators, including the AST one
9-
* @param markdownInput
10-
* @param jsInput
11-
*
127
* This method creates a system that allows you to register generators
138
* and then execute them in a specific order, keeping track of the
149
* generation process, and handling errors that may occur from the
@@ -21,18 +16,20 @@ import WorkerPool from './threading/index.mjs';
2116
* Generators can also write to files. These would usually be considered
2217
* the final generators in the chain.
2318
*
24-
* @param {ApiDocMetadataEntry} markdownInput The parsed API doc metadata entries
25-
* @param {Array<import('acorn').Program>} parsedJsFiles
19+
* @typedef {{ ast: GeneratorMetadata<ParserOutput, ParserOutput>}} AstGenerator The AST "generator" is a facade for the AST tree and it isn't really a generator
20+
* @typedef {AvailableGenerators & AstGenerator} AllGenerators A complete set of the available generators, including the AST one
21+
*
22+
* @param {ParserOutput} input The API doc AST tree
2623
*/
27-
const createGenerator = markdownInput => {
24+
const createGenerator = input => {
2825
/**
2926
* We store all the registered generators to be processed
3027
* within a Record, so we can access their results at any time whenever needed
3128
* (we store the Promises of the generator outputs)
3229
*
3330
* @type {{ [K in keyof AllGenerators]: ReturnType<AllGenerators[K]['generate']> }}
3431
*/
35-
const cachedGenerators = { ast: Promise.resolve(markdownInput) };
32+
const cachedGenerators = { ast: Promise.resolve(input) };
3633

3734
const threadPool = new WorkerPool();
3835

src/generators/addon-verify/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default {
3030
description:
3131
'Generates a file list from code blocks extracted from `doc/api/addons.md` to facilitate C++ compilation and JavaScript runtime validations',
3232

33-
dependsOn: 'ast',
33+
dependsOn: 'metadata',
3434

3535
/**
3636
* Generates a file list from code blocks.

src/generators/ast-js/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default {
2020

2121
description: 'Parses Javascript source files passed into the input.',
2222

23-
dependsOn: 'ast',
23+
dependsOn: 'metadata',
2424

2525
/**
2626
* @param {Input} _

src/generators/index.mjs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import legacyJson from './legacy-json/index.mjs';
1111
import legacyJsonAll from './legacy-json-all/index.mjs';
1212
import llmsTxt from './llms-txt/index.mjs';
1313
import manPage from './man-page/index.mjs';
14+
import metadata from './metadata/index.mjs';
1415
import oramaDb from './orama-db/index.mjs';
1516

1617
export const publicGenerators = {
@@ -27,9 +28,14 @@ export const publicGenerators = {
2728
'jsx-ast': jsxAst,
2829
};
2930

31+
// These are a bit special: we don't want them to run unless needed,
32+
// and we also don't want them publicly accessible via the CLI.
33+
const internalGenerators = {
34+
metadata,
35+
'ast-js': astJs,
36+
};
37+
3038
export const allGenerators = {
3139
...publicGenerators,
32-
// This one is a little special since we don't want it to run unless we need
33-
// it and we also don't want it to be publicly accessible through the CLI.
34-
'ast-js': astJs,
40+
...internalGenerators,
3541
};

src/generators/json-simple/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default {
2626
description:
2727
'Generates the simple JSON version of the API docs, and returns it as a string',
2828

29-
dependsOn: 'ast',
29+
dependsOn: 'metadata',
3030

3131
/**
3232
* Generates the simplified JSON version of the API docs

src/generators/legacy-html/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default {
4040
description:
4141
'Generates the legacy version of the API docs in HTML, with the assets and styles included as files',
4242

43-
dependsOn: 'ast',
43+
dependsOn: 'metadata',
4444

4545
/**
4646
* Generates the legacy version of the API docs in HTML

src/generators/legacy-json/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default {
2626

2727
description: 'Generates the legacy version of the JSON API docs.',
2828

29-
dependsOn: 'ast',
29+
dependsOn: 'metadata',
3030

3131
/**
3232
* Generates a legacy JSON file.

src/generators/llms-txt/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default {
1919
description:
2020
'Generates a llms.txt file to provide information to LLMs at inference time',
2121

22-
dependsOn: 'ast',
22+
dependsOn: 'metadata',
2323

2424
/**
2525
* Generates a llms.txt file

src/generators/man-page/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default {
2424

2525
description: 'Generates the Node.js man-page.',
2626

27-
dependsOn: 'ast',
27+
dependsOn: 'metadata',
2828

2929
/**
3030
* Generates the Node.js man-page

src/generators/metadata/index.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
import { parseApiDoc } from './utils/parse.mjs';
4+
5+
/**
6+
* This generator generates a flattened list of metadata entries from a API doc
7+
*
8+
* @typedef {Array<ParserOutput<import('mdast').Root>>} Input
9+
*
10+
* @type {GeneratorMetadata<Input, Array<ApiDocMetadataEntry>>}
11+
*/
12+
export default {
13+
name: 'metadata',
14+
15+
version: '1.0.0',
16+
17+
description: 'generates a flattened list of API doc metadata entries',
18+
19+
dependsOn: 'ast',
20+
21+
/**
22+
* @param {Input} inputs
23+
* @returns {Promise<Array<ApiDocMetadataEntry>>}
24+
*/
25+
async generate(inputs) {
26+
return inputs.flatMap(input => parseApiDoc(input));
27+
},
28+
};

src/utils/tests/slugger.test.mjs renamed to src/generators/metadata/tests/slugger.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { strictEqual } from 'node:assert';
22
import { describe, it } from 'node:test';
33

4-
import { createNodeSlugger } from '../slugger/index.mjs';
4+
import { createNodeSlugger } from '../utils/slugger.mjs';
55

66
describe('createNodeSlugger', () => {
77
it('should create a new instance of the GitHub Slugger', () => {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
'use strict';
2+
3+
import { u as createTree } from 'unist-builder';
4+
import { findAfter } from 'unist-util-find-after';
5+
import { remove } from 'unist-util-remove';
6+
import { selectAll } from 'unist-util-select';
7+
import { SKIP, visit } from 'unist-util-visit';
8+
9+
import { createNodeSlugger } from './slugger.mjs';
10+
import createMetadata from '../../../metadata.mjs';
11+
import createQueries from '../../../utils/queries/index.mjs';
12+
import { getRemark } from '../../../utils/remark.mjs';
13+
14+
/**
15+
* This generator generates a flattened list of metadata entries from a API doc
16+
*
17+
* @param {ParserOutput<import('mdast').Root>} input
18+
* @returns {Promise<Array<ApiDocMetadataEntry>>}
19+
*/
20+
export const parseApiDoc = ({ file, tree }) => {
21+
/**
22+
* This holds references to all the Metadata entries for a given file
23+
* this is used so we can traverse the AST tree and keep mutating things
24+
* and then stringify the whole api doc file at once without creating sub traversals
25+
*
26+
* Then once we have the whole file parsed, we can split the resulting string into sections
27+
* and seal the Metadata Entries (`.create()`) and return the result to the caller of parae.
28+
*
29+
* @type {Array<ApiDocMetadataEntry>}
30+
*/
31+
const metadataCollection = [];
32+
33+
const {
34+
setHeadingMetadata,
35+
addYAMLMetadata,
36+
updateMarkdownLink,
37+
updateTypeReference,
38+
updateLinkReference,
39+
addStabilityMetadata,
40+
} = createQueries();
41+
42+
// Creates an instance of the Remark processor with GFM support
43+
// which is used for stringifying the AST tree back to Markdown
44+
const remarkProcessor = getRemark();
45+
46+
// Creates a new Slugger instance for the current API doc file
47+
const nodeSlugger = createNodeSlugger();
48+
49+
// Get all Markdown Footnote definitions from the tree
50+
const markdownDefinitions = selectAll('definition', tree);
51+
52+
// Get all Markdown Heading entries from the tree
53+
const headingNodes = selectAll('heading', tree);
54+
55+
// Handles Markdown link references and updates them to be plain links
56+
visit(tree, createQueries.UNIST.isLinkReference, node =>
57+
updateLinkReference(node, markdownDefinitions)
58+
);
59+
60+
// Removes all the original definitions from the tree as they are not needed
61+
// anymore, since all link references got updated to be plain links
62+
remove(tree, markdownDefinitions);
63+
64+
// Handles the normalisation URLs that reference to API doc files with .md extension
65+
// to replace the .md into .html, since the API doc files get eventually compiled as HTML
66+
visit(tree, createQueries.UNIST.isMarkdownUrl, node =>
67+
updateMarkdownLink(node)
68+
);
69+
70+
// If the document has no headings but it has content, we add a fake heading to the top
71+
// so that our parsing logic can work correctly, and generate content for the whole file
72+
if (headingNodes.length === 0 && tree.children.length > 0) {
73+
tree.children.unshift(createTree('heading', { depth: 1 }, []));
74+
}
75+
76+
// Handles iterating the tree and creating subtrees for each API doc entry
77+
// where an API doc entry is defined by a Heading Node
78+
// (so all elements after a Heading until the next Heading)
79+
// and then it creates and updates a Metadata entry for each API doc entry
80+
// and then generates the final content for each API doc entry and pushes it to the collection
81+
visit(tree, createQueries.UNIST.isHeading, (headingNode, index) => {
82+
// Creates a new Metadata entry for the current API doc file
83+
const apiEntryMetadata = createMetadata(nodeSlugger);
84+
85+
// Adds the Metadata of the current Heading Node to the Metadata entry
86+
setHeadingMetadata(headingNode, apiEntryMetadata);
87+
88+
// We retrieve the immediate next Heading if it exists
89+
// This is used for ensuring that we don't include items that would
90+
// belong only to the next heading to the current Heading metadata
91+
// Note that if there is no next heading, we use the current node as the next one
92+
const nextHeadingNode =
93+
findAfter(tree, index, createQueries.UNIST.isHeading) ?? headingNode;
94+
95+
// This is the cutover index of the subtree that we should get
96+
// of all the Nodes within the AST tree that belong to this section
97+
// If `next` is equals the current heading, it means there's no next heading
98+
// and we are reaching the end of the document, hence the cutover should be the end of
99+
// the document itself.
100+
const stop =
101+
headingNode === nextHeadingNode
102+
? tree.children.length
103+
: tree.children.indexOf(nextHeadingNode);
104+
105+
// Retrieves all the nodes that should belong to the current API docs section
106+
// `index + 1` is used to skip the current Heading Node
107+
const subTree = createTree('root', tree.children.slice(index, stop));
108+
109+
// Visits all Stability Index nodes from the current subtree if there's any
110+
// and then apply the Stability Index metadata to the current metadata entry
111+
visit(subTree, createQueries.UNIST.isStabilityNode, node =>
112+
addStabilityMetadata(node, apiEntryMetadata)
113+
);
114+
115+
// Visits all HTML nodes from the current subtree and if there's any that matches
116+
// our YAML metadata structure, it transforms into YAML metadata
117+
// and then apply the YAML Metadata to the current Metadata entry
118+
visit(subTree, createQueries.UNIST.isYamlNode, node => {
119+
// TODO: Is there always only one YAML node?
120+
apiEntryMetadata.setYamlPosition(node.position);
121+
addYAMLMetadata(node, apiEntryMetadata);
122+
});
123+
124+
// Visits all Text nodes from the current subtree and if there's any that matches
125+
// any API doc type reference and then updates the type reference to be a Markdown link
126+
visit(subTree, createQueries.UNIST.isTextWithType, (node, _, parent) =>
127+
updateTypeReference(node, parent)
128+
);
129+
130+
// Removes already parsed items from the subtree so that they aren't included in the final content
131+
remove(subTree, [createQueries.UNIST.isYamlNode]);
132+
133+
// Applies the AST transformations to the subtree based on the API doc entry Metadata
134+
// Note that running the transformation on the subtree isn't costly as it is a reduced tree
135+
// and the GFM transformations aren't that heavy
136+
const parsedSubTree = remarkProcessor.runSync(subTree);
137+
138+
// We seal and create the API doc entry Metadata and push them to the collection
139+
const parsedApiEntryMetadata = apiEntryMetadata.create(file, parsedSubTree);
140+
141+
// We push the parsed API doc entry Metadata to the collection
142+
metadataCollection.push(parsedApiEntryMetadata);
143+
144+
return SKIP;
145+
});
146+
147+
return metadataCollection;
148+
};

src/utils/slugger/index.mjs renamed to src/generators/metadata/utils/slugger.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import GitHubSlugger from 'github-slugger';
44

5-
import { DOC_API_SLUGS_REPLACEMENTS } from './constants.mjs';
5+
import { DOC_API_SLUGS_REPLACEMENTS } from '../constants.mjs';
66

77
/**
88
* Creates a modified version of the GitHub Slugger

src/generators/orama-db/index.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export default {
6262
name: 'orama-db',
6363
version: '1.0.0',
6464
description: 'Generates the Orama database for the API docs.',
65-
dependsOn: 'ast',
65+
66+
dependsOn: 'metadata',
6667

6768
/**
6869
* Generates the Orama database.

src/loaders/markdown.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const createLoader = () => {
1515
/**
1616
* Loads API Doc files and transforms it into VFiles
1717
*
18-
* @param {string} searchPath A glob/path for API docs to be loaded
19-
* @param {string | undefined} ignorePath A glob/path of files to ignore
18+
* @param {Array<string>} searchPath A glob/path for API docs to be loaded
19+
* @param {Array<string> | undefined} [ignorePath] A glob/path of files to ignore
2020
* The input string can be a simple path (relative or absolute)
2121
* The input string can also be any allowed glob string
2222
*

src/metadata.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const createMetadata = slugger => {
9191
* The Navigation entries has a dedicated separate method for retrieval
9292
* as it can be manipulated outside of the scope of the generation of the content
9393
*
94-
* @param {import('vfile').VFile} apiDoc The API doc file being parsed
94+
* @param {{stem?: string, basename?: string}} apiDoc The API doc file being parsed
9595
* @param {ApiDocMetadataEntry['content']} section An AST tree containing the Nodes of the API doc entry section
9696
* @returns {ApiDocMetadataEntry} The locally created Metadata entries
9797
*/

0 commit comments

Comments
 (0)