From 6683109d06ab937dfbb9a615a74882d3e2922c1a Mon Sep 17 00:00:00 2001 From: Sohil Kshirsagar Date: Wed, 6 May 2026 17:37:10 -0700 Subject: [PATCH 1/2] fix(coverage): use dynamic import for ast-v8-to-istanbul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ast-v8-to-istanbul is ESM-only and pulls in estree-walker, whose package.json `exports` field has no `require` condition. Under tsx's ESM loader, `require("ast-v8-to-istanbul")` resolves transitive imports with the require condition and dies on estree-walker with ERR_PACKAGE_PATH_NOT_EXPORTED — every coverage snapshot fails and no data is uploaded. Switching to dynamic `await import()` resolves with the import condition throughout and works under both plain Node and tsx. --- src/core/coverageProcessor.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/coverageProcessor.ts b/src/core/coverageProcessor.ts index b04afb1..72ce9a1 100644 --- a/src/core/coverageProcessor.ts +++ b/src/core/coverageProcessor.ts @@ -258,10 +258,13 @@ export async function processV8CoverageFile( includeAll: boolean = false, preParsedData?: V8CoverageData, ): Promise { - // Lazy-loaded: these are only needed when coverage is enabled, which is opt-in. - // Using require() avoids loading them on every SDK startup (adds ~50ms + memory). - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { convert } = require("ast-v8-to-istanbul"); + // Lazy-loaded: only needed when coverage is enabled (opt-in), saves ~50ms + memory at SDK startup. + // Using dynamic import (not require) because ast-v8-to-istanbul is ESM-only and pulls in + // estree-walker, whose package.json `exports` field has no `require` condition. Under tsx's + // ESM loader, `require("ast-v8-to-istanbul")` resolves transitive imports with the require + // condition and dies on estree-walker with ERR_PACKAGE_PATH_NOT_EXPORTED. Dynamic import + // resolves with the import condition throughout and works under both plain Node and tsx. + const { convert } = await import("ast-v8-to-istanbul"); const data: V8CoverageData = preParsedData ?? JSON.parse(fs.readFileSync(v8FilePath, "utf-8")); const coverage: CoverageResult = {}; From fa0fad86e2fb3ca8a8ba52f66671446c2b6e886c Mon Sep 17 00:00:00 2001 From: Sohil Kshirsagar Date: Wed, 6 May 2026 17:46:09 -0700 Subject: [PATCH 2/2] fix(coverage): tighten V8FunctionCoverage type and adjust source-map cast Tighten V8FunctionCoverage to match Node's Profiler.FunctionCoverage contract (functionName and isBlockCoverage are always emitted by V8), which lets script.functions pass through to ast-v8-to-istanbul.convert without a cast. Update the test fixture accordingly. Source map still needs a cast since loadSourceMap returns Record. --- src/core/coverageProcessor.test.ts | 1 + src/core/coverageProcessor.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/coverageProcessor.test.ts b/src/core/coverageProcessor.test.ts index 155d2a7..b8abdb5 100644 --- a/src/core/coverageProcessor.test.ts +++ b/src/core/coverageProcessor.test.ts @@ -60,6 +60,7 @@ test("takeAndProcessSnapshot: skips coverage files without user scripts", async { functionName: "", ranges: [{ startOffset: 0, endOffset: 10, count: 1 }], + isBlockCoverage: false, }, ], }, diff --git a/src/core/coverageProcessor.ts b/src/core/coverageProcessor.ts index 72ce9a1..7867e47 100644 --- a/src/core/coverageProcessor.ts +++ b/src/core/coverageProcessor.ts @@ -38,9 +38,11 @@ export interface V8CoverageRange { } export interface V8FunctionCoverage { - functionName?: string; + // V8 always emits these per Node's Profiler.FunctionCoverage contract; matching that + // shape lets us pass V8FunctionCoverage[] to ast-v8-to-istanbul.convert without a cast. + functionName: string; ranges: V8CoverageRange[]; - isBlockCoverage?: boolean; + isBlockCoverage: boolean; } export interface V8ScriptCoverage { @@ -331,7 +333,10 @@ export async function processV8CoverageFile( code: codeForConvert, ast, coverage: { functions: script.functions, url: script.url }, - ...(sourceMap ? { sourceMap } : {}), + // sourceMap is parsed JSON — loadSourceMap returns Record rather + // than committing to a specific source-map type from a transitive dep. Cast to the + // shape convert expects. + ...(sourceMap ? { sourceMap: sourceMap as Parameters[0]["sourceMap"] } : {}), ...(cjsWrapperLength ? { wrapperLength: cjsWrapperLength } : {}), });