Skip to content

Commit 7a6219e

Browse files
feat(coverage): replace acorn with @babel/parser for JS and TS support (#152)
1 parent 00434a2 commit 7a6219e

File tree

5 files changed

+164
-44
lines changed

5 files changed

+164
-44
lines changed

package-lock.json

Lines changed: 4 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,8 @@
108108
"typescript": "^5.0.0"
109109
},
110110
"dependencies": {
111+
"@babel/parser": "^7.29.2",
111112
"@use-tusk/drift-core-node": "^0.1.8",
112-
"acorn": "^8.14.0",
113-
"acorn-typescript": "^1.4.13",
114113
"ast-v8-to-istanbul": "^1.0.0",
115114
"import-in-the-middle": "^1.14.4",
116115
"js-yaml": "^4.1.0",

src/core/coverageProcessor.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,139 @@ test("takeAndProcessSnapshot: skips coverage files without user scripts", async
8080
fs.rmSync(tmpDir, { recursive: true, force: true });
8181
}
8282
});
83+
84+
// --- Babel plugin selection and parsing tests ---
85+
86+
test("Babel plugin selection: TypeScript files get typescript and decorators-legacy plugins", (t) => {
87+
const scriptPath = "/project/src/service.ts";
88+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
89+
const isTSX = /\.tsx$/.test(scriptPath);
90+
const babelPlugins: string[] = isTypeScript
91+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
92+
: ["decorators-legacy"];
93+
94+
t.deepEqual(babelPlugins, ["typescript", "decorators-legacy"]);
95+
});
96+
97+
test("Babel plugin selection: TSX files get typescript, decorators-legacy, and jsx plugins", (t) => {
98+
const scriptPath = "/project/src/Component.tsx";
99+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
100+
const isTSX = /\.tsx$/.test(scriptPath);
101+
const babelPlugins: string[] = isTypeScript
102+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
103+
: ["decorators-legacy"];
104+
105+
t.deepEqual(babelPlugins, ["typescript", "decorators-legacy", "jsx"]);
106+
});
107+
108+
test("Babel plugin selection: .mts files get typescript and decorators-legacy plugins", (t) => {
109+
const scriptPath = "/project/src/module.mts";
110+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
111+
const isTSX = /\.tsx$/.test(scriptPath);
112+
const babelPlugins: string[] = isTypeScript
113+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
114+
: ["decorators-legacy"];
115+
116+
t.deepEqual(babelPlugins, ["typescript", "decorators-legacy"]);
117+
});
118+
119+
test("Babel plugin selection: .cts files get typescript and decorators-legacy plugins", (t) => {
120+
const scriptPath = "/project/src/commonjs.cts";
121+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
122+
const isTSX = /\.tsx$/.test(scriptPath);
123+
const babelPlugins: string[] = isTypeScript
124+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
125+
: ["decorators-legacy"];
126+
127+
t.deepEqual(babelPlugins, ["typescript", "decorators-legacy"]);
128+
});
129+
130+
test("Babel plugin selection: JavaScript files get only decorators-legacy plugin", (t) => {
131+
const scriptPath = "/project/src/service.js";
132+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
133+
const isTSX = /\.tsx$/.test(scriptPath);
134+
const babelPlugins: string[] = isTypeScript
135+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
136+
: ["decorators-legacy"];
137+
138+
t.deepEqual(babelPlugins, ["decorators-legacy"]);
139+
});
140+
141+
test("Babel plugin selection: .mjs files get only decorators-legacy plugin", (t) => {
142+
const scriptPath = "/project/src/module.mjs";
143+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
144+
const isTSX = /\.tsx$/.test(scriptPath);
145+
const babelPlugins: string[] = isTypeScript
146+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
147+
: ["decorators-legacy"];
148+
149+
t.deepEqual(babelPlugins, ["decorators-legacy"]);
150+
});
151+
152+
test("Babel plugin selection: .cjs files get only decorators-legacy plugin", (t) => {
153+
const scriptPath = "/project/src/commonjs.cjs";
154+
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
155+
const isTSX = /\.tsx$/.test(scriptPath);
156+
const babelPlugins: string[] = isTypeScript
157+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
158+
: ["decorators-legacy"];
159+
160+
t.deepEqual(babelPlugins, ["decorators-legacy"]);
161+
});
162+
163+
// --- Babel parser integration tests ---
164+
165+
test("Babel parser: can parse TypeScript with parameter decorators (decorators-legacy)", (t) => {
166+
// eslint-disable-next-line @typescript-eslint/no-var-requires
167+
const babelParser = require("@babel/parser");
168+
const tsCode = `
169+
class Service {
170+
constructor(@inject('repo') private repo: any) {}
171+
}
172+
`;
173+
174+
// Should parse without error using decorators-legacy
175+
const ast = babelParser.parse(tsCode, {
176+
sourceType: "module",
177+
plugins: ["typescript", "decorators-legacy"],
178+
});
179+
180+
t.truthy(ast);
181+
t.is(ast.type, "File");
182+
});
183+
184+
test("Babel parser: can parse TSX with JSX syntax", (t) => {
185+
// eslint-disable-next-line @typescript-eslint/no-var-requires
186+
const babelParser = require("@babel/parser");
187+
const tsxCode = `
188+
const Component = () => <div>Hello</div>;
189+
export { Component };
190+
`;
191+
192+
// Should parse without error using typescript + jsx + decorators-legacy
193+
const ast = babelParser.parse(tsxCode, {
194+
sourceType: "module",
195+
plugins: ["typescript", "decorators-legacy", "jsx"],
196+
});
197+
198+
t.truthy(ast);
199+
t.is(ast.type, "File");
200+
});
201+
202+
test("Babel parser: can parse JavaScript with class decorators", (t) => {
203+
// eslint-disable-next-line @typescript-eslint/no-var-requires
204+
const babelParser = require("@babel/parser");
205+
const jsCode = `
206+
@decorator
207+
class Example {}
208+
`;
209+
210+
// Should parse without error using decorators-legacy
211+
const ast = babelParser.parse(jsCode, {
212+
sourceType: "module",
213+
plugins: ["decorators-legacy"],
214+
});
215+
216+
t.truthy(ast);
217+
t.is(ast.type, "File");
218+
});

src/core/coverageProcessor.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ function resolveSourceCode(scriptPath: string, projectRoot: string): {
179179
}
180180
}
181181

182-
// Fallback: read the .ts file directly — acorn-typescript can parse it.
182+
// Fallback: read the .ts file directly — @babel/parser can parse it.
183183
// This handles ts-node, ts-node-dev, and --experimental-strip-types.
184184
const code = fs.readFileSync(scriptPath, "utf-8");
185185
return { code, resolvedPath: scriptPath, sourceMap: null };
@@ -208,8 +208,6 @@ export async function processV8CoverageFile(
208208
// Using require() avoids loading them on every SDK startup (adds ~50ms + memory).
209209
// eslint-disable-next-line @typescript-eslint/no-var-requires
210210
const { convert } = require("ast-v8-to-istanbul");
211-
// eslint-disable-next-line @typescript-eslint/no-var-requires
212-
const acorn = require("acorn");
213211
const data: V8CoverageData = preParsedData ?? JSON.parse(fs.readFileSync(v8FilePath, "utf-8"));
214212
const coverage: CoverageResult = {};
215213

@@ -226,34 +224,35 @@ export async function processV8CoverageFile(
226224

227225
// Try parsing as script first (CJS), fall back to module (ESM).
228226
// Track which succeeded — CJS modules have a V8 wrapper that shifts byte offsets.
229-
// For .ts/.tsx files run via --experimental-strip-types, use acorn-typescript
230-
// plugin since acorn can't parse TypeScript syntax natively.
227+
// Use @babel/parser for all files — handles JS and TypeScript uniformly,
228+
// including decorators, generics, import type, etc.
231229
let isCJS = false;
232230
const isTypeScript = /\.(ts|tsx|mts|cts)$/.test(scriptPath);
233-
const parserOptions: Record<string, unknown> = {
234-
ecmaVersion: "latest",
235-
locations: true,
236-
};
237-
238-
// Resolve the parser: use acorn-typescript for .ts files, plain acorn for .js
239-
let parser = acorn;
240-
if (isTypeScript) {
241-
try {
242-
const { tsPlugin } = require("acorn-typescript");
243-
parser = acorn.Parser.extend(tsPlugin()) as typeof acorn;
244-
} catch {
245-
// acorn-typescript not available — plain acorn will be used
246-
// (may fail for TS files, but the outer try/catch handles that)
247-
}
248-
}
231+
const babelParser = require("@babel/parser");
232+
const isTSX = /\.tsx$/.test(scriptPath);
233+
// Use decorators-legacy (not decorators) to support TypeScript's experimental
234+
// parameter decorators used by inversify, NestJS, TypeORM, etc.
235+
// This covers ~99% of TS codebases. If a project uses TC39 Stage 3 decorators
236+
// (experimentalDecorators: false in tsconfig.json), we'd need to switch to the
237+
// "decorators" plugin instead. Could be inferred from tsconfig.json if needed.
238+
const babelPlugins: string[] = isTypeScript
239+
? ["typescript", "decorators-legacy", ...(isTSX ? ["jsx"] : [])]
240+
: ["decorators-legacy"];
249241

250-
// Try script (CJS) first, fall back to module (ESM)
251242
let ast;
252243
try {
253-
ast = parser.parse(code, { ...parserOptions, sourceType: "script" });
244+
ast = babelParser.parse(code, {
245+
sourceType: "script",
246+
plugins: babelPlugins,
247+
ranges: true,
248+
});
254249
isCJS = true;
255250
} catch {
256-
ast = parser.parse(code, { ...parserOptions, sourceType: "module" });
251+
ast = babelParser.parse(code, {
252+
sourceType: "module",
253+
plugins: babelPlugins,
254+
ranges: true,
255+
});
257256
}
258257

259258
// Strip sourceMappingURL from code passed to convert() — we already loaded

use-tusk-drift-node-sdk-0.1.40.tgz

1.79 MB
Binary file not shown.

0 commit comments

Comments
 (0)