From ad2584550301e0abed1dd32ae10cf93286f2e74d Mon Sep 17 00:00:00 2001 From: Kirill Gavrilov Date: Mon, 20 Oct 2025 00:31:36 +0300 Subject: [PATCH] build: add task producing instrumented build for code coverage collection --- package.json | 3 +++ scripts/esbuild.build.js | 9 +++++++ scripts/utils/instrument.js | 49 +++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 scripts/utils/instrument.js diff --git a/package.json b/package.json index 8ddee73..5381bff 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "node scripts/dev.js", "build": "tsc -noEmit -skipLibCheck && node scripts/esbuild.build.js production", + "build:coverage": "node scripts/esbuild.build.js coverage", "test:eslint": "eslint --cache --cache-location out/.eslintcache", "test:unit": "vitest --coverage" }, @@ -18,6 +19,7 @@ "devDependencies": { "@eslint/js": "9.37.0", "@stylistic/eslint-plugin": "5.4.0", + "@types/istanbul-lib-instrument": "1.7.8", "@types/node": "24.6.2", "@vitest/coverage-v8": "3.2.4", "enquirer": "2.4.1", @@ -26,6 +28,7 @@ "eslint-plugin-perfectionist": "4.15.1", "eslint-plugin-wdio": "9.16.2", "globals": "16.4.0", + "istanbul-lib-instrument": "6.0.3", "obsidian": "1.10.0", "obsidian-utils": "0.10.2", "typescript": "5.9.3", diff --git a/scripts/esbuild.build.js b/scripts/esbuild.build.js index cf0a7f2..c3633d0 100644 --- a/scripts/esbuild.build.js +++ b/scripts/esbuild.build.js @@ -5,21 +5,30 @@ import process from 'node:process' import esbuild from 'esbuild' import { sharedEsbuildConfig } from './esbuild.config.js' +import { instrumentWithSourceMaps } from './utils/instrument.js' const config = sharedEsbuildConfig const prod = process.argv[2] === 'production' +const coverage = process.argv[2] === 'coverage' const context = await esbuild.context({ ...config, ...prod ? { minify: true, sourcemap: false } : {}, + ...coverage + ? { minify: false, sourcemap: 'inline' } + : {}, }) if (prod) { await context.rebuild() process.exit(0) +} else if (coverage) { + await context.rebuild() + instrumentWithSourceMaps('out/main.js', 'out/main.js') + process.exit(0) } else { await context.watch() } diff --git a/scripts/utils/instrument.js b/scripts/utils/instrument.js new file mode 100644 index 0000000..e92de48 --- /dev/null +++ b/scripts/utils/instrument.js @@ -0,0 +1,49 @@ +// @ts-check +import * as fs from 'node:fs' + +import { createInstrumenter } from 'istanbul-lib-instrument' + +/** + * Instrument a JavaScript file with Istanbul coverage instrumentation + * while preserving and remapping original sourcemaps + * @param {string} inputFile + * @param {string} outputFile + */ +export function instrumentWithSourceMaps(inputFile, outputFile) { + const code = fs.readFileSync(inputFile, 'utf8') + + // Extract the original sourcemap from the bundled file + const sourceMapMatch + = code.match(/\/\/# sourceMappingURL=data:application\/json;base64,(.+)/) + if (!sourceMapMatch) throw new Error(`No sourcemap found in ${inputFile}`) + + const sourceMapJson = Buffer.from(sourceMapMatch[1].trim(), 'base64').toString('utf8') + const originalSourceMap = JSON.parse(sourceMapJson) + + const instrumenter = createInstrumenter({ + esModules: false, // esbuild outputs Obsidian plugins as CommonJS + compact: false, + preserveComments: true, + produceSourceMap: true, + autoWrap: true, + }) + + const instrumentedCode = instrumenter.instrumentSync( + code, + inputFile, + originalSourceMap, // <- This is critical for sourcemaps remapping! + ) + const sourceMap = instrumenter.lastSourceMap() + + let output = instrumentedCode + + if (sourceMap) { + const base64SourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64') + output + += `\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}` + } + + fs.writeFileSync(outputFile, output, 'utf8') + + console.log(`✓ Instrumented: ${inputFile} -> ${outputFile}`) +}