mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-04 23:11:23 +08:00
Merge branch 'next' into dependabot/npm_and_yarn/commander-5.0.0
This commit is contained in:
commit
b753582d43
40
MIGRATION.md
40
MIGRATION.md
@ -18,6 +18,8 @@
|
||||
- [Actions Addon API changes](#actions-addon-api-changes)
|
||||
- [Actions Addon uses parameters](#actions-addon-uses-parameters)
|
||||
- [Removed action decorator APIs](#removed-action-decorator-apis)
|
||||
- [Removed addon centered](#removed-addon-centered)
|
||||
- [Removed withA11y decorator](#removed-witha11y-decorator)
|
||||
- [From version 5.2.x to 5.3.x](#from-version-52x-to-53x)
|
||||
- [To main.js configuration](#to-mainjs-configuration)
|
||||
- [Using main.js](#using-mainjs)
|
||||
@ -201,41 +203,41 @@ In Storybook 5.3 we introduced a declarative [main.js configuration](#to-mainjs-
|
||||
|
||||
This breaking change currently applies to: `addon-a11y`, `addon-actions`, `addon-knobs`, `addon-links`, `addon-queryparams`.
|
||||
|
||||
Consider the following `main.js` config for the accessibility addon, `addon-a11y`:
|
||||
Consider the following `main.js` config for the accessibility addon, `addon-knobs`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
stories: ['../**/*.stories.js'],
|
||||
addons: ['@storybook/addon-a11y'],
|
||||
addons: ['@storybook/addon-knobs'],
|
||||
};
|
||||
```
|
||||
|
||||
In earlier versions of Storybook, this would automatically call `@storybook/addon-a11y/register`, which adds the the a11y panel to the Storybook UI. As a user you would also add a decorator:
|
||||
In earlier versions of Storybook, this would automatically call `@storybook/addon-knobs/register`, which adds the the knobs panel to the Storybook UI. As a user you would also add a decorator:
|
||||
|
||||
```js
|
||||
import { withA11y } from '../index';
|
||||
import { withKnobs } from '../index';
|
||||
|
||||
addDecorator(withA11y);
|
||||
addDecorator(withKnobs);
|
||||
```
|
||||
|
||||
Now in 6.0, `addon-a11y` comes with a preset, `@storybook/addon-a11y/preset`, that does this automatically for you. This change simplifies configuration, since now you don't need to add that decorator.
|
||||
Now in 6.0, `addon-knobs` comes with a preset, `@storybook/addon-knobs/preset`, that does this automatically for you. This change simplifies configuration, since now you don't need to add that decorator.
|
||||
|
||||
If you wish to disable this new behavior, you can modify your `main.js` to force it to use the `register` logic rather than the `preset`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
stories: ['../**/*.stories.js'],
|
||||
addons: ['@storybook/addon-a11y/register'],
|
||||
addons: ['@storybook/addon-knobs/register'],
|
||||
};
|
||||
```
|
||||
|
||||
If you wish to selectively disable `a11y` checks for a subset of stories, you can control this with story parameters:
|
||||
If you wish to selectively disable `knobs` checks for a subset of stories, you can control this with story parameters:
|
||||
|
||||
```js
|
||||
export const MyNonCheckedStory = () => <SomeComponent />;
|
||||
MyNonCheckedStory.story = {
|
||||
parameters: {
|
||||
a11y: { disable: true },
|
||||
knobs: { disable: true },
|
||||
},
|
||||
};
|
||||
```
|
||||
@ -386,10 +388,28 @@ export const MyStory = () => <div>my story</div>;
|
||||
MyStory.story = {
|
||||
parameters: { layout: 'centered' },
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
Other possible values are: `padded` (default) and `fullscreen`.
|
||||
|
||||
#### Removed withA11y decorator
|
||||
|
||||
In 6.0 we removed the `withA11y` decorator. The code that runs accessibility checks is now directly injected in the preview.
|
||||
|
||||
Remove the addon-a11y decorator.
|
||||
To configure a11y now, you have to specify configuration using `addParameters`.
|
||||
|
||||
```js
|
||||
addParameters({
|
||||
a11y: {
|
||||
element: "#root",
|
||||
config: {},
|
||||
options: {},
|
||||
manual: true,
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## From version 5.2.x to 5.3.x
|
||||
|
||||
### To main.js configuration
|
||||
|
@ -56,11 +56,8 @@ You can override these options [at story level too](https://storybook.js.org/doc
|
||||
import React from 'react';
|
||||
import { storiesOf, addDecorator, addParameters } from '@storybook/react';
|
||||
|
||||
import { withA11y } from '@storybook/addon-a11y';
|
||||
|
||||
export default {
|
||||
title: 'button',
|
||||
decorators: [withA11y],
|
||||
parameters: {
|
||||
a11y: {
|
||||
// optional selector which element to inspect
|
||||
|
@ -41,7 +41,7 @@
|
||||
"@storybook/components": "6.0.0-alpha.33",
|
||||
"@storybook/core-events": "6.0.0-alpha.33",
|
||||
"@storybook/theming": "6.0.0-alpha.33",
|
||||
"axe-core": "^3.3.2",
|
||||
"axe-core": "^3.5.2",
|
||||
"core-js": "^3.0.1",
|
||||
"global": "^4.3.2",
|
||||
"lodash": "^4.17.15",
|
||||
|
60
addons/a11y/src/a11yRunner.ts
Normal file
60
addons/a11y/src/a11yRunner.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { document, window } from 'global';
|
||||
import { STORY_RENDERED } from '@storybook/core-events';
|
||||
import axe, { ElementContext, RunOptions, Spec } from 'axe-core';
|
||||
import addons from '@storybook/addons';
|
||||
import { EVENTS } from './constants';
|
||||
|
||||
if (module && module.hot && module.hot.decline) {
|
||||
module.hot.decline();
|
||||
}
|
||||
|
||||
interface Setup {
|
||||
element?: ElementContext;
|
||||
config: Spec;
|
||||
options: RunOptions;
|
||||
}
|
||||
|
||||
const channel = addons.getChannel();
|
||||
let active = false;
|
||||
|
||||
const getElement = () => {
|
||||
const storyRoot = document.getElementById('story-root');
|
||||
return storyRoot ? storyRoot.children : document.getElementById('root');
|
||||
};
|
||||
|
||||
const run = async (storyId: string) => {
|
||||
try {
|
||||
const input = getParams(storyId);
|
||||
|
||||
if (!active) {
|
||||
active = true;
|
||||
const {
|
||||
element = getElement(),
|
||||
config,
|
||||
options = {
|
||||
restoreScroll: true,
|
||||
},
|
||||
} = input;
|
||||
axe.reset();
|
||||
if (config) {
|
||||
axe.configure(config);
|
||||
}
|
||||
|
||||
const result = await axe.run(element, options);
|
||||
channel.emit(EVENTS.RESULT, result);
|
||||
}
|
||||
} catch (error) {
|
||||
channel.emit(EVENTS.ERROR, error);
|
||||
} finally {
|
||||
active = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getParams = (storyId: string): Setup => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const { parameters } = window.__STORYBOOK_STORY_STORE__._stories[storyId] || {};
|
||||
return parameters.a11y;
|
||||
};
|
||||
|
||||
channel.on(STORY_RENDERED, run);
|
||||
channel.on(EVENTS.REQUEST, run);
|
@ -12,6 +12,8 @@ function createApi() {
|
||||
jest.spyOn(emitter, 'emit');
|
||||
jest.spyOn(emitter, 'on');
|
||||
jest.spyOn(emitter, 'off');
|
||||
|
||||
emitter.getCurrentStoryData = () => ({ id: '1' });
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@ -130,7 +132,7 @@ describe('A11YPanel', () => {
|
||||
|
||||
// then
|
||||
expect(wrapper.text()).toMatch(/Please wait while the accessibility scan is running/);
|
||||
expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST);
|
||||
expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST, '1');
|
||||
});
|
||||
|
||||
it('should handle "ran" status', () => {
|
||||
|
@ -169,7 +169,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
|
||||
status: 'running',
|
||||
},
|
||||
() => {
|
||||
api.emit(EVENTS.REQUEST);
|
||||
api.emit(EVENTS.REQUEST, api.getCurrentStoryData().id);
|
||||
// removes all elements from the redux map in store from the previous panel
|
||||
store.dispatch(clearElements());
|
||||
}
|
||||
|
@ -1,119 +1,3 @@
|
||||
import { document } from 'global';
|
||||
import debounce from 'lodash/debounce';
|
||||
import memoize from 'memoizerific';
|
||||
import axe, { AxeResults, ElementContext, RunOptions, Spec } from 'axe-core';
|
||||
|
||||
import addons, { DecoratorFunction } from '@storybook/addons';
|
||||
import { STORY_RENDERED } from '@storybook/core-events';
|
||||
import { Listener } from '@storybook/channels';
|
||||
import { EVENTS, PARAM_KEY } from './constants';
|
||||
|
||||
interface Setup {
|
||||
element?: ElementContext;
|
||||
config: Spec;
|
||||
options: RunOptions;
|
||||
manual: boolean;
|
||||
}
|
||||
|
||||
const setup: Setup = { element: undefined, config: {}, options: {}, manual: false };
|
||||
|
||||
const getElement = () => {
|
||||
const storyRoot = document.getElementById('story-root');
|
||||
|
||||
if (storyRoot) {
|
||||
return storyRoot.children;
|
||||
}
|
||||
return document.getElementById('root');
|
||||
};
|
||||
|
||||
const performRun = (() => {
|
||||
let isRunning = false;
|
||||
|
||||
return debounce(async (s, callback) => {
|
||||
if (isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
|
||||
await run(s)
|
||||
.then(
|
||||
(result) => callback(undefined, result),
|
||||
(error) => callback(error)
|
||||
)
|
||||
.then(() => {
|
||||
isRunning = false;
|
||||
});
|
||||
}, 100);
|
||||
})();
|
||||
|
||||
const run = async (input: Setup) => {
|
||||
const {
|
||||
element = getElement(),
|
||||
config,
|
||||
options = {
|
||||
restoreScroll: true,
|
||||
},
|
||||
} = input;
|
||||
|
||||
await axe.reset();
|
||||
|
||||
if (config) {
|
||||
await axe.configure(config);
|
||||
}
|
||||
|
||||
return axe.run(element, options);
|
||||
};
|
||||
|
||||
if (module && module.hot && module.hot.decline) {
|
||||
module.hot.decline();
|
||||
}
|
||||
|
||||
let storedDefaultSetup: Setup | null = null;
|
||||
|
||||
const performSetup = (parameter: Partial<Setup> | undefined) => {
|
||||
if (parameter) {
|
||||
if (storedDefaultSetup === null) {
|
||||
storedDefaultSetup = { ...setup };
|
||||
}
|
||||
Object.assign(setup, parameter);
|
||||
}
|
||||
if (storedDefaultSetup !== null) {
|
||||
Object.assign(setup, storedDefaultSetup);
|
||||
storedDefaultSetup = null;
|
||||
}
|
||||
};
|
||||
|
||||
const usePermanentChannel = memoize(1)((eventMap: Record<string, Listener>) => {
|
||||
const channel = addons.getChannel();
|
||||
const emit = channel.emit.bind(channel);
|
||||
|
||||
Object.entries(eventMap).forEach(([type, handler]) => {
|
||||
channel.on(type, handler);
|
||||
});
|
||||
|
||||
return emit;
|
||||
});
|
||||
|
||||
export const withA11y: DecoratorFunction = (storyFn, storyContext) => {
|
||||
const respond = () => {
|
||||
const parameter = storyContext.parameters[PARAM_KEY] as Partial<Setup>;
|
||||
|
||||
performSetup(parameter);
|
||||
|
||||
performRun(setup, (error: Error, result: AxeResults) => {
|
||||
if (error) {
|
||||
emit(EVENTS.ERROR, String(error));
|
||||
} else {
|
||||
emit(EVENTS.RESULT, result);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const emit = usePermanentChannel({
|
||||
[EVENTS.REQUEST]: respond,
|
||||
[STORY_RENDERED]: respond,
|
||||
});
|
||||
|
||||
return storyFn(storyContext);
|
||||
};
|
||||
|
7
addons/a11y/src/preset.ts
Normal file
7
addons/a11y/src/preset.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function managerEntries(entry: any[] = []) {
|
||||
return [...entry, require.resolve('./register')];
|
||||
}
|
||||
|
||||
export function config(entry: any[] = []) {
|
||||
return [...entry, require.resolve('./a11yRunner')];
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
import { withA11y } from '../index';
|
||||
|
||||
export const decorators = [withA11y];
|
@ -1,15 +0,0 @@
|
||||
type A11yOptions = {
|
||||
addDecorator?: boolean;
|
||||
};
|
||||
|
||||
export function managerEntries(entry: any[] = []) {
|
||||
return [...entry, require.resolve('../register')];
|
||||
}
|
||||
|
||||
export function config(entry: any[] = [], { addDecorator = true }: A11yOptions = {}) {
|
||||
const a11yConfig = [];
|
||||
if (addDecorator) {
|
||||
a11yConfig.push(require.resolve('./addDecorator'));
|
||||
}
|
||||
return [...entry, ...a11yConfig];
|
||||
}
|
@ -1 +0,0 @@
|
||||
module.exports = require('../dist/frameworks/react/config');
|
@ -54,12 +54,7 @@ function getConfigPathParts(input: string): Output {
|
||||
(pattern: string | { path: string; recursive: boolean; match: string }) => {
|
||||
const { path: basePath, recursive, match } = toRequireContext(pattern);
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return global.__requireContext(
|
||||
configDir,
|
||||
basePath,
|
||||
recursive,
|
||||
new RegExp(match.slice(1, -1))
|
||||
);
|
||||
return global.__requireContext(configDir, basePath, recursive, match);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -99,6 +99,10 @@ Add different paddings to your preview. Useful for checking how components behav
|
||||
Given possible values for each prop, renders your component with all combinations of prop values.
|
||||
Useful for finding edge cases or seeing all component states at once.
|
||||
|
||||
### [Performance](https://github.com/atlassian-labs/storybook-addon-performance)
|
||||
|
||||
This addon will help you better understand and debug performance for `React` components.
|
||||
|
||||
### [Material-UI](https://github.com/sm-react/storybook-addon-material-ui)
|
||||
|
||||
Wraps your story into MuiThemeProvider.
|
||||
|
@ -48,9 +48,9 @@ In `preview.js` you can add global [decorators](../../basics/writing-stories/#de
|
||||
```js
|
||||
// preview.js
|
||||
import { addDecorator } from '@storybook/svelte';
|
||||
import { withA11y } from '@storybook/addon-a11y';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
addDecorator(withA11y);
|
||||
addDecorator(withKnobs);
|
||||
```
|
||||
|
||||
In `manager.js` you can add [UI options](../options-parameter/#global-options).
|
||||
|
@ -15,6 +15,12 @@ It's possible to theme Storybook globally.
|
||||
|
||||
We've created two basic themes that look good out of the box: "normal" (a light theme) and "dark" (a dark theme). Unless you've set your preferred color scheme as dark Storybook will use the light theme as default.
|
||||
|
||||
Make sure you have installed `@storybook/addons` and `@storybook/theming` packages.
|
||||
```sh
|
||||
npm install @storybook/addons --save-dev
|
||||
npm install @storybook/theming --save-dev
|
||||
```
|
||||
|
||||
As an example, you can tell Storybook to use the "dark" theme by modifying `.storybook/manager.js`:
|
||||
|
||||
```js
|
||||
|
@ -71,7 +71,7 @@ And finally, story-level decorators are provided via parameters:
|
||||
storiesOf('Button', module).add(
|
||||
'with text',
|
||||
() => <Button onClick={action('clicked')}>Hello Button</Button>,
|
||||
{ decorators: withA11y }
|
||||
{ decorators: withKnobs }
|
||||
);
|
||||
```
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"version":"6.0.0-alpha.32","info":{"plain":"### Features\n\n- CSF: Warn when there are no exported stories ([#10357](https://github.com/storybookjs/storybook/pull/10357))\n\n### Bug Fixes\n\n- Marko: Always destroy old component when switching stories ([#10345](https://github.com/storybookjs/storybook/pull/10345))\n\n### Maintenance\n\n- Dev: Build script for package development ([#10343](https://github.com/storybookjs/storybook/pull/10343))"}}
|
||||
{"version":"6.0.0-alpha.33","info":{"plain":"### Breaking prerelease\n\n- Core: Rename ParameterEnhancer to ArgsEnhancer ([#10398](https://github.com/storybookjs/storybook/pull/10398))\n\n### Bug Fixes\n\n- Core: Fix `webpackFinal` being called twice ([#10402](https://github.com/storybookjs/storybook/pull/10402))\n- Core: Fix legacy redirect ([#10404](https://github.com/storybookjs/storybook/pull/10404))\n\n### Maintenance\n\n- CLI: Update fixtures used for CLI tests ([#10396](https://github.com/storybookjs/storybook/pull/10396))\n- Build: Update bootstrap to install optional deps on CI ([#10408](https://github.com/storybookjs/storybook/pull/10408))\n- Addon-docs: Format source at render time ([#10383](https://github.com/storybookjs/storybook/pull/10383))"}}
|
@ -1,9 +1,7 @@
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { withA11y } from '@storybook/addon-a11y';
|
||||
|
||||
export default {
|
||||
title: 'Addon/a11y',
|
||||
decorators: [withA11y],
|
||||
|
||||
parameters: {
|
||||
options: { selectedPanel: '@storybook/a11y/panel' },
|
||||
|
@ -47,7 +47,11 @@ export type RefUrl = string;
|
||||
export const getSourceType = (source: string) => {
|
||||
const { origin, pathname } = location;
|
||||
|
||||
if (source === origin || source === `${origin + pathname}iframe.html`) {
|
||||
if (
|
||||
source === origin ||
|
||||
source === `${origin + pathname}iframe.html` ||
|
||||
source === `${origin + pathname.replace(/(?!.*\/).*\.html$/, '')}iframe.html`
|
||||
) {
|
||||
return 'local';
|
||||
}
|
||||
return 'external';
|
||||
|
41
lib/api/src/tests/refs.test.js
Normal file
41
lib/api/src/tests/refs.test.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { getSourceType } from '../modules/refs';
|
||||
|
||||
jest.mock('global', () => {
|
||||
const globalMock = {};
|
||||
// Change global.location value to handle edge cases
|
||||
// Add additional variations of global.location mock return values in this array.
|
||||
// NOTE: The order must match the order that global.location is called in the unit tests.
|
||||
const edgecaseLocations = [
|
||||
{ origin: 'https://storybook.js.org', pathname: '/storybook/index.html' },
|
||||
];
|
||||
// global.location value after all edgecaseLocations are returned
|
||||
const lastLocation = { origin: 'https://storybook.js.org', pathname: '/storybook/' };
|
||||
Object.defineProperties(globalMock, {
|
||||
location: {
|
||||
get: edgecaseLocations
|
||||
.reduce((mockFn, location) => mockFn.mockReturnValueOnce(location), jest.fn())
|
||||
.mockReturnValue(lastLocation),
|
||||
},
|
||||
});
|
||||
return globalMock;
|
||||
});
|
||||
|
||||
describe('refs', () => {
|
||||
describe('getSourceType(source)', () => {
|
||||
// These tests must be run first and in correct order.
|
||||
// The order matches the "edgecaseLocations" order in the 'global' mock function above.
|
||||
describe('edge cases', () => {
|
||||
it('returns "local" when source matches location with /index.html in path', () => {
|
||||
// mockReturnValue(edgecaseLocations[0])
|
||||
expect(getSourceType('https://storybook.js.org/storybook/iframe.html')).toBe('local');
|
||||
});
|
||||
});
|
||||
// Other tests use "lastLocation" for the 'global' mock
|
||||
it('returns "local" when source matches location', () => {
|
||||
expect(getSourceType('https://storybook.js.org/storybook/iframe.html')).toBe('local');
|
||||
});
|
||||
it('returns "external" when source does not match location', () => {
|
||||
expect(getSourceType('https://external.com/storybook/iframe.html')).toBe('external');
|
||||
});
|
||||
});
|
||||
});
|
@ -1 +1 @@
|
||||
export const version = '6.0.0-alpha.32';
|
||||
export const version = '6.0.0-alpha.33';
|
||||
|
@ -58,6 +58,7 @@
|
||||
"babel-plugin-emotion": "^10.0.20",
|
||||
"babel-plugin-macros": "^2.8.0",
|
||||
"babel-preset-minify": "^0.5.0 || 0.6.0-alpha.5",
|
||||
"better-opn": "^2.0.0",
|
||||
"boxen": "^4.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.2.0",
|
||||
"chalk": "^3.0.0",
|
||||
@ -84,9 +85,8 @@
|
||||
"ip": "^1.1.5",
|
||||
"json5": "^2.1.1",
|
||||
"lazy-universal-dotenv": "^3.0.1",
|
||||
"micromatch": "^4.0.2",
|
||||
"nanomatch": "^1.2.13",
|
||||
"node-fetch": "^2.6.0",
|
||||
"open": "^7.0.1",
|
||||
"pkg-dir": "^4.2.0",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
|
@ -9,7 +9,7 @@ import chalk from 'chalk';
|
||||
import { logger, colors, instance as npmLog } from '@storybook/node-logger';
|
||||
import fetch from 'node-fetch';
|
||||
import Cache from 'file-system-cache';
|
||||
import open from 'open';
|
||||
import open from 'better-opn';
|
||||
import boxen from 'boxen';
|
||||
import semver from 'semver';
|
||||
import dedent from 'ts-dedent';
|
||||
@ -234,13 +234,15 @@ async function outputStats(previewStats, managerStats) {
|
||||
}
|
||||
|
||||
function openInBrowser(address) {
|
||||
open(address).catch(() => {
|
||||
try {
|
||||
open(address);
|
||||
} catch (error) {
|
||||
logger.error(dedent`
|
||||
Could not open ${address} inside a browser. If you're running this command inside a
|
||||
docker container or on a CI, you need to pass the '--ci' flag to prevent opening a
|
||||
browser by default.
|
||||
`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildDevStandalone(options) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import serverRequire from '../utils/server-require';
|
||||
import { serverRequire, serverResolve } from '../utils/server-require';
|
||||
import validateConfigurationFiles from '../utils/validate-configuration-files';
|
||||
|
||||
export default function loadCustomPresets({ configDir }) {
|
||||
@ -9,7 +9,7 @@ export default function loadCustomPresets({ configDir }) {
|
||||
const main = serverRequire(path.resolve(configDir, 'main'));
|
||||
|
||||
if (main) {
|
||||
return [path.resolve(configDir, 'main')];
|
||||
return [serverResolve(path.resolve(configDir, 'main'))];
|
||||
}
|
||||
|
||||
return presets || [];
|
||||
|
@ -128,7 +128,7 @@ export default ({
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.jsx', '.json'],
|
||||
extensions: ['.mjs', '.js', '.jsx', '.json', '.cjs'],
|
||||
modules: ['node_modules'].concat(raw.NODE_PATH || []),
|
||||
alias: {
|
||||
...themingPaths,
|
||||
|
@ -147,7 +147,7 @@ export default async ({
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.jsx', '.json'],
|
||||
extensions: ['.mjs', '.js', '.jsx', '.json', '.cjs'],
|
||||
modules: ['node_modules'].concat(raw.NODE_PATH || []),
|
||||
alias: {
|
||||
'babel-runtime/core-js/object/assign': require.resolve('core-js/es/object/assign'),
|
||||
|
@ -1,16 +1,12 @@
|
||||
import globBase from 'glob-base';
|
||||
import { makeRe } from 'micromatch';
|
||||
import { makeRe } from 'nanomatch';
|
||||
|
||||
const isObject = (val) => val != null && typeof val === 'object' && Array.isArray(val) === false;
|
||||
export const toRequireContext = (input) => {
|
||||
switch (true) {
|
||||
case typeof input === 'string': {
|
||||
const { base, glob } = globBase(input);
|
||||
const regex = makeRe(glob)
|
||||
.toString()
|
||||
// webpack prepends the relative path with './'
|
||||
.replace(/^\/\^/, '/^\\.\\/')
|
||||
.replace(/\?:\^/g, '?:');
|
||||
const regex = makeRe(glob);
|
||||
|
||||
return { path: base, recursive: glob.startsWith('**'), match: regex };
|
||||
}
|
||||
|
68
lib/core/src/server/preview/to-require-context.test.js
Normal file
68
lib/core/src/server/preview/to-require-context.test.js
Normal file
@ -0,0 +1,68 @@
|
||||
import path from 'path';
|
||||
import { toRequireContext } from './to-require-context';
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
glob: '**/*.stories.tsx',
|
||||
validPaths: [
|
||||
'./Icon.stories.tsx',
|
||||
'./src/Icon.stories.tsx',
|
||||
'./src/components/Icon.stories.tsx',
|
||||
'./src/components/Icon.stories/Icon.stories.tsx',
|
||||
],
|
||||
invalidPaths: [
|
||||
'./stories.tsx',
|
||||
'./Icon.stories.ts',
|
||||
'./Icon.stories.js',
|
||||
'./src/components/stories.tsx',
|
||||
'./src/components/Icon.stories/stories.tsx',
|
||||
'./src/components/Icon.stories.ts',
|
||||
'./src/components/Icon.stories.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
glob: '../src/stories/**/*.stories.(js|mdx)',
|
||||
validPaths: [
|
||||
'../src/stories/components/Icon.stories.js',
|
||||
'../src/stories/Icon.stories.js',
|
||||
'../src/stories/Icon.stories.mdx',
|
||||
'../src/stories/components/Icon/Icon.stories.js',
|
||||
'../src/stories/components/Icon.stories/Icon.stories.mdx',
|
||||
],
|
||||
invalidPaths: [
|
||||
'./stories.js',
|
||||
'./src/stories/Icon.stories.js',
|
||||
'./Icon.stories.js',
|
||||
'../src/Icon.stories.mdx',
|
||||
'../src/stories/components/Icon/Icon.stories.ts',
|
||||
'../src/stories/components/Icon/Icon.mdx',
|
||||
],
|
||||
},
|
||||
{
|
||||
glob: 'dirname/../stories/*.stories.*',
|
||||
validPaths: [
|
||||
'./dirname/../stories/App.stories.js',
|
||||
'./dirname/../stories/addon-centered.stories.js',
|
||||
],
|
||||
invalidPaths: ['./dirname/../stories.js', './dirname/../App.stories.js'],
|
||||
},
|
||||
];
|
||||
|
||||
describe('toRequireContext', () => {
|
||||
it('matches only suitable paths', () => {
|
||||
testCases.forEach(({ glob, validPaths, invalidPaths }) => {
|
||||
const { path: base, match: regex } = toRequireContext(glob);
|
||||
|
||||
function isMatched(filePath) {
|
||||
const relativePath = `./${path.relative(base, filePath)}`;
|
||||
return filePath.includes(base) && regex.test(relativePath);
|
||||
}
|
||||
|
||||
const isMatchedForValidPaths = validPaths.every(isMatched);
|
||||
const isMatchedForInvalidPaths = invalidPaths.every(filePath => !isMatched(filePath));
|
||||
|
||||
expect(isMatchedForValidPaths).toBe(true);
|
||||
expect(isMatchedForInvalidPaths).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -22,9 +22,6 @@ export function getInterpretedFile(pathToFile) {
|
||||
|
||||
export function getInterpretedFileWithExt(pathToFile) {
|
||||
return possibleExtensions
|
||||
.map((ext) => ({
|
||||
path: `${pathToFile}${ext}`,
|
||||
ext,
|
||||
}))
|
||||
.map((ext) => ({ path: `${pathToFile}${ext}`, ext }))
|
||||
.find((candidate) => fs.existsSync(candidate.path));
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import serverRequire from './server-require';
|
||||
import { serverRequire } from './server-require';
|
||||
|
||||
const webpackConfigs = ['webpack.config', 'webpackfile'];
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import interpret from 'interpret';
|
||||
import path from 'path';
|
||||
import { logger } from '@storybook/node-logger';
|
||||
import { getInterpretedFileWithExt } from './interpret-files';
|
||||
|
||||
@ -68,16 +69,14 @@ function getCandidate(paths) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default function serverRequire(filePath) {
|
||||
const paths = Array.isArray(filePath) ? filePath : [filePath];
|
||||
const existingCandidate = getCandidate(paths);
|
||||
export function serverRequire(filePath) {
|
||||
const candidatePath = serverResolve(filePath);
|
||||
|
||||
if (!existingCandidate) {
|
||||
if (!candidatePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { path: candidatePath, ext: candidateExt } = existingCandidate;
|
||||
|
||||
const candidateExt = path.extname(candidatePath);
|
||||
const moduleDescriptor = interpret.extensions[candidateExt];
|
||||
|
||||
// The "moduleDescriptor" either "undefined" or "null". The warning isn't needed in these cases.
|
||||
@ -90,3 +89,14 @@ export default function serverRequire(filePath) {
|
||||
|
||||
return interopRequireDefault(candidatePath);
|
||||
}
|
||||
|
||||
export function serverResolve(filePath) {
|
||||
const paths = Array.isArray(filePath) ? filePath : [filePath];
|
||||
const existingCandidate = getCandidate(paths);
|
||||
|
||||
if (!existingCandidate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return existingCandidate.path;
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import Provider from './provider';
|
||||
|
||||
// @ts-ignore
|
||||
ThemeProvider.displayName = 'ThemeProvider';
|
||||
// @ts-ignore
|
||||
HelmetProvider.displayName = 'HelmetProvider';
|
||||
|
||||
const getDocsMode = () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user