Skip to content

Commit 11a7953

Browse files
flakey5ovflowd
andauthored
feat apilinks.json generator (#153)
* feat apilinks.json generator Closes #152 Signed-off-by: flakey5 <[email protected]> Co-authored-by: Claudio W <[email protected]> * Update getBaseGitHubUrl.mjs --------- Co-authored-by: Claudio W <[email protected]>
1 parent 3452cf2 commit 11a7953

22 files changed

+832
-20
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ Options:
3939
-o, --output <path> Specify the relative or absolute output directory
4040
-v, --version <semver> Specify the target version of Node.js, semver compliant (default: "v22.6.0")
4141
-c, --changelog <url> Specify the path (file: or https://) to the CHANGELOG.md file (default: "https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md")
42-
-t, --target [mode...] Set the processing target modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page", "legacy-json", "legacy-json-all", "addon-verify")
42+
-t, --target [mode...] Set the processing target modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page", "legacy-json", "legacy-json-all", "addon-verify", "api-links")
4343
-h, --help display help for command
4444
```

bin/cli.mjs

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { coerce } from 'semver';
99
import { DOC_NODE_CHANGELOG_URL, DOC_NODE_VERSION } from '../src/constants.mjs';
1010
import createGenerator from '../src/generators.mjs';
1111
import generators from '../src/generators/index.mjs';
12-
import createLoader from '../src/loader.mjs';
13-
import createParser from '../src/parser.mjs';
12+
import createMarkdownLoader from '../src/loaders/markdown.mjs';
13+
import createMarkdownParser from '../src/parsers/markdown.mjs';
1414
import createNodeReleases from '../src/releases.mjs';
1515

1616
const availableGenerators = Object.keys(generators);
@@ -68,8 +68,8 @@ program
6868
*/
6969
const { input, output, target = [], version, changelog } = program.opts();
7070

71-
const { loadFiles } = createLoader();
72-
const { parseApiDocs } = createParser();
71+
const { loadFiles } = createMarkdownLoader();
72+
const { parseApiDocs } = createMarkdownParser();
7373

7474
const apiDocFiles = loadFiles(input);
7575

@@ -83,6 +83,8 @@ const { getAllMajors } = createNodeReleases(changelog);
8383
await runGenerators({
8484
// A list of target modes for the API docs parser
8585
generators: target,
86+
// Resolved `input` to be used
87+
input: input,
8688
// Resolved `output` path to be used
8789
output: resolve(output),
8890
// Resolved SemVer of current Node.js version

package-lock.json

+26-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"prettier": "3.4.2"
3030
},
3131
"dependencies": {
32+
"acorn": "^8.14.0",
3233
"commander": "^13.1.0",
34+
"estree-util-visit": "^2.0.0",
3335
"dedent": "^1.5.3",
3436
"github-slugger": "^2.0.0",
3537
"glob": "^11.0.1",

src/generators.mjs

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
'use strict';
22

3-
import availableGenerators from './generators/index.mjs';
3+
import publicGenerators from './generators/index.mjs';
4+
import astJs from './generators/ast-js/index.mjs';
5+
6+
const availableGenerators = {
7+
...publicGenerators,
8+
// This one is a little special since we don't want it to run unless we need
9+
// it and we also don't want it to be publicly accessible through the CLI.
10+
'ast-js': astJs,
11+
};
412

513
/**
614
* @typedef {{ ast: import('./generators/types.d.ts').GeneratorMetadata<ApiDocMetadataEntry, ApiDocMetadataEntry>}} AstGenerator The AST "generator" is a facade for the AST tree and it isn't really a generator
715
* @typedef {import('./generators/types.d.ts').AvailableGenerators & AstGenerator} AllGenerators A complete set of the available generators, including the AST one
16+
* @param markdownInput
17+
* @param jsInput
818
*
919
* This method creates a system that allows you to register generators
1020
* and then execute them in a specific order, keeping track of the
@@ -18,17 +28,20 @@ import availableGenerators from './generators/index.mjs';
1828
* Generators can also write to files. These would usually be considered
1929
* the final generators in the chain.
2030
*
21-
* @param {ApiDocMetadataEntry} input The parsed API doc metadata entries
31+
* @param {ApiDocMetadataEntry} markdownInput The parsed API doc metadata entries
32+
* @param {Array<import('acorn').Program>} parsedJsFiles
2233
*/
23-
const createGenerator = input => {
34+
const createGenerator = markdownInput => {
2435
/**
2536
* We store all the registered generators to be processed
2637
* within a Record, so we can access their results at any time whenever needed
2738
* (we store the Promises of the generator outputs)
2839
*
2940
* @type {{ [K in keyof AllGenerators]: ReturnType<AllGenerators[K]['generate']> }}
3041
*/
31-
const cachedGenerators = { ast: Promise.resolve(input) };
42+
const cachedGenerators = {
43+
ast: Promise.resolve(markdownInput),
44+
};
3245

3346
/**
3447
* Runs the Generator engine with the provided top-level input and the given generator options
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
'use strict';
2+
3+
// Checks if a string is a valid name for a constructor in JavaScript
4+
export const CONSTRUCTOR_EXPRESSION = /^[A-Z]/;

src/generators/api-links/index.mjs

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use strict';
2+
3+
import { basename, dirname, join } from 'node:path';
4+
import { writeFile } from 'node:fs/promises';
5+
import {
6+
getBaseGitHubUrl,
7+
getCurrentGitHash,
8+
} from './utils/getBaseGitHubUrl.mjs';
9+
import { extractExports } from './utils/extractExports.mjs';
10+
import { findDefinitions } from './utils/findDefinitions.mjs';
11+
import { checkIndirectReferences } from './utils/checkIndirectReferences.mjs';
12+
13+
/**
14+
* This generator is responsible for mapping publicly accessible functions in
15+
* Node.js to their source locations in the Node.js repository.
16+
*
17+
* This is a top-level generator. It takes in the raw AST tree of the JavaScript
18+
* source files. It outputs a `apilinks.json` file into the specified output
19+
* directory.
20+
*
21+
* @typedef {Array<JsProgram>} Input
22+
*
23+
* @type {import('../types.d.ts').GeneratorMetadata<Input, Record<string, string>>}
24+
*/
25+
export default {
26+
name: 'api-links',
27+
28+
version: '1.0.0',
29+
30+
description:
31+
'Creates a mapping of publicly accessible functions to their source locations in the Node.js repository.',
32+
33+
// Unlike the rest of the generators, this utilizes Javascript sources being
34+
// passed into the input field rather than Markdown.
35+
dependsOn: 'ast-js',
36+
37+
/**
38+
* Generates the `apilinks.json` file.
39+
*
40+
* @param {Input} input
41+
* @param {Partial<GeneratorOptions>} options
42+
*/
43+
async generate(input, { output }) {
44+
/**
45+
* @type Record<string, string>
46+
*/
47+
const definitions = {};
48+
49+
/**
50+
* @type {string}
51+
*/
52+
let baseGithubLink;
53+
54+
if (input.length > 0) {
55+
const repositoryDirectory = dirname(input[0].path);
56+
57+
const repository = getBaseGitHubUrl(repositoryDirectory);
58+
59+
const tag = getCurrentGitHash(repositoryDirectory);
60+
61+
baseGithubLink = `${repository}/blob/${tag}`;
62+
}
63+
64+
input.forEach(program => {
65+
/**
66+
* Mapping of definitions to their line number
67+
* @type {Record<string, number>}
68+
* @example { 'someclass.foo': 10 }
69+
*/
70+
const nameToLineNumberMap = {};
71+
72+
// `http.js` -> `http`
73+
const programBasename = basename(program.path, '.js');
74+
75+
const exports = extractExports(
76+
program,
77+
programBasename,
78+
nameToLineNumberMap
79+
);
80+
81+
findDefinitions(program, programBasename, nameToLineNumberMap, exports);
82+
83+
checkIndirectReferences(program, exports, nameToLineNumberMap);
84+
85+
const githubLink =
86+
`${baseGithubLink}/lib/${programBasename}.js`.replaceAll('\\', '/');
87+
88+
// Add the exports we found in this program to our output
89+
Object.keys(nameToLineNumberMap).forEach(key => {
90+
const lineNumber = nameToLineNumberMap[key];
91+
92+
definitions[key] = `${githubLink}#L${lineNumber}`;
93+
});
94+
});
95+
96+
if (output) {
97+
await writeFile(
98+
join(output, 'apilinks.json'),
99+
JSON.stringify(definitions)
100+
);
101+
}
102+
103+
return definitions;
104+
},
105+
};

src/generators/api-links/types.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface ProgramExports {
2+
ctors: Array<string>;
3+
identifiers: Array<string>;
4+
indirects: Record<string, string>;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { visit } from 'estree-util-visit';
2+
3+
/**
4+
* @param {import('acorn').Program} program
5+
* @param {import('../types.d.ts').ProgramExports} exports
6+
* @param {Record<string, number>} nameToLineNumberMap
7+
*/
8+
export function checkIndirectReferences(program, exports, nameToLineNumberMap) {
9+
if (Object.keys(exports.indirects).length === 0) {
10+
return;
11+
}
12+
13+
visit(program, node => {
14+
if (!node.loc || node.type !== 'FunctionDeclaration') {
15+
return;
16+
}
17+
18+
const name = node.id.name;
19+
20+
if (name in exports.indirects) {
21+
nameToLineNumberMap[exports.indirects[name]] = node.loc.start.line;
22+
}
23+
});
24+
}

0 commit comments

Comments
 (0)