diff --git a/lib/data.js b/lib/data.js index c11873c7084264208c01abd66ee4a5c43e9e9aee..0858ce94961c27e880dbe6226b44a36e4553ed2f 100644 --- a/lib/data.js +++ b/lib/data.js @@ -49,6 +49,28 @@ const v8_to_istanbul_1 = __importDefault(require("v8-to-istanbul")); const convertSourceMap = __importStar(require("convert-source-map")); exports.attachmentName = '@bgotink/playwright-coverage'; const fetch = import('node-fetch'); +async function tryReadLocalSourceMap(url) { + try { + const parsed = new url_1.URL(url); + const urlPath = parsed.pathname.replace(/^\//, ''); + const candidates = [ + (0, path_1.join)(process.cwd(), 'dist', urlPath), + (0, path_1.join)(process.cwd(), urlPath), + ]; + for (const candidate of candidates) { + try { + return await fs_1.promises.readFile(candidate, 'utf8'); + } + catch { + // try next candidate + } + } + } + catch { + // invalid URL + } + return null; +} async function getSourceMap(url, source) { const inlineMap = convertSourceMap.fromSource(source); if (inlineMap != null) { @@ -72,10 +94,19 @@ async function getSourceMap(url, source) { return dataString; } default: { - const response = await (await fetch).default(resolved.href, { - method: 'GET', - }); - return await response.text(); + try { + const response = await (await fetch).default(resolved.href, { + method: 'GET', + }); + return await response.text(); + } + catch { + const local = await tryReadLocalSourceMap(resolved.href); + if (local != null) { + return local; + } + throw new Error(`Failed to fetch sourcemap: ${resolved.href}`); + } } } }); @@ -94,6 +125,15 @@ async function getSourceMap(url, source) { return (await response.json()); } catch { + try { + const local = await tryReadLocalSourceMap(`${url}.map`); + if (local != null) { + return JSON.parse(local); + } + } + catch { + // ignore + } return undefined; } } @@ -104,10 +144,40 @@ async function convertToIstanbulCoverage(v8Coverage, sources, sourceMaps, exclud const istanbulCoverage = (0, istanbul_lib_coverage_1.createCoverageMap)({}); for (const script of v8Coverage.result) { const source = sources.get(script.url); - const sourceMap = sourceMaps.get(script.url); + let sourceMap = sourceMaps.get(script.url); if (source == null || !(sourceMap === null || sourceMap === void 0 ? void 0 : sourceMap.mappings)) { continue; } + // Rewrite sourcemap sources from build-output-relative paths to + // project-relative paths. Vite emits sources like "../../src/foo.ts" + // relative to dist/assets/. Resolve them against the script URL to + // get server-absolute paths, then strip the origin. + if (sourceMap.sources != null) { + const scriptUrl = script.url; + try { + const origin = new url_1.URL(scriptUrl).origin; + sourceMap = { + ...sourceMap, + sources: sourceMap.sources.map(s => { + if (s == null) + return s; + try { + const resolved = new url_1.URL(s, scriptUrl).href; + if (resolved.startsWith(origin + '/')) { + return resolved.slice(origin.length + 1); + } + } + catch { + // not a valid URL combo + } + return s; + }), + }; + } + catch { + // scriptUrl is not a valid URL — skip rewriting + } + } function sanitizePath(path) { let url; try { diff --git a/src/data.ts b/src/data.ts index 539a70e5bde5c2ab8644b0bfa3ff52625cf4490e..29bc26bf729cbf9496594d9b9e91745a095658c5 100644 --- a/src/data.ts +++ b/src/data.ts @@ -12,6 +12,33 @@ export const attachmentName = '@bgotink/playwright-coverage'; const fetch = import('node-fetch'); +/** + * Try to read a sourcemap from the local filesystem by mapping a URL path + * (e.g. /assets/index-abc.js.map) to a local file (e.g. dist/assets/index-abc.js.map). + * Falls back to common build output directories. + */ +async function tryReadLocalSourceMap(url: string): Promise { + try { + const parsed = new URL(url); + // Try mapping URL pathname to dist/ directory + const urlPath = parsed.pathname.replace(/^\//, ''); + const candidates = [ + join(process.cwd(), 'dist', urlPath), + join(process.cwd(), urlPath), + ]; + for (const candidate of candidates) { + try { + return await fs.readFile(candidate, 'utf8'); + } catch { + // try next candidate + } + } + } catch { + // invalid URL + } + return null; +} + export async function getSourceMap( url: string, source: string, @@ -44,13 +71,23 @@ export async function getSourceMap( return dataString; } default: { - const response = await ( - await fetch - ).default(resolved.href, { - method: 'GET', - }); + // Try HTTP fetch first, fall back to reading from local filesystem + try { + const response = await ( + await fetch + ).default(resolved.href, { + method: 'GET', + }); - return await response.text(); + return await response.text(); + } catch { + // HTTP fetch failed — try reading from local dist/ directory + const local = await tryReadLocalSourceMap(resolved.href); + if (local != null) { + return local; + } + throw new Error(`Failed to fetch sourcemap: ${resolved.href}`); + } } } }, @@ -73,6 +110,15 @@ export async function getSourceMap( return (await response.json()) as EncodedSourceMap; } catch { + // HTTP fetch failed — try reading from local dist/ directory + try { + const local = await tryReadLocalSourceMap(`${url}.map`); + if (local != null) { + return JSON.parse(local) as EncodedSourceMap; + } + } catch { + // ignore + } return undefined; } } @@ -102,12 +148,41 @@ export async function convertToIstanbulCoverage( for (const script of v8Coverage.result) { const source = sources.get(script.url); - const sourceMap = sourceMaps.get(script.url); + let sourceMap = sourceMaps.get(script.url); if (source == null || !sourceMap?.mappings) { continue; } + // Rewrite sourcemap sources from build-output-relative paths to + // project-relative paths. Vite emits sources like "../../src/foo.ts" + // relative to dist/assets/. Resolve them against the script URL to + // get server-absolute paths, then strip the origin to get + // project-relative paths (e.g. "src/foo.ts"). + if (sourceMap.sources != null) { + const scriptUrl = script.url; + try { + const origin = new URL(scriptUrl).origin; + sourceMap = { + ...sourceMap, + sources: sourceMap.sources.map(s => { + if (s == null) return s; + try { + const resolved = new URL(s, scriptUrl).href; + if (resolved.startsWith(origin + '/')) { + return resolved.slice(origin.length + 1); + } + } catch { + // not a valid URL combo + } + return s; + }), + }; + } catch { + // scriptUrl is not a valid URL — skip rewriting + } + } + function sanitizePath(path: string) { let url;