mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
refactor(napi): use vitest (#6307)
This commit is contained in:
parent
58a8615747
commit
5b5daec392
15 changed files with 926 additions and 341 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -274,13 +274,7 @@ jobs:
|
|||
cache-key: warm
|
||||
- uses: ./.github/actions/pnpm
|
||||
if: steps.filter.outputs.src == 'true'
|
||||
- name: Test napi/parser
|
||||
run: pnpm build && pnpm test
|
||||
- run: pnpm run build
|
||||
if: steps.filter.outputs.src == 'true'
|
||||
working-directory: napi/parser
|
||||
- name: Test napi/transform
|
||||
if: steps.filter.outputs.src == 'true'
|
||||
working-directory: napi/transform
|
||||
run: pnpm build && pnpm test
|
||||
- run: git diff --exit-code
|
||||
- run: pnpm run test
|
||||
if: steps.filter.outputs.src == 'true'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
"test": "node test.mjs"
|
||||
"test": "echo 'skip'"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.*"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
"test": "node test.mjs",
|
||||
"bench": "node parse.bench.mjs"
|
||||
"test": "vitest run ./test"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.*"
|
||||
|
|
@ -22,10 +21,5 @@
|
|||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-darwin"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "catalog:",
|
||||
"es-module-lexer": "^1.4.1",
|
||||
"tinybench": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { join as pathJoin } from 'path';
|
||||
import { Bench } from 'tinybench';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { parseSync } from './index.js';
|
||||
|
||||
const IS_CI = !!process.env.CI,
|
||||
ACCURATE = IS_CI || process.env.ACCURATE;
|
||||
|
||||
const urls = [
|
||||
// TypeScript syntax (2.81MB)
|
||||
'https://raw.githubusercontent.com/microsoft/TypeScript/v5.3.3/src/compiler/checker.ts',
|
||||
// Real world app tsx (1.0M)
|
||||
'https://raw.githubusercontent.com/oxc-project/benchmark-files/main/cal.com.tsx',
|
||||
// Real world content-heavy app jsx (3K)
|
||||
'https://raw.githubusercontent.com/oxc-project/benchmark-files/main/RadixUIAdoptionSection.jsx',
|
||||
// Heavy with classes (554K)
|
||||
'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.269/build/pdf.mjs',
|
||||
// ES5 (3.9M)
|
||||
'https://cdn.jsdelivr.net/npm/antd@5.12.5/dist/antd.js',
|
||||
];
|
||||
|
||||
// Same directory as Rust benchmarks use for downloaded files
|
||||
const cacheDirPath = pathJoin(fileURLToPath(import.meta.url), '../../../target');
|
||||
|
||||
const files = await Promise.all(urls.map(async (url) => {
|
||||
const filename = url.split('/').at(-1),
|
||||
path = pathJoin(cacheDirPath, filename);
|
||||
|
||||
let code;
|
||||
try {
|
||||
code = await readFile(path, 'utf8');
|
||||
if (IS_CI) console.log('Found cached file:', filename);
|
||||
} catch {
|
||||
if (IS_CI) console.log('Downloading:', filename);
|
||||
const res = await fetch(url);
|
||||
code = await res.text();
|
||||
await writeFile(path, code);
|
||||
}
|
||||
|
||||
return { filename, code };
|
||||
}));
|
||||
|
||||
const bench = new Bench(
|
||||
ACCURATE
|
||||
? {
|
||||
warmupIterations: 20, // Default is 5
|
||||
time: 5000, // 5 seconds, default is 500 ms
|
||||
iterations: 100, // Default is 10
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
for (const { filename, code } of files) {
|
||||
bench.add(`parser_napi[${filename}]`, () => {
|
||||
const res = parseSync(code, { sourceFilename: filename });
|
||||
JSON.parse(res.program);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Warming up');
|
||||
await bench.warmup();
|
||||
console.log('Running benchmarks');
|
||||
await bench.run();
|
||||
console.table(bench.table());
|
||||
|
||||
// If running on CI, save results to file
|
||||
if (IS_CI) {
|
||||
const dataDir = process.env.DATA_DIR;
|
||||
const results = bench.tasks.map(task => ({
|
||||
filename: task.name.match(/\[(.+)\]$/)[1],
|
||||
duration: task.result.period / 1000, // In seconds
|
||||
}));
|
||||
await writeFile(pathJoin(dataDir, 'results.json'), JSON.stringify(results));
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import assert from 'assert';
|
||||
import oxc from './index.js';
|
||||
|
||||
console.log(`Testing on ${process.platform}-${process.arch}`);
|
||||
|
||||
function test(ret) {
|
||||
console.log(ret);
|
||||
assert(JSON.parse(ret.program).body.length == 1);
|
||||
assert(ret.errors.length == 0);
|
||||
assert(ret.comments.length == 1);
|
||||
}
|
||||
|
||||
const sourceText = '/* comment */ foo';
|
||||
|
||||
test(oxc.parseSync(sourceText));
|
||||
|
||||
async function main() {
|
||||
test(await oxc.parseAsync(sourceText));
|
||||
}
|
||||
|
||||
main();
|
||||
17
napi/parser/test/module_lexer.test.mjs
Normal file
17
napi/parser/test/module_lexer.test.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { assert, describe, it } from 'vitest';
|
||||
|
||||
import oxc from './index.js';
|
||||
|
||||
describe('module lexer', () => {
|
||||
const code = 'export { foo }';
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.moduleLexerSync(code);
|
||||
assert(ret.exports.length == 1);
|
||||
});
|
||||
|
||||
it('matches output async ', async () => {
|
||||
const ret = await oxc.moduleLexerAsync(code);
|
||||
assert(ret.exports.length == 1);
|
||||
});
|
||||
});
|
||||
21
napi/parser/test/parse.test.mjs
Normal file
21
napi/parser/test/parse.test.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { assert, describe, it } from 'vitest';
|
||||
|
||||
import oxc from './index.js';
|
||||
|
||||
describe('parse', () => {
|
||||
const code = '/* comment */ foo';
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.parseSync(code);
|
||||
assert(JSON.parse(ret.program).body.length == 1);
|
||||
assert(ret.errors.length == 0);
|
||||
assert(ret.comments.length == 1);
|
||||
});
|
||||
|
||||
it('matches output async ', async () => {
|
||||
const ret = await oxc.parseAsync(code);
|
||||
assert(JSON.parse(ret.program).body.length == 1);
|
||||
assert(ret.errors.length == 0);
|
||||
assert(ret.comments.length == 1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Test and bench against `es-module-lexer`
|
||||
*/
|
||||
import assert from 'assert';
|
||||
import fs from 'fs';
|
||||
|
||||
import * as esModuleLexer from 'es-module-lexer';
|
||||
import oxc from './index.js';
|
||||
|
||||
const root = '/Users/boshen/github/es-module-lexer/test/samples';
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
await esModuleLexer.init;
|
||||
|
||||
const files = fs
|
||||
.readdirSync(root)
|
||||
.filter((x) => x.endsWith('.js'))
|
||||
.map((file) => {
|
||||
const source = fs.readFileSync(`${root}/${file}`);
|
||||
return {
|
||||
file,
|
||||
code: source.toString(),
|
||||
size: source.byteLength,
|
||||
};
|
||||
});
|
||||
|
||||
function test(source) {
|
||||
const [imports, exports, facade, hasModuleSyntax] = esModuleLexer.parse(source);
|
||||
const moduleLexer = oxc.moduleLexerSync(source);
|
||||
assert(moduleLexer.imports.length == imports.length);
|
||||
assert(moduleLexer.exports.length == exports.length);
|
||||
assert(moduleLexer.facade == facade);
|
||||
assert(moduleLexer.hasModuleSyntax == hasModuleSyntax);
|
||||
}
|
||||
|
||||
function testAll(files) {
|
||||
for (const file of files) {
|
||||
test(file.code);
|
||||
}
|
||||
}
|
||||
|
||||
testAll(files);
|
||||
|
||||
const n = 25;
|
||||
|
||||
bench('es-module-lexer', esModuleLexer.parse);
|
||||
bench('oxc', oxc.moduleLexerSync);
|
||||
|
||||
function bench(name, fn) {
|
||||
console.log('-----------');
|
||||
console.log(name);
|
||||
|
||||
doRun();
|
||||
|
||||
function timeRun(code) {
|
||||
const start = process.hrtime.bigint();
|
||||
fn(code);
|
||||
const end = process.hrtime.bigint();
|
||||
return Math.round(Number(end - start) / 1e6);
|
||||
}
|
||||
|
||||
function doRun() {
|
||||
console.log('Cold Run, All Samples');
|
||||
let totalSize = 0;
|
||||
{
|
||||
let total = 0;
|
||||
files.forEach(({ code, size }) => {
|
||||
totalSize += size;
|
||||
total += timeRun(code);
|
||||
});
|
||||
console.log(`test/samples/*.js (${Math.round(totalSize / 1e3)} KiB)`);
|
||||
console.log(`> ${total + 'ms'}`);
|
||||
}
|
||||
|
||||
console.log(`\nWarm Runs (average of ${n} runs)`);
|
||||
files.forEach(({ file, code, size }) => {
|
||||
console.log(`${file} (${Math.round(size / 1e3)} KiB)`);
|
||||
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
total += timeRun(code);
|
||||
}
|
||||
|
||||
console.log(`> ${total / n + 'ms'}`);
|
||||
});
|
||||
|
||||
console.log(`\nWarm Runs, All Samples (average of ${n} runs)`);
|
||||
{
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
files.forEach(({ code }) => {
|
||||
total += timeRun(code);
|
||||
});
|
||||
}
|
||||
console.log(`test/samples/*.js (${Math.round(totalSize / 1e3)} KiB)`);
|
||||
console.log(`> ${total / n + 'ms'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
"test": "node test.mjs"
|
||||
"test": "vitest run ./test"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.*"
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
import assert from 'assert';
|
||||
import oxc from './index.js';
|
||||
|
||||
console.log(`Testing on ${process.platform}-${process.arch}`);
|
||||
|
||||
function test(ret, expected) {
|
||||
assert.equal(ret.code, expected.code);
|
||||
assert.deepEqual(ret.map, expected.map);
|
||||
assert(ret.errors.length == 0);
|
||||
}
|
||||
|
||||
const id = `
|
||||
/**
|
||||
* jsdoc 1
|
||||
*/
|
||||
export class A {
|
||||
/**
|
||||
* jsdoc 2
|
||||
*/
|
||||
foo = "bar";
|
||||
}
|
||||
`;
|
||||
|
||||
test(oxc.isolatedDeclaration('test.ts', id, { sourcemap: true }), {
|
||||
code: '/**\n' +
|
||||
'* jsdoc 1\n' +
|
||||
'*/\n' +
|
||||
'export declare class A {\n' +
|
||||
'\t/**\n' +
|
||||
'\t* jsdoc 2\n' +
|
||||
'\t*/\n' +
|
||||
'\tfoo: string;\n' +
|
||||
'}\n',
|
||||
map: {
|
||||
mappings: ';;;AAIA,OAAO,cAAM,EAAE;;;;CAIb;AACD',
|
||||
names: [],
|
||||
sources: ['test.ts'],
|
||||
sourcesContent: [id],
|
||||
version: 3,
|
||||
},
|
||||
});
|
||||
|
||||
test(oxc.transform('test.ts', 'class A<T> {}', { sourcemap: true }), {
|
||||
code: 'class A {}\n',
|
||||
map: {
|
||||
mappings: 'AAAA,MAAM,EAAK,CAAE',
|
||||
names: [],
|
||||
sources: ['test.ts'],
|
||||
sourcesContent: ['class A<T> {}'],
|
||||
version: 3,
|
||||
},
|
||||
});
|
||||
|
||||
// Test react refresh plugin
|
||||
test(
|
||||
oxc.transform(
|
||||
'test.tsx',
|
||||
`
|
||||
import { useState } from "react";
|
||||
export const App = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(count + 1)}>count is {count}</button>;
|
||||
};
|
||||
`,
|
||||
{ jsx: { refresh: {} } },
|
||||
),
|
||||
{
|
||||
code: 'var _s = $RefreshSig$();\n' +
|
||||
'import { useState } from "react";\n' +
|
||||
'import { jsxs as _jsxs } from "react/jsx-runtime";\n' +
|
||||
'export const App = () => {\n' +
|
||||
'\t_s();\n' +
|
||||
'\tconst [count, setCount] = useState(0);\n' +
|
||||
'\treturn _jsxs("button", {\n' +
|
||||
'\t\tonClick: () => setCount(count + 1),\n' +
|
||||
'\t\tchildren: ["count is ", count]\n' +
|
||||
'\t});\n' +
|
||||
'};\n' +
|
||||
'_s(App, "oDgYfYHkD9Wkv4hrAPCkI/ev3YU=");\n' +
|
||||
'_c = App;\n' +
|
||||
'var _c;\n' +
|
||||
'$RefreshReg$(_c, "App");\n',
|
||||
},
|
||||
);
|
||||
|
||||
// Test define plugin
|
||||
// TODO: should be constant folded
|
||||
test(
|
||||
oxc.transform('test.ts', 'if (process.env.NODE_ENV === "production") { foo; }', {
|
||||
define: {
|
||||
'process.env.NODE_ENV': 'false',
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'if (false === "production") {\n\tfoo;\n}\n',
|
||||
},
|
||||
);
|
||||
|
||||
// Test inject plugin
|
||||
test(
|
||||
oxc.transform('test.ts', 'let _ = Object.assign', {
|
||||
inject: {
|
||||
'Object.assign': 'foo',
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'import $inject_Object_assign from "foo";\nlet _ = $inject_Object_assign;\n',
|
||||
},
|
||||
);
|
||||
|
||||
console.log('Success.');
|
||||
39
napi/transform/test/id.test.mjs
Normal file
39
napi/transform/test/id.test.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { assert, describe, it } from 'vitest';
|
||||
|
||||
import oxc from './index.js';
|
||||
|
||||
describe('isolated declaration', () => {
|
||||
const code = `
|
||||
/**
|
||||
* jsdoc 1
|
||||
*/
|
||||
export class A {
|
||||
/**
|
||||
* jsdoc 2
|
||||
*/
|
||||
foo = "bar";
|
||||
}
|
||||
`;
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.isolatedDeclaration('test.ts', code, { sourcemap: true });
|
||||
assert(ret, {
|
||||
code: '/**\n' +
|
||||
'* jsdoc 1\n' +
|
||||
'*/\n' +
|
||||
'export declare class A {\n' +
|
||||
'\t/**\n' +
|
||||
'\t* jsdoc 2\n' +
|
||||
'\t*/\n' +
|
||||
'\tfoo: string;\n' +
|
||||
'}\n',
|
||||
map: {
|
||||
mappings: ';;;AAIA,OAAO,cAAM,EAAE;;;;CAIb;AACD',
|
||||
names: [],
|
||||
sources: ['test.ts'],
|
||||
sourcesContent: [code],
|
||||
version: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
81
napi/transform/test/transform.test.mjs
Normal file
81
napi/transform/test/transform.test.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { assert, describe, it } from 'vitest';
|
||||
|
||||
import oxc from './index.js';
|
||||
|
||||
describe('transform', () => {
|
||||
const code = 'class A<T> {}';
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.transform('test.ts', code, { sourcemap: true });
|
||||
assert(ret, {
|
||||
code: 'class A {}\n',
|
||||
map: {
|
||||
mappings: 'AAAA,MAAM,EAAK,CAAE',
|
||||
names: [],
|
||||
sources: ['test.ts'],
|
||||
sourcesContent: ['class A<T> {}'],
|
||||
version: 3,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('react refresh plugin', () => {
|
||||
const code = `import { useState } from "react";
|
||||
export const App = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(count + 1)}>count is {count}</button>;
|
||||
};`;
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.transform('test.tsx', code, { jsx: { refresh: {} } });
|
||||
assert(ret, {
|
||||
code: 'var _s = $RefreshSig$();\n' +
|
||||
'import { useState } from "react";\n' +
|
||||
'import { jsxs as _jsxs } from "react/jsx-runtime";\n' +
|
||||
'export const App = () => {\n' +
|
||||
'\t_s();\n' +
|
||||
'\tconst [count, setCount] = useState(0);\n' +
|
||||
'\treturn _jsxs("button", {\n' +
|
||||
'\t\tonClick: () => setCount(count + 1),\n' +
|
||||
'\t\tchildren: ["count is ", count]\n' +
|
||||
'\t});\n' +
|
||||
'};\n' +
|
||||
'_s(App, "oDgYfYHkD9Wkv4hrAPCkI/ev3YU=");\n' +
|
||||
'_c = App;\n' +
|
||||
'var _c;\n' +
|
||||
'$RefreshReg$(_c, "App");\n',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('define plugin', () => {
|
||||
const code = 'if (process.env.NODE_ENV === "production") { foo; }';
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.transform('test.tsx', code, {
|
||||
define: {
|
||||
'process.env.NODE_ENV': 'false',
|
||||
},
|
||||
});
|
||||
assert(ret, {
|
||||
// TODO: should be constant folded
|
||||
code: 'if (false === "production") {\n\tfoo;\n}\n',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inject plugin', () => {
|
||||
const code = 'let _ = Object.assign';
|
||||
|
||||
it('matches output', () => {
|
||||
const ret = oxc.transform('test.tsx', code, {
|
||||
define: {
|
||||
'process.env.NODE_ENV': 'false',
|
||||
},
|
||||
});
|
||||
assert(ret, {
|
||||
code: 'import $inject_Object_assign from "foo";\nlet _ = $inject_Object_assign;\n',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,7 +5,12 @@
|
|||
"volta": {
|
||||
"node": "22.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm --workspace-concurrency=1 --filter './napi/*' build",
|
||||
"test": "pnpm --workspace-concurrency=1 --filter './napi/*' test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "catalog:"
|
||||
"@napi-rs/cli": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
770
pnpm-lock.yaml
770
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -6,3 +6,4 @@ packages:
|
|||
|
||||
catalog:
|
||||
"@napi-rs/cli": 3.0.0-alpha.61
|
||||
"vitest": 2.1.2
|
||||
|
|
|
|||
Loading…
Reference in a new issue