Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat apilinks.json generator #153

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ Options:
-o, --output <path> Specify the relative or absolute output directory
-v, --version <semver> Specify the target version of Node.js, semver compliant (default: "v22.6.0")
-c, --changelog <url> Specify the path (file: or https://) to the CHANGELOG.md file (default: "https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md")
-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")
-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")
-h, --help display help for command
```
12 changes: 8 additions & 4 deletions bin/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,18 @@ program
*/
const { input, output, target = [], version, changelog } = program.opts();

const { loadFiles } = createLoader();
const { parseApiDocs } = createParser();
const { loadMarkdownFiles, loadJsFiles } = createLoader();
const { parseApiDocs, parseJsSources } = createParser();

const apiDocFiles = loadFiles(input);
const apiDocFiles = loadMarkdownFiles(input);

const parsedApiDocs = await parseApiDocs(apiDocFiles);

const { runGenerators } = createGenerator(parsedApiDocs);
const sourceFiles = loadJsFiles(input);

const parsedJsFiles = await parseJsSources(sourceFiles);
flakey5 marked this conversation as resolved.
Show resolved Hide resolved

const { runGenerators } = createGenerator(parsedApiDocs, parsedJsFiles);

// Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file
const { getAllMajors } = createNodeReleases(changelog);
Expand Down
28 changes: 26 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"prettier": "3.4.2"
},
"dependencies": {
"acorn": "^8.14.0",
"commander": "^13.0.0",
"estree-util-visit": "^2.0.0",
"github-slugger": "^2.0.0",
"glob": "^11.0.0",
"hast-util-to-string": "^3.0.1",
Expand Down
12 changes: 9 additions & 3 deletions src/generators.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import availableGenerators from './generators/index.mjs';
/**
* @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
* @typedef {import('./generators/types.d.ts').AvailableGenerators & AstGenerator} AllGenerators A complete set of the available generators, including the AST one
* @param markdownInput
* @param jsInput
*
* This method creates a system that allows you to register generators
* and then execute them in a specific order, keeping track of the
Expand All @@ -18,17 +20,21 @@ import availableGenerators from './generators/index.mjs';
* Generators can also write to files. These would usually be considered
* the final generators in the chain.
*
* @param {ApiDocMetadataEntry} input The parsed API doc metadata entries
* @param {ApiDocMetadataEntry} markdownInput The parsed API doc metadata entries
* @param {Array<import('acorn').Program>} parsedJsFiles
*/
const createGenerator = input => {
const createGenerator = (markdownInput, jsInput) => {
/**
* We store all the registered generators to be processed
* within a Record, so we can access their results at any time whenever needed
* (we store the Promises of the generator outputs)
*
* @type {{ [K in keyof AllGenerators]: ReturnType<AllGenerators[K]['generate']> }}
*/
const cachedGenerators = { ast: Promise.resolve(input) };
const cachedGenerators = {
ast: Promise.resolve(markdownInput),
'ast-js': Promise.resolve(jsInput),
};

/**
* Runs the Generator engine with the provided top-level input and the given generator options
Expand Down
4 changes: 4 additions & 0 deletions src/generators/api-links/constants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';

// Checks if a string is a valid name for a constructor in JavaScript
export const CONSTRUCTOR_EXPRESSION = /^[A-Z]/;
103 changes: 103 additions & 0 deletions src/generators/api-links/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use strict';

import { basename, dirname, join } from 'node:path';
import { writeFile } from 'node:fs/promises';
import {
getBaseGitHubUrl,
getCurrentGitHash,
} from './utils/getBaseGitHubUrl.mjs';
import { extractExports } from './utils/extractExports.mjs';
import { findDefinitions } from './utils/findDefinitions.mjs';
import { checkIndirectReferences } from './utils/checkIndirectReferences.mjs';

/**
* This generator is responsible for mapping publicly accessible functions in
* Node.js to their source locations in the Node.js repository.
*
* This is a top-level generator. It takes in the raw AST tree of the JavaScript
* source files. It outputs a `apilinks.json` file into the specified output
* directory.
*
* @typedef {Array<JsProgram>} Input
*
* @type {import('../types.d.ts').GeneratorMetadata<Input, Record<string, string>>}
*/
export default {
name: 'api-links',

version: '1.0.0',

description:
'Creates a mapping of publicly accessible functions to their source locations in the Node.js repository.',

dependsOn: 'ast-js',

/**
* Generates the `apilinks.json` file.
*
* @param {Input} input
* @param {Partial<GeneratorOptions>} options
*/
async generate(input, { output }) {
/**
* @type Record<string, string>
*/
const definitions = {};

/**
* @type {string}
*/
let baseGithubLink;

if (input.length > 0) {
const repositoryDirectory = dirname(input[0].path);

const repository = getBaseGitHubUrl(repositoryDirectory);

const tag = getCurrentGitHash(repositoryDirectory);

baseGithubLink = `${repository}/blob/${tag}`;
}

input.forEach(program => {
/**
* Mapping of definitions to their line number
* @type {Record<string, number>}
* @example { 'someclass.foo': 10 }
*/
const nameToLineNumberMap = {};

// `http.js` -> `http`
const programBasename = basename(program.path, '.js');

const exports = extractExports(
program,
programBasename,
nameToLineNumberMap
);

findDefinitions(program, programBasename, nameToLineNumberMap, exports);

checkIndirectReferences(program, exports, nameToLineNumberMap);

const githubLink =
`${baseGithubLink}/lib/${programBasename}.js`.replaceAll('\\', '/');

// Add the exports we found in this program to our output
Object.keys(nameToLineNumberMap).forEach(key => {
const lineNumber = nameToLineNumberMap[key];

definitions[key] = `${githubLink}#L${lineNumber}`;
});
});

if (output) {
await writeFile(
join(output, 'apilinks.json'),
JSON.stringify(definitions)
);
}

return definitions;
},
};
5 changes: 5 additions & 0 deletions src/generators/api-links/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ProgramExports {
ctors: Array<string>;
identifiers: Array<string>;
indirects: Record<string, string>;
}
25 changes: 25 additions & 0 deletions src/generators/api-links/utils/checkIndirectReferences.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { visit } from 'estree-util-visit';

/**
*
* @param program
* @param {import('../types.d.ts').ProgramExports} exports
* @param {Record<string, number>} nameToLineNumberMap
*/
export function checkIndirectReferences(program, exports, nameToLineNumberMap) {
if (Object.keys(exports.indirects).length === 0) {
return;
}

visit(program, node => {
if (!node.loc || node.type !== 'FunctionDeclaration') {
return;
}

const name = node.id.name;

if (name in exports.indirects) {
nameToLineNumberMap[exports.indirects[name]] = node.loc.start.line;
}
});
}
Loading
Loading