Skip to content

Commit

Permalink
Add include and exclude options
Browse files Browse the repository at this point in the history
  • Loading branch information
SoraKumo001 committed Nov 1, 2023
1 parent b29059a commit abb666a
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 270 deletions.
51 changes: 22 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,43 @@

Provides module mocking functionality like `jest.mock` on Storybook.

![](https://raw.githubusercontent.com/ReactLibraries/storybook-addon-module-mock/master/document/image/image01.png)
![](https://raw.githubusercontent.com/ReactLibraries/storybook-addon-module-mock/master/document/image/image01.png)
![](https://raw.githubusercontent.com/ReactLibraries/storybook-addon-module-mock/master/document/image/image02.png)

## usage

Added 'storybook-addon-module-mock' to Storybook addons.
Only works if Webpack is used in the Builder.

- Sample code
https://github.com/SoraKumo001/storybook-module-mock

### Storybook@6 & Next.js
## Regarding how to interrupt a mock

- .storybook/main.js
Interruptions vary depending on the Storybook mode.

```js
// @ts-check
/**
* @type { import("@storybook/react/types").StorybookConfig}
*/
module.exports = {
core: {
builder: 'webpack5',
},
stories: ['../src/**/*.stories.@(tsx)'],
- storybook dev
- Make `module.exports` writable using Webpack functionality
- storybook build
- Insert code to rewrite `module.exports` using Babel functionality

## Addon options

Include and exclude are enabled for `storybook build` where Babel is used.
Not used in `storybook dev`.

If include is omitted, all modules are covered.

```tsx
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
{
name: '@storybook/addon-coverage',
name: 'storybook-addon-module-mock',
options: {
istanbul: {
exclude: ['**/components/**/index.ts'],
},
},
},
'storybook-addon-next',
'storybook-addon-module-mock',
include: [/message/,"**/action.*"], // RegExp or glob pattern
exclude: ["**/node_modules/**"],
}
}
],
features: {
storyStoreV7: true,
interactionsDebugger: true,
},
typescript: { reactDocgen: 'react-docgen' },
};
```

### Storybook@7 & Next.js
Expand Down
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "storybook-addon-module-mock",
"version": "1.1.1",
"version": "1.1.2",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
"exports": {
Expand All @@ -18,23 +18,25 @@
"build": "tsc && tsc -p ./tsconfig.esm.json && cpy esm dist",
"watch": "tsc -b -w",
"lint": "eslint --fix ./src",
"lint:fix": "eslint --fix ./src"
"lint:fix": "eslint --fix ./src",
"cp": "cpy dist ../../../test/storybook-module-mock/node_modules/storybook-addon-module-mock"
},
"dependencies": {
"minimatch": "*",
"react-json-tree": "^0.18.0"
},
"devDependencies": {
"@babel/core": "^7.23.2",
"@storybook/addons": "^7.5.1",
"@storybook/api": "^7.5.1",
"@storybook/components": "^7.5.1",
"@storybook/addons": "^7.5.2",
"@storybook/api": "^7.5.2",
"@storybook/components": "^7.5.2",
"@storybook/jest": "^0.2.3",
"@storybook/react": "^7.5.1",
"@storybook/types": "^7.5.1",
"@storybook/react": "^7.5.2",
"@storybook/types": "^7.5.2",
"@types/babel__core": "^7.20.3",
"@types/react": "^18.2.31",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"@types/react": "^18.2.33",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"cpy-cli": "^5.0.0",
"eslint": "8.52.0",
"eslint-config-prettier": "^9.0.0",
Expand Down
75 changes: 49 additions & 26 deletions src/plugins/babel-import-writer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { types as t, PluginObj, template, NodePath } from '@babel/core';
import { minimatch } from 'minimatch';
import { AddonOptions } from '../types';

const buildMocks = template(`
const MOCKS = {};
Expand All @@ -19,6 +21,7 @@ const buildMock = template(`

type PluginState = {
moduleExports: [string, string][];
isTarget: boolean;
};

const isModuleExports = (path: NodePath<t.Program>) => {
Expand All @@ -36,16 +39,32 @@ const isModuleExports = (path: NodePath<t.Program>) => {
return hasModuleExports;
};

const plugin = (): PluginObj<PluginState> => {
const isTarget = (fileName: string | undefined, options: AddonOptions) => {
const { include, exclude } = options;
if (!fileName) return true;
const isTarget =
include?.some((i) => (i instanceof RegExp ? i.test(fileName) : minimatch(fileName, i))) ?? true;
if (!isTarget || !exclude) return isTarget;
return !exclude.some((i) => (i instanceof RegExp ? i.test(fileName) : minimatch(fileName, i)));
};

const getFileName = (path: NodePath<t.Program>) => {
return (path.hub as (typeof path)['hub'] & { file: { opts: { filename: string } } }).file.opts
.filename;
};

const plugin = (_: unknown, options: AddonOptions): PluginObj<PluginState> => {
return {
name: 'mocks',
visitor: {
Program: {
enter(_, state) {
enter(path, state) {
const fileName = getFileName(path);
state.isTarget = isTarget(fileName, options);
state.moduleExports = [];
},
exit(path, { moduleExports }) {
if (!isModuleExports(path)) {
exit(path, { isTarget: isEnable, moduleExports }) {
if (isEnable && !isModuleExports(path)) {
const mocks = path.scope.generateDeclaredUidIdentifier('$$mocks$$');
path.pushContainer('body', buildMocks({ MOCKS: mocks }));
moduleExports.forEach(([name, local]) => {
Expand All @@ -59,31 +78,35 @@ const plugin = (): PluginObj<PluginState> => {
}
},
},
ExportNamedDeclaration(path, { moduleExports }) {
const identifiers = path.getOuterBindingIdentifiers();
moduleExports.push(
...Object.keys(identifiers).map<[string, string]>((name) => [name, name])
);
ExportNamedDeclaration(path, { isTarget: isEnable, moduleExports }) {
if (isEnable) {
const identifiers = path.getOuterBindingIdentifiers();
moduleExports.push(
...Object.keys(identifiers).map<[string, string]>((name) => [name, name])
);
}
},
ExportDefaultDeclaration(path, { moduleExports }) {
const declaration = path.node.declaration;
const name = t.isIdentifier(declaration) && declaration.name;
if (!name) {
if (t.isArrowFunctionExpression(declaration)) {
const id = path.scope.generateUidIdentifier('default');
const variableDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(id, declaration),
ExportDefaultDeclaration(path, { isTarget: isEnable, moduleExports }) {
if (isEnable) {
const declaration = path.node.declaration;
const name = t.isIdentifier(declaration) && declaration.name;
if (!name) {
if (t.isArrowFunctionExpression(declaration)) {
const id = path.scope.generateUidIdentifier('default');
const variableDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(id, declaration),
]);
path.replaceWith(t.exportDefaultDeclaration(id));
path.insertBefore(variableDeclaration);
moduleExports.push(['default', id.name]);
}
} else {
const decl = t.exportNamedDeclaration(null, [
t.exportSpecifier(t.identifier(name), t.identifier('default')),
]);
path.replaceWith(t.exportDefaultDeclaration(id));
path.insertBefore(variableDeclaration);
moduleExports.push(['default', id.name]);
path.replaceWith(decl);
moduleExports.push(['default', name]);
}
} else {
const decl = t.exportNamedDeclaration(null, [
t.exportSpecifier(t.identifier(name), t.identifier('default')),
]);
path.replaceWith(decl);
moduleExports.push(['default', name]);
}
},
},
Expand Down
9 changes: 7 additions & 2 deletions src/preset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TransformOptions } from '@babel/core';
import { ImportWriterPlugin } from './plugins/webpack-import-writer.js';
import type { AddonOptions } from './types.js';
import type { StorybookConfig } from '@storybook/types';
import type { Options } from '@storybook/types';
import type { Configuration } from 'webpack';
Expand All @@ -10,12 +11,16 @@ export const managerEntries = (entry: string[] = []): string[] => [

export const babel = async (
config: TransformOptions,
options: Options
options: Options & AddonOptions
): Promise<TransformOptions> => {
if (options.configType !== 'PRODUCTION') return config;
const { include, exclude } = options;
return {
...config,
plugins: [...(config.plugins ?? []), require.resolve('./plugins/babel-import-writer')],
plugins: [
...(config.plugins ?? []),
[require.resolve('./plugins/babel-import-writer'), { include, exclude }],
],
};
};

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type AddonOptions = { include: (string | RegExp)[]; exclude: (string | RegExp)[] };
Loading

0 comments on commit abb666a

Please sign in to comment.