Skip to content

Commit

Permalink
Finalize istanbul webpack5 loader
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinpalkovic committed Nov 10, 2023
1 parent 053738e commit 95139ce
Show file tree
Hide file tree
Showing 10 changed files with 637 additions and 197 deletions.
54 changes: 29 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Storybook Addon Coverage

Tools to support code coverage in Storybook and the [Storybook test runner](https://github.com/storybookjs/test-runner). It supports Storybook projects that use **Babel** or **Vite**.
Tools to support code coverage in Storybook and the [Storybook test runner](https://github.com/storybookjs/test-runner). It supports Storybook projects that use **Webpack5** or **Vite**.

### Installation

Expand All @@ -13,17 +13,17 @@ yarn add -D @storybook/addon-coverage
And by registering it in your `.storybook/main.js`:

```js
module.exports = {
export default {
addons: ["@storybook/addon-coverage"],
};
```

### Configuring the addon

This addon instruments your code by using [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul) if your project uses Babel or [vite-plugin-istanbul](https://github.com/iFaxity/vite-plugin-istanbul) if your project uses Vite. It provides some default configuration, but if you want to add yours, you can do so by setting the options in your `.storybook/main.js`:
This addon instruments your code by using a custom wrapper around [istanbul-lib-instrument](https://www.npmjs.com/package/istanbul-lib-instrument) if your project uses Webpack5 or [vite-plugin-istanbul](https://github.com/iFaxity/vite-plugin-istanbul) if your project uses Vite. It provides some default configuration, but if you want to add yours, you can do so by setting the options in your `.storybook/main.js`:

```js
module.exports = {
export default {
addons: [
{
name: "@storybook/addon-coverage",
Expand All @@ -37,26 +37,29 @@ module.exports = {
};
```

**The available options if your project uses Babel are as follows:**

| Option name | Description | Type | Default |
| --------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `cwd` | Set the working directory | `String` | `process.cwd()` |
| `include` | See [here](https://github.com/istanbuljs/nyc#selecting-files-for-coverage) for more info | `Array<String>` | `['**']` |
| `exclude` | See [here](https://github.com/istanbuljs/nyc#selecting-files-for-coverage) for more info | `Array<String>` | [list](https://github.com/storybookjs/addon-coverage/blob/main/src/constants.ts) |
| `extension` | List of extensions that nyc should attempt to handle in addition to `.js` | `Array<String>` | `['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.vue', '.svelte]` |
| `excludeNodeModules` | Whether or not to exclude all node_module folders (i.e. **/node_modules/**) by default | `boolean` | `true` |
| `ignoreClassMethods` | Class method names to ignore for coverage` | `Array<String>` | `[]` |
| `useInlineSourceMaps` | Variable to pass sourcemap explicitly | `object` | `-` |
| `inputSourceMap` | Scope to store the coverage variable | `string` | `-` |
| `nycrcPath` | Path to nyc config file | `string` | `-` |
| `onCover` | Hook used to track coverage for all files | `(fileName: string, fileCoverage: FileCoverage) => unknown` | `-` |
| `fileName` | File name to use in onCover hook | `string` | `-` |

> **Note:**
**The available options if your project uses Webpack5 are as follows:**

| Option name | Description | Type | Default |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------ |
| `cwd` | Set the working directory | `String` | `process.cwd()` |
| `nycrcPath` | Path to specific nyc config to use instead of automatically searching for a nycconfig. | `string` | - |
| `include` | Glob pattern to include files. It has precedence over the include definition from your nyc config | `Array<String>` | - |
| `exclude` | Glob pattern to exclude files. It has precedence over the exclude definition from your nyc config | `Array<String>` | - |
| `extension` | List of supported extensions. It has precedence over the extension definition from your nyc config | `Array<String>` | `['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.vue', '.svelte]` |
| `coverageVariable` | The global variable name that Istanbul will use to store coverage results. | `string` | - |
| `preserveComments` | Indicates whether comments in the code should be preserved during the instrumentation process. | `boolean` | `true` |
| `compact` | Controls whether the output of instrumented code is compacted. Useful for debugging when set to `false`. | `boolean` | `false` |
| `esModules` | Determines whether the code to be instrumented uses ES Module syntax. | `boolean` | `true` |
| `autoWrap` | When set to `true`, wraps program code in a function to enable top-level return statements. | `boolean` | `true` |
| `produceSourceMap` | If `true`, instructs Istanbul to produce a source map for the instrumented code. | `boolean` | `true` |
| `sourceMapUrlCallback` | A callback function that gets invoked with the filename and the source map URL when a source map is generated. | `function` | - |
| `debug` | Enables the debug mode, providing additional logging information during the instrumentation process. | `boolean` | - |

> **Note:**
> If you're using typescript, you can import the type for the options like so:
>
> ```ts
> import type { AddonOptionsBabel } from '@storybook/addon-coverage'
> import type { AddonOptionsWebpack } from "@storybook/addon-coverage";
> ```
**The available options if your project uses Vite are as follows:**
Expand All @@ -67,16 +70,17 @@ module.exports = {
| `include` | See [here](https://github.com/istanbuljs/nyc#selecting-files-for-coverage) for more info | `Array<String>` or `string` | `['**']` |
| `exclude` | See [here](https://github.com/istanbuljs/nyc#selecting-files-for-coverage) for more info | `Array<String>` or `string` | [list](https://github.com/storybookjs/addon-coverage/blob/main/src/constants.ts) |
| `extension` | List of extensions that nyc should attempt to handle in addition to `.js` | `Array<String>` or `string` | `['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.vue', '.svelte]` |
| `requireEnv ` | Optional boolean to require the environment variable (defaults to VITE_COVERAGE) to equal true in order to instrument the code. Otherwise it will instrument even if env variable is not set. However if requireEnv is not set the instrumentation will stop if the environment variable is equal to false. | `boolean` | `-` |
| `requireEnv ` | Optional boolean to require the environment variable (defaults to VITE_COVERAGE) to equal true in order to instrument the code. Otherwise it will instrument even if env variable is not set. However if requireEnv is not set the instrumentation will stop if the environment variable is equal to false. | `boolean` | `-` |
| `cypress ` | Optional boolean to change the environment variable to CYPRESS_COVERAGE instead of VITE_COVERAGE. For ease of use with `@cypress/code-coverage` coverage | `boolean` | `-` |
| `checkProd ` | Optional boolean to enforce the plugin to skip instrumentation for production environments. Looks at Vite's isProduction key from the ResolvedConfig. | `boolean` | `-` |
| `forceBuildInstrument ` | Optional boolean to enforce the plugin to add instrumentation in build mode. | `boolean` | `false` |
| `nycrcPath ` | Path to specific nyc config to use instead of automatically searching for a nycconfig. This parameter is just passed down to @istanbuljs/load-nyc-config. | `string` | `-` |
> **Note:**
> **Note:**
> If you're using typescript, you can import the type for the options like so:
>
> ```ts
> import type { AddonOptionsVite } from '@storybook/addon-coverage'
> import type { AddonOptionsVite } from "@storybook/addon-coverage";
> ```
### Development scripts
Expand Down
3 changes: 3 additions & 0 deletions examples/webpack5/.nycrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": []
}
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"@babel/preset-react": "^7.12.5",
"@babel/preset-typescript": "^7.13.0",
"@storybook/core-common": "^7.0.0-alpha.34",
"@types/convert-source-map": "^2.0.3",
"@types/istanbul-lib-instrument": "^1.7.7",
"@types/test-exclude": "^6.0.2",
"auto": "^10.3.0",
"concurrently": "^6.2.0",
"prettier": "^2.3.1",
Expand All @@ -53,7 +56,8 @@
"react-dom": "^17.0.1",
"rimraf": "^3.0.2",
"typescript": "^4.2.4",
"vite": "^3.1.0"
"vite": "^3.1.0",
"webpack": "^5.89.0"
},
"publishConfig": {
"access": "public"
Expand All @@ -71,11 +75,14 @@
"icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png"
},
"dependencies": {
"@istanbuljs/load-nyc-config": "^1.1.0",
"@jsdevtools/coverage-istanbul-loader": "^3.0.5",
"@types/babel__core": "^7.1.19",
"@types/istanbul-lib-coverage": "^2.0.4",
"babel-plugin-istanbul": "^6.1.1",
"swc-plugin-coverage-instrument": "^0.0.20",
"convert-source-map": "^2.0.0",
"istanbul-lib-instrument": "^6.0.1",
"loader-utils": "^3.2.1",
"merge-source-map": "^1.1.0",
"test-exclude": "^6.0.0",
"vite-plugin-istanbul": "^3.0.1"
}
}
26 changes: 1 addition & 25 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const testFileExtensions = defaultExtensions
.join(",");

export const defaultExclude = [
"**/node_modules/**",
".storybook/**",
"coverage/**",
"packages/*/test{,s}/**",
Expand All @@ -29,28 +30,3 @@ export const defaultExclude = [
"**/{karma,rollup,webpack}.config.js",
"**/.{eslint,mocha}rc.{js,cjs}",
];

export const defaultExcludeRegexes = [
"node_modules",
"\\.storybook/.*",
"coverage/.*",
"packages/[^/]+/test(s?)/.*",
".*\\.d\\.ts$",
"test(s?)/.*",
`test(-[^.]+)?\\.(${testFileExtensions})$`,
`.*(-|\\.)((spec|stories|types)\\.(${testFileExtensions}))$`,
"__tests__/.*",
".*-entry\\.js",

/* Exclude common development tool configuration files */
`.*\\/(ava|babel|nyc)\\.config\\.(js|cjs|mjs)$`,
`.*\\/jest\\.config\\.(js|cjs|mjs|ts)$`,
`.*\\/(karma|rollup|webpack)\\.config\\.js$`,
`.*\\/.(eslint|mocha)rc\\.(js|cjs)$`,

// angular
"\.(e2e|spec|stories)\.ts$",
"(ngfactory|ngstyle)\.js",
"polyfills.ts"
].map(pattern => new RegExp(pattern));

108 changes: 108 additions & 0 deletions src/loader/webpack5-istanbul-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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 fs from "fs";
import path from "path";
import { AddonOptionsWebpack } from "../types";

export type Options = Partial<InstrumenterOptions> & AddonOptionsWebpack;

type RawSourceMap = {
version: number;
sources: string[];
mappings: string;
file?: string;
sourceRoot?: string;
sourcesContent?: string[];
names?: string[];
};

export const defaultOptions: Partial<InstrumenterOptions> = {
preserveComments: true,
produceSourceMap: true,
autoWrap: true,
esModules: true,
compact: false,
};

export default function (
this: LoaderContext<Options>,
source: string,
sourceMap?: RawSourceMap
) {
let map = sourceMap;
let options = Object.assign(defaultOptions, this.getOptions());

// If there's no external sourceMap file, then check for an inline sourceMap
if (!map) {
map = sourceMap = getInlineSourceMap.call(this, source);
}

// Instrument the code
let instrumenter = createInstrumenter(options);
instrumenter.instrument(
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
);
}

/**
* If the source code has an inline base64-encoded source map,
* then this function decodes it, parses it, and returns it.
*/
function getInlineSourceMap(
this: LoaderContext<Options>,
source: string
): RawSourceMap | undefined {
try {
// Check for an inline source map
const inlineSourceMap =
fromSource(source) ||
fromMapFileSource(source, function (filename) {
return fs.readFileSync(
path.resolve(path.dirname(this.resourcePath), filename),
"utf-8"
);
});

if (inlineSourceMap) {
// Use the inline source map
return inlineSourceMap.sourcemap as RawSourceMap;
}
} catch (e) {
// Exception is thrown by fromMapFileSource when there is no source map file
if (
e instanceof Error &&
e.message.includes(
"An error occurred while trying to read the map file at"
)
) {
this.emitWarning(e);
} else {
throw e;
}
}
}
14 changes: 14 additions & 0 deletions src/nyc-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AddonOptionsWebpack } from "./types";
// @ts-expect-error no types
import { loadNycConfig } from "@istanbuljs/load-nyc-config";

export async function getNycConfig(
opts: Pick<AddonOptionsWebpack["istanbul"], "cwd" | "nycrcPath"> = {}
) {
const cwd = opts.cwd ?? process.cwd();

return loadNycConfig({
cwd,
nycrcPath: opts.nycrcPath,
});
}
Loading

0 comments on commit 95139ce

Please sign in to comment.