diff --git a/package.json b/package.json index 00b0b77..e639d49 100644 --- a/package.json +++ b/package.json @@ -79,9 +79,9 @@ "@jsdevtools/coverage-istanbul-loader": "^3.0.5", "@types/istanbul-lib-coverage": "^2.0.4", "convert-source-map": "^2.0.0", + "espree": "^9.6.1", "istanbul-lib-instrument": "^6.0.1", - "loader-utils": "^3.2.1", - "merge-source-map": "^1.1.0", + "source-map": "^0.7.4", "test-exclude": "^6.0.0", "vite-plugin-istanbul": "^3.0.1" } diff --git a/src/loader/webpack5-istanbul-loader.ts b/src/loader/webpack5-istanbul-loader.ts index 0e08dc9..2d21da7 100644 --- a/src/loader/webpack5-istanbul-loader.ts +++ b/src/loader/webpack5-istanbul-loader.ts @@ -1,16 +1,19 @@ +import { Instrumenter, InstrumenterOptions } from "istanbul-lib-instrument"; import { fromSource, fromMapFileSource } from "convert-source-map"; -import { - createInstrumenter, - InstrumenterOptions, -} from "istanbul-lib-instrument"; + // @ts-expect-error no types -import mergeSourceMap from "merge-source-map"; -import { LoaderContext } from "webpack"; +import * as espree from "espree"; import fs from "fs"; import path from "path"; +import { LoaderContext } from "webpack"; +import { SourceMapGenerator, StartOfSourceMap } from "source-map"; + import { AddonOptionsWebpack } from "../types"; -export type Options = Partial & AddonOptionsWebpack; +export type Options = Partial & + AddonOptionsWebpack & { + instrumenter: Instrumenter; + }; type RawSourceMap = { version: number; @@ -22,51 +25,74 @@ type RawSourceMap = { names?: string[]; }; -export const defaultOptions: Partial = { - preserveComments: true, - produceSourceMap: true, - autoWrap: true, - esModules: true, - compact: false, -}; +function sanitizeSourceMap(rawSourceMap: RawSourceMap): RawSourceMap { + const { sourcesContent, ...sourceMap } = rawSourceMap ?? {}; + + // JSON parse/stringify trick required for istanbul to accept the SourceMap + return JSON.parse(JSON.stringify(sourceMap)); +} + +function createIdentitySourceMap( + file: string, + source: string, + option: StartOfSourceMap +) { + const gen = new SourceMapGenerator(option); + const tokens = espree.tokenize(source, { loc: true, ecmaVersion: "latest" }); + + tokens.forEach((token: any) => { + const loc = token.loc.start; + gen.addMapping({ + source: file, + original: loc, + generated: loc, + }); + }); + + return JSON.parse(gen.toString()); +} export default function ( this: LoaderContext, source: string, sourceMap?: RawSourceMap ) { - let map = sourceMap; - let options = Object.assign(defaultOptions, this.getOptions()); + let map = sourceMap ?? getInlineSourceMap.call(this, source); + const options = this.getOptions(); + const callback = this.async(); - // If there's no external sourceMap file, then check for an inline sourceMap if (!map) { - map = sourceMap = getInlineSourceMap.call(this, source); + callback(null, source, sourceMap); + return; } // Instrument the code - let instrumenter = createInstrumenter(options); - instrumenter.instrument( + const instrumenter = options.instrumenter; + + const combinedSourceMap = sanitizeSourceMap(sourceMap); + + const code = instrumenter.instrumentSync( source, this.resourcePath, - (error, instrumentedSource) => { - let instrumentedSourceMap = instrumenter.lastSourceMap(); - - if (sourceMap && instrumentedSourceMap) { - // Re-map the source map to the original source code - instrumentedSourceMap = mergeSourceMap( - sourceMap, - instrumentedSourceMap - ); - } - - this.callback( - error, - instrumentedSource, - instrumentedSourceMap as any as RawSourceMap - ); - }, - sourceMap as any + combinedSourceMap as any + ); + + const identitySourceMap = sanitizeSourceMap( + createIdentitySourceMap(this.resourcePath, source, { + file: combinedSourceMap.file, + sourceRoot: combinedSourceMap.sourceRoot, + }) ); + + instrumenter.instrumentSync( + source, + this.resourcePath, + identitySourceMap as any + ); + + const lastSourceMap = instrumenter.lastSourceMap(); + + callback(null, code, lastSourceMap as any); } /** diff --git a/src/preset.ts b/src/preset.ts index 6af9b39..019ed29 100644 --- a/src/preset.ts +++ b/src/preset.ts @@ -3,6 +3,10 @@ import { defaultExclude, defaultExtensions } from "./constants"; import type { AddonOptionsVite, AddonOptionsWebpack } from "./types"; import { createTestExclude } from "./webpack5-exclude"; import { getNycConfig } from "./nyc-config"; +import { + InstrumenterOptions, + createInstrumenter, +} from "istanbul-lib-instrument"; export const viteFinal = async ( viteConfig: Record, @@ -32,6 +36,14 @@ export const viteFinal = async ( return viteConfig; }; +const defaultOptions: Partial = { + preserveComments: true, + produceSourceMap: true, + autoWrap: true, + esModules: true, + compact: false, +}; + export const webpackFinal = async ( webpackConfig: Record, options: Options & AddonOptionsWebpack @@ -45,11 +57,17 @@ export const webpackFinal = async ( const testExclude = await createTestExclude(options.istanbul); - webpackConfig.module.rules.push({ + let instrumenterOptions = Object.assign(defaultOptions, options.istanbul); + let instrumenter = createInstrumenter(instrumenterOptions); + + webpackConfig.module.rules.unshift({ test: new RegExp(extensions?.join("|").replace(/\./g, "\\.")), loader: require.resolve("./loader/webpack5-istanbul-loader"), enforce: "post", - options: options.istanbul || {}, + options: { + ...(options.istanbul ?? {}), + instrumenter, + }, include: (modulePath: string) => testExclude.shouldInstrument(modulePath), }); diff --git a/yarn.lock b/yarn.lock index 829c8ce..91d0457 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1903,7 +1903,12 @@ acorn-import-assertions@^1.9.0: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== -acorn@^8.7.1, acorn@^8.8.2: +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== @@ -3039,6 +3044,20 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -4045,11 +4064,6 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5282,6 +5296,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + spawn-command@^0.0.2-1: version "0.0.2-1" resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"