Skip to content

Commit

Permalink
fix: add regex for vue SFC to prevent instrumenting style and templat…
Browse files Browse the repository at this point in the history
…es (#180)

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

Single file Components no longer have their html or css instrumented instead of their script

Closes #96 #178 #89

* build: add prepare script to build the package when using a git version

See https://stackoverflow.com/a/57829251

* build: fix dependencies' vulnerabilities

run npm audit fix

* build: bump devDependencies (patch version only)

* fix: instrument both option and composition API

* style: fix sonarlint issues

* revert: reverted changes to dependencies and package.json

Refs: 1e67efc, ea8fa18, 614a88a

* style: changed to single quotes for strings

---------

Co-authored-by: Hugo <[email protected]>
Co-authored-by: Christian Norrman <[email protected]>
  • Loading branch information
3 people authored Feb 17, 2024
1 parent 0a70378 commit c8a7ca7
Showing 1 changed file with 73 additions and 26 deletions.
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

0 comments on commit c8a7ca7

Please sign in to comment.