Skip to content
This repository has been archived by the owner on Feb 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #17 from microsoft/feat/react-components-unstable
Browse files Browse the repository at this point in the history
feat: Configuration for dependency replacement
  • Loading branch information
ling1726 authored May 30, 2022
2 parents 2b793cb + c3fa622 commit a26d106
Show file tree
Hide file tree
Showing 15 changed files with 1,859 additions and 1,684 deletions.
73 changes: 68 additions & 5 deletions storybook-addon-export-to-codesandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,33 @@ This Storybook plugin adds "Open in CodeSandbox" button to each story displayed

1. Install the plugin `npm i storybook-addon-export-to-codesandbox`.
2. Register the plugin in `.storybook/main.js` - Add `'storybook-addon-export-to-codesandbox'` to the list of addons.

```js
// .storybook/main.js

module.exports = {
// ...
addons: ['storybook-addon-export-to-codesandbox', /* ... */],
addons: ['storybook-addon-export-to-codesandbox' /* ... */],
};
```

3. Use .storybook/babel.plugin.js in your Storybook.
4. Define required parameters in your `.storybook/preview.js`:

> ⚠️ Make sure all necessary dependencies are included in the `requiredDependencies` config.
```js
export const parameters = {
exportToCodeSandbox: {
// Dependencies that should be included with every story
requiredDependencies: {
react: 'latest',
'react-dom': 'latest', // for React
'react-scripts': 'latest', // necessary when using typescript in CodeSandbox
'@fluentui/react-components': '^9.0.0-beta', // necessary for FluentProvider
},
// Dependencies that should be included in the story if it exists in the code
optionalDependencies: {
'@fluentui/react-icons': '^9.0.0-beta',
},
// Content of index.tsx in CodeSandbox
indexTsx: dedent`
Expand All @@ -51,10 +58,66 @@ Each story should be put into its own file with a `.stories.tsx` extension. This

This practice is recommended so that the "Open in CodeSandbox" button would export a single story.

## Relative imports
## Dependency replacement with Babel plugin

### Configure dependency replacement

It's possible to further configure dependency replacement using babel plugin options.
By default only dependencies that are declared in the babel plugin options will be replaced,
any other dependencies will remain as is.

By default all dependencies will be replaced with `@fluentui/react-components`

```js
// main.js
module.exports = {
stories: [],
addons: [],
webpackFinal: config => {
if (config.module && config.module.rules) {
config.module.rules.unshift({
test: /\.stories\.tsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
[
require('storybook-addon-export-to-codesandbox').babelPlugin,
{
'@fluentui/react-button': { replace: '@fluentui/react-components' },
// imports of @fluentui/react-unstable-component will be replaced with @fluentui/react-components/unstable
'@fluentui/react-unstable-component': { replace: '@fluentui/react-components/unstable'}
}
]
],
},
},
});
}
};


```
### Relative imports
It’s recommended that your stories don’t contain relative imports. If this is unavoidable, in your case, you might use a `codesandbox-dependency` comment to replace the `from` part of your import, during export. It also allows you to specify required version of your dependency.
> This step runs before any other dependency replacement
When the addon encounters relative imports in a story, the package name of the closest
`package.json` in the file tree will be used.
```json
{
"name": "@fluentui/react-button",
"version": "9.0.0-rc.11"
}
```
```ts
import { Button } from '../../Button'; // codesandbox-dependency: @fluentui/react-button ^9.0.0-beta
// Before
import { Button } from '../index';

// After
import { Button } from '@fluentui/react-button';
```
3 changes: 2 additions & 1 deletion storybook-addon-export-to-codesandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png"
},
"dependencies": {
"codesandbox-import-utils": "^2.2.3"
"codesandbox-import-utils": "^2.2.3",
"pkg-up": "^3.1.0"
}
}
49 changes: 49 additions & 0 deletions storybook-addon-export-to-codesandbox/src/getDepdencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type PackageDependencies = { [dependencyName: string]: string };

/**
*
* @param fileContent - code
* @param requiredDependencies - dependencies that will always be incldued in package.json
* @param optionalDependencies - whose versions will override those found in the code
* @returns - Map of dependencies and their versions to include in package.json
*/
export const getDependencies = (
fileContent: string,
requiredDependencies: PackageDependencies,
optionalDependencies: PackageDependencies,
) => {
const matches = fileContent.matchAll(/import .* from ['"](.*?)['"];/g);

const dependenciesInCode = Array.from(matches).reduce((dependencies, match) => {
if (!match[1].startsWith('react/')) {
const dependency = parsePackageName(match[1]).name;

if (!dependencies.hasOwnProperty(dependency)) {
dependencies[dependency] = optionalDependencies[dependency] ?? 'latest';
}
}

return dependencies;
}, {} as PackageDependencies);

return { ...dependenciesInCode, ...requiredDependencies };
};

// Parsed a scoped package name into name, version, and path.
const RE_SCOPED = /^(@[^\/]+\/[^@\/]+)(?:@([^\/]+))?(\/.*)?$/;
// Parsed a non-scoped package name into name, version, path
const RE_NON_SCOPED = /^([^@\/]+)(?:@([^\/]+))?(\/.*)?$/;

function parsePackageName(input: string) {
const m = RE_SCOPED.exec(input) || RE_NON_SCOPED.exec(input);

if (!m) {
throw new Error(`[parse-package-name] invalid package name: ${input}`);
}

return {
name: m[1] || '',
version: m[2] || 'latest',
path: m[3] || '',
};
}
67 changes: 67 additions & 0 deletions storybook-addon-export-to-codesandbox/src/getDependencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { getDependencies } from './getDepdencies';

describe('getDependencies', () => {
it('should find all dependencies in a file', () => {
const code = `
import { stuff } from 'dependency';
import * as allStuff from 'dependency1';
import { moreStuff } from '@dependency/dependency';
`;
const deps = getDependencies(code, {}, {});

expect(deps).toEqual({
'@dependency/dependency': 'latest',
dependency: 'latest',
dependency1: 'latest',
});
});

it('should ignore separate entrypoints', () => {
const code = `
import { stuff } from 'dependency/unstable';
import { moreStuff } from 'dependency/unstable/component';
import { moreStuff2 } from '@dependency/unstable';
import { moreStuff3 } from '@dependency/unstable/component';
`;
const deps = getDependencies(code, {}, {});

expect(deps).toEqual({
dependency: 'latest',
'@dependency/unstable': 'latest',
});
});

it('versions in optionalDependencies should win ', () => {
const code = `
import { stuff } from 'dependency';
`;
const deps = getDependencies(code, {}, { dependency: '1.0.0' });

expect(deps).toEqual({
dependency: '1.0.0',
});
});

it('versions in requiredDependencies should win ', () => {
const code = `
import { stuff } from 'dependency';
`;
const deps = getDependencies(code, { dependency: '1.0.0' }, {});

expect(deps).toEqual({
dependency: '1.0.0',
});
});

it('versions in requiredDependencies should win ', () => {
const code = `
import { stuff } from 'dependency';
`;
const deps = getDependencies(code, { required: '1.0.0' }, {});

expect(deps).toEqual({
dependency: 'latest',
required: '1.0.0',
});
});
});
3 changes: 2 additions & 1 deletion storybook-addon-export-to-codesandbox/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Temporary solution until this issue gets implemented:
// https://github.com/microsoft/fluentui-storybook-addons/issues/2
export { fullSourcePlugin as babelPlugin } from "./plugins";
export { fullSourcePlugin as babelPlugin } from './plugins';
export type { BabelPluginOptions } from './plugins';

if (module && module.hot && module.hot.decline) {
module.hot.decline();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Button } from '@fluentui/react-unstable-component';

export const ButtonStories = () => console.log(Button);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { Button } from '@fluentui/react-components/unstable';
export const ButtonStories = () => console.log(Button);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@fluentui/react-button"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ import * as path from 'path';
import plugin, { PLUGIN_NAME } from './fullsource';

const fixturesDir = path.join(__dirname, `__fixtures__/${PLUGIN_NAME}`);

const defaultDependencyReplace = { replace: '@fluentui/react-components' };

pluginTester({
pluginOptions: {
'@fluentui/react-button': defaultDependencyReplace,
'@fluentui/react-menu': defaultDependencyReplace,
'@fluentui/react-link': defaultDependencyReplace,
},
pluginName: PLUGIN_NAME,
plugin,
fixtures: fixturesDir,
})
});
14 changes: 12 additions & 2 deletions storybook-addon-export-to-codesandbox/src/plugins/fullsource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import modifyImportsPlugin from './modifyImports';

export const PLUGIN_NAME = 'storybook-stories-fullsource';

interface DependencyEntry {
/**
* Replaces the dependency with another
* @default - @fluentui/react-components
*/
replace: string;
}

export type BabelPluginOptions = Record<string, DependencyEntry>;

/**
* This Babel plugin adds `context.parameters.fullSource` property to Storybook stories,
* which contains source of of the file where story is present.
Expand All @@ -19,7 +29,7 @@ export const PLUGIN_NAME = 'storybook-stories-fullsource';
* @param {import('@babel/core')} babel
* @returns {import('@babel/core').PluginObj}
*/
export default function (babel: typeof Babel): Babel.PluginObj {
export default function (babel: typeof Babel, options: BabelPluginOptions): Babel.PluginObj {
const { types: t } = babel;
return {
name: PLUGIN_NAME,
Expand All @@ -34,7 +44,7 @@ export default function (babel: typeof Babel): Babel.PluginObj {
const transformedCode = babel.transformSync(path.node.init.value, {
...state.file.opts,
comments: false,
plugins: [modifyImportsPlugin],
plugins: [[modifyImportsPlugin, options]],
}).code;

path.get('init').replaceWith(t.stringLiteral(transformedCode));
Expand Down
3 changes: 2 additions & 1 deletion storybook-addon-export-to-codesandbox/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as fullSourcePlugin } from './fullsource';
export { default as fullSourcePlugin } from './fullsource';
export type { BabelPluginOptions } from './fullsource';
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import pluginTester from 'babel-plugin-tester';
import * as path from 'path';
import plugin, { PLUGIN_NAME} from './modifyImports';
import plugin, { PLUGIN_NAME } from './modifyImports';

const defaultDependencyReplace = { replace: '@fluentui/react-components' };
const fixturesDir = path.join(__dirname, `__fixtures__/${PLUGIN_NAME}`);
pluginTester({
pluginOptions: {
'@fluentui/react-button': defaultDependencyReplace,
'@fluentui/react-menu': defaultDependencyReplace,
'@fluentui/react-link': defaultDependencyReplace,
'@fluentui/react-unstable-component': { replace: '@fluentui/react-components/unstable' },
},
pluginName: PLUGIN_NAME,
plugin,
fixtures: fixturesDir,
})
});
Loading

0 comments on commit a26d106

Please sign in to comment.