From bba6f62440e292c7a572fc7bbcf73a38cf061b64 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:50:52 -0800 Subject: [PATCH 1/6] feat apilinks.json generator Closes #152 Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- README.md | 2 +- bin/cli.mjs | 14 +- package-lock.json | 1 + package.json | 1 + src/generators.mjs | 8 +- src/generators/api-links/index.mjs | 95 ++++++++ src/generators/api-links/types.d.ts | 4 + .../api-links/utils/extractExports.mjs | 215 ++++++++++++++++++ .../api-links/utils/findDefinitions.mjs | 173 ++++++++++++++ src/generators/index.mjs | 2 + src/loader.mjs | 18 +- src/metadata.mjs | 5 + src/parser.mjs | 52 ++++- src/types.d.ts | 8 + src/utils/git.mjs | 52 +++++ 15 files changed, 642 insertions(+), 8 deletions(-) create mode 100644 src/generators/api-links/index.mjs create mode 100644 src/generators/api-links/types.d.ts create mode 100644 src/generators/api-links/utils/extractExports.mjs create mode 100644 src/generators/api-links/utils/findDefinitions.mjs create mode 100644 src/utils/git.mjs diff --git a/README.md b/README.md index f6a9c1c..73b7afb 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,6 @@ Options: -o, --output Specify the relative or absolute output directory -v, --version Specify the target version of Node.js, semver compliant (default: "v22.6.0") -c, --changelog 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 ``` diff --git a/bin/cli.mjs b/bin/cli.mjs index 5bb332b..3d11ae7 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -68,14 +68,22 @@ program */ const { input, output, target = [], version, changelog } = program.opts(); -const { loadFiles } = createLoader(); -const { parseApiDocs } = createParser(); +const { loadFiles, loadJsFiles } = createLoader(); +const { parseApiDocs, parseJsSources } = createParser(); const apiDocFiles = loadFiles(input); const parsedApiDocs = await parseApiDocs(apiDocFiles); -const { runGenerators } = createGenerator(parsedApiDocs); +const sourceFiles = loadJsFiles( + parsedApiDocs + .map(apiDoc => apiDoc.source_link_local) + .filter(path => path !== undefined && path.endsWith('.js')) +); + +const parsedJsFiles = await parseJsSources(sourceFiles); + +const { runGenerators } = createGenerator(parsedApiDocs, parsedJsFiles); // Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file const { getAllMajors } = createNodeReleases(changelog); diff --git a/package-lock.json b/package-lock.json index 49d430f..42d4173 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@node-core/api-docs-tooling", "dependencies": { + "acorn": "^8.14.0", "commander": "^13.1.0", "dedent": "^1.5.3", "github-slugger": "^2.0.0", diff --git a/package.json b/package.json index 049087a..3edbdc7 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "prettier": "3.4.2" }, "dependencies": { + "acorn": "^8.14.0", "commander": "^13.1.0", "dedent": "^1.5.3", "github-slugger": "^2.0.0", diff --git a/src/generators.mjs b/src/generators.mjs index 1e1c345..3e29222 100644 --- a/src/generators.mjs +++ b/src/generators.mjs @@ -19,8 +19,9 @@ import availableGenerators from './generators/index.mjs'; * the final generators in the chain. * * @param {ApiDocMetadataEntry} input The parsed API doc metadata entries + * @param {Array} parsedJsFiles */ -const createGenerator = input => { +const createGenerator = (input, parsedJsFiles) => { /** * We store all the registered generators to be processed * within a Record, so we can access their results at any time whenever needed @@ -28,7 +29,10 @@ const createGenerator = input => { * * @type {{ [K in keyof AllGenerators]: ReturnType }} */ - const cachedGenerators = { ast: Promise.resolve(input) }; + const cachedGenerators = { + ast: Promise.resolve(input), + 'ast-js': Promise.resolve(parsedJsFiles), + }; /** * Runs the Generator engine with the provided top-level input and the given generator options diff --git a/src/generators/api-links/index.mjs b/src/generators/api-links/index.mjs new file mode 100644 index 0000000..879e47b --- /dev/null +++ b/src/generators/api-links/index.mjs @@ -0,0 +1,95 @@ +'use strict'; + +import { basename, dirname, join } from 'node:path'; +import { writeFile } from 'node:fs/promises'; +import { getGitRepository, getGitTag } from '../../utils/git.mjs'; +import { extractExports } from './utils/extractExports.mjs'; +import { findDefinitions } from './utils/findDefinitions.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} Input + * + * @type {import('../types.d.ts').GeneratorMetadata>} + */ +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} options + */ + async generate(input, { output }) { + /** + * @type {Record} + */ + const definitions = {}; + + /** + * @type {string} + */ + let baseGithubLink; + + input.forEach(program => { + /** + * Mapping of definitions to their line number + * @type {Record} + * @example { 'someclass.foo', 10 } + */ + const nameToLineNumberMap = {}; + + const programBasename = basename(program.path, '.js'); + + const exports = extractExports( + program, + programBasename, + nameToLineNumberMap + ); + + findDefinitions(program, programBasename, nameToLineNumberMap, exports); + + if (!baseGithubLink) { + const directory = dirname(program.path); + + const repository = getGitRepository(directory); + + const tag = getGitTag(directory); + + baseGithubLink = `https://github.com/${repository}/blob/${tag}`; + } + + const githubLink = + `${baseGithubLink}/lib/${programBasename}.js`.replaceAll('\\', '/'); + + 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; + }, +}; diff --git a/src/generators/api-links/types.d.ts b/src/generators/api-links/types.d.ts new file mode 100644 index 0000000..52db84d --- /dev/null +++ b/src/generators/api-links/types.d.ts @@ -0,0 +1,4 @@ +export interface ProgramExports { + ctors: Array; + identifiers: Array; +} diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs new file mode 100644 index 0000000..896ed9e --- /dev/null +++ b/src/generators/api-links/utils/extractExports.mjs @@ -0,0 +1,215 @@ +// @ts-check +'use strict'; + +/** + * @param {import('acorn').AssignmentExpression} expression + * @param {import('acorn').SourceLocation} loc + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @returns {import('../types').ProgramExports | undefined} + */ +function extractExpression(expression, loc, basename, nameToLineNumberMap) { + /** + * @example `a=b`, lhs=`a` and rhs=`b` + */ + let { left: lhs, right: rhs } = expression; + + if (lhs.type !== 'MemberExpression') { + return undefined; + } + + if (lhs.object.type === 'MemberExpression') { + lhs = lhs.object; + } + + /** + * @type {import('../types').ProgramExports} + */ + const exports = { + ctors: [], + identifiers: [], + }; + + if (lhs.object.name === 'exports') { + // Assigning a property in `module.exports` (i.e. `module.exports.asd = ...`) + const { name } = lhs.property; + + switch (rhs.type) { + case 'FunctionExpression': + // module.exports.something = () => {} + nameToLineNumberMap[`${basename}.${name}`] = loc.start.line; + break; + + case 'Identifier': + // module.exports.asd = something + // TODO indirects? + console.log('indir', name); + break; + + default: + exports.identifiers.push(name); + break; + } + } else if (lhs.object.name === 'module' && lhs.property.name === 'exports') { + // Assigning `module.exports` as a whole, (i.e. `module.exports = {}`) + while (rhs.type === 'AssignmentExpression') { + // Move right until we find the value of the assignment + // (i.e. `a=b`, we want `b`). + rhs = rhs.right; + } + + switch (rhs.type) { + case 'NewExpression': + // module.exports = new Asd() + exports.ctors.push(rhs.callee.name); + break; + + case 'ObjectExpression': + // module.exports = {} + // We need to go through all of the properties and add register them + rhs.properties.forEach(({ value }) => { + if (value.type !== 'Identifier') { + return; + } + + exports.identifiers.push(value.name); + + if (/^[A-Z]/.test(value.name[0])) { + exports.ctors.push(value.name); + } + }); + + break; + + default: + exports.identifiers.push(rhs.name); + break; + } + } + + return exports; +} + +/** + * @param {import('acorn').VariableDeclarator} declaration + * @param {import('acorn').SourceLocation} loc + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @returns {import('../types').ProgramExports | undefined} + */ +function extractVariableDeclaration( + { id, init }, + loc, + basename, + nameToLineNumberMap +) { + while (init && init.type === 'AssignmentExpression') { + // Move left until we get to what we're assigning to + // (i.e. `a=b`, we want `a`) + init = init.left; + } + + if (!init || init.type !== 'MemberExpression') { + // Doesn't exist or we're not writing to a member (probably a normal var, + // like `const a = 123`) + return undefined; + } + + /** + * @type {import('../types').ProgramExports} + */ + const exports = { + ctors: [], + identifiers: [], + }; + + if (init.object.name === 'exports') { + // Assigning a property in `module.exports` (i.e. `module.exports.asd = ...`) + nameToLineNumberMap[`${basename}.${init.property.name}`] = loc.start.line; + } else if ( + init.object.name === 'module' && + init.property.name === 'exports' + ) { + // Assigning `module.exports` as a whole, (i.e. `module.exports = {}`) + exports.ctors.push(id.name); + nameToLineNumberMap[id.name] = loc.start.line; + } + + return exports; +} + +/** + * We need to find what a source file exports so we know what to include in + * the final result. We can do this by going through every statement in the + * program looking for assignments to `module.exports`. + * + * Noteworthy that exports can happen throughout the program so we need to + * go through the entire thing. + * + * @param {import('acorn').Program} program + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @returns {import('../types').ProgramExports} + */ +export function extractExports(program, basename, nameToLineNumberMap) { + /** + * @type {import('../types').ProgramExports} + */ + const exports = { + ctors: [], + identifiers: [], + }; + + program.body.forEach(statement => { + const { loc } = statement; + if (!loc) { + return; + } + + switch (statement.type) { + case 'ExpressionStatement': { + const { expression } = statement; + if (expression.type !== 'AssignmentExpression' || !loc) { + break; + } + + const expressionExports = extractExpression( + expression, + loc, + basename, + nameToLineNumberMap + ); + + if (expressionExports) { + exports.ctors.push(...expressionExports.ctors); + exports.identifiers.push(...expressionExports.identifiers); + } + + break; + } + + case 'VariableDeclaration': { + statement.declarations.forEach(declaration => { + const variableExports = extractVariableDeclaration( + declaration, + loc, + basename, + nameToLineNumberMap + ); + + if (variableExports) { + exports.ctors.push(...variableExports.ctors); + exports.identifiers.push(...variableExports.identifiers); + } + }); + + break; + } + + default: + break; + } + }); + + return exports; +} diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs new file mode 100644 index 0000000..4e7235c --- /dev/null +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -0,0 +1,173 @@ +// @ts-check +'use strict'; + +/** + * @param {import('acorn').AssignmentExpression} expression + * @param {import('acorn').SourceLocation} loc + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + */ +function handleAssignmentExpression( + expression, + loc, + nameToLineNumberMap, + exports +) { + const { left: lhs } = expression; + if (lhs.type !== 'MemberExpression') { + // We're not assigning to a member, don't care + return; + } + + let object; + let objectName; + switch (lhs.object.type) { + case 'MemberExpression': { + if (lhs.object.property.name !== 'prototype') { + return; + } + + // Something like `ClassName.prototype.asd = 123` + object = lhs.object.object; + + objectName = object.name.toLowerCase(); + + // Special case for buffer because ??? + if (objectName === 'buffer') { + objectName = 'buf'; + } + + break; + } + + case 'Identifier': { + object = lhs.object; + objectName = object.name; + break; + } + + default: + return; + } + + if (!exports.ctors.includes(object.name)) { + // The object this property is being assigned to isn't exported + return; + } + + let name = `${objectName}${lhs.computed ? `[${lhs.property.name}]` : `.${lhs.property.name}`}`; + nameToLineNumberMap[name] = loc.start.line; +} + +/** + * @param {import('acorn').FunctionDeclaration} declaration + * @param {import('acorn').SourceLocation} loc + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + */ +function handleFunctionDeclaration( + { id }, + loc, + basename, + nameToLineNumberMap, + exports +) { + if (!exports.identifiers.includes(id.name)) { + // Function isn't exported, we don't care about it + return; + } + + if (basename.startsWith('_')) { + // Internal function, we don't want to include it in docs + return; + } + + nameToLineNumberMap[`${basename}.${id.name}`] = loc.start.line; +} + +/** + * @param {import('acorn').ClassDeclaration} declaration + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + */ +function handleClassDeclaration({ id, body }, nameToLineNumberMap, exports) { + if (!exports.ctors.includes(id.name)) { + // Class isn't exported + return; + } + + const name = id.name.slice(0, 1).toLowerCase() + id.name.slice(1); + + // Iterate through the class's properties so we can include all of its + // public methods + body.body.forEach(({ key, type, kind, loc }) => { + if (!loc || type !== 'MethodDefinition') { + return; + } + + if (kind === 'constructor') { + nameToLineNumberMap[`new ${id.name}`] = loc.start.line; + } else if (kind === 'method') { + nameToLineNumberMap[`${name}.${key.name}`] = loc.start.line; + } + }); +} + +/** + * @param {import('acorn').Program} program + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + */ +export function findDefinitions( + program, + basename, + nameToLineNumberMap, + exports +) { + program.body.forEach(statement => { + const { loc } = statement; + if (!loc) { + return; + } + + switch (statement.type) { + case 'ExpressionStatement': { + const { expression } = statement; + + if (expression.type !== 'AssignmentExpression') { + return; + } + + handleAssignmentExpression( + expression, + loc, + nameToLineNumberMap, + exports + ); + + break; + } + + case 'FunctionDeclaration': { + handleFunctionDeclaration( + statement, + loc, + basename, + nameToLineNumberMap, + exports + ); + break; + } + + case 'ClassDeclaration': { + handleClassDeclaration(statement, nameToLineNumberMap, exports); + break; + } + + default: + break; + } + }); +} diff --git a/src/generators/index.mjs b/src/generators/index.mjs index 45a0f54..512271c 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -7,6 +7,7 @@ import manPage from './man-page/index.mjs'; import legacyJson from './legacy-json/index.mjs'; import legacyJsonAll from './legacy-json-all/index.mjs'; import addonVerify from './addon-verify/index.mjs'; +import apiLinks from './api-links/index.mjs'; export default { 'json-simple': jsonSimple, @@ -16,4 +17,5 @@ export default { 'legacy-json': legacyJson, 'legacy-json-all': legacyJsonAll, 'addon-verify': addonVerify, + 'api-links': apiLinks, }; diff --git a/src/loader.mjs b/src/loader.mjs index 12715e0..9b12cee 100644 --- a/src/loader.mjs +++ b/src/loader.mjs @@ -5,6 +5,7 @@ import { extname } from 'node:path'; import { globSync } from 'glob'; import { VFile } from 'vfile'; +import { existsSync } from 'node:fs'; /** * This method creates a simple abstract "Loader", which technically @@ -33,7 +34,22 @@ const createLoader = () => { }); }; - return { loadFiles }; + /** + * Loads the JavaScript source files and transforms them into VFiles + * + * @param {Array} filePaths + */ + const loadJsFiles = filePaths => { + filePaths = filePaths.filter(filePath => existsSync(filePath)); + + return filePaths.map(async filePath => { + const fileContents = await readFile(filePath, 'utf-8'); + + return new VFile({ path: filePath, value: fileContents }); + }); + }; + + return { loadFiles, loadJsFiles }; }; export default createLoader; diff --git a/src/metadata.mjs b/src/metadata.mjs index 579061f..546f0b3 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -1,5 +1,7 @@ 'use strict'; +import { join } from 'node:path'; + import { u as createTree } from 'unist-builder'; import { compare } from 'semver'; @@ -147,6 +149,9 @@ const createMetadata = slugger => { api: apiDoc.stem, slug: sectionSlug, source_link, + source_link_local: source_link + ? join(apiDoc.history[0], '..', '..', '..', source_link) + : undefined, api_doc_source: `doc/api/${apiDoc.basename}`, added_in: added, deprecated_in: deprecated, diff --git a/src/parser.mjs b/src/parser.mjs index c4c3434..281e142 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -1,3 +1,4 @@ +// @ts-check 'use strict'; import { u as createTree } from 'unist-builder'; @@ -5,6 +6,7 @@ import { findAfter } from 'unist-util-find-after'; import { remove } from 'unist-util-remove'; import { selectAll } from 'unist-util-select'; import { SKIP, visit } from 'unist-util-visit'; +import * as acorn from 'acorn'; import createMetadata from './metadata.mjs'; import createQueries from './queries.mjs'; @@ -182,7 +184,55 @@ const createParser = () => { return resolvedApiDocEntries.flat(); }; - return { parseApiDocs, parseApiDoc }; + /** + * TODO + * + * @param {import('vfile').VFile | Promise} apiDoc + * @returns {Promise} + */ + const parseJsSource = async apiDoc => { + // We allow the API doc VFile to be a Promise of a VFile also, + // hence we want to ensure that it first resolves before we pass it to the parser + const resolvedApiDoc = await Promise.resolve(apiDoc); + + if (typeof resolvedApiDoc.value !== 'string') { + throw new TypeError( + `expected resolvedApiDoc.value to be string but got ${typeof resolvedApiDoc.value}` + ); + } + + try { + const res = acorn.parse(resolvedApiDoc.value, { + allowReturnOutsideFunction: true, + ecmaVersion: 'latest', + locations: true, + }); + + return { + ...res, + path: resolvedApiDoc.path, + }; + } catch (err) { + console.log(`error parsing ${resolvedApiDoc.basename}`); + throw err; + } + }; + + /** + * TODO + * + * @param {Array>} apiDocs List of API doc files to be parsed + * @returns {Promise>} + */ + const parseJsSources = async apiDocs => { + // We do a Promise.all, to ensure that each API doc is resolved asynchronously + // but all need to be resolved first before we return the result to the caller + const resolvedApiDocEntries = await Promise.all(apiDocs.map(parseJsSource)); + + return resolvedApiDocEntries; + }; + + return { parseApiDocs, parseApiDoc, parseJsSources, parseJsSource }; }; export default createParser; diff --git a/src/types.d.ts b/src/types.d.ts index a0be7da..b23d2ba 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,4 +1,5 @@ import type { Heading, Root } from '@types/mdast'; +import type { Program } from 'acorn'; import type { SemVer } from 'semver'; import type { Data, Node, Parent } from 'unist'; @@ -70,6 +71,8 @@ declare global { slug: string; // The GitHub URL to the source of the API entry source_link: string | Array | undefined; + // The path to the JavaScript source for the API entry relative to its location locally (ex. `../node/lib/zlib.js`) + source_link_local: string | undefined; // Path to the api doc file relative to the root of the nodejs repo root (ex/ `doc/api/addons.md`) api_doc_source: string; // When a said API section got added (in which version(s) of Node.js) @@ -100,6 +103,11 @@ declare global { tags: Array; } + export interface JsProgram extends Program { + // Path to the program's source (i.e. `../node/lib/zlib.js`) + path: string; + } + export interface ApiDocReleaseEntry { version: SemVer; isLts: boolean; diff --git a/src/utils/git.mjs b/src/utils/git.mjs new file mode 100644 index 0000000..ed541d3 --- /dev/null +++ b/src/utils/git.mjs @@ -0,0 +1,52 @@ +'use strict'; + +import { execSync } from 'child_process'; + +/** + * Grabs the remote repository name in a directory + * + * @example getGitRepository('../node/lib') = 'nodejs/node' + * + * @param {string} directory Directory to check + * @returns {string | undefined} + */ +export function getGitRepository(directory) { + try { + const trackingRemote = execSync(`cd ${directory} && git remote`); + const remoteUrl = execSync( + `cd ${directory} && git remote get-url ${trackingRemote}` + ); + + return (remoteUrl.match(/(\w+\/\w+)\.git\r?\n?$/) || [ + '', + 'nodejs/node', + ])[1]; + // eslint-disable-next-line no-unused-vars + } catch (_) { + return undefined; + } +} + +/** + * Grabs the current tag or commit hash (if tag isn't present) ina directory + * + * @example getGitTag('../node/lib') = 'v20.0.0' + * + * @param {string} directory Directory to check + * @returns {string | undefined} + */ +export function getGitTag(directory) { + try { + const hash = + execSync(`cd ${directory} && git log -1 --pretty=%H`) || 'main'; + const tag = + execSync(`cd ${directory} && git describe --contains ${hash}`).split( + '\n' + )[0] || hash; + + return tag; + // eslint-disable-next-line no-unused-vars + } catch (_) { + return undefined; + } +} From 6814543822d984bea18ef4ce097962b9b5f6c72a Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:46:15 -0800 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Claudio W --- bin/cli.mjs | 4 ++-- src/generators.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/cli.mjs b/bin/cli.mjs index 3d11ae7..5f2461a 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -77,8 +77,8 @@ const parsedApiDocs = await parseApiDocs(apiDocFiles); const sourceFiles = loadJsFiles( parsedApiDocs - .map(apiDoc => apiDoc.source_link_local) - .filter(path => path !== undefined && path.endsWith('.js')) + .map(({ source_link_local }) => source_link_local) + .filter(path => path?.endsWith('.js')) ); const parsedJsFiles = await parseJsSources(sourceFiles); diff --git a/src/generators.mjs b/src/generators.mjs index 3e29222..923a87a 100644 --- a/src/generators.mjs +++ b/src/generators.mjs @@ -21,7 +21,7 @@ import availableGenerators from './generators/index.mjs'; * @param {ApiDocMetadataEntry} input The parsed API doc metadata entries * @param {Array} parsedJsFiles */ -const createGenerator = (input, parsedJsFiles) => { +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 From a3d41825a9eda611a2a506513ed0a4b91708ae30 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Mon, 6 Jan 2025 23:04:19 -0800 Subject: [PATCH 3/6] review --- bin/cli.mjs | 10 +- package-lock.json | 43 ++- package.json | 2 + src/generators/api-links/constants.mjs | 4 + src/generators/api-links/index.mjs | 32 ++- src/generators/api-links/types.d.ts | 1 + .../utils/checkIndirectReferences.mjs | 25 ++ .../api-links/utils/extractExports.mjs | 260 +++++++++++------- .../api-links/utils/findDefinitions.mjs | 169 ++++++------ .../api-links/utils/getBaseGitHubUrl.mjs | 35 +++ src/generators/types.d.ts | 2 +- src/loader.mjs | 15 +- src/parser.mjs | 4 +- 13 files changed, 390 insertions(+), 212 deletions(-) create mode 100644 src/generators/api-links/constants.mjs create mode 100644 src/generators/api-links/utils/checkIndirectReferences.mjs create mode 100644 src/generators/api-links/utils/getBaseGitHubUrl.mjs diff --git a/bin/cli.mjs b/bin/cli.mjs index 5f2461a..af8d198 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -68,18 +68,14 @@ program */ const { input, output, target = [], version, changelog } = program.opts(); -const { loadFiles, loadJsFiles } = createLoader(); +const { loadMarkdownFiles, loadJsFiles } = createLoader(); const { parseApiDocs, parseJsSources } = createParser(); -const apiDocFiles = loadFiles(input); +const apiDocFiles = loadMarkdownFiles(input); const parsedApiDocs = await parseApiDocs(apiDocFiles); -const sourceFiles = loadJsFiles( - parsedApiDocs - .map(({ source_link_local }) => source_link_local) - .filter(path => path?.endsWith('.js')) -); +const sourceFiles = loadJsFiles(input); const parsedJsFiles = await parseJsSources(sourceFiles); diff --git a/package-lock.json b/package-lock.json index 42d4173..a2056a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "dependencies": { "acorn": "^8.14.0", "commander": "^13.1.0", + "estree-util-visit": "^2.0.0", + "gitconfiglocal": "^2.1.0", "dedent": "^1.5.3", "github-slugger": "^2.0.0", "glob": "^11.0.1", @@ -472,8 +474,16 @@ "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } }, "node_modules/@types/hast": { "version": "3.0.4", @@ -1260,6 +1270,20 @@ "node": ">=4.0" } }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1432,6 +1456,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gitconfiglocal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz", + "integrity": "sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==", + "license": "BSD", + "dependencies": { + "ini": "^1.3.2" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -1699,6 +1732,12 @@ "node": ">=0.8.19" } }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", diff --git a/package.json b/package.json index 3edbdc7..070ca17 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "dependencies": { "acorn": "^8.14.0", "commander": "^13.1.0", + "estree-util-visit": "^2.0.0", + "gitconfiglocal": "^2.1.0", "dedent": "^1.5.3", "github-slugger": "^2.0.0", "glob": "^11.0.1", diff --git a/src/generators/api-links/constants.mjs b/src/generators/api-links/constants.mjs new file mode 100644 index 0000000..bff6072 --- /dev/null +++ b/src/generators/api-links/constants.mjs @@ -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]/; diff --git a/src/generators/api-links/index.mjs b/src/generators/api-links/index.mjs index 879e47b..86d9de2 100644 --- a/src/generators/api-links/index.mjs +++ b/src/generators/api-links/index.mjs @@ -2,9 +2,13 @@ import { basename, dirname, join } from 'node:path'; import { writeFile } from 'node:fs/promises'; -import { getGitRepository, getGitTag } from '../../utils/git.mjs'; +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 @@ -36,7 +40,7 @@ export default { */ async generate(input, { output }) { /** - * @type {Record} + * @type Record */ const definitions = {}; @@ -45,14 +49,25 @@ export default { */ 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} - * @example { 'someclass.foo', 10 } + * @example { 'someclass.foo': 10 } */ const nameToLineNumberMap = {}; + // `http.js` -> `http` const programBasename = basename(program.path, '.js'); const exports = extractExports( @@ -63,19 +78,12 @@ export default { findDefinitions(program, programBasename, nameToLineNumberMap, exports); - if (!baseGithubLink) { - const directory = dirname(program.path); - - const repository = getGitRepository(directory); - - const tag = getGitTag(directory); - - baseGithubLink = `https://github.com/${repository}/blob/${tag}`; - } + 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]; diff --git a/src/generators/api-links/types.d.ts b/src/generators/api-links/types.d.ts index 52db84d..1fc8ea2 100644 --- a/src/generators/api-links/types.d.ts +++ b/src/generators/api-links/types.d.ts @@ -1,4 +1,5 @@ export interface ProgramExports { ctors: Array; identifiers: Array; + indirects: Record; } diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs new file mode 100644 index 0000000..d18b10a --- /dev/null +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -0,0 +1,25 @@ +import { visit } from 'estree-util-visit'; + +/** + * + * @param program + * @param {import('../types.d.ts').ProgramExports} exports + * @param {Record} 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; + } + }); +} diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 896ed9e..d55729e 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,18 +1,25 @@ -// @ts-check 'use strict'; +import { visit } from 'estree-util-visit'; +import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; + /** - * @param {import('acorn').AssignmentExpression} expression - * @param {import('acorn').SourceLocation} loc + * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression + * + * @param {import('acorn').ExpressionStatement} node * @param {string} basename * @param {Record} nameToLineNumberMap * @returns {import('../types').ProgramExports | undefined} */ -function extractExpression(expression, loc, basename, nameToLineNumberMap) { - /** - * @example `a=b`, lhs=`a` and rhs=`b` - */ - let { left: lhs, right: rhs } = expression; +function handleExpression(node, basename, nameToLineNumberMap) { + const { expression } = node; + + if (expression.type !== 'AssignmentExpression') { + return; + } + + // `a=b`, lhs=`a` and rhs=`b` + let { left: lhs, right: rhs, loc } = expression; if (lhs.type !== 'MemberExpression') { return undefined; @@ -28,62 +35,110 @@ function extractExpression(expression, loc, basename, nameToLineNumberMap) { const exports = { ctors: [], identifiers: [], + indirects: {}, }; if (lhs.object.name === 'exports') { - // Assigning a property in `module.exports` (i.e. `module.exports.asd = ...`) - const { name } = lhs.property; + // This is an assignment to a property in `module.exports` or `exports` + // (i.e. `module.exports.asd = ...`) switch (rhs.type) { - case 'FunctionExpression': + /** @see https://github.com/estree/estree/blob/master/es5.md#functionexpression */ + case 'FunctionExpression': { // module.exports.something = () => {} - nameToLineNumberMap[`${basename}.${name}`] = loc.start.line; - break; + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = + loc.start.line; - case 'Identifier': + break; + } + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ + case 'Identifier': { + // Save this for later in case it's referenced // module.exports.asd = something - // TODO indirects? - console.log('indir', name); + if (rhs.name === lhs.property.name) { + exports.indirects[lhs.property.name] = + `${basename}.${lhs.property.name}`; + } + break; + } + default: { + if (lhs.property.name !== undefined) { + // Something else, let's save it for when we're searching for + // declarations + exports.identifiers.push(lhs.property.name); + } - default: - exports.identifiers.push(name); break; + } } } else if (lhs.object.name === 'module' && lhs.property.name === 'exports') { - // Assigning `module.exports` as a whole, (i.e. `module.exports = {}`) + // This is an assignment to `module.exports` as a whole + // (i.e. `module.exports = {}`) + + // We need to move right until we find the value of the assignment. + // (if `a=b`, we want `b`) while (rhs.type === 'AssignmentExpression') { - // Move right until we find the value of the assignment - // (i.e. `a=b`, we want `b`). rhs = rhs.right; } switch (rhs.type) { - case 'NewExpression': + /** @see https://github.com/estree/estree/blob/master/es5.md#newexpression */ + case 'NewExpression': { // module.exports = new Asd() exports.ctors.push(rhs.callee.name); break; - - case 'ObjectExpression': + } + /** @see https://github.com/estree/estree/blob/master/es5.md#objectexpression */ + case 'ObjectExpression': { // module.exports = {} - // We need to go through all of the properties and add register them + // we need to go through all of the properties and register them rhs.properties.forEach(({ value }) => { - if (value.type !== 'Identifier') { - return; - } + switch (value.type) { + case 'Identifier': { + exports.identifiers.push(value.name); + + if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { + exports.ctors.push(value.name); + } + + break; + } + case 'CallExpression': { + if (value.callee.name !== 'deprecate') { + break; + } + + // Handle exports wrapped in the `deprecate` function + // Ex/ https://github.com/nodejs/node/blob/e96072ad57348ce423a8dd7639dcc3d1c34e847d/lib/buffer.js#L1334 - exports.identifiers.push(value.name); + exports.identifiers.push(value.arguments[0].name); - if (/^[A-Z]/.test(value.name[0])) { - exports.ctors.push(value.name); + break; + } + default: { + // Not relevant + } } }); break; + } + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ + case 'Identifier': { + // Something else, let's save it for when we're searching for + // declarations + + if (rhs.name !== undefined) { + exports.identifiers.push(rhs.name); + } - default: - exports.identifiers.push(rhs.name); break; + } + default: { + // Not relevant + break; + } } } @@ -91,49 +146,59 @@ function extractExpression(expression, loc, basename, nameToLineNumberMap) { } /** - * @param {import('acorn').VariableDeclarator} declaration - * @param {import('acorn').SourceLocation} loc + * @see https://github.com/estree/estree/blob/master/es5.md#variabledeclaration + * + * @param {import('acorn').VariableDeclaration} node * @param {string} basename * @param {Record} nameToLineNumberMap * @returns {import('../types').ProgramExports | undefined} */ -function extractVariableDeclaration( - { id, init }, - loc, - basename, - nameToLineNumberMap -) { - while (init && init.type === 'AssignmentExpression') { - // Move left until we get to what we're assigning to - // (i.e. `a=b`, we want `a`) - init = init.left; - } - - if (!init || init.type !== 'MemberExpression') { - // Doesn't exist or we're not writing to a member (probably a normal var, - // like `const a = 123`) - return undefined; - } - +function handleVariableDeclaration(node, basename, nameToLineNumberMap) { /** * @type {import('../types').ProgramExports} */ const exports = { ctors: [], identifiers: [], + indirects: {}, }; - if (init.object.name === 'exports') { - // Assigning a property in `module.exports` (i.e. `module.exports.asd = ...`) - nameToLineNumberMap[`${basename}.${init.property.name}`] = loc.start.line; - } else if ( - init.object.name === 'module' && - init.property.name === 'exports' - ) { - // Assigning `module.exports` as a whole, (i.e. `module.exports = {}`) - exports.ctors.push(id.name); - nameToLineNumberMap[id.name] = loc.start.line; - } + node.declarations.forEach(({ init: lhs, id }) => { + while (lhs && lhs.type === 'AssignmentExpression') { + // Move left until we get to what we're assigning to + // (if `a=b`, we want `a`) + lhs = lhs.left; + } + + if (!lhs || lhs.type !== 'MemberExpression') { + // Doesn't exist or we're not writing to an object + // (aka it's just a regular variable like `const a = 123`) + return; + } + + switch (lhs.object.name) { + case 'exports': { + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = + node.start.line; + + break; + } + case 'module': { + if (lhs.property.name !== 'exports') { + break; + } + + exports.ctors.push(id.name); + nameToLineNumberMap[id.name] = node.loc.start.line; + + break; + } + default: { + // Not relevant to us + break; + } + } + }); return exports; } @@ -158,56 +223,43 @@ export function extractExports(program, basename, nameToLineNumberMap) { const exports = { ctors: [], identifiers: [], + indirects: {}, + }; + + const TYPE_TO_HANDLER_MAP = { + /** + * + * @param node + */ + ExpressionStatement: node => + handleExpression(node, basename, nameToLineNumberMap), + + /** + * + * @param node + */ + VariableDeclaration: node => + handleVariableDeclaration(node, basename, nameToLineNumberMap), }; - program.body.forEach(statement => { - const { loc } = statement; - if (!loc) { + visit(program, node => { + if (!node.loc) { return; } - switch (statement.type) { - case 'ExpressionStatement': { - const { expression } = statement; - if (expression.type !== 'AssignmentExpression' || !loc) { - break; - } - - const expressionExports = extractExpression( - expression, - loc, - basename, - nameToLineNumberMap - ); + if (node.type in TYPE_TO_HANDLER_MAP) { + const handler = TYPE_TO_HANDLER_MAP[node.type]; - if (expressionExports) { - exports.ctors.push(...expressionExports.ctors); - exports.identifiers.push(...expressionExports.identifiers); - } + const output = handler(node); - break; - } + if (output) { + exports.ctors.push(...output.ctors); + exports.identifiers.push(...output.identifiers); - case 'VariableDeclaration': { - statement.declarations.forEach(declaration => { - const variableExports = extractVariableDeclaration( - declaration, - loc, - basename, - nameToLineNumberMap - ); - - if (variableExports) { - exports.ctors.push(...variableExports.ctors); - exports.identifiers.push(...variableExports.identifiers); - } + Object.keys(output.indirects).forEach(key => { + exports.indirects[key] = output.indirects[key]; }); - - break; } - - default: - break; } }); diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index 4e7235c..a9adbb3 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,27 +1,41 @@ // @ts-check 'use strict'; +import { visit } from 'estree-util-visit'; + /** - * @param {import('acorn').AssignmentExpression} expression - * @param {import('acorn').SourceLocation} loc + * @see https://github.com/estree/estree/blob/master/es5.md#expressionstatement + * + * @param {import('acorn').ExpressionStatement} node * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports */ -function handleAssignmentExpression( - expression, - loc, - nameToLineNumberMap, - exports -) { - const { left: lhs } = expression; +function handleAssignmentExpression(node, nameToLineNumberMap, exports) { + const { expression } = node; + + if (expression.type !== 'AssignmentExpression') { + return; + } + + const { left: lhs, right: rhs } = expression; + if (lhs.type !== 'MemberExpression') { - // We're not assigning to a member, don't care + // Not an assignment to a member, not relevant to us return; } + /** + * The property that's being written to + */ let object; + + /** + * The lowercase name of the object that's being written to + */ let objectName; + switch (lhs.object.type) { + /** @see https://github.com/estree/estree/blob/master/es5.md#memberexpression */ case 'MemberExpression': { if (lhs.object.property.name !== 'prototype') { return; @@ -30,87 +44,98 @@ function handleAssignmentExpression( // Something like `ClassName.prototype.asd = 123` object = lhs.object.object; - objectName = object.name.toLowerCase(); + objectName = object.name ? object.name : object.object.name; + objectName = objectName.toLowerCase(); - // Special case for buffer because ??? + // Special case for buffer since some of the docs refer to it as `buf` + // https://github.com/nodejs/node/pull/22405#issuecomment-414452461 if (objectName === 'buffer') { objectName = 'buf'; } break; } - + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ case 'Identifier': { object = lhs.object; objectName = object.name; + break; } - - default: + default: { + // Not relevant to us return; + } } if (!exports.ctors.includes(object.name)) { - // The object this property is being assigned to isn't exported + // The object being written to isn't exported, not relevant to us return; } - let name = `${objectName}${lhs.computed ? `[${lhs.property.name}]` : `.${lhs.property.name}`}`; - nameToLineNumberMap[name] = loc.start.line; + /** + * Name/key for this exported object that we're putting in the output + * @example `clientrequest._finish` + */ + const name = `${objectName}${lhs.computed ? `[${lhs.property.name}]` : `.${lhs.property.name}`}`; + + nameToLineNumberMap[name] = node.loc.start.line; + + if (lhs.property.name === rhs.name) { + exports.indirects[rhs.name] = name; + } } /** - * @param {import('acorn').FunctionDeclaration} declaration - * @param {import('acorn').SourceLocation} loc + * @param {import('acorn').FunctionDeclaration} node * @param {string} basename * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports */ function handleFunctionDeclaration( - { id }, - loc, + node, basename, nameToLineNumberMap, exports ) { - if (!exports.identifiers.includes(id.name)) { - // Function isn't exported, we don't care about it + if (!exports.identifiers.includes(node.id.name)) { + // Function isn't exported, not relevant to us return; } if (basename.startsWith('_')) { - // Internal function, we don't want to include it in docs + // Internal function, don't include it in the docs return; } - nameToLineNumberMap[`${basename}.${id.name}`] = loc.start.line; + nameToLineNumberMap[`${basename}.${node.id.name}`] = node.loc.start.line; } /** - * @param {import('acorn').ClassDeclaration} declaration + * @param {import('acorn').ClassDeclaration} node * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports */ -function handleClassDeclaration({ id, body }, nameToLineNumberMap, exports) { - if (!exports.ctors.includes(id.name)) { - // Class isn't exported +function handleClassDeclaration(node, nameToLineNumberMap, exports) { + if (!exports.ctors.includes(node.id.name)) { + // Class isn't exported, not relevant to us return; } - const name = id.name.slice(0, 1).toLowerCase() + id.name.slice(1); + // WASI -> wASI, Agent -> agent + const name = node.id.name[0].toLowerCase() + node.id.name.substring(1); + + nameToLineNumberMap[node.id.name] = node.loc.start.line; - // Iterate through the class's properties so we can include all of its - // public methods - body.body.forEach(({ key, type, kind, loc }) => { + node.body.body.forEach(({ key, type, kind, loc }) => { if (!loc || type !== 'MethodDefinition') { return; } - if (kind === 'constructor') { - nameToLineNumberMap[`new ${id.name}`] = loc.start.line; - } else if (kind === 'method') { - nameToLineNumberMap[`${name}.${key.name}`] = loc.start.line; - } + const outputKey = + kind === 'constructor' ? `new ${node.id.name}` : `${name}.${key.name}`; + + nameToLineNumberMap[outputKey] = loc.start.line; }); } @@ -126,48 +151,38 @@ export function findDefinitions( nameToLineNumberMap, exports ) { - program.body.forEach(statement => { - const { loc } = statement; - if (!loc) { + const TYPE_TO_HANDLER_MAP = { + /** + * + * @param node + */ + ExpressionStatement: node => + handleAssignmentExpression(node, nameToLineNumberMap, exports), + + /** + * + * @param node + */ + FunctionDeclaration: node => + handleFunctionDeclaration(node, basename, nameToLineNumberMap, exports), + + /** + * + * @param node + */ + ClassDeclaration: node => + handleClassDeclaration(node, nameToLineNumberMap, exports), + }; + + visit(program, node => { + if (!node.loc) { return; } - switch (statement.type) { - case 'ExpressionStatement': { - const { expression } = statement; - - if (expression.type !== 'AssignmentExpression') { - return; - } - - handleAssignmentExpression( - expression, - loc, - nameToLineNumberMap, - exports - ); - - break; - } - - case 'FunctionDeclaration': { - handleFunctionDeclaration( - statement, - loc, - basename, - nameToLineNumberMap, - exports - ); - break; - } - - case 'ClassDeclaration': { - handleClassDeclaration(statement, nameToLineNumberMap, exports); - break; - } + if (node.type in TYPE_TO_HANDLER_MAP) { + const handler = TYPE_TO_HANDLER_MAP[node.type]; - default: - break; + handler(node); } }); } diff --git a/src/generators/api-links/utils/getBaseGitHubUrl.mjs b/src/generators/api-links/utils/getBaseGitHubUrl.mjs new file mode 100644 index 0000000..a1c5013 --- /dev/null +++ b/src/generators/api-links/utils/getBaseGitHubUrl.mjs @@ -0,0 +1,35 @@ +'use strict'; + +import { execSync } from 'node:child_process'; + +/** + * @param {string} cwd + */ +export function getBaseGitHubUrl(cwd) { + let url = execSync('git remote get-url origin', { cwd }).toString().trim(); + + if (url.startsWith('git@')) { + // It's an ssh url, we need to transform it to be https + // Ex/ git@github.com:nodejs/node.git -> https://github.com/nodejs/node.git + let [, repository] = url.split(':'); + + // Trim off the trailing .git if it exists + if (repository.endsWith('.git')) { + repository = repository.substring(0, repository.length - 4); + } + + url = `https://github.com/${repository}`; + } + + return url; +} + +/** + * + * @param cwd + */ +export function getCurrentGitHash(cwd) { + const hash = execSync('git rev-parse HEAD', { cwd }).toString().trim(); + + return hash; +} diff --git a/src/generators/types.d.ts b/src/generators/types.d.ts index 348ceed..718b2ab 100644 --- a/src/generators/types.d.ts +++ b/src/generators/types.d.ts @@ -56,7 +56,7 @@ declare global { * The 'ast' generator is the top-level parser, and if 'ast' is passed to `dependsOn`, then the generator * will be marked as a top-level generator. */ - dependsOn: keyof AvailableGenerators | 'ast'; + dependsOn: keyof AvailableGenerators | 'ast' | 'ast-js'; /** * Generators are abstract and the different generators have different sort of inputs and outputs. diff --git a/src/loader.mjs b/src/loader.mjs index 9b12cee..2a7e886 100644 --- a/src/loader.mjs +++ b/src/loader.mjs @@ -5,7 +5,6 @@ import { extname } from 'node:path'; import { globSync } from 'glob'; import { VFile } from 'vfile'; -import { existsSync } from 'node:fs'; /** * This method creates a simple abstract "Loader", which technically @@ -22,7 +21,7 @@ const createLoader = () => { * * @see https://code.visualstudio.com/docs/editor/glob-patterns */ - const loadFiles = searchPath => { + const loadMarkdownFiles = searchPath => { const resolvedFiles = globSync(searchPath).filter( filePath => extname(filePath) === '.md' ); @@ -37,19 +36,21 @@ const createLoader = () => { /** * Loads the JavaScript source files and transforms them into VFiles * - * @param {Array} filePaths + * @param {Array} searchPath */ - const loadJsFiles = filePaths => { - filePaths = filePaths.filter(filePath => existsSync(filePath)); + const loadJsFiles = searchPath => { + const resolvedFiles = globSync(searchPath).filter( + filePath => extname(filePath) === '.js' + ); - return filePaths.map(async filePath => { + return resolvedFiles.map(async filePath => { const fileContents = await readFile(filePath, 'utf-8'); return new VFile({ path: filePath, value: fileContents }); }); }; - return { loadFiles, loadJsFiles }; + return { loadMarkdownFiles, loadJsFiles }; }; export default createLoader; diff --git a/src/parser.mjs b/src/parser.mjs index 281e142..2a5a841 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -185,7 +185,7 @@ const createParser = () => { }; /** - * TODO + * Parses a given JavaScript file into an ESTree AST representation of it * * @param {import('vfile').VFile | Promise} apiDoc * @returns {Promise} @@ -219,7 +219,7 @@ const createParser = () => { }; /** - * TODO + * Parses multiple JavaScript files into ESTree ASTs by wrapping parseJsSource * * @param {Array>} apiDocs List of API doc files to be parsed * @returns {Promise>} From 2939177a9f0cdfeca586bdee5eacf91f67ee9c34 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:55:28 -0800 Subject: [PATCH 4/6] cleanup Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- package-lock.json | 16 ------ package.json | 1 - src/generators.mjs | 8 +-- .../api-links/utils/extractExports.mjs | 6 +-- .../api-links/utils/findDefinitions.mjs | 10 ++-- src/generators/legacy-html/assets/style.css | 4 +- src/metadata.mjs | 5 -- src/parser.mjs | 1 - src/types.d.ts | 2 - src/utils/git.mjs | 52 ------------------- 10 files changed, 12 insertions(+), 93 deletions(-) delete mode 100644 src/utils/git.mjs diff --git a/package-lock.json b/package-lock.json index a2056a3..787913b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "acorn": "^8.14.0", "commander": "^13.1.0", "estree-util-visit": "^2.0.0", - "gitconfiglocal": "^2.1.0", "dedent": "^1.5.3", "github-slugger": "^2.0.0", "glob": "^11.0.1", @@ -1456,15 +1455,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gitconfiglocal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-2.1.0.tgz", - "integrity": "sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==", - "license": "BSD", - "dependencies": { - "ini": "^1.3.2" - } - }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -1732,12 +1722,6 @@ "node": ">=0.8.19" } }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", diff --git a/package.json b/package.json index 070ca17..e335033 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "acorn": "^8.14.0", "commander": "^13.1.0", "estree-util-visit": "^2.0.0", - "gitconfiglocal": "^2.1.0", "dedent": "^1.5.3", "github-slugger": "^2.0.0", "glob": "^11.0.1", diff --git a/src/generators.mjs b/src/generators.mjs index 923a87a..ef51628 100644 --- a/src/generators.mjs +++ b/src/generators.mjs @@ -5,6 +5,8 @@ import availableGenerators from './generators/index.mjs'; /** * @typedef {{ ast: import('./generators/types.d.ts').GeneratorMetadata}} 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 @@ -18,7 +20,7 @@ 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} parsedJsFiles */ const createGenerator = (markdownInput, jsInput) => { @@ -30,8 +32,8 @@ const createGenerator = (markdownInput, jsInput) => { * @type {{ [K in keyof AllGenerators]: ReturnType }} */ const cachedGenerators = { - ast: Promise.resolve(input), - 'ast-js': Promise.resolve(parsedJsFiles), + ast: Promise.resolve(markdownInput), + 'ast-js': Promise.resolve(jsInput), }; /** diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index d55729e..228eb5b 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -228,15 +228,13 @@ export function extractExports(program, basename, nameToLineNumberMap) { const TYPE_TO_HANDLER_MAP = { /** - * - * @param node + * @param {import('acorn').Node} node */ ExpressionStatement: node => handleExpression(node, basename, nameToLineNumberMap), /** - * - * @param node + * @param {import('acorn').Node} node */ VariableDeclaration: node => handleVariableDeclaration(node, basename, nameToLineNumberMap), diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index a9adbb3..a161e3e 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,4 +1,3 @@ -// @ts-check 'use strict'; import { visit } from 'estree-util-visit'; @@ -153,22 +152,19 @@ export function findDefinitions( ) { const TYPE_TO_HANDLER_MAP = { /** - * - * @param node + * @param {import('acorn').Node} node */ ExpressionStatement: node => handleAssignmentExpression(node, nameToLineNumberMap, exports), /** - * - * @param node + * @param {import('acorn').Node} node */ FunctionDeclaration: node => handleFunctionDeclaration(node, basename, nameToLineNumberMap, exports), /** - * - * @param node + * @param {import('acorn').Node} node */ ClassDeclaration: node => handleClassDeclaration(node, nameToLineNumberMap, exports), diff --git a/src/generators/legacy-html/assets/style.css b/src/generators/legacy-html/assets/style.css index 5f7fc50..9086b6b 100644 --- a/src/generators/legacy-html/assets/style.css +++ b/src/generators/legacy-html/assets/style.css @@ -137,8 +137,8 @@ code, .pre, span.type, a.type { - font-family: SFMono-Regular, Menlo, Consolas, 'Liberation Mono', 'Courier New', - monospace; + font-family: SFMono-Regular, Menlo, Consolas, 'Liberation Mono', + 'Courier New', monospace; font-size: 0.9em; } diff --git a/src/metadata.mjs b/src/metadata.mjs index 546f0b3..579061f 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -1,7 +1,5 @@ 'use strict'; -import { join } from 'node:path'; - import { u as createTree } from 'unist-builder'; import { compare } from 'semver'; @@ -149,9 +147,6 @@ const createMetadata = slugger => { api: apiDoc.stem, slug: sectionSlug, source_link, - source_link_local: source_link - ? join(apiDoc.history[0], '..', '..', '..', source_link) - : undefined, api_doc_source: `doc/api/${apiDoc.basename}`, added_in: added, deprecated_in: deprecated, diff --git a/src/parser.mjs b/src/parser.mjs index 2a5a841..59f88ea 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -1,4 +1,3 @@ -// @ts-check 'use strict'; import { u as createTree } from 'unist-builder'; diff --git a/src/types.d.ts b/src/types.d.ts index b23d2ba..ebae255 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -71,8 +71,6 @@ declare global { slug: string; // The GitHub URL to the source of the API entry source_link: string | Array | undefined; - // The path to the JavaScript source for the API entry relative to its location locally (ex. `../node/lib/zlib.js`) - source_link_local: string | undefined; // Path to the api doc file relative to the root of the nodejs repo root (ex/ `doc/api/addons.md`) api_doc_source: string; // When a said API section got added (in which version(s) of Node.js) diff --git a/src/utils/git.mjs b/src/utils/git.mjs deleted file mode 100644 index ed541d3..0000000 --- a/src/utils/git.mjs +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -import { execSync } from 'child_process'; - -/** - * Grabs the remote repository name in a directory - * - * @example getGitRepository('../node/lib') = 'nodejs/node' - * - * @param {string} directory Directory to check - * @returns {string | undefined} - */ -export function getGitRepository(directory) { - try { - const trackingRemote = execSync(`cd ${directory} && git remote`); - const remoteUrl = execSync( - `cd ${directory} && git remote get-url ${trackingRemote}` - ); - - return (remoteUrl.match(/(\w+\/\w+)\.git\r?\n?$/) || [ - '', - 'nodejs/node', - ])[1]; - // eslint-disable-next-line no-unused-vars - } catch (_) { - return undefined; - } -} - -/** - * Grabs the current tag or commit hash (if tag isn't present) ina directory - * - * @example getGitTag('../node/lib') = 'v20.0.0' - * - * @param {string} directory Directory to check - * @returns {string | undefined} - */ -export function getGitTag(directory) { - try { - const hash = - execSync(`cd ${directory} && git log -1 --pretty=%H`) || 'main'; - const tag = - execSync(`cd ${directory} && git describe --contains ${hash}`).split( - '\n' - )[0] || hash; - - return tag; - // eslint-disable-next-line no-unused-vars - } catch (_) { - return undefined; - } -} From 953f2485c576ed67b2115b36d5d7d59970943b1b Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:21:38 -0800 Subject: [PATCH 5/6] partial review addressed Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- bin/cli.mjs | 18 ++++--- package-lock.json | 2 +- src/generators/api-links/index.mjs | 2 + .../api-links/utils/getBaseGitHubUrl.mjs | 10 ++-- src/generators/ast-js/index.mjs | 47 ++++++++++++++++++ src/generators/index.mjs | 2 + src/generators/types.d.ts | 4 ++ src/index.mjs | 4 +- src/loader.mjs | 19 +++++--- src/parser.mjs | 48 +++++++++---------- 10 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 src/generators/ast-js/index.mjs diff --git a/bin/cli.mjs b/bin/cli.mjs index af8d198..012cea9 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -9,8 +9,8 @@ import { coerce } from 'semver'; import { DOC_NODE_CHANGELOG_URL, DOC_NODE_VERSION } from '../src/constants.mjs'; import createGenerator from '../src/generators.mjs'; import generators from '../src/generators/index.mjs'; -import createLoader from '../src/loader.mjs'; -import createParser from '../src/parser.mjs'; +import { createMarkdownLoader } from '../src/loader.mjs'; +import { createMarkdownParser } from '../src/parser.mjs'; import createNodeReleases from '../src/releases.mjs'; const availableGenerators = Object.keys(generators); @@ -68,18 +68,14 @@ program */ const { input, output, target = [], version, changelog } = program.opts(); -const { loadMarkdownFiles, loadJsFiles } = createLoader(); -const { parseApiDocs, parseJsSources } = createParser(); +const { loadFiles } = createMarkdownLoader(); +const { parseApiDocs } = createMarkdownParser(); -const apiDocFiles = loadMarkdownFiles(input); +const apiDocFiles = loadFiles(input); const parsedApiDocs = await parseApiDocs(apiDocFiles); -const sourceFiles = loadJsFiles(input); - -const parsedJsFiles = await parseJsSources(sourceFiles); - -const { runGenerators } = createGenerator(parsedApiDocs, parsedJsFiles); +const { runGenerators } = createGenerator(parsedApiDocs); // Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file const { getAllMajors } = createNodeReleases(changelog); @@ -87,6 +83,8 @@ const { getAllMajors } = createNodeReleases(changelog); await runGenerators({ // A list of target modes for the API docs parser generators: target, + // Resolved `input` to be used + input: input, // Resolved `output` path to be used output: resolve(output), // Resolved SemVer of current Node.js version diff --git a/package-lock.json b/package-lock.json index 787913b..777215e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,8 @@ "dependencies": { "acorn": "^8.14.0", "commander": "^13.1.0", - "estree-util-visit": "^2.0.0", "dedent": "^1.5.3", + "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "glob": "^11.0.1", "hast-util-to-string": "^3.0.1", diff --git a/src/generators/api-links/index.mjs b/src/generators/api-links/index.mjs index 86d9de2..b0b0064 100644 --- a/src/generators/api-links/index.mjs +++ b/src/generators/api-links/index.mjs @@ -30,6 +30,8 @@ export default { description: 'Creates a mapping of publicly accessible functions to their source locations in the Node.js repository.', + // Unlike the rest of the generators, this utilizes Javascript sources being + // passed into the input field rather than Markdown. dependsOn: 'ast-js', /** diff --git a/src/generators/api-links/utils/getBaseGitHubUrl.mjs b/src/generators/api-links/utils/getBaseGitHubUrl.mjs index a1c5013..8a4088a 100644 --- a/src/generators/api-links/utils/getBaseGitHubUrl.mjs +++ b/src/generators/api-links/utils/getBaseGitHubUrl.mjs @@ -13,14 +13,14 @@ export function getBaseGitHubUrl(cwd) { // Ex/ git@github.com:nodejs/node.git -> https://github.com/nodejs/node.git let [, repository] = url.split(':'); - // Trim off the trailing .git if it exists - if (repository.endsWith('.git')) { - repository = repository.substring(0, repository.length - 4); - } - url = `https://github.com/${repository}`; } + // https://github.com/nodejs/node.git -> https://github.com/nodejs/node + if (url.endsWith('.git')) { + url = url.substring(0, url.length - 4); + } + return url; } diff --git a/src/generators/ast-js/index.mjs b/src/generators/ast-js/index.mjs new file mode 100644 index 0000000..8aa1e26 --- /dev/null +++ b/src/generators/ast-js/index.mjs @@ -0,0 +1,47 @@ +import { createJsLoader } from '../../loader.mjs'; +import { createJsParser } from '../../parser.mjs'; + +/** + * This generator parses Javascript sources passed into the generator's input + * field. This is separate from the Markdown parsing step since it's not as + * commonly used and can take up a significant amount of memory. + * + * Putting this with the rest of the generators allows it to be lazily loaded + * so we're only parsing the Javascript sources when we need to. + * + * @typedef {unknown} Input + * + * @type {import('../types.d.ts').GeneratorMetadata>} + */ +export default { + name: 'ast-js', + + version: '1.0.0', + + description: 'Parses Javascript source files passed into the input.', + + dependsOn: 'ast', + + /** + * @param {Input} _ + * @param {Partial} options + */ + async generate(_, options) { + const { loadFiles } = createJsLoader(); + + if (!options.input) { + return []; + } + + // Load all of the Javascript sources into memory + const sourceFiles = loadFiles(options.input); + + const { parseJsSources } = createJsParser(); + + // Parse the Javascript sources into ASTs + const parsedJsFiles = await parseJsSources(sourceFiles); + + // Return the ASTs so they can be used in another generator + return parsedJsFiles; + }, +}; diff --git a/src/generators/index.mjs b/src/generators/index.mjs index 512271c..d2ff094 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -8,6 +8,7 @@ import legacyJson from './legacy-json/index.mjs'; import legacyJsonAll from './legacy-json-all/index.mjs'; import addonVerify from './addon-verify/index.mjs'; import apiLinks from './api-links/index.mjs'; +import astJs from './ast-js/index.mjs'; export default { 'json-simple': jsonSimple, @@ -18,4 +19,5 @@ export default { 'legacy-json-all': legacyJsonAll, 'addon-verify': addonVerify, 'api-links': apiLinks, + 'ast-js': astJs, }; diff --git a/src/generators/types.d.ts b/src/generators/types.d.ts index 718b2ab..bae5e4d 100644 --- a/src/generators/types.d.ts +++ b/src/generators/types.d.ts @@ -9,6 +9,10 @@ declare global { // This is the runtime config passed to the API doc generators export interface GeneratorOptions { + // The path to the input source files. This parameter accepts globs and can + // be a glob when passed to a generator. + input: string | string[]; + // The path used to output generated files, this is to be considered // the base path that any generator will use for generating files // This parameter accepts globs but when passed to generators will contain diff --git a/src/index.mjs b/src/index.mjs index af51f3d..4c9cfc9 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,8 +1,8 @@ export * as constants from './constants.mjs'; export { default as generators } from './generators/index.mjs'; export { default as createGenerator } from './generators.mjs'; -export { default as createLoader } from './loader.mjs'; +export * from './loader.mjs'; export { default as createMetadata } from './metadata.mjs'; -export { default as createParser } from './parser.mjs'; +export * from './parser.mjs'; export { default as createQueries } from './queries.mjs'; export { default as createNodeReleases } from './releases.mjs'; diff --git a/src/loader.mjs b/src/loader.mjs index 2a7e886..f86fbf6 100644 --- a/src/loader.mjs +++ b/src/loader.mjs @@ -11,7 +11,7 @@ import { VFile } from 'vfile'; * could be used for different things, but here we want to use it to load * Markdown files and transform them into VFiles */ -const createLoader = () => { +export const createMarkdownLoader = () => { /** * Loads API Doc files and transforms it into VFiles * @@ -21,7 +21,7 @@ const createLoader = () => { * * @see https://code.visualstudio.com/docs/editor/glob-patterns */ - const loadMarkdownFiles = searchPath => { + const loadFiles = searchPath => { const resolvedFiles = globSync(searchPath).filter( filePath => extname(filePath) === '.md' ); @@ -33,12 +33,19 @@ const createLoader = () => { }); }; + return { loadFiles }; +}; + +/** + * This creates a "loader" for loading Javascript source files into VFiles. + */ +export const createJsLoader = () => { /** * Loads the JavaScript source files and transforms them into VFiles * - * @param {Array} searchPath + * @param {string | Array} searchPath */ - const loadJsFiles = searchPath => { + const loadFiles = searchPath => { const resolvedFiles = globSync(searchPath).filter( filePath => extname(filePath) === '.js' ); @@ -50,7 +57,5 @@ const createLoader = () => { }); }; - return { loadMarkdownFiles, loadJsFiles }; + return { loadFiles }; }; - -export default createLoader; diff --git a/src/parser.mjs b/src/parser.mjs index 59f88ea..aba84b8 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -16,7 +16,7 @@ import { createNodeSlugger } from './utils/slugger.mjs'; /** * Creates an API doc parser for a given Markdown API doc file */ -const createParser = () => { +export const createMarkdownParser = () => { // Creates an instance of the Remark processor with GFM support // which is used for stringifying the AST tree back to Markdown const remarkProcessor = getRemark(); @@ -183,38 +183,40 @@ const createParser = () => { return resolvedApiDocEntries.flat(); }; + return { parseApiDocs, parseApiDoc }; +}; + +/** + * Creates a Javascript source parser for a given source file + */ +export const createJsParser = () => { /** * Parses a given JavaScript file into an ESTree AST representation of it * - * @param {import('vfile').VFile | Promise} apiDoc + * @param {import('vfile').VFile | Promise} sourceFile * @returns {Promise} */ - const parseJsSource = async apiDoc => { + const parseJsSource = async sourceFile => { // We allow the API doc VFile to be a Promise of a VFile also, // hence we want to ensure that it first resolves before we pass it to the parser - const resolvedApiDoc = await Promise.resolve(apiDoc); + const resolvedSourceFile = await Promise.resolve(sourceFile); - if (typeof resolvedApiDoc.value !== 'string') { + if (typeof resolvedSourceFile.value !== 'string') { throw new TypeError( - `expected resolvedApiDoc.value to be string but got ${typeof resolvedApiDoc.value}` + `expected resolvedSourceFile.value to be string but got ${typeof resolvedSourceFile.value}` ); } - try { - const res = acorn.parse(resolvedApiDoc.value, { - allowReturnOutsideFunction: true, - ecmaVersion: 'latest', - locations: true, - }); - - return { - ...res, - path: resolvedApiDoc.path, - }; - } catch (err) { - console.log(`error parsing ${resolvedApiDoc.basename}`); - throw err; - } + const res = acorn.parse(resolvedSourceFile.value, { + allowReturnOutsideFunction: true, + ecmaVersion: 'latest', + locations: true, + }); + + return { + ...res, + path: resolvedSourceFile.path, + }; }; /** @@ -231,7 +233,5 @@ const createParser = () => { return resolvedApiDocEntries; }; - return { parseApiDocs, parseApiDoc, parseJsSources, parseJsSource }; + return { parseJsSource, parseJsSources }; }; - -export default createParser; From 355e992cbb175f897cbb698d5fcce8573c9814e0 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:26:52 -0800 Subject: [PATCH 6/6] cleanup Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- src/generators.mjs | 3 +- .../utils/checkIndirectReferences.mjs | 1 - .../api-links/utils/extractExports.mjs | 109 ++---------------- .../utils/handleExportedObjectExpression.mjs | 95 +++++++++++++++ .../handleExportedPropertyExpression.mjs | 44 +++++++ 5 files changed, 150 insertions(+), 102 deletions(-) create mode 100644 src/generators/api-links/utils/handleExportedObjectExpression.mjs create mode 100644 src/generators/api-links/utils/handleExportedPropertyExpression.mjs diff --git a/src/generators.mjs b/src/generators.mjs index ef51628..028cef3 100644 --- a/src/generators.mjs +++ b/src/generators.mjs @@ -23,7 +23,7 @@ import availableGenerators from './generators/index.mjs'; * @param {ApiDocMetadataEntry} markdownInput The parsed API doc metadata entries * @param {Array} parsedJsFiles */ -const createGenerator = (markdownInput, jsInput) => { +const createGenerator = markdownInput => { /** * We store all the registered generators to be processed * within a Record, so we can access their results at any time whenever needed @@ -33,7 +33,6 @@ const createGenerator = (markdownInput, jsInput) => { */ const cachedGenerators = { ast: Promise.resolve(markdownInput), - 'ast-js': Promise.resolve(jsInput), }; /** diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index d18b10a..1210677 100644 --- a/src/generators/api-links/utils/checkIndirectReferences.mjs +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -1,7 +1,6 @@ import { visit } from 'estree-util-visit'; /** - * * @param program * @param {import('../types.d.ts').ProgramExports} exports * @param {Record} nameToLineNumberMap diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 228eb5b..0c0d9c0 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,7 +1,8 @@ 'use strict'; import { visit } from 'estree-util-visit'; -import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; +import { handleExportedPropertyExpression } from './handleExportedPropertyExpression.mjs'; +import { handleExportedObjectExpression } from './handleExportedObjectExpression.mjs'; /** * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression @@ -18,8 +19,7 @@ function handleExpression(node, basename, nameToLineNumberMap) { return; } - // `a=b`, lhs=`a` and rhs=`b` - let { left: lhs, right: rhs, loc } = expression; + let { left: lhs } = expression; if (lhs.type !== 'MemberExpression') { return undefined; @@ -41,105 +41,16 @@ function handleExpression(node, basename, nameToLineNumberMap) { if (lhs.object.name === 'exports') { // This is an assignment to a property in `module.exports` or `exports` // (i.e. `module.exports.asd = ...`) - - switch (rhs.type) { - /** @see https://github.com/estree/estree/blob/master/es5.md#functionexpression */ - case 'FunctionExpression': { - // module.exports.something = () => {} - nameToLineNumberMap[`${basename}.${lhs.property.name}`] = - loc.start.line; - - break; - } - /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ - case 'Identifier': { - // Save this for later in case it's referenced - // module.exports.asd = something - if (rhs.name === lhs.property.name) { - exports.indirects[lhs.property.name] = - `${basename}.${lhs.property.name}`; - } - - break; - } - default: { - if (lhs.property.name !== undefined) { - // Something else, let's save it for when we're searching for - // declarations - exports.identifiers.push(lhs.property.name); - } - - break; - } - } + handleExportedPropertyExpression( + exports, + expression, + basename, + nameToLineNumberMap + ); } else if (lhs.object.name === 'module' && lhs.property.name === 'exports') { // This is an assignment to `module.exports` as a whole // (i.e. `module.exports = {}`) - - // We need to move right until we find the value of the assignment. - // (if `a=b`, we want `b`) - while (rhs.type === 'AssignmentExpression') { - rhs = rhs.right; - } - - switch (rhs.type) { - /** @see https://github.com/estree/estree/blob/master/es5.md#newexpression */ - case 'NewExpression': { - // module.exports = new Asd() - exports.ctors.push(rhs.callee.name); - break; - } - /** @see https://github.com/estree/estree/blob/master/es5.md#objectexpression */ - case 'ObjectExpression': { - // module.exports = {} - // we need to go through all of the properties and register them - rhs.properties.forEach(({ value }) => { - switch (value.type) { - case 'Identifier': { - exports.identifiers.push(value.name); - - if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { - exports.ctors.push(value.name); - } - - break; - } - case 'CallExpression': { - if (value.callee.name !== 'deprecate') { - break; - } - - // Handle exports wrapped in the `deprecate` function - // Ex/ https://github.com/nodejs/node/blob/e96072ad57348ce423a8dd7639dcc3d1c34e847d/lib/buffer.js#L1334 - - exports.identifiers.push(value.arguments[0].name); - - break; - } - default: { - // Not relevant - } - } - }); - - break; - } - /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ - case 'Identifier': { - // Something else, let's save it for when we're searching for - // declarations - - if (rhs.name !== undefined) { - exports.identifiers.push(rhs.name); - } - - break; - } - default: { - // Not relevant - break; - } - } + handleExportedObjectExpression(exports, expression); } return exports; diff --git a/src/generators/api-links/utils/handleExportedObjectExpression.mjs b/src/generators/api-links/utils/handleExportedObjectExpression.mjs new file mode 100644 index 0000000..b66d810 --- /dev/null +++ b/src/generators/api-links/utils/handleExportedObjectExpression.mjs @@ -0,0 +1,95 @@ +'use strict'; + +import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; + +/** + * @param {import('../types').ProgramExports} exports + * @param {import('acorn').NewExpression} rhs + */ +function handleNewExpression(exports, rhs) { + // module.exports = new Asd() + exports.ctors.push(rhs.callee.name); +} + +/** + * @param {import('../types').ProgramExports} exports + * @param {import('acorn').ObjectExpression} rhs + */ +function handleObjectExpression(exports, rhs) { + // module.exports = {} + // We need to go through all of the properties and register them + rhs.properties.forEach(({ value }) => { + switch (value.type) { + case 'Identifier': { + exports.identifiers.push(value.name); + + if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { + exports.ctors.push(value.name); + } + + break; + } + case 'CallExpression': { + if (value.callee.name !== 'deprecate') { + break; + } + + // Handle exports wrapped in the `deprecate` function + // Ex/ https://github.com/nodejs/node/blob/e96072ad57348ce423a8dd7639dcc3d1c34e847d/lib/buffer.js#L1334 + + exports.identifiers.push(value.arguments[0].name); + + break; + } + default: { + // Not relevant + } + } + }); +} + +/** + * @param {import('../types').ProgramExports} exports + * @param {import('acorn').Identifier} rhs + */ +function handleIdentifier(exports, rhs) { + // Something else, let's save it for when we're searching for + // declarations + if (rhs.name !== undefined) { + exports.identifiers.push(rhs.name); + } +} + +/** + * @param {import('../types').ProgramExports} exports + * @param {import('acorn').AssignmentExpression} param0 + */ +export function handleExportedObjectExpression(exports, { right: rhs }) { + // We need to move right until we find the value of the assignment. + // (if `a=b`, we want `b`) + while (rhs.type === 'AssignmentExpression') { + rhs = rhs.right; + } + + switch (rhs.type) { + /** @see https://github.com/estree/estree/blob/master/es5.md#newexpression */ + case 'NewExpression': { + handleNewExpression(exports, rhs); + break; + } + /** @see https://github.com/estree/estree/blob/master/es5.md#objectexpression */ + case 'ObjectExpression': { + handleObjectExpression(exports, rhs); + break; + } + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ + case 'Identifier': { + handleIdentifier(exports, rhs); + break; + } + default: { + // Not relevant + break; + } + } +} diff --git a/src/generators/api-links/utils/handleExportedPropertyExpression.mjs b/src/generators/api-links/utils/handleExportedPropertyExpression.mjs new file mode 100644 index 0000000..6f6ee58 --- /dev/null +++ b/src/generators/api-links/utils/handleExportedPropertyExpression.mjs @@ -0,0 +1,44 @@ +'use strict'; + +/** + * @param {import('../types.d.ts').ProgramExports} exports + * @param {import('acorn').AssignmentExpression} param1 + * @param {string} basename + * @param {Record} nameToLineNumberMap + */ +export function handleExportedPropertyExpression( + exports, + { left: lhs, right: rhs, loc }, + basename, + nameToLineNumberMap +) { + switch (rhs.type) { + /** @see https://github.com/estree/estree/blob/master/es5.md#functionexpression */ + case 'FunctionExpression': { + // module.exports.something = () => {} + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = loc.start.line; + + break; + } + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ + case 'Identifier': { + // Save this for later in case it's referenced + // module.exports.asd = something + if (rhs.name === lhs.property.name) { + exports.indirects[lhs.property.name] = + `${basename}.${lhs.property.name}`; + } + + break; + } + default: { + if (lhs.property.name !== undefined) { + // Something else, let's save it for when we're searching for + // declarations + exports.identifiers.push(lhs.property.name); + } + + break; + } + } +}