diff --git a/.c8rc.json b/.c8rc.json index 3c63a3ff..2594824a 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -2,9 +2,15 @@ "all": true, "exclude": [ "eslint.config.mjs", + "playwright.config.js", "**/fixtures", + "**/__tests__", "src/generators/legacy-html/assets", "src/generators/web/ui", - "**/*.d.ts" + "**/*.d.ts", + "www", + "beta", + "scripts", + "e2e" ] } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 845dd7f5..fc11aa74 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -64,11 +64,13 @@ updates: - 'rehype-*' ast: patterns: - - 'estree-*' - 'hast-*' - 'mdast-*' - 'hastscript' - - 'acorn' + oxc: + patterns: + - 'oxc-*' + - 'esrap' recma: patterns: - 'recma-*' diff --git a/package-lock.json b/package-lock.json index eff2cd84..a2157e15 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,11 +17,9 @@ "@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", - "estree-util-visit": "^2.0.0", + "esrap": "^2.3.0", "github-slugger": "^2.0.0", "glob-parent": "^6.0.2", "hast-util-to-string": "^3.0.1", @@ -903,7 +901,6 @@ "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": { @@ -3371,7 +3368,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" @@ -5419,6 +5416,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 715add8a..35f4a16d 100644 --- a/package.json +++ b/package.json @@ -70,11 +70,9 @@ "@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", - "estree-util-visit": "^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/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..2810ceea --- /dev/null +++ b/src/generators/api-links/__tests__/utils.test.mjs @@ -0,0 +1,468 @@ +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('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 () {};`; + + 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 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;`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + assert.deepEqual(result.indirects, { + Buffer: 'api.Buffer', + }); +}); + +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();`; + + const program = await parseProgram(source); + + const map = {}; + + const result = extractExports(program, 'api', map); + + 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, + 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('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; + +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 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 () {};`; + + 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 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() {} + 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); +}); + +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, + }); +}); diff --git a/src/generators/api-links/utils/checkIndirectReferences.mjs b/src/generators/api-links/utils/checkIndirectReferences.mjs index 982bb111..07e01277 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 { Visitor } from 'oxc-parser'; + +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 */ @@ -10,15 +12,21 @@ export function checkIndirectReferences(program, exports, nameToLineNumberMap) { return; } - visit(program, node => { - if (!node.loc || node.type !== 'FunctionDeclaration') { - return; - } - - const name = node.id.name; + const visitor = new Visitor({ + /** + * + */ + FunctionDeclaration(node) { + const name = node.id?.name; - if (name in exports.indirects) { - nameToLineNumberMap[exports.indirects[name]] = node.loc.start.line; - } + if (name && name in exports.indirects) { + nameToLineNumberMap[exports.indirects[name]] = getLineNumber( + program.sourceText, + node.range[0] + ); + } + }, }); + + visitor.visit(program); } diff --git a/src/generators/api-links/utils/extractExports.mjs b/src/generators/api-links/utils/extractExports.mjs index 60746eca..a3700c7e 100644 --- a/src/generators/api-links/utils/extractExports.mjs +++ b/src/generators/api-links/utils/extractExports.mjs @@ -1,18 +1,20 @@ 'use strict'; -import { visit } from 'estree-util-visit'; +import { Visitor } from 'oxc-parser'; +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; } @@ -203,7 +218,6 @@ function handleVariableDeclaration(node, basename, nameToLineNumberMap) { 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 @@ -212,7 +226,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} @@ -227,40 +241,51 @@ export function extractExports(program, basename, nameToLineNumberMap) { indirects: {}, }; - const TYPE_TO_HANDLER_MAP = { - /** - * @param {import('acorn').Node} node - */ - ExpressionStatement: node => - handleExpression(node, basename, nameToLineNumberMap), - - /** - * @param {import('acorn').Node} node - */ - VariableDeclaration: node => - handleVariableDeclaration(node, basename, nameToLineNumberMap), - }; - - visit(program, node => { - if (!node.loc) { + /** + * + */ + function mergeExports(output) { + if (!output) { return; } - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; + exports.ctors.push(...output.ctors); + exports.identifiers.push(...output.identifiers); - const output = handler(node); + Object.assign(exports.indirects, output.indirects); + } - if (output) { - exports.ctors.push(...output.ctors); - exports.identifiers.push(...output.identifiers); + const visitor = new Visitor({ + /** + * @param {import('@oxc-project/types').ExpressionStatement} node + */ + ExpressionStatement(node) { + mergeExports( + handleExpression( + node, + basename, + nameToLineNumberMap, + program.sourceText + ) + ); + }, - Object.keys(output.indirects).forEach(key => { - exports.indirects[key] = output.indirects[key]; - }); - } - } + /** + * @param {import('@oxc-project/types').VariableDeclaration} node + */ + 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 a161e3ed..d7e34668 100644 --- a/src/generators/api-links/utils/findDefinitions.mjs +++ b/src/generators/api-links/utils/findDefinitions.mjs @@ -1,15 +1,23 @@ 'use strict'; -import { visit } from 'estree-util-visit'; +import { Visitor } from 'oxc-parser'; + +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') { @@ -19,7 +27,6 @@ function handleAssignmentExpression(node, nameToLineNumberMap, exports) { const { left: lhs, right: rhs } = expression; if (lhs.type !== 'MemberExpression') { - // Not an assignment to a member, not relevant to us return; } @@ -34,68 +41,64 @@ function handleAssignmentExpression(node, nameToLineNumberMap, exports) { 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] = node.loc.start.line; + nameToLineNumberMap[name] = getLineNumber(sourceText, node.range[0]); - if (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; } } /** - * @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 @@ -103,43 +106,51 @@ function handleFunctionDeclaration( } if (basename.startsWith('_')) { - // Internal function, don't include it in the docs 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; } - // 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 @@ -150,35 +161,44 @@ export function findDefinitions( nameToLineNumberMap, exports ) { - const TYPE_TO_HANDLER_MAP = { + const visitor = new Visitor({ /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').ExpressionStatement} node */ - ExpressionStatement: node => - handleAssignmentExpression(node, nameToLineNumberMap, exports), + ExpressionStatement(node) { + handleAssignmentExpression( + node, + nameToLineNumberMap, + exports, + program.sourceText + ); + }, /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').FunctionDeclaration} node */ - FunctionDeclaration: node => - handleFunctionDeclaration(node, basename, nameToLineNumberMap, exports), + FunctionDeclaration(node) { + handleFunctionDeclaration( + node, + basename, + nameToLineNumberMap, + exports, + program.sourceText + ); + }, /** - * @param {import('acorn').Node} node + * @param {import('@oxc-project/types').ClassDeclaration} node */ - ClassDeclaration: node => - handleClassDeclaration(node, nameToLineNumberMap, exports), - }; - - visit(program, node => { - if (!node.loc) { - return; - } - - if (node.type in TYPE_TO_HANDLER_MAP) { - const handler = TYPE_TO_HANDLER_MAP[node.type]; - - handler(node); - } + ClassDeclaration(node) { + handleClassDeclaration( + node, + nameToLineNumberMap, + exports, + program.sourceText + ); + }, }); + + visitor.visit(program); } 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); diff --git a/src/generators/jsx-ast/generate.mjs b/src/generators/jsx-ast/generate.mjs index 055e0efb..dee1360d 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,13 @@ export async function processChunk(slicedInput, itemIndices) { const content = await buildContent(entries, head); - const { value: code } = toJs(content, { handlers: jsx }); + const { code } = print( + content, + tsx({ + // SUPER IMPORTANT: use double instead roldown will fail on certain unicode + quotes: 'double', + }) + ); 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) => {