Skip to content

Commit

Permalink
Merge pull request #6 from elevatebart/master
Browse files Browse the repository at this point in the history
fix: use a strict typescript and make the plugin a post
  • Loading branch information
iFaxity authored Jul 14, 2021
2 parents 35f6425 + 2af62a7 commit f1390a7
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 273 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ vite-plugin-istanbul
[![npm bundle size (scoped)](https://img.shields.io/bundlephobia/min/vite-plugin-istanbul?label=Bundle%20size&style=for-the-badge)](https://npmjs.org/package/vite-plugin-istanbul)
[![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/vite-plugin-istanbul?label=Bundle%20size%20%28gzip%29&style=for-the-badge)](https://npmjs.org/package/vite-plugin-istanbul)

A Vite plugin to instrument your code for nyc/istanbul code coverage. In similar way as the Webpack Loader istanbul-instrumenter-loader. Only intended for use in development.
A Vite plugin to instrument your code for nyc/istanbul code coverage. In similar way as the Webpack Loader istanbul-instrumenter-loader. Only intended for use in development while running tests.

Version v2.x for Vite v2.0, for Vite v1.0 install v1.x of this plugin.

Expand Down Expand Up @@ -37,9 +37,9 @@ Creates the vite plugin from a set of optional plugin options.
* `opts {IstanbulPluginOptions}` - Object of optional options to pass to the plugin
* `opts.include {string|string[]}` - Optional string or array of strings of glob patterns to include
* `opts.exclude {string|string[]}` - Optional string or array of strings of glob patterns to exclude
* `opts.extension {string|string[]}` - Optional string or array of strings of extensions to include (dot prefixed like .js or .ts)
* `opts.extension {string|string[]}` - Optional string or array of strings of extensions to include (dot prefixed like .js or .ts). By default this is set to `['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.vue']`
* `opts.requireEnv {boolean}` - Optional boolean to require env to be true to instrument to code, otherwise it will instrument even if env variable is not set
* `opts.cypress {boolean}` - Optional boolean to change the env to CYPRESS_COVERAGE instead of VITE_COVERAGE. For more ease of use with @cypress/code-coverage
* `opts.cypress {boolean}` - Optional boolean to change the env to CYPRESS_COVERAGE instead of VITE_COVERAGE. For ease of use with @cypress/code-coverage

Examples
--------------------------
Expand All @@ -48,16 +48,17 @@ To use this plugin define it using vite.config.js

```js
// vite.config.js
const istanbul = require('vite-plugin-istanbul');
import istanbul from 'vite-plugin-istanbul';

module.exports = {
export default {
open: true,
port: 3000,
plugins: [
istanbul({
include: 'src/*',
exclude: ['node_modules', 'test/'],
extension: [ '.js', '.ts' ],
extension: [ '.js', '.ts', '.vue' ],
requireEnv: true,
}),
],
};
Expand Down
14 changes: 6 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,14 @@
"nyc"
],
"dependencies": {
"vite": "^2.1.2"
"istanbul-lib-instrument": "^4.0.3",
"test-exclude": "^6.0.0"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@types/babel__core": "^7.1.12",
"@types/node": "^14.0.27",
"babel-plugin-istanbul": "^6.0.0",
"test-exclude": "^6.0.0",
"typescript": "^3.9.7",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0"
"@semantic-release/git": "^9.0.0",
"@types/node": "^16.3.1",
"typescript": "^4.3.5",
"vite": "^2.4.2"
}
}
97 changes: 36 additions & 61 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Plugin, ServerHook, TransformResult } from 'vite';
import type { TransformHook, TransformPluginContext } from 'rollup';
import { transformSync } from '@babel/core';
import BabelPluginIstanbul from 'babel-plugin-istanbul';
import * as TestExclude from 'test-exclude';
import type { TransformHook, TransformPluginContext, SourceMap } from 'rollup';
import { createInstrumenter } from 'istanbul-lib-instrument';
import TestExclude from 'test-exclude';

interface IstanbulPluginOptions {
include?: string|string[];
Expand All @@ -12,15 +11,24 @@ interface IstanbulPluginOptions {
cypress?: boolean;
}

// Required for typing to work in createConfigureServer()
declare global {
var __coverage__: any;
}

// Custom extensions to include .vue files
const DEFAULT_EXTENSION = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.vue'];
const COVERAGE_PUBLIC_PATH = '/__coverage__';
const PLUGIN_NAME = 'vite:istanbul';

function sanitizeSourceMap(sourceMap: SourceMap): SourceMap {
// JSON parse/stringify trick required for istanbul to accept the SourceMap
return JSON.parse(JSON.stringify(sourceMap));
}

function createConfigureServer(): ServerHook {
return ({ middlewares }) => {
// Return global code coverage (will probably be null).
// Returns the current code coverage in the global scope
middlewares.use((req, res, next) => {
if (req.url !== COVERAGE_PUBLIC_PATH) {
return next();
Expand All @@ -42,89 +50,56 @@ function createConfigureServer(): ServerHook {
};
}

function transformCode(this: TransformPluginContext, srcCode: string, id: string, opts: IstanbulPluginOptions): TransformResult {
const plugins = [[ BabelPluginIstanbul, opts ]];
const cwd = process.cwd();

const { code, map } = transformSync(srcCode, {
plugins, cwd,
filename: id,
ast: false,
sourceMaps: true,
comments: true,
compact: true,
babelrc: false,
configFile: false,
parserOpts: {
allowReturnOutsideFunction: true,
sourceType: 'module',
},
// Only keep primitive properties
inputSourceMap: JSON.parse(JSON.stringify(this.getCombinedSourcemap())),
});

// Required to cast to correct mapping value
return { code, map: JSON.parse(JSON.stringify(map)) };
}

function createTransform(opts: IstanbulPluginOptions = {}): TransformHook {
const exclude = new TestExclude({
cwd: process.cwd(),
include: opts.include,
exclude: opts.exclude,
extension: opts.extension,
extension: opts.extension ?? DEFAULT_EXTENSION,
excludeNodeModules: true,
});
const instrumenter = createInstrumenter({
preserveComments: true,
produceSourceMap: true,
autoWrap: true,
esModules: true,
});

return function (srcCode: string, id: string) {
if (process.env.NODE_ENV == 'production' || id.startsWith('/@modules/')) {
return function (this: TransformPluginContext, srcCode: string, id: string): TransformResult | undefined {
if (id.startsWith('/@modules/')) {
// do not transform if this is a dep
// do not transform for production builds
return;
}

if (exclude.shouldInstrument(id)) {
if (!id.endsWith('.vue')) {
return transformCode.call(this, srcCode, id, opts);
}

// Vue files are special, it requires a hack to fix the source mappings
// We take the source code from within the <script> tag and instrument this
// Then we pad the lines to get the correct line numbers for the mappings
let startIndex = srcCode.indexOf('<script>');
const endIndex = srcCode.indexOf('</script>');

if (startIndex == -1 || endIndex == -1) {
// ignore this vue file, doesn't contain any javascript
return;
}

const lines = srcCode.slice(0, endIndex).match(/\n/g)?.length ?? 0;
const startOffset = '<script>'.length;

srcCode = '\n'.repeat(lines) + srcCode.slice(startIndex + startOffset, endIndex);

const res = transformCode.call(this, srcCode, id, opts);
const sourceMap = sanitizeSourceMap(this.getCombinedSourcemap());
const code = instrumenter.instrumentSync(srcCode, id, sourceMap);
const map = instrumenter.lastSourceMap();

res.code = `${srcCode.slice(0, startIndex + startOffset)}\n${res.code}\n${srcCode.slice(endIndex)}`;
return res;
// Required to cast to correct mapping value
return { code, map } as TransformResult;
}
};
}

function istanbulPlugin(opts?: IstanbulPluginOptions): Plugin {
function istanbulPlugin(opts: IstanbulPluginOptions = {}): Plugin {
// Only instrument when we want to, as we only want instrumentation in test
// By default the plugin is always on
const env = opts.cypress ? process.env.CYPRESS_COVERAGE : process.env.VITE_COVERAGE;
const requireEnv = opts.requireEnv ?? false;

if (requireEnv && env?.toLowerCase() === 'false') {
return { name: 'vite:istanbul' };
if (process.env.NODE_ENV == 'production' && requireEnv && env?.toLowerCase() === 'false') {
return { name: PLUGIN_NAME };
}

return {
name: 'vite:istanbul',
name: PLUGIN_NAME,
transform: createTransform(opts),
configureServer: createConfigureServer(),
// istanbul only knows how to instrument JavaScript,
// this allows us to wait until the whole code is JavaScript to
// instrument and sourcemap
enforce: 'post',
};
}

Expand Down
29 changes: 29 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
declare module 'istanbul-lib-instrument' {
import { SourceMap } from 'rollup';
interface Instrumenter {
instrumentSync(code: string, filename: string, inputSourceMap?: SourceMap | undefined): string;
lastSourceMap(): SourceMap;
}

export function createInstrumenter(opts: {
preserveComments?: boolean,
produceSourceMap?: boolean,
autoWrap?: boolean,
esModules?: boolean,
}): Instrumenter;
}

declare module 'test-exclude' {
class TestExclude {
constructor(opts: {
cwd?: string | string[],
include?: string | string[],
exclude?: string | string[],
extension?: string | string[],
excludeNodeModules?: boolean,
})

shouldInstrument(filePath:string):boolean
}
export = TestExclude
}
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"allowSyntheticDefaultImports": true
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": [ "src/*" ],
"exclude": [ "**/node_modules" ]
Expand Down
Loading

0 comments on commit f1390a7

Please sign in to comment.