Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add regex for vue SFC to prevent instrumenting style and templates #180

Merged
merged 8 commits into from
Feb 17, 2024
99 changes: 73 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { ExistingRawSourceMap } from 'rollup';
import { Plugin, TransformResult, createLogger } from 'vite';
import { createInstrumenter } from 'istanbul-lib-instrument';
import TestExclude from 'test-exclude';
import { loadNycConfig } from '@istanbuljs/load-nyc-config';
import { createInstrumenter } from 'istanbul-lib-instrument';
import picocolors from 'picocolors';
import {createIdentitySourceMap} from "./source-map";
import type { ExistingRawSourceMap } from 'rollup';
import TestExclude from 'test-exclude';
import { Plugin, TransformResult, createLogger } from 'vite';
import { createIdentitySourceMap } from './source-map';

const { yellow } = picocolors;

Expand All @@ -14,9 +14,9 @@ declare global {
}

export interface IstanbulPluginOptions {
include?: string|string[];
exclude?: string|string[];
extension?: string|string[];
include?: string | string[];
exclude?: string | string[];
extension?: string | string[];
requireEnv?: boolean;
cypress?: boolean;
checkProd?: boolean;
Expand All @@ -40,9 +40,9 @@ function sanitizeSourceMap(rawSourceMap: ExistingRawSourceMap): ExistingRawSourc
return JSON.parse(JSON.stringify(sourceMap));
}

function getEnvVariable(key: string, prefix: string|string[], env: Record<string, any>): string {
function getEnvVariable(key: string, prefix: string | string[], env: Record<string, any>): string {
if (Array.isArray(prefix)) {
const envPrefix = prefix.find(pre => {
const envPrefix = prefix.find((pre) => {
const prefixedName = `${pre}${key}`;

return env[prefixedName] != null;
Expand Down Expand Up @@ -77,11 +77,51 @@ async function createTestExclude(opts: IstanbulPluginOptions): Promise<TestExclu
function resolveFilename(id: string): string {
// Fix for @vitejs/plugin-vue in serve mode (#67)
// To remove the annoying query parameters from the filename
const [ filename ] = id.split('?vue');
const [filename] = id.split('?vue');

return filename;
}

/** # Fix for vue Single-File Components instrumentation in build mode. (cf issue #96)
*
* ## Option API SFC splits file into 2
*
* 1. id: /path/to/file.vue which contains all the code **What we need to instrument**
* 2. id: /path/to/file.vue?vue&type=style&... which contains no source code
*
* ## Composition API SFC splits file into 3 chunks
*
* 1. id: /path/to/file.vue which contains only impors and exports but no user's source code
* 2. id: /path/to/file.vue?vue&type=style&... which contains no source code
* 3. id: /path/to/file.vue?vue&type=script&... which contains all the user's source code **What we need to instrument**
*
* ## Diff of chunk 1
*
* - Composition API: starts with `import _sfc_main from '/path/to/file.vue?vue&type=script...'\n`
* - Option API: starts with `\nconst _sfc_main = {\n`
*
*/
function canInstrumentChunk(id: string, srcCode: string): boolean {
const is1stChunk = id.endsWith('.vue');
const is2ndChunk = /\?vue&type=style/.test(id);
const is3rdChunk = /\?vue&type=script/.test(id);
const isCompositionAPI = /import _sfc_main from/.test(srcCode);
if (is2ndChunk) {
// never instrument type=style
return false;
}
if (is3rdChunk) {
// always instrument type=script
return true;
}
if (is1stChunk) {
// instrument 1st chunk only if it's Option API
return !isCompositionAPI;
}
// instrument if not a vue chunk
return true;
}

export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin {
const requireEnv = opts?.requireEnv ?? false;
const checkProd = opts?.checkProd ?? true;
Expand All @@ -107,9 +147,7 @@ export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin
name: PLUGIN_NAME,
apply(_, env) {
// If forceBuildInstrument is true run for both serve and build
return forceBuildInstrument
? true
: env.command == 'serve';
return forceBuildInstrument ? true : env.command == 'serve';
},
// istanbul only knows how to instrument JavaScript,
// this allows us to wait until the whole code is JavaScript to
Expand All @@ -118,14 +156,17 @@ export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin
async config(config) {
// If sourcemap is not set (either undefined or false)
if (!config.build?.sourcemap) {
logger.warn(`${PLUGIN_NAME}> ${yellow(`Sourcemaps was automatically enabled for code coverage to be accurate.
To hide this message set build.sourcemap to true, 'inline' or 'hidden'.`)}`);
logger.warn(
`${PLUGIN_NAME}> ${yellow(
'Sourcemaps was automatically enabled for code coverage to be accurate.\n To hide this message set build.sourcemap to true, "inline" or "hidden".'
)}`
);

// Enforce sourcemapping,
config.build = config.build || {};
config.build ??= {};
config.build.sourcemap = true;
}
testExclude = await createTestExclude(opts)
testExclude = await createTestExclude(opts);
},
configResolved(config) {
// We need to check if the plugin should enable after all configuration is resolved
Expand All @@ -139,9 +180,11 @@ export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin
: getEnvVariable('COVERAGE', envPrefix, env);
const envVar = envCoverage?.toLowerCase() ?? '';

if ((checkProd && isProduction && !forceBuildInstrument) ||
if (
(checkProd && isProduction && !forceBuildInstrument) ||
(!requireEnv && envVar === 'false') ||
(requireEnv && envVar !== 'true')) {
(requireEnv && envVar !== 'true')
) {
enabled = false;
}
},
Expand All @@ -157,7 +200,7 @@ export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin
return next();
}

const coverage = (global.__coverage__) ?? null;
const coverage = global.__coverage__ ?? null;
let data: string;

try {
Expand All @@ -178,7 +221,9 @@ export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin
// do not transform if ssr
return;
}

if (!canInstrumentChunk(id, srcCode)) {
return;
}
const filename = resolveFilename(id);

if (testExclude.shouldInstrument(filename)) {
Expand All @@ -187,10 +232,12 @@ export default function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin
const code = instrumenter.instrumentSync(srcCode, filename, combinedSourceMap);

// Create an identity source map with the same number of fields as the combined source map
const identitySourceMap = sanitizeSourceMap(createIdentitySourceMap(filename, srcCode, {
file: combinedSourceMap.file,
sourceRoot: combinedSourceMap.sourceRoot
}));
const identitySourceMap = sanitizeSourceMap(
createIdentitySourceMap(filename, srcCode, {
file: combinedSourceMap.file,
sourceRoot: combinedSourceMap.sourceRoot,
})
);

// Create a result source map to combine with the source maps of previous plugins
instrumenter.instrumentSync(srcCode, filename, identitySourceMap);
Expand Down
Loading