build: add task producing instrumented build for code coverage collection

This commit is contained in:
Kirill Gavrilov 2025-10-20 00:31:36 +03:00
parent 56f5a6362c
commit ad25845503
3 changed files with 61 additions and 0 deletions

View file

@ -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",

View file

@ -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()
}

View file

@ -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}`)
}