From 36655096635b852cabe16fe90b3bdbb1cbc26328 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:49:32 +0200 Subject: [PATCH 01/12] remove: acorn and use oxc-parser --- .c8rc.json | 4 +- .github/dependabot.yml | 2 +- package.json | 1 - .../utils/checkIndirectReferences.mjs | 11 ++- .../api-links/utils/extractExports.mjs | 72 ++++++++++------- .../api-links/utils/findDefinitions.mjs | 80 +++++++++++++------ .../api-links/utils/getLineNumber.mjs | 15 ++++ src/generators/ast-js/README.md | 2 +- src/generators/ast-js/generate.mjs | 12 +-- 9 files changed, 135 insertions(+), 64 deletions(-) create mode 100644 src/generators/api-links/utils/getLineNumber.mjs diff --git a/.c8rc.json b/.c8rc.json index 3c63a3ff..226cc592 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -3,8 +3,10 @@ "exclude": [ "eslint.config.mjs", "**/fixtures", + "**/__tests__", "src/generators/legacy-html/assets", "src/generators/web/ui", - "**/*.d.ts" + "**/*.d.ts", + "www" ] } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 845dd7f5..5e47b54f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -68,7 +68,7 @@ updates: - 'hast-*' - 'mdast-*' - 'hastscript' - - 'acorn' + - '@oxc-project/types' recma: patterns: - 'recma-*' diff --git a/package.json b/package.json index 715add8a..27b2142d 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "@orama/ui": "^1.5.4", "@rollup/plugin-virtual": "^3.0.2", "@swc/html-wasm": "^1.15.43", - "acorn": "^8.17.0", "commander": "^15.0.0", "dedent": "^1.7.2", "estree-util-to-js": "^2.0.0", diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index 982bb111..047e87b3 100644 --- a/src/generators/api-links/utils/checkIndirectReferences.mjs +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -1,7 +1,9 @@ import { visit } from 'estree-util-visit'; +import { getLineNumber } from './getLineNumber.mjs'; + /** - * @param {import('acorn').Program} program + * @param {import('@oxc-project/types').Program} program * @param {import('../types.d.ts').ProgramExports} exports * @param {Record} nameToLineNumberMap */ @@ -11,14 +13,17 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { } visit(program, node => { - if (!node.loc || node.type !== 'FunctionDeclaration') { + if (node.type !== 'FunctionDeclaration') { return; } const name = node.id.name; if (name in exports.indirects) { - nameToLineNumberMap[exports.indirects[name]] = node.loc.start.line; + nameToLineNumberMap[exports.indirects[name]] = getLineNumber( + program.sourceText, + node.range[0] + ); } }); } diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 60746eca..0e4685ac 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -2,17 +2,19 @@ import { visit } from 'estree-util-visit'; +import { getLineNumber } from './getLineNumber.mjs'; import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; /** * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression * - * @param {import('acorn').ExpressionStatement} node + * @param {import('@oxc-project/types').ExpressionStatement} node * @param {string} basename * @param {Record} nameToLineNumberMap + * @param {string} sourceText * @returns {import('../types').ProgramExports | undefined} */ -function handleExpression(node, basename, nameToLineNumberMap) { +function handleExpression(node, basename, nameToLineNumberMap, sourceText) { const { expression } = node; if (expression.type !== 'AssignmentExpression') { @@ -20,7 +22,7 @@ function handleExpression(node, basename, nameToLineNumberMap) { } // `a=b`, lhs=`a` and rhs=`b` - let { left: lhs, right: rhs, loc } = expression; + let { left: lhs, right: rhs } = expression; if (lhs.type !== 'MemberExpression') { return undefined; @@ -41,32 +43,29 @@ 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 = ...`) + // (i.e. `module.exports.asd = ...` or `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; + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( + sourceText, + node.range[0] + ); 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}`; - } + // exports.Buffer = Buffer -> indirect mapping + exports.indirects[rhs.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); } @@ -132,6 +131,9 @@ function handleExpression(node, basename, nameToLineNumberMap) { if (rhs.name !== undefined) { exports.identifiers.push(rhs.name); + if (CONSTRUCTOR_EXPRESSION.test(rhs.name[0])) { + exports.ctors.push(rhs.name); + } } break; @@ -149,12 +151,18 @@ function handleExpression(node, basename, nameToLineNumberMap) { /** * @see https://github.com/estree/estree/blob/master/es5.md#variabledeclaration * - * @param {import('acorn').VariableDeclaration} node + * @param {import('@oxc-project/types').VariableDeclaration} node * @param {string} basename * @param {Record} nameToLineNumberMap + * @param {string} sourceText * @returns {import('../types').ProgramExports | undefined} */ -function handleVariableDeclaration(node, basename, nameToLineNumberMap) { +function handleVariableDeclaration( + node, + basename, + nameToLineNumberMap, + sourceText +) { /** * @type {import('../types').ProgramExports} */ @@ -164,7 +172,10 @@ function handleVariableDeclaration(node, basename, nameToLineNumberMap) { indirects: {}, }; - node.declarations.forEach(({ init: lhs, id }) => { + node.declarations.forEach(declarator => { + let lhs = declarator.init; + const id = declarator.id; + while (lhs && lhs.type === 'AssignmentExpression') { // Move left until we get to what we're assigning to // (if `a=b`, we want `a`) @@ -177,10 +188,14 @@ function handleVariableDeclaration(node, basename, nameToLineNumberMap) { return; } + const range = declarator.range || node.range; + switch (lhs.object.name) { case 'exports': { - nameToLineNumberMap[`${basename}.${lhs.property.name}`] = - node.loc.start.line; + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( + sourceText, + range[0] + ); break; } @@ -190,7 +205,7 @@ function handleVariableDeclaration(node, basename, nameToLineNumberMap) { } exports.ctors.push(id.name); - nameToLineNumberMap[id.name] = node.loc.start.line; + nameToLineNumberMap[id.name] = getLineNumber(sourceText, range[0]); break; } @@ -212,7 +227,7 @@ function handleVariableDeclaration(node, basename, nameToLineNumberMap) { * Noteworthy that exports can happen throughout the program so we need to * go through the entire thing. * - * @param {import('acorn').Program} program + * @param {import('@oxc-project/types').Program} program * @param {string} basename * @param {Record} nameToLineNumberMap * @returns {import('../types').ProgramExports} @@ -229,23 +244,24 @@ export function extractExports(program, basename, nameToLineNumberMap) { const TYPE_TO_HANDLER_MAP = { /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').Node} node */ ExpressionStatement: node => - handleExpression(node, basename, nameToLineNumberMap), + handleExpression(node, basename, nameToLineNumberMap, program.sourceText), /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').Node} node */ VariableDeclaration: node => - handleVariableDeclaration(node, basename, nameToLineNumberMap), + handleVariableDeclaration( + node, + basename, + nameToLineNumberMap, + program.sourceText + ), }; visit(program, node => { - if (!node.loc) { - return; - } - if (node.type in TYPE_TO_HANDLER_MAP) { const handler = TYPE_TO_HANDLER_MAP[node.type]; diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index a161e3ed..8b4295c4 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -2,14 +2,22 @@ import { visit } from 'estree-util-visit'; +import { getLineNumber } from './getLineNumber.mjs'; + /** * @see https://github.com/estree/estree/blob/master/es5.md#expressionstatement * - * @param {import('acorn').ExpressionStatement} node + * @param {import('@oxc-project/types').ExpressionStatement} node * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports + * @param {string} sourceText */ -function handleAssignmentExpression(node, nameToLineNumberMap, exports) { +function handleAssignmentExpression( + node, + nameToLineNumberMap, + exports, + sourceText +) { const { expression } = node; if (expression.type !== 'AssignmentExpression') { @@ -78,24 +86,26 @@ function handleAssignmentExpression(node, nameToLineNumberMap, exports) { */ const name = `${objectName}${lhs.computed ? `[${lhs.property.name}]` : `.${lhs.property.name}`}`; - nameToLineNumberMap[name] = node.loc.start.line; + nameToLineNumberMap[name] = getLineNumber(sourceText, node.range[0]); - if (lhs.property.name === rhs.name) { + if (rhs && rhs.type === 'Identifier' && lhs.property.name === rhs.name) { exports.indirects[rhs.name] = name; } } /** - * @param {import('acorn').FunctionDeclaration} node + * @param {import('@oxc-project/types').FunctionDeclaration} node * @param {string} basename * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports + * @param {string} sourceText */ function handleFunctionDeclaration( node, basename, nameToLineNumberMap, - exports + exports, + sourceText ) { if (!exports.identifiers.includes(node.id.name)) { // Function isn't exported, not relevant to us @@ -107,15 +117,24 @@ function handleFunctionDeclaration( return; } - nameToLineNumberMap[`${basename}.${node.id.name}`] = node.loc.start.line; + nameToLineNumberMap[`${basename}.${node.id.name}`] = getLineNumber( + sourceText, + node.range[0] + ); } /** - * @param {import('acorn').ClassDeclaration} node + * @param {import('@oxc-project/types').ClassDeclaration} node * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports + * @param {string} sourceText */ -function handleClassDeclaration(node, nameToLineNumberMap, exports) { +function handleClassDeclaration( + node, + nameToLineNumberMap, + exports, + sourceText +) { if (!exports.ctors.includes(node.id.name)) { // Class isn't exported, not relevant to us return; @@ -124,22 +143,23 @@ function handleClassDeclaration(node, nameToLineNumberMap, exports) { // WASI -> wASI, Agent -> agent const name = node.id.name[0].toLowerCase() + node.id.name.substring(1); - nameToLineNumberMap[node.id.name] = node.loc.start.line; + nameToLineNumberMap[node.id.name] = getLineNumber(sourceText, node.range[0]); - node.body.body.forEach(({ key, type, kind, loc }) => { - if (!loc || type !== 'MethodDefinition') { + node.body.body.forEach(member => { + if (member.type !== 'MethodDefinition') { return; } + const { key, kind, range } = member; const outputKey = kind === 'constructor' ? `new ${node.id.name}` : `${name}.${key.name}`; - nameToLineNumberMap[outputKey] = loc.start.line; + nameToLineNumberMap[outputKey] = getLineNumber(sourceText, range[0]); }); } /** - * @param {import('acorn').Program} program + * @param {import('@oxc-project/types').Program} program * @param {string} basename * @param {Record} nameToLineNumberMap * @param {import('../types').ProgramExports} exports @@ -152,29 +172,41 @@ export function findDefinitions( ) { const TYPE_TO_HANDLER_MAP = { /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').Node} node */ ExpressionStatement: node => - handleAssignmentExpression(node, nameToLineNumberMap, exports), + handleAssignmentExpression( + node, + nameToLineNumberMap, + exports, + program.sourceText + ), /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').Node} node */ FunctionDeclaration: node => - handleFunctionDeclaration(node, basename, nameToLineNumberMap, exports), + handleFunctionDeclaration( + node, + basename, + nameToLineNumberMap, + exports, + program.sourceText + ), /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').Node} node */ ClassDeclaration: node => - handleClassDeclaration(node, nameToLineNumberMap, exports), + handleClassDeclaration( + node, + nameToLineNumberMap, + exports, + program.sourceText + ), }; visit(program, node => { - if (!node.loc) { - return; - } - if (node.type in TYPE_TO_HANDLER_MAP) { const handler = TYPE_TO_HANDLER_MAP[node.type]; diff --git a/src/generators/api-links/utils/getLineNumber.mjs b/src/generators/api-links/utils/getLineNumber.mjs new file mode 100644 index 00000000..8e24a4cb --- /dev/null +++ b/src/generators/api-links/utils/getLineNumber.mjs @@ -0,0 +1,15 @@ +/** + * + * @param {String} code + * @param {Number} offset + * @returns {Number} + */ +export function getLineNumber(code = '', offset = 0) { + let line = 1; + for (let i = 0; i < offset && i < code.length; i++) { + if (code[i] === '\n') { + line++; + } + } + return line; +} diff --git a/src/generators/ast-js/README.md b/src/generators/ast-js/README.md index a6b7b74c..c0b53010 100644 --- a/src/generators/ast-js/README.md +++ b/src/generators/ast-js/README.md @@ -1,6 +1,6 @@ # `ast-js` Generator -The `ast-js` generator parses JavaScript source files into AST (Abstract Syntax Tree) representations using the Acorn parser. +The `ast-js` generator parses JavaScript source files into AST (Abstract Syntax Tree) representations using the `oxc-parser` parser. ## Configuring diff --git a/src/generators/ast-js/generate.mjs b/src/generators/ast-js/generate.mjs index 303e5ec0..27c7aed7 100644 --- a/src/generators/ast-js/generate.mjs +++ b/src/generators/ast-js/generate.mjs @@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises'; import { extname } from 'node:path'; -import { parse } from 'acorn'; +import { parse } from 'oxc-parser'; import { globSync } from 'tinyglobby'; import getConfig from '../../utils/configuration/index.mjs'; @@ -22,12 +22,14 @@ export async function processChunk(inputSlice, itemIndices) { for (const path of filePaths) { const value = await readFile(path, 'utf-8'); - const parsed = parse(value, { - allowReturnOutsideFunction: true, - ecmaVersion: 'latest', - locations: true, + const result = await parse(path, value, { + lang: 'js', + sourceType: 'commonjs', + range: true, }); + const parsed = result.program; + parsed.sourceText = value; parsed.path = path; results.push(parsed); From d385c365ef3caad420d7262df271b61b00ba45e1 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:53:41 +0200 Subject: [PATCH 02/12] Update package-lock.json --- package-lock.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eff2cd84..46799f09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@node-core/doc-kit", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@node-core/doc-kit", - "version": "1.4.2", + "version": "1.4.3", "dependencies": { "@actions/core": "^3.0.0", "@heroicons/react": "^2.2.0", @@ -17,7 +17,6 @@ "@orama/ui": "^1.5.4", "@rollup/plugin-virtual": "^3.0.2", "@swc/html-wasm": "^1.15.43", - "acorn": "^8.17.0", "commander": "^15.0.0", "dedent": "^1.7.2", "estree-util-to-js": "^2.0.0", From 6f6ba88132f9e3bb1aa3030d57a98fd6d5516b30 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:07:33 +0200 Subject: [PATCH 03/12] try to optimize --- .../utils/checkIndirectReferences.mjs | 34 ++- .../api-links/utils/extractExports.mjs | 274 ++++++------------ .../api-links/utils/findDefinitions.mjs | 185 +++++------- .../api-links/utils/getLineNumber.mjs | 55 +++- 4 files changed, 225 insertions(+), 323 deletions(-) diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index 047e87b3..7c38b8e5 100644 --- a/src/generators/api-links/utils/checkIndirectReferences.mjs +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -1,4 +1,4 @@ -import { visit } from 'estree-util-visit'; +'use strict'; import { getLineNumber } from './getLineNumber.mjs'; @@ -12,18 +12,28 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { return; } - visit(program, node => { - if (node.type !== 'FunctionDeclaration') { - return; - } + const body = program.body; + if (!body) { + return; + } - const name = node.id.name; + const sourceText = program.sourceText; - if (name in exports.indirects) { - nameToLineNumberMap[exports.indirects[name]] = getLineNumber( - program.sourceText, - node.range[0] - ); + for (let i = 0; i < body.length; i++) { + let node = body[i]; + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + node = node.declaration; } - }); + + if (node.type === 'FunctionDeclaration') { + const name = node.id.name; + if (name in exports.indirects) { + nameToLineNumberMap[exports.indirects[name]] = getLineNumber( + sourceText, + node.range[0], + program + ); + } + } + } } diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 0e4685ac..2c6adbaa 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,282 +1,180 @@ 'use strict'; -import { visit } from 'estree-util-visit'; - import { getLineNumber } from './getLineNumber.mjs'; import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; /** - * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression * - * @param {import('@oxc-project/types').ExpressionStatement} node - * @param {string} basename - * @param {Record} nameToLineNumberMap - * @param {string} sourceText - * @returns {import('../types').ProgramExports | undefined} */ -function handleExpression(node, basename, nameToLineNumberMap, sourceText) { - const { expression } = node; - +function handleExpression( + node, + basename, + nameToLineNumberMap, + sourceText, + program, + exports +) { + const expression = node.expression; if (expression.type !== 'AssignmentExpression') { return; } - // `a=b`, lhs=`a` and rhs=`b` - let { left: lhs, right: rhs } = expression; + let lhs = expression.left; + let rhs = expression.right; if (lhs.type !== 'MemberExpression') { - return undefined; + return; } - if (lhs.object.type === 'MemberExpression') { lhs = lhs.object; } - /** - * @type {import('../types').ProgramExports} - */ - const exports = { - ctors: [], - identifiers: [], - indirects: {}, - }; - if (lhs.object.name === 'exports') { - // This is an assignment to a property in `module.exports` or `exports` - // (i.e. `module.exports.asd = ...` or `exports.asd = ...`) - switch (rhs.type) { - /** @see https://github.com/estree/estree/blob/master/es5.md#functionexpression */ - case 'FunctionExpression': { - // module.exports.something = () => {} + case 'FunctionExpression': nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( sourceText, - node.range[0] + node.range[0], + program ); - break; - } - /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ - case 'Identifier': { - // Save this for later in case it's referenced - // exports.Buffer = Buffer -> indirect mapping + case 'Identifier': exports.indirects[rhs.name] = `${basename}.${lhs.property.name}`; - break; - } - default: { + default: if (lhs.property.name !== undefined) { exports.identifiers.push(lhs.property.name); } - break; - } } } 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() + case 'NewExpression': 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); + case 'ObjectExpression': + // eslint-disable-next-line no-case-declarations + const props = rhs.properties; - if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { - exports.ctors.push(value.name); - } + for (let i = 0; i < props.length; i++) { + const value = props[i].value; - 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); + if (!value) { + continue; + } - break; + if (value.type === 'Identifier') { + exports.identifiers.push(value.name); + if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { + exports.ctors.push(value.name); } - default: { - // Not relevant + } else if (value.type === 'CallExpression') { + if (value.callee.name === 'deprecate' && value.arguments[0]) { + exports.identifiers.push(value.arguments[0].name); } } - }); - + } 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 - + case 'Identifier': if (rhs.name !== undefined) { exports.identifiers.push(rhs.name); if (CONSTRUCTOR_EXPRESSION.test(rhs.name[0])) { exports.ctors.push(rhs.name); } } - break; - } - default: { - // Not relevant - break; - } } } - - return exports; } /** - * @see https://github.com/estree/estree/blob/master/es5.md#variabledeclaration * - * @param {import('@oxc-project/types').VariableDeclaration} node - * @param {string} basename - * @param {Record} nameToLineNumberMap - * @param {string} sourceText - * @returns {import('../types').ProgramExports | undefined} */ function handleVariableDeclaration( node, basename, nameToLineNumberMap, - sourceText + sourceText, + program, + exports ) { - /** - * @type {import('../types').ProgramExports} - */ - const exports = { - ctors: [], - identifiers: [], - indirects: {}, - }; - - node.declarations.forEach(declarator => { + const declarations = node.declarations; + for (let i = 0; i < declarations.length; i++) { + const declarator = declarations[i]; let lhs = declarator.init; const id = declarator.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; + continue; } const range = declarator.range || node.range; - - switch (lhs.object.name) { - case 'exports': { - nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( - sourceText, - range[0] - ); - - break; - } - case 'module': { - if (lhs.property.name !== 'exports') { - break; - } - - exports.ctors.push(id.name); - nameToLineNumberMap[id.name] = getLineNumber(sourceText, range[0]); - - break; - } - default: { - // Not relevant to us - break; - } + if (lhs.object.name === 'exports') { + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( + sourceText, + range[0], + program + ); + } else if ( + lhs.object.name === 'module' && + lhs.property.name === 'exports' + ) { + exports.ctors.push(id.name); + nameToLineNumberMap[id.name] = getLineNumber( + sourceText, + range[0], + program + ); } - }); - - 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('@oxc-project/types').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: [], - indirects: {}, - }; + const exports = { ctors: [], identifiers: [], indirects: {} }; + const body = program.body; + if (!body) { + return exports; + } - const TYPE_TO_HANDLER_MAP = { - /** - * @param {import('@oxc-project/types').Node} node - */ - ExpressionStatement: node => - handleExpression(node, basename, nameToLineNumberMap, program.sourceText), + const sourceText = program.sourceText; - /** - * @param {import('@oxc-project/types').Node} node - */ - VariableDeclaration: node => + for (let i = 0; i < body.length; i++) { + let node = body[i]; + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + node = node.declaration; + } + + if (node.type === 'ExpressionStatement') { + handleExpression( + node, + basename, + nameToLineNumberMap, + sourceText, + program, + exports + ); + } else if (node.type === 'VariableDeclaration') { handleVariableDeclaration( node, basename, nameToLineNumberMap, - program.sourceText - ), - }; - - visit(program, node => { - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; - - const output = handler(node); - - if (output) { - exports.ctors.push(...output.ctors); - exports.identifiers.push(...output.identifiers); - - Object.keys(output.indirects).forEach(key => { - exports.indirects[key] = output.indirects[key]; - }); - } + sourceText, + program, + exports + ); } - }); + } return exports; } diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index 8b4295c4..6854b7c7 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,92 +1,55 @@ 'use strict'; -import { visit } from 'estree-util-visit'; - import { getLineNumber } from './getLineNumber.mjs'; /** - * @see https://github.com/estree/estree/blob/master/es5.md#expressionstatement * - * @param {import('@oxc-project/types').ExpressionStatement} node - * @param {Record} nameToLineNumberMap - * @param {import('../types').ProgramExports} exports - * @param {string} sourceText */ function handleAssignmentExpression( node, nameToLineNumberMap, exports, - sourceText + sourceText, + program ) { - const { expression } = node; - + const expression = node.expression; if (expression.type !== 'AssignmentExpression') { return; } - const { left: lhs, right: rhs } = expression; + const lhs = expression.left; + const rhs = expression.right; if (lhs.type !== 'MemberExpression') { - // 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; - } - - // Something like `ClassName.prototype.asd = 123` - object = lhs.object.object; - - objectName = object.name ? object.name : object.object.name; - objectName = objectName.toLowerCase(); - - // 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: { - // Not relevant to us + if (lhs.object.type === 'MemberExpression') { + if (lhs.object.property.name !== 'prototype') { return; } + object = lhs.object.object; + objectName = object.name ? object.name : object.object.name; + objectName = objectName.toLowerCase(); + if (objectName === 'buffer') { + objectName = 'buf'; + } + } else if (lhs.object.type === 'Identifier') { + object = lhs.object; + objectName = object.name; + } else { + return; } if (!exports.ctors.includes(object.name)) { - // The object being written to isn't exported, not relevant to us return; } - /** - * 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] = getLineNumber(sourceText, node.range[0]); + nameToLineNumberMap[name] = getLineNumber(sourceText, node.range[0], program); if (rhs && rhs.type === 'Identifier' && lhs.property.name === rhs.name) { exports.indirects[rhs.name] = name; @@ -94,75 +57,72 @@ function handleAssignmentExpression( } /** - * @param {import('@oxc-project/types').FunctionDeclaration} node - * @param {string} basename - * @param {Record} nameToLineNumberMap - * @param {import('../types').ProgramExports} exports - * @param {string} sourceText + * */ function handleFunctionDeclaration( node, basename, nameToLineNumberMap, exports, - sourceText + sourceText, + program ) { if (!exports.identifiers.includes(node.id.name)) { - // Function isn't exported, not relevant to us return; } - - if (basename.startsWith('_')) { - // Internal function, don't include it in the docs + // 95 is '_' - avoids string allocation/slicing in startsWith + if (basename.charCodeAt(0) === 95) { return; } nameToLineNumberMap[`${basename}.${node.id.name}`] = getLineNumber( sourceText, - node.range[0] + node.range[0], + program ); } /** - * @param {import('@oxc-project/types').ClassDeclaration} node - * @param {Record} nameToLineNumberMap - * @param {import('../types').ProgramExports} exports - * @param {string} sourceText + * */ function handleClassDeclaration( node, nameToLineNumberMap, exports, - sourceText + sourceText, + program ) { if (!exports.ctors.includes(node.id.name)) { - // Class isn't exported, not relevant to us return; } - // WASI -> wASI, Agent -> agent const name = node.id.name[0].toLowerCase() + node.id.name.substring(1); + nameToLineNumberMap[node.id.name] = getLineNumber( + sourceText, + node.range[0], + program + ); - nameToLineNumberMap[node.id.name] = getLineNumber(sourceText, node.range[0]); - - node.body.body.forEach(member => { + const members = node.body.body; + for (let i = 0; i < members.length; i++) { + const member = members[i]; if (member.type !== 'MethodDefinition') { - return; + continue; } const { key, kind, range } = member; const outputKey = kind === 'constructor' ? `new ${node.id.name}` : `${name}.${key.name}`; - - nameToLineNumberMap[outputKey] = getLineNumber(sourceText, range[0]); - }); + nameToLineNumberMap[outputKey] = getLineNumber( + sourceText, + range[0], + program + ); + } } /** - * @param {import('@oxc-project/types').Program} program - * @param {string} basename - * @param {Record} nameToLineNumberMap - * @param {import('../types').ProgramExports} exports + * */ export function findDefinitions( program, @@ -170,47 +130,44 @@ export function findDefinitions( nameToLineNumberMap, exports ) { - const TYPE_TO_HANDLER_MAP = { - /** - * @param {import('@oxc-project/types').Node} node - */ - ExpressionStatement: node => + const body = program.body; + if (!body) { + return; + } + + const sourceText = program.sourceText; + + for (let i = 0; i < body.length; i++) { + let node = body[i]; + if (node.type === 'ExportNamedDeclaration' && node.declaration) { + node = node.declaration; + } + + if (node.type === 'ExpressionStatement') { handleAssignmentExpression( node, nameToLineNumberMap, exports, - program.sourceText - ), - - /** - * @param {import('@oxc-project/types').Node} node - */ - FunctionDeclaration: node => + sourceText, + program + ); + } else if (node.type === 'FunctionDeclaration') { handleFunctionDeclaration( node, basename, nameToLineNumberMap, exports, - program.sourceText - ), - - /** - * @param {import('@oxc-project/types').Node} node - */ - ClassDeclaration: node => + sourceText, + program + ); + } else if (node.type === 'ClassDeclaration') { handleClassDeclaration( node, nameToLineNumberMap, exports, - program.sourceText - ), - }; - - visit(program, node => { - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; - - handler(node); + sourceText, + program + ); } - }); + } } diff --git a/src/generators/api-links/utils/getLineNumber.mjs b/src/generators/api-links/utils/getLineNumber.mjs index 8e24a4cb..1d245c4f 100644 --- a/src/generators/api-links/utils/getLineNumber.mjs +++ b/src/generators/api-links/utils/getLineNumber.mjs @@ -1,15 +1,52 @@ +'use strict'; + +const programOffsetsCache = new WeakMap(); + /** * - * @param {String} code - * @param {Number} offset - * @returns {Number} */ -export function getLineNumber(code = '', offset = 0) { - let line = 1; - for (let i = 0; i < offset && i < code.length; i++) { - if (code[i] === '\n') { - line++; +export function getLineNumber(code = '', offset = 0, program) { + if (!code || offset <= 0) { + return 1; + } + + let offsets; + + if (program && typeof program === 'object') { + offsets = programOffsetsCache.get(program); + if (!offsets) { + offsets = [0]; + const len = code.length; + for (let i = 0; i < len; i++) { + // 10 is the char code for '\n'. Faster than string comparison. + if (code.charCodeAt(i) === 10) { + offsets.push(i + 1); + } + } + programOffsetsCache.set(program, offsets); + } + } else { + // Fallback if program AST isn't provided (avoids array allocation entirely) + let line = 1; + const len = Math.min(offset, code.length); + for (let i = 0; i < len; i++) { + if (code.charCodeAt(i) === 10) { + line++; + } + } + return line; + } + + // Binary search + let low = 0; + let high = offsets.length - 1; + while (low <= high) { + const mid = (low + high) >> 1; + if (offsets[mid] <= offset) { + low = mid + 1; + } else { + high = mid - 1; } } - return line; + return low; } From 6f20da707baa566dc561cdef12224a21ca5452de Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:20:16 +0200 Subject: [PATCH 04/12] Revert "try to optimize" This reverts commit 6f6ba88132f9e3bb1aa3030d57a98fd6d5516b30. --- .../utils/checkIndirectReferences.mjs | 34 +-- .../api-links/utils/extractExports.mjs | 274 ++++++++++++------ .../api-links/utils/findDefinitions.mjs | 185 +++++++----- .../api-links/utils/getLineNumber.mjs | 55 +--- 4 files changed, 323 insertions(+), 225 deletions(-) diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index 7c38b8e5..047e87b3 100644 --- a/src/generators/api-links/utils/checkIndirectReferences.mjs +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -1,4 +1,4 @@ -'use strict'; +import { visit } from 'estree-util-visit'; import { getLineNumber } from './getLineNumber.mjs'; @@ -12,28 +12,18 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { return; } - const body = program.body; - if (!body) { - return; - } - - const sourceText = program.sourceText; - - for (let i = 0; i < body.length; i++) { - let node = body[i]; - if (node.type === 'ExportNamedDeclaration' && node.declaration) { - node = node.declaration; + visit(program, node => { + if (node.type !== 'FunctionDeclaration') { + return; } - if (node.type === 'FunctionDeclaration') { - const name = node.id.name; - if (name in exports.indirects) { - nameToLineNumberMap[exports.indirects[name]] = getLineNumber( - sourceText, - node.range[0], - program - ); - } + const name = node.id.name; + + if (name in exports.indirects) { + nameToLineNumberMap[exports.indirects[name]] = getLineNumber( + program.sourceText, + node.range[0] + ); } - } + }); } diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 2c6adbaa..0e4685ac 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,180 +1,282 @@ 'use strict'; +import { visit } from 'estree-util-visit'; + import { getLineNumber } from './getLineNumber.mjs'; import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; /** + * @see https://github.com/estree/estree/blob/master/es5.md#assignmentexpression * + * @param {import('@oxc-project/types').ExpressionStatement} node + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @param {string} sourceText + * @returns {import('../types').ProgramExports | undefined} */ -function handleExpression( - node, - basename, - nameToLineNumberMap, - sourceText, - program, - exports -) { - const expression = node.expression; +function handleExpression(node, basename, nameToLineNumberMap, sourceText) { + const { expression } = node; + if (expression.type !== 'AssignmentExpression') { return; } - let lhs = expression.left; - let rhs = expression.right; + // `a=b`, lhs=`a` and rhs=`b` + let { left: lhs, right: rhs } = expression; if (lhs.type !== 'MemberExpression') { - return; + return undefined; } + if (lhs.object.type === 'MemberExpression') { lhs = lhs.object; } + /** + * @type {import('../types').ProgramExports} + */ + const exports = { + ctors: [], + identifiers: [], + indirects: {}, + }; + if (lhs.object.name === 'exports') { + // This is an assignment to a property in `module.exports` or `exports` + // (i.e. `module.exports.asd = ...` or `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}.${lhs.property.name}`] = getLineNumber( sourceText, - node.range[0], - program + node.range[0] ); + break; - case 'Identifier': + } + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ + case 'Identifier': { + // Save this for later in case it's referenced + // exports.Buffer = Buffer -> indirect mapping exports.indirects[rhs.name] = `${basename}.${lhs.property.name}`; + break; - default: + } + default: { if (lhs.property.name !== undefined) { exports.identifiers.push(lhs.property.name); } + break; + } } } 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) { - 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': - // eslint-disable-next-line no-case-declarations - const props = rhs.properties; + } + /** @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); - for (let i = 0; i < props.length; i++) { - const value = props[i].value; + if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { + exports.ctors.push(value.name); + } - if (!value) { - continue; - } - - if (value.type === 'Identifier') { - exports.identifiers.push(value.name); - if (CONSTRUCTOR_EXPRESSION.test(value.name[0])) { - exports.ctors.push(value.name); + break; } - } else if (value.type === 'CallExpression') { - if (value.callee.name === 'deprecate' && value.arguments[0]) { + 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; - case 'Identifier': + } + /** @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); if (CONSTRUCTOR_EXPRESSION.test(rhs.name[0])) { exports.ctors.push(rhs.name); } } + break; + } + default: { + // Not relevant + break; + } } } + + return exports; } /** + * @see https://github.com/estree/estree/blob/master/es5.md#variabledeclaration * + * @param {import('@oxc-project/types').VariableDeclaration} node + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @param {string} sourceText + * @returns {import('../types').ProgramExports | undefined} */ function handleVariableDeclaration( node, basename, nameToLineNumberMap, - sourceText, - program, - exports + sourceText ) { - const declarations = node.declarations; - for (let i = 0; i < declarations.length; i++) { - const declarator = declarations[i]; + /** + * @type {import('../types').ProgramExports} + */ + const exports = { + ctors: [], + identifiers: [], + indirects: {}, + }; + + node.declarations.forEach(declarator => { let lhs = declarator.init; const id = declarator.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') { - continue; + // Doesn't exist or we're not writing to an object + // (aka it's just a regular variable like `const a = 123`) + return; } const range = declarator.range || node.range; - if (lhs.object.name === 'exports') { - nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( - sourceText, - range[0], - program - ); - } else if ( - lhs.object.name === 'module' && - lhs.property.name === 'exports' - ) { - exports.ctors.push(id.name); - nameToLineNumberMap[id.name] = getLineNumber( - sourceText, - range[0], - program - ); + + switch (lhs.object.name) { + case 'exports': { + nameToLineNumberMap[`${basename}.${lhs.property.name}`] = getLineNumber( + sourceText, + range[0] + ); + + break; + } + case 'module': { + if (lhs.property.name !== 'exports') { + break; + } + + exports.ctors.push(id.name); + nameToLineNumberMap[id.name] = getLineNumber(sourceText, range[0]); + + break; + } + default: { + // Not relevant to us + break; + } } - } + }); + + 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('@oxc-project/types').Program} program + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @returns {import('../types').ProgramExports} */ export function extractExports(program, basename, nameToLineNumberMap) { - const exports = { ctors: [], identifiers: [], indirects: {} }; - const body = program.body; - if (!body) { - return exports; - } + /** + * @type {import('../types').ProgramExports} + */ + const exports = { + ctors: [], + identifiers: [], + indirects: {}, + }; - const sourceText = program.sourceText; + const TYPE_TO_HANDLER_MAP = { + /** + * @param {import('@oxc-project/types').Node} node + */ + ExpressionStatement: node => + handleExpression(node, basename, nameToLineNumberMap, program.sourceText), - for (let i = 0; i < body.length; i++) { - let node = body[i]; - if (node.type === 'ExportNamedDeclaration' && node.declaration) { - node = node.declaration; - } - - if (node.type === 'ExpressionStatement') { - handleExpression( - node, - basename, - nameToLineNumberMap, - sourceText, - program, - exports - ); - } else if (node.type === 'VariableDeclaration') { + /** + * @param {import('@oxc-project/types').Node} node + */ + VariableDeclaration: node => handleVariableDeclaration( node, basename, nameToLineNumberMap, - sourceText, - program, - exports - ); + program.sourceText + ), + }; + + visit(program, node => { + if (node.type in TYPE_TO_HANDLER_MAP) { + const handler = TYPE_TO_HANDLER_MAP[node.type]; + + const output = handler(node); + + if (output) { + exports.ctors.push(...output.ctors); + exports.identifiers.push(...output.identifiers); + + Object.keys(output.indirects).forEach(key => { + exports.indirects[key] = output.indirects[key]; + }); + } } - } + }); return exports; } diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index 6854b7c7..8b4295c4 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,55 +1,92 @@ 'use strict'; +import { visit } from 'estree-util-visit'; + import { getLineNumber } from './getLineNumber.mjs'; /** + * @see https://github.com/estree/estree/blob/master/es5.md#expressionstatement * + * @param {import('@oxc-project/types').ExpressionStatement} node + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + * @param {string} sourceText */ function handleAssignmentExpression( node, nameToLineNumberMap, exports, - sourceText, - program + sourceText ) { - const expression = node.expression; + const { expression } = node; + if (expression.type !== 'AssignmentExpression') { return; } - const lhs = expression.left; - const rhs = expression.right; + const { left: lhs, right: rhs } = expression; if (lhs.type !== 'MemberExpression') { + // 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; - if (lhs.object.type === 'MemberExpression') { - if (lhs.object.property.name !== 'prototype') { - return; + switch (lhs.object.type) { + /** @see https://github.com/estree/estree/blob/master/es5.md#memberexpression */ + case 'MemberExpression': { + if (lhs.object.property.name !== 'prototype') { + return; + } + + // Something like `ClassName.prototype.asd = 123` + object = lhs.object.object; + + objectName = object.name ? object.name : object.object.name; + objectName = objectName.toLowerCase(); + + // 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; } - object = lhs.object.object; - objectName = object.name ? object.name : object.object.name; - objectName = objectName.toLowerCase(); - if (objectName === 'buffer') { - objectName = 'buf'; + /** @see https://github.com/estree/estree/blob/master/es5.md#identifier */ + case 'Identifier': { + object = lhs.object; + objectName = object.name; + + break; + } + default: { + // Not relevant to us + return; } - } else if (lhs.object.type === 'Identifier') { - object = lhs.object; - objectName = object.name; - } else { - return; } if (!exports.ctors.includes(object.name)) { + // The object being written to isn't exported, not relevant to us return; } + /** + * 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] = getLineNumber(sourceText, node.range[0], program); + + nameToLineNumberMap[name] = getLineNumber(sourceText, node.range[0]); if (rhs && rhs.type === 'Identifier' && lhs.property.name === rhs.name) { exports.indirects[rhs.name] = name; @@ -57,72 +94,75 @@ function handleAssignmentExpression( } /** - * + * @param {import('@oxc-project/types').FunctionDeclaration} node + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + * @param {string} sourceText */ function handleFunctionDeclaration( node, basename, nameToLineNumberMap, exports, - sourceText, - program + sourceText ) { if (!exports.identifiers.includes(node.id.name)) { + // Function isn't exported, not relevant to us return; } - // 95 is '_' - avoids string allocation/slicing in startsWith - if (basename.charCodeAt(0) === 95) { + + if (basename.startsWith('_')) { + // Internal function, don't include it in the docs return; } nameToLineNumberMap[`${basename}.${node.id.name}`] = getLineNumber( sourceText, - node.range[0], - program + node.range[0] ); } /** - * + * @param {import('@oxc-project/types').ClassDeclaration} node + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports + * @param {string} sourceText */ function handleClassDeclaration( node, nameToLineNumberMap, exports, - sourceText, - program + sourceText ) { if (!exports.ctors.includes(node.id.name)) { + // Class isn't exported, not relevant to us return; } + // WASI -> wASI, Agent -> agent const name = node.id.name[0].toLowerCase() + node.id.name.substring(1); - nameToLineNumberMap[node.id.name] = getLineNumber( - sourceText, - node.range[0], - program - ); - const members = node.body.body; - for (let i = 0; i < members.length; i++) { - const member = members[i]; + nameToLineNumberMap[node.id.name] = getLineNumber(sourceText, node.range[0]); + + node.body.body.forEach(member => { if (member.type !== 'MethodDefinition') { - continue; + return; } const { key, kind, range } = member; const outputKey = kind === 'constructor' ? `new ${node.id.name}` : `${name}.${key.name}`; - nameToLineNumberMap[outputKey] = getLineNumber( - sourceText, - range[0], - program - ); - } + + nameToLineNumberMap[outputKey] = getLineNumber(sourceText, range[0]); + }); } /** - * + * @param {import('@oxc-project/types').Program} program + * @param {string} basename + * @param {Record} nameToLineNumberMap + * @param {import('../types').ProgramExports} exports */ export function findDefinitions( program, @@ -130,44 +170,47 @@ export function findDefinitions( nameToLineNumberMap, exports ) { - const body = program.body; - if (!body) { - return; - } - - const sourceText = program.sourceText; - - for (let i = 0; i < body.length; i++) { - let node = body[i]; - if (node.type === 'ExportNamedDeclaration' && node.declaration) { - node = node.declaration; - } - - if (node.type === 'ExpressionStatement') { + const TYPE_TO_HANDLER_MAP = { + /** + * @param {import('@oxc-project/types').Node} node + */ + ExpressionStatement: node => handleAssignmentExpression( node, nameToLineNumberMap, exports, - sourceText, - program - ); - } else if (node.type === 'FunctionDeclaration') { + program.sourceText + ), + + /** + * @param {import('@oxc-project/types').Node} node + */ + FunctionDeclaration: node => handleFunctionDeclaration( node, basename, nameToLineNumberMap, exports, - sourceText, - program - ); - } else if (node.type === 'ClassDeclaration') { + program.sourceText + ), + + /** + * @param {import('@oxc-project/types').Node} node + */ + ClassDeclaration: node => handleClassDeclaration( node, nameToLineNumberMap, exports, - sourceText, - program - ); + program.sourceText + ), + }; + + visit(program, node => { + if (node.type in TYPE_TO_HANDLER_MAP) { + const handler = TYPE_TO_HANDLER_MAP[node.type]; + + handler(node); } - } + }); } diff --git a/src/generators/api-links/utils/getLineNumber.mjs b/src/generators/api-links/utils/getLineNumber.mjs index 1d245c4f..8e24a4cb 100644 --- a/src/generators/api-links/utils/getLineNumber.mjs +++ b/src/generators/api-links/utils/getLineNumber.mjs @@ -1,52 +1,15 @@ -'use strict'; - -const programOffsetsCache = new WeakMap(); - /** * + * @param {String} code + * @param {Number} offset + * @returns {Number} */ -export function getLineNumber(code = '', offset = 0, program) { - if (!code || offset <= 0) { - return 1; - } - - let offsets; - - if (program && typeof program === 'object') { - offsets = programOffsetsCache.get(program); - if (!offsets) { - offsets = [0]; - const len = code.length; - for (let i = 0; i < len; i++) { - // 10 is the char code for '\n'. Faster than string comparison. - if (code.charCodeAt(i) === 10) { - offsets.push(i + 1); - } - } - programOffsetsCache.set(program, offsets); - } - } else { - // Fallback if program AST isn't provided (avoids array allocation entirely) - let line = 1; - const len = Math.min(offset, code.length); - for (let i = 0; i < len; i++) { - if (code.charCodeAt(i) === 10) { - line++; - } - } - return line; - } - - // Binary search - let low = 0; - let high = offsets.length - 1; - while (low <= high) { - const mid = (low + high) >> 1; - if (offsets[mid] <= offset) { - low = mid + 1; - } else { - high = mid - 1; +export function getLineNumber(code = '', offset = 0) { + let line = 1; + for (let i = 0; i < offset && i < code.length; i++) { + if (code[i] === '\n') { + line++; } } - return low; + return line; } From 714cb0f42c101ad13d7f91ad82f35f1dc453276e Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:39:22 +0200 Subject: [PATCH 05/12] use `oxc-walker` --- package-lock.json | 142 +++++++++++++++++- package.json | 2 +- .../utils/checkIndirectReferences.mjs | 29 ++-- .../api-links/utils/extractExports.mjs | 29 ++-- .../api-links/utils/findDefinitions.mjs | 17 ++- 5 files changed, 184 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46799f09..81583c42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "commander": "^15.0.0", "dedent": "^1.7.2", "estree-util-to-js": "^2.0.0", - "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", @@ -28,6 +27,7 @@ "lightningcss-wasm": "^1.32.0", "mdast-util-slice-markdown": "^2.0.1", "oxc-parser": "^0.139.0", + "oxc-walker": "^1.0.0", "piscina": "^5.2.0", "preact": "^10.29.2", "preact-render-to-string": "^6.7.0", @@ -888,11 +888,30 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -902,14 +921,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -7265,6 +7282,27 @@ "node": "20 || >=22" } }, + "node_modules/magic-regexp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.11.0.tgz", + "integrity": "sha512-LG77Z/gVnwz7oaDpD4heX6ryl+lcr4l1B2gnP4MMvt2pGhGC1Dfj7dl1pXpP4ih+VQFLuAadeKVa+lARAzfW+Q==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.21", + "regexp-tree": "^0.1.27", + "type-level-regexp": "~0.1.17", + "unplugin": "^3.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8730,6 +8768,27 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/oxc-walker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-1.0.0.tgz", + "integrity": "sha512-eMsHflAGfOskpWxtp9xP/f5b96XLEU8ifTd2gOOCkdux9HMxKGy5S1ru0Gh1B3aPu+YbfmWUUVkcb7MrZz3XyQ==", + "license": "MIT", + "dependencies": { + "magic-regexp": "^0.11.0" + }, + "peerDependencies": { + "oxc-parser": ">=0.98.0", + "rolldown": ">=1.0.0" + }, + "peerDependenciesMeta": { + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } + } + }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -9368,6 +9427,15 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10464,6 +10532,12 @@ "node": ">= 0.8.0" } }, + "node_modules/type-level-regexp": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", + "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", + "license": "MIT" + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -10760,6 +10834,60 @@ "node": ">= 4.0.0" } }, + "node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -10925,6 +11053,12 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 27b2142d..3b1f7f34 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "commander": "^15.0.0", "dedent": "^1.7.2", "estree-util-to-js": "^2.0.0", - "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", @@ -81,6 +80,7 @@ "lightningcss-wasm": "^1.32.0", "mdast-util-slice-markdown": "^2.0.1", "oxc-parser": "^0.139.0", + "oxc-walker": "^1.0.0", "piscina": "^5.2.0", "preact": "^10.29.2", "preact-render-to-string": "^6.7.0", diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index 047e87b3..389d3f4b 100644 --- a/src/generators/api-links/utils/checkIndirectReferences.mjs +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -1,4 +1,4 @@ -import { visit } from 'estree-util-visit'; +import { walk } from 'oxc-walker'; import { getLineNumber } from './getLineNumber.mjs'; @@ -12,18 +12,23 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { return; } - visit(program, node => { - if (node.type !== 'FunctionDeclaration') { - return; - } + walk(program, { + /** + * + */ + enter(node) { + if (node.type !== 'FunctionDeclaration') { + return; + } - const name = node.id.name; + const name = node.id.name; - if (name in exports.indirects) { - nameToLineNumberMap[exports.indirects[name]] = getLineNumber( - program.sourceText, - node.range[0] - ); - } + if (name in exports.indirects) { + nameToLineNumberMap[exports.indirects[name]] = getLineNumber( + program.sourceText, + node.range[0] + ); + } + }, }); } diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 0e4685ac..1fc24265 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { visit } from 'estree-util-visit'; +import { walk } from 'oxc-walker'; import { getLineNumber } from './getLineNumber.mjs'; import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; @@ -261,21 +261,26 @@ export function extractExports(program, basename, nameToLineNumberMap) { ), }; - visit(program, node => { - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; + walk(program, { + /** + * + */ + enter(node) { + if (node.type in TYPE_TO_HANDLER_MAP) { + const handler = TYPE_TO_HANDLER_MAP[node.type]; - const output = handler(node); + const output = handler(node); - if (output) { - exports.ctors.push(...output.ctors); - exports.identifiers.push(...output.identifiers); + if (output) { + exports.ctors.push(...output.ctors); + exports.identifiers.push(...output.identifiers); - Object.keys(output.indirects).forEach(key => { - exports.indirects[key] = output.indirects[key]; - }); + Object.keys(output.indirects).forEach(key => { + exports.indirects[key] = output.indirects[key]; + }); + } } - } + }, }); return exports; diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index 8b4295c4..0b36b9e2 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { visit } from 'estree-util-visit'; +import { walk } from 'oxc-walker'; import { getLineNumber } from './getLineNumber.mjs'; @@ -206,11 +206,16 @@ export function findDefinitions( ), }; - visit(program, node => { - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; + walk(program, { + /** + * + */ + enter(node) { + if (node.type in TYPE_TO_HANDLER_MAP) { + const handler = TYPE_TO_HANDLER_MAP[node.type]; - handler(node); - } + handler(node); + } + }, }); } From aa1a145c7cbfc0f5149d95b9e514b5f23018f238 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:28:26 +0200 Subject: [PATCH 06/12] replace `estree-util-to-js` to `esrap` --- .github/dependabot.yml | 2 -- package-lock.json | 21 +++++++++++++++++-- package.json | 2 +- src/generators/jsx-ast/generate.mjs | 5 +++-- .../web/__tests__/generate.test.mjs | 5 +++-- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5e47b54f..02659360 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -64,11 +64,9 @@ updates: - 'rehype-*' ast: patterns: - - 'estree-*' - 'hast-*' - 'mdast-*' - 'hastscript' - - '@oxc-project/types' recma: patterns: - 'recma-*' diff --git a/package-lock.json b/package-lock.json index 81583c42..c87b54f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@swc/html-wasm": "^1.15.43", "commander": "^15.0.0", "dedent": "^1.7.2", - "estree-util-to-js": "^2.0.0", + "esrap": "^2.3.0", "github-slugger": "^2.0.0", "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", @@ -3387,7 +3387,7 @@ "version": "8.62.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5435,6 +5435,23 @@ "node": ">=0.10" } }, + "node_modules/esrap": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", diff --git a/package.json b/package.json index 3b1f7f34..f958454b 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@swc/html-wasm": "^1.15.43", "commander": "^15.0.0", "dedent": "^1.7.2", - "estree-util-to-js": "^2.0.0", + "esrap": "^2.3.0", "github-slugger": "^2.0.0", "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", diff --git a/src/generators/jsx-ast/generate.mjs b/src/generators/jsx-ast/generate.mjs index 055e0efb..0e11ce93 100644 --- a/src/generators/jsx-ast/generate.mjs +++ b/src/generators/jsx-ast/generate.mjs @@ -1,4 +1,5 @@ -import { jsx, toJs } from 'estree-util-to-js'; +import { print } from 'esrap'; +import tsx from 'esrap/languages/tsx'; import buildContent from './utils/buildContent.mjs'; import { getSortedHeadNodes } from './utils/getSortedHeadNodes.mjs'; @@ -46,7 +47,7 @@ export async function processChunk(slicedInput, itemIndices) { const content = await buildContent(entries, head); - const { value: code } = toJs(content, { handlers: jsx }); + const { code } = print(content, tsx()); results.push({ data: content.data, code }); } diff --git a/src/generators/web/__tests__/generate.test.mjs b/src/generators/web/__tests__/generate.test.mjs index e5f70c76..95cdd794 100644 --- a/src/generators/web/__tests__/generate.test.mjs +++ b/src/generators/web/__tests__/generate.test.mjs @@ -1,7 +1,8 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { jsx, toJs } from 'estree-util-to-js'; +import { print } from 'esrap'; +import tsx from 'esrap/languages/tsx'; import { setConfig } from '../../../utils/configuration/index.mjs'; import buildContent from '../../jsx-ast/utils/buildContent.mjs'; @@ -14,7 +15,7 @@ import { generate } from '../generate.mjs'; */ const toCodeItem = content => ({ data: content.data, - code: toJs(content, { handlers: jsx }).value, + code: print(content, tsx()).code, }); const createEntry = (api, name) => { From 2af8ed086da5d2cee7eb88c813125579c0ad832a Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:46:17 +0200 Subject: [PATCH 07/12] Update generate.mjs --- src/generators/jsx-ast/generate.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/generators/jsx-ast/generate.mjs b/src/generators/jsx-ast/generate.mjs index 0e11ce93..dee1360d 100644 --- a/src/generators/jsx-ast/generate.mjs +++ b/src/generators/jsx-ast/generate.mjs @@ -47,7 +47,13 @@ export async function processChunk(slicedInput, itemIndices) { const content = await buildContent(entries, head); - const { code } = print(content, tsx()); + const { code } = print( + content, + tsx({ + // SUPER IMPORTANT: use double instead roldown will fail on certain unicode + quotes: 'double', + }) + ); results.push({ data: content.data, code }); } From 37fafb8eae4cff832620a160935c31c37d34196f Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:57:18 +0200 Subject: [PATCH 08/12] remove usage of oxc-walker --- package-lock.json | 140 +----------------- package.json | 1 - .../utils/checkIndirectReferences.mjs | 16 +- .../api-links/utils/extractExports.mjs | 72 ++++----- .../api-links/utils/findDefinitions.mjs | 57 +++---- 5 files changed, 65 insertions(+), 221 deletions(-) diff --git a/package-lock.json b/package-lock.json index c87b54f1..a2157e15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "lightningcss-wasm": "^1.32.0", "mdast-util-slice-markdown": "^2.0.1", "oxc-parser": "^0.139.0", - "oxc-walker": "^1.0.0", "piscina": "^5.2.0", "preact": "^10.29.2", "preact-render-to-string": "^6.7.0", @@ -888,30 +887,11 @@ "node": ">=8" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -927,6 +907,7 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -7299,27 +7280,6 @@ "node": "20 || >=22" } }, - "node_modules/magic-regexp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.11.0.tgz", - "integrity": "sha512-LG77Z/gVnwz7oaDpD4heX6ryl+lcr4l1B2gnP4MMvt2pGhGC1Dfj7dl1pXpP4ih+VQFLuAadeKVa+lARAzfW+Q==", - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.21", - "regexp-tree": "^0.1.27", - "type-level-regexp": "~0.1.17", - "unplugin": "^3.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8785,27 +8745,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/oxc-walker": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-1.0.0.tgz", - "integrity": "sha512-eMsHflAGfOskpWxtp9xP/f5b96XLEU8ifTd2gOOCkdux9HMxKGy5S1ru0Gh1B3aPu+YbfmWUUVkcb7MrZz3XyQ==", - "license": "MIT", - "dependencies": { - "magic-regexp": "^0.11.0" - }, - "peerDependencies": { - "oxc-parser": ">=0.98.0", - "rolldown": ">=1.0.0" - }, - "peerDependenciesMeta": { - "oxc-parser": { - "optional": true - }, - "rolldown": { - "optional": true - } - } - }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -9444,15 +9383,6 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10549,12 +10479,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-level-regexp": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", - "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", - "license": "MIT" - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -10851,60 +10775,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unplugin": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", - "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.4", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@farmfe/core": "*", - "@rspack/core": "*", - "bun-types-no-globals": "*", - "esbuild": "*", - "rolldown": "*", - "rollup": "*", - "unloader": "*", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "@farmfe/core": { - "optional": true - }, - "@rspack/core": { - "optional": true - }, - "bun-types-no-globals": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - }, - "unloader": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", @@ -11070,12 +10940,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "license": "MIT" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index f958454b..35f4a16d 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "lightningcss-wasm": "^1.32.0", "mdast-util-slice-markdown": "^2.0.1", "oxc-parser": "^0.139.0", - "oxc-walker": "^1.0.0", "piscina": "^5.2.0", "preact": "^10.29.2", "preact-render-to-string": "^6.7.0", diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index 389d3f4b..07e01277 100644 --- a/src/generators/api-links/utils/checkIndirectReferences.mjs +++ b/src/generators/api-links/utils/checkIndirectReferences.mjs @@ -1,4 +1,4 @@ -import { walk } from 'oxc-walker'; +import { Visitor } from 'oxc-parser'; import { getLineNumber } from './getLineNumber.mjs'; @@ -12,18 +12,14 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { return; } - walk(program, { + const visitor = new Visitor({ /** * */ - enter(node) { - if (node.type !== 'FunctionDeclaration') { - return; - } - - const name = node.id.name; + FunctionDeclaration(node) { + const name = node.id?.name; - if (name in exports.indirects) { + if (name && name in exports.indirects) { nameToLineNumberMap[exports.indirects[name]] = getLineNumber( program.sourceText, node.range[0] @@ -31,4 +27,6 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { } }, }); + + visitor.visit(program); } diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 1fc24265..a3700c7e 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { walk } from 'oxc-walker'; +import { Visitor } from 'oxc-parser'; import { getLineNumber } from './getLineNumber.mjs'; import { CONSTRUCTOR_EXPRESSION } from '../constants.mjs'; @@ -218,7 +218,6 @@ function handleVariableDeclaration( 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 @@ -242,46 +241,51 @@ export function extractExports(program, basename, nameToLineNumberMap) { indirects: {}, }; - const TYPE_TO_HANDLER_MAP = { - /** - * @param {import('@oxc-project/types').Node} node - */ - ExpressionStatement: node => - handleExpression(node, basename, nameToLineNumberMap, program.sourceText), + /** + * + */ + function mergeExports(output) { + if (!output) { + return; + } + + exports.ctors.push(...output.ctors); + exports.identifiers.push(...output.identifiers); + + Object.assign(exports.indirects, output.indirects); + } + const visitor = new Visitor({ /** - * @param {import('@oxc-project/types').Node} node + * @param {import('@oxc-project/types').ExpressionStatement} node */ - VariableDeclaration: node => - handleVariableDeclaration( - node, - basename, - nameToLineNumberMap, - program.sourceText - ), - }; + ExpressionStatement(node) { + mergeExports( + handleExpression( + node, + basename, + nameToLineNumberMap, + program.sourceText + ) + ); + }, - walk(program, { /** - * + * @param {import('@oxc-project/types').VariableDeclaration} node */ - enter(node) { - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; - - const output = handler(node); - - if (output) { - exports.ctors.push(...output.ctors); - exports.identifiers.push(...output.identifiers); - - Object.keys(output.indirects).forEach(key => { - exports.indirects[key] = output.indirects[key]; - }); - } - } + VariableDeclaration(node) { + mergeExports( + handleVariableDeclaration( + node, + basename, + nameToLineNumberMap, + program.sourceText + ) + ); }, }); + visitor.visit(program); + return exports; } diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index 0b36b9e2..fb4d3d1e 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { walk } from 'oxc-walker'; +import { Visitor } from 'oxc-parser'; import { getLineNumber } from './getLineNumber.mjs'; @@ -27,7 +27,6 @@ function handleAssignmentExpression( const { left: lhs, right: rhs } = expression; if (lhs.type !== 'MemberExpression') { - // Not an assignment to a member, not relevant to us return; } @@ -42,48 +41,39 @@ function handleAssignmentExpression( 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; } - // Something like `ClassName.prototype.asd = 123` object = lhs.object.object; objectName = object.name ? object.name : object.object.name; objectName = objectName.toLowerCase(); - // 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: { - // Not relevant to us return; } } if (!exports.ctors.includes(object.name)) { - // The object being written to isn't exported, not relevant to us return; } - /** - * 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] = getLineNumber(sourceText, node.range[0]); @@ -108,12 +98,10 @@ function handleFunctionDeclaration( sourceText ) { if (!exports.identifiers.includes(node.id.name)) { - // Function isn't exported, not relevant to us return; } if (basename.startsWith('_')) { - // Internal function, don't include it in the docs return; } @@ -136,11 +124,9 @@ function handleClassDeclaration( sourceText ) { if (!exports.ctors.includes(node.id.name)) { - // Class isn't exported, not relevant to us return; } - // WASI -> wASI, Agent -> agent const name = node.id.name[0].toLowerCase() + node.id.name.substring(1); nameToLineNumberMap[node.id.name] = getLineNumber(sourceText, node.range[0]); @@ -151,6 +137,7 @@ function handleClassDeclaration( } const { key, kind, range } = member; + const outputKey = kind === 'constructor' ? `new ${node.id.name}` : `${name}.${key.name}`; @@ -170,52 +157,44 @@ export function findDefinitions( nameToLineNumberMap, exports ) { - const TYPE_TO_HANDLER_MAP = { + const visitor = new Visitor({ /** - * @param {import('@oxc-project/types').Node} node + * @param {import('@oxc-project/types').ExpressionStatement} node */ - ExpressionStatement: node => + ExpressionStatement(node) { handleAssignmentExpression( node, nameToLineNumberMap, exports, program.sourceText - ), + ); + }, /** - * @param {import('@oxc-project/types').Node} node + * @param {import('@oxc-project/types').FunctionDeclaration} node */ - FunctionDeclaration: node => + FunctionDeclaration(node) { handleFunctionDeclaration( node, basename, nameToLineNumberMap, exports, program.sourceText - ), + ); + }, /** - * @param {import('@oxc-project/types').Node} node + * @param {import('@oxc-project/types').ClassDeclaration} node */ - ClassDeclaration: node => + ClassDeclaration(node) { handleClassDeclaration( node, nameToLineNumberMap, exports, program.sourceText - ), - }; - - walk(program, { - /** - * - */ - enter(node) { - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; - - handler(node); - } + ); }, }); + + visitor.visit(program); } From f74f38ae0b276713b6e7d68a93046c3bc2f4f0f6 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:11:11 +0200 Subject: [PATCH 09/12] Update findDefinitions.mjs --- src/generators/api-links/utils/findDefinitions.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index fb4d3d1e..a89b87ab 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -98,6 +98,7 @@ function handleFunctionDeclaration( sourceText ) { if (!exports.identifiers.includes(node.id.name)) { + // Function isn't exported, not relevant to us return; } From 70ce7bb545cb8be2949aed0c4a612cdc2dead245 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:11:57 +0200 Subject: [PATCH 10/12] Update dependabot.yml --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 02659360..fc11aa74 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -67,6 +67,10 @@ updates: - 'hast-*' - 'mdast-*' - 'hastscript' + oxc: + patterns: + - 'oxc-*' + - 'esrap' recma: patterns: - 'recma-*' From 331939109d09b8f47be6b5167f0f56e50713af81 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:17:37 +0200 Subject: [PATCH 11/12] improve test coverage --- .c8rc.json | 6 +- .../api-links/__tests__/fixtures.test.mjs | 3 +- .../api-links/__tests__/utils.test.mjs | 209 ++++++++++++++++++ .../api-links/utils/findDefinitions.mjs | 5 +- 4 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 src/generators/api-links/__tests__/utils.test.mjs diff --git a/.c8rc.json b/.c8rc.json index 226cc592..2594824a 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -2,11 +2,15 @@ "all": true, "exclude": [ "eslint.config.mjs", + "playwright.config.js", "**/fixtures", "**/__tests__", "src/generators/legacy-html/assets", "src/generators/web/ui", "**/*.d.ts", - "www" + "www", + "beta", + "scripts", + "e2e" ] } diff --git a/src/generators/api-links/__tests__/fixtures.test.mjs b/src/generators/api-links/__tests__/fixtures.test.mjs index 6259aeb3..971571d2 100644 --- a/src/generators/api-links/__tests__/fixtures.test.mjs +++ b/src/generators/api-links/__tests__/fixtures.test.mjs @@ -1,8 +1,7 @@ +import { globSync } from 'node:fs'; import { basename, join, relative, sep } from 'node:path'; import { after, before, describe, it } from 'node:test'; -import { globSync } from 'tinyglobby'; - import createWorkerPool from '../../../threading/index.mjs'; import createParallelWorker from '../../../threading/parallel.mjs'; import { setConfig } from '../../../utils/configuration/index.mjs'; diff --git a/src/generators/api-links/__tests__/utils.test.mjs b/src/generators/api-links/__tests__/utils.test.mjs new file mode 100644 index 00000000..8db6976b --- /dev/null +++ b/src/generators/api-links/__tests__/utils.test.mjs @@ -0,0 +1,209 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { parse } from 'oxc-parser'; + +import { checkIndirectReferences } from '../utils/checkIndirectReferences.mjs'; +import { extractExports } from '../utils/extractExports.mjs'; +import { findDefinitions } from '../utils/findDefinitions.mjs'; + +async function parseProgram(source) { + const result = await parse('test.js', source, { + lang: 'js', + sourceType: 'commonjs', + range: true, + }); + + // oxc-parser's ParseResult doesn't attach the original source text to + // `program`, but checkIndirectReferences/findDefinitions rely on + // `program.sourceText` to compute real line numbers via getLineNumber. + // Without this, getLineNumber falls back to a default and line numbers + // silently come back wrong for anything beyond a single line. + result.program.sourceText = source; + + return result.program; +} + +test('checkIndirectReferences resolves indirect function references', async () => { + const source = `exports.Buffer = Buffer; + +function Buffer() {}`; + + const program = await parseProgram(source); + + const exports = { + ctors: [], + identifiers: [], + indirects: { + Buffer: 'buffer.Buffer', + }, + }; + + const map = {}; + + checkIndirectReferences(program, exports, map); + + assert.equal(map['buffer.Buffer'], 3); +}); + +test('checkIndirectReferences ignores unknown functions', async () => { + const source = `function Other() {}`; + + const program = await parseProgram(source); + + const exports = { + ctors: [], + identifiers: [], + indirects: { + Buffer: 'buffer.Buffer', + }, + }; + + const map = {}; + + checkIndirectReferences(program, exports, map); + + assert.deepEqual(map, {}); +}); + +test('extractExports handles exports.function assignment', async () => { + const source = `exports.foo = function () {};`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.ctors, []); + assert.deepEqual(result.identifiers, []); + assert.deepEqual(result.indirects, {}); + + assert.equal(map['api.foo'], 1); +}); + +test('extractExports handles indirect exports', async () => { + const source = `exports.Buffer = Buffer;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.indirects, { + Buffer: 'api.Buffer', + }); +}); + +test('extractExports handles module.exports constructor', async () => { + const source = `module.exports = new Buffer();`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.ctors, ['Buffer']); +}); + +test('extractExports handles module.exports object exports', async () => { + const source = `module.exports = { + Buffer, + foo +};`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.identifiers, ['Buffer', 'foo']); + + assert.deepEqual(result.ctors, ['Buffer']); +}); + +test('findDefinitions finds exported function declaration', async () => { + const source = `exports.foo = foo; + +function foo() {}`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: [], + identifiers: ['foo'], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.equal(map['api.foo'], 3); +}); + +test('findDefinitions finds prototype assignments', async () => { + const source = `Buffer.prototype.write = function () {};`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: ['Buffer'], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.equal(map['buf.write'], 1); +}); + +test('findDefinitions tracks prototype indirect references', async () => { + const source = `Buffer.prototype.write = write;`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: ['Buffer'], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.equal(map['buf.write'], 1); + + assert.deepEqual(exports.indirects, { + write: 'buf.write', + }); +}); + +test('findDefinitions finds class constructors and methods', async () => { + const source = `class Buffer { + constructor() {} + write() {} +}`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: ['Buffer'], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.equal(map.Buffer, 1); + assert.equal(map['new Buffer'], 2); + assert.equal(map['buffer.write'], 3); +}); diff --git a/src/generators/api-links/utils/findDefinitions.mjs b/src/generators/api-links/utils/findDefinitions.mjs index a89b87ab..d7e34668 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -78,7 +78,10 @@ function handleAssignmentExpression( nameToLineNumberMap[name] = getLineNumber(sourceText, node.range[0]); - if (rhs && rhs.type === 'Identifier' && lhs.property.name === rhs.name) { + // Track any identifier assignment as an indirect reference + // This handles cases like: Buffer.prototype.write = write; + // Which creates an indirect mapping: write -> buffer.write + if (rhs && rhs.type === 'Identifier') { exports.indirects[rhs.name] = name; } } From 9b7806f94d9ce3b32322aff6e2dcb72d4b537d12 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:26:28 +0200 Subject: [PATCH 12/12] Update utils.test.mjs --- .../api-links/__tests__/utils.test.mjs | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) diff --git a/src/generators/api-links/__tests__/utils.test.mjs b/src/generators/api-links/__tests__/utils.test.mjs index 8db6976b..2810ceea 100644 --- a/src/generators/api-links/__tests__/utils.test.mjs +++ b/src/generators/api-links/__tests__/utils.test.mjs @@ -66,6 +66,24 @@ test('checkIndirectReferences ignores unknown functions', async () => { assert.deepEqual(map, {}); }); +test('checkIndirectReferences exits when there are no indirect references', async () => { + const source = `function Buffer() {}`; + + const program = await parseProgram(source); + + const exports = { + ctors: [], + identifiers: [], + indirects: {}, + }; + + const map = {}; + + checkIndirectReferences(program, exports, map); + + assert.deepEqual(map, {}); +}); + test('extractExports handles exports.function assignment', async () => { const source = `exports.foo = function () {};`; @@ -82,6 +100,24 @@ test('extractExports handles exports.function assignment', async () => { assert.equal(map['api.foo'], 1); }); +test('extractExports handles nested exports member assignments', async () => { + const source = `exports.foo.bar = function () {};`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result, { + ctors: [], + identifiers: [], + indirects: {}, + }); + + assert.equal(map['api.foo'], 1); +}); + test('extractExports handles indirect exports', async () => { const source = `exports.Buffer = Buffer;`; @@ -96,6 +132,21 @@ test('extractExports handles indirect exports', async () => { }); }); +test('extractExports handles non-identifier exports assignments', async () => { + const source = `exports.foo = {}; +exports[bar] = {}; +exports['baz'] = {};`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.identifiers, ['foo', 'bar']); + assert.deepEqual(result.indirects, {}); +}); + test('extractExports handles module.exports constructor', async () => { const source = `module.exports = new Buffer();`; @@ -108,6 +159,18 @@ test('extractExports handles module.exports constructor', async () => { assert.deepEqual(result.ctors, ['Buffer']); }); +test('extractExports resolves chained module.exports assignments', async () => { + const source = `module.exports = exports.foo = new Buffer();`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.ctors, ['Buffer']); +}); + test('extractExports handles module.exports object exports', async () => { const source = `module.exports = { Buffer, @@ -125,6 +188,100 @@ test('extractExports handles module.exports object exports', async () => { assert.deepEqual(result.ctors, ['Buffer']); }); +test('extractExports handles deprecated and ignored object exports', async () => { + const source = `module.exports = { + oldFn: deprecate(oldFn), + ignoredCall: wrap(ignoredCall), + ignoredLiteral: 1 +};`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.identifiers, ['oldFn']); + assert.deepEqual(result.ctors, []); +}); + +test('extractExports handles module.exports identifier exports', async () => { + const source = `module.exports = api;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.identifiers, ['api']); + assert.deepEqual(result.ctors, []); +}); + +test('extractExports handles module.exports constructor identifier exports', async () => { + const source = `module.exports = Buffer;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.identifiers, ['Buffer']); + assert.deepEqual(result.ctors, ['Buffer']); +}); + +test('extractExports ignores unsupported module.exports assignments', async () => { + const source = `module.exports = 1;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result, { + ctors: [], + identifiers: [], + indirects: {}, + }); +}); + +test('extractExports handles variable declaration export aliases', async () => { + const source = `const foo = exports.foo = function () {}; +const Buffer = module.exports = function Buffer() {}; +const ignoredProperty = module.foo = function () {}; +const ignoredTarget = local.foo = function () {}; +const ignoredValue = 1;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.ctors, ['Buffer']); + assert.equal(map['api.foo'], 1); + assert.equal(map.Buffer, 2); +}); + +test('extractExports ignores expressions that are not export writes', async () => { + const source = `foo; +foo = 1;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result, { + ctors: [], + identifiers: [], + indirects: {}, + }); +}); + test('findDefinitions finds exported function declaration', async () => { const source = `exports.foo = foo; @@ -145,6 +302,42 @@ function foo() {}`; assert.equal(map['api.foo'], 3); }); +test('findDefinitions ignores unexported function declarations', async () => { + const source = `function foo() {}`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: [], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.deepEqual(map, {}); +}); + +test('findDefinitions ignores exported function declarations for private modules', async () => { + const source = `function foo() {}`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: [], + identifiers: ['foo'], + indirects: {}, + }; + + findDefinitions(program, '_api', map, exports); + + assert.deepEqual(map, {}); +}); + test('findDefinitions finds prototype assignments', async () => { const source = `Buffer.prototype.write = function () {};`; @@ -185,6 +378,44 @@ test('findDefinitions tracks prototype indirect references', async () => { }); }); +test('findDefinitions ignores expressions that are not definition writes', async () => { + const source = `Buffer; +Buffer = function () {};`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: ['Buffer'], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.deepEqual(map, {}); +}); + +test('findDefinitions ignores unsupported assignment targets', async () => { + const source = `Buffer.static.property = function () {}; +getTarget().method = function () {};`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: ['Buffer'], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.deepEqual(map, {}); +}); + test('findDefinitions finds class constructors and methods', async () => { const source = `class Buffer { constructor() {} @@ -207,3 +438,31 @@ test('findDefinitions finds class constructors and methods', async () => { assert.equal(map['new Buffer'], 2); assert.equal(map['buffer.write'], 3); }); + +test('findDefinitions ignores unexported classes and class fields', async () => { + const source = `class Internal { + method() {} +} + +class Public { + field = 1; + method() {} +}`; + + const program = await parseProgram(source); + + const map = {}; + + const exports = { + ctors: ['Public'], + identifiers: [], + indirects: {}, + }; + + findDefinitions(program, 'api', map, exports); + + assert.deepEqual(map, { + Public: 5, + 'public.method': 7, + }); +});