Refactor router utils into CSF library

This commit is contained in:
Michael Shilman 2019-12-09 12:16:01 +08:00
parent d6e55273b9
commit b436339376
32 changed files with 182 additions and 133 deletions

View File

@ -48,8 +48,8 @@
"@storybook/addons": "5.3.0-beta.19",
"@storybook/api": "5.3.0-beta.19",
"@storybook/components": "5.3.0-beta.19",
"@storybook/csf": "5.3.0-beta.19",
"@storybook/postinstall": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"@storybook/source-loader": "5.3.0-beta.19",
"@storybook/theming": "5.3.0-beta.19",
"acorn": "^7.1.0",

View File

@ -1,5 +1,5 @@
import React, { useContext, FunctionComponent } from 'react';
import { parseKind } from '@storybook/router';
import { parseKind } from '@storybook/csf';
import { Title as PureTitle } from '@storybook/components';
import { DocsContext } from './DocsContext';
import { StringSlot } from './shared';

View File

@ -3,7 +3,7 @@ const parser = require('@babel/parser');
const generate = require('@babel/generator').default;
const camelCase = require('lodash/camelCase');
const jsStringEscape = require('js-string-escape');
const { toId, storyNameFromExport } = require('@storybook/router/utils');
const { toId, storyNameFromExport } = require('@storybook/csf');
// Generate the MDX as is, but append named exports for every
// story in the contents

View File

@ -32,6 +32,7 @@
"@storybook/addons": "5.3.0-beta.19",
"@storybook/client-logger": "5.3.0-beta.19",
"@storybook/core-events": "5.3.0-beta.19",
"@storybook/csf": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"core-js": "^3.0.1",
"global": "^4.3.2",

View File

@ -7,7 +7,7 @@ import {
import qs from 'qs';
import addons from '@storybook/addons';
import { STORY_CHANGED, SELECT_STORY } from '@storybook/core-events';
import { toId } from '@storybook/router/utils';
import { toId } from '@storybook/csf';
import { logger } from '@storybook/client-logger';
interface ParamsId {

View File

@ -30,8 +30,8 @@
},
"dependencies": {
"@hypnosphi/jest-puppeteer-axe": "^1.4.0",
"@storybook/csf": "5.3.0-beta.19",
"@storybook/node-logger": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"@types/jest-image-snapshot": "^2.8.0",
"core-js": "^3.0.1",
"jest-image-snapshot": "^2.8.2",

View File

@ -1,4 +1,4 @@
import { toId } from '@storybook/router/utils';
import { toId } from '@storybook/csf';
import { URL } from 'url';

View File

@ -29,6 +29,7 @@
"@storybook/channels": "5.3.0-beta.19",
"@storybook/client-logger": "5.3.0-beta.19",
"@storybook/core-events": "5.3.0-beta.19",
"@storybook/csf": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"@storybook/theming": "5.3.0-beta.19",
"core-js": "^3.0.1",

View File

@ -1,5 +1,5 @@
// FIXME: we shouldn't import from dist but there are no types otherwise
import { toId, sanitize, parseKind } from '@storybook/router';
import { toId, sanitize, parseKind } from '@storybook/csf';
import deprecate from 'util-deprecate';
import { Module } from '../index';

View File

@ -1,5 +1,5 @@
import { queryFromLocation } from '@storybook/router';
import { toId } from '@storybook/router/dist/utils';
import { toId } from '@storybook/csf';
import { Module } from '../index';
import { PanelPositions } from './layout';

View File

@ -32,7 +32,7 @@
"@storybook/channels": "5.3.0-beta.19",
"@storybook/client-logger": "5.3.0-beta.19",
"@storybook/core-events": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"@storybook/csf": "5.3.0-beta.19",
"core-js": "^3.0.1",
"eventemitter3": "^4.0.0",
"global": "^4.3.2",

View File

@ -4,7 +4,7 @@ import isPlainObject from 'is-plain-object';
import { logger } from '@storybook/client-logger';
import addons, { StoryContext, StoryFn, Parameters } from '@storybook/addons';
import Events from '@storybook/core-events';
import { toId } from '@storybook/router/utils';
import { toId } from '@storybook/csf';
import mergeWith from 'lodash/mergeWith';
import isEqual from 'lodash/isEqual';

View File

@ -1,5 +1,5 @@
import createChannel from '@storybook/channel-postmessage';
import { toId } from '@storybook/router/utils';
import { toId } from '@storybook/csf';
import addons from '@storybook/addons';
import StoryStore from './story_store';

View File

@ -29,8 +29,8 @@
"dependencies": {
"@hypnosphi/jscodeshift": "^0.6.4",
"@mdx-js/mdx": "^1.5.1",
"@storybook/csf": "5.3.0-beta.19",
"@storybook/node-logger": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"core-js": "^3.0.1",
"cross-spawn": "^7.0.0",
"globby": "^10.0.1",

View File

@ -1,18 +1,5 @@
import recast from 'recast';
// FIXME: duplicate code from @storybook/core start.js
function isExportStory(key, { includeStories, excludeStories }) {
function matches(storyKey, arrayOrRegex) {
if (Array.isArray(arrayOrRegex)) {
return arrayOrRegex.includes(storyKey);
}
return storyKey.match(arrayOrRegex);
}
return (
(!includeStories || matches(key, includeStories)) &&
(!excludeStories || !matches(key, excludeStories))
);
}
import { isExportStory } from '@storybook/csf';
function exportMdx(root, options) {
// eslint-disable-next-line no-underscore-dangle

View File

@ -1,6 +1,6 @@
import prettier from 'prettier';
import { logger } from '@storybook/node-logger';
import { storyNameFromExport } from '@storybook/router';
import { storyNameFromExport } from '@storybook/csf';
import { sanitizeName } from '../lib/utils';
/**

View File

@ -37,6 +37,7 @@
"@storybook/client-api": "5.3.0-beta.19",
"@storybook/client-logger": "5.3.0-beta.19",
"@storybook/core-events": "5.3.0-beta.19",
"@storybook/csf": "5.3.0-beta.19",
"@storybook/node-logger": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"@storybook/theming": "5.3.0-beta.19",

View File

@ -1,5 +1,5 @@
import { ClientApi, StoryStore, ConfigApi } from '@storybook/client-api';
import { toId } from '@storybook/router/utils';
import { toId } from '@storybook/csf';
import start from './start';
export default {

View File

@ -7,7 +7,7 @@ import AnsiToHtml from 'ansi-to-html';
import addons from '@storybook/addons';
import createChannel from '@storybook/channel-postmessage';
import { ClientApi, StoryStore, ConfigApi } from '@storybook/client-api';
import { toId, storyNameFromExport } from '@storybook/router/utils';
import { toId, storyNameFromExport, isExportStory } from '@storybook/csf';
import { logger } from '@storybook/client-logger';
import Events from '@storybook/core-events';
@ -24,22 +24,6 @@ const classes = {
ERROR: 'sb-show-errordisplay',
};
function matches(storyKey, arrayOrRegex) {
if (Array.isArray(arrayOrRegex)) {
return arrayOrRegex.includes(storyKey);
}
return storyKey.match(arrayOrRegex);
}
export function isExportStory(key, { includeStories, excludeStories }) {
return (
// https://babeljs.io/docs/en/babel-plugin-transform-modules-commonjs
key !== '__esModule' &&
(!includeStories || matches(key, includeStories)) &&
(!excludeStories || !matches(key, excludeStories))
);
}
function showMain() {
document.body.classList.remove(classes.NOPREVIEW);
document.body.classList.remove(classes.ERROR);

View File

@ -2,7 +2,7 @@
import { history, document, window } from 'global';
import Events from '@storybook/core-events';
import start, { isExportStory } from './start';
import start from './start';
jest.mock('@storybook/client-logger');
jest.mock('global', () => ({
@ -143,41 +143,3 @@ describe('STORY_INIT', () => {
expect(store.setSelection).toHaveBeenCalledWith({ storyId: 'kind--story' });
});
});
describe('story filters for module exports', () => {
it('should exclude __esModule', () => {
expect(isExportStory('__esModule', {})).toBeFalsy();
});
it('should include all stories when there are no filters', () => {
expect(isExportStory('a', {})).toBeTruthy();
});
it('should filter stories by arrays', () => {
expect(isExportStory('a', { includeStories: ['a'] })).toBeTruthy();
expect(isExportStory('a', { includeStories: [] })).toBeFalsy();
expect(isExportStory('a', { includeStories: ['b'] })).toBeFalsy();
expect(isExportStory('a', { excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { excludeStories: [] })).toBeTruthy();
expect(isExportStory('a', { excludeStories: ['b'] })).toBeTruthy();
expect(isExportStory('a', { includeStories: ['a'], excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { includeStories: [], excludeStories: [] })).toBeFalsy();
expect(isExportStory('a', { includeStories: ['a'], excludeStories: ['b'] })).toBeTruthy();
});
it('should filter stories by regex', () => {
expect(isExportStory('a', { includeStories: /a/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /.*/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /b/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /a/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /.*/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /b/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /a/, excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { includeStories: /.*/, excludeStories: /.*/ })).toBeFalsy();
expect(isExportStory('a', { includeStories: /a/, excludeStories: /b/ })).toBeTruthy();
});
});

View File

@ -1,6 +1,6 @@
import { history, document } from 'global';
import qs from 'qs';
import { toId } from '@storybook/router/utils';
import { toId } from '@storybook/csf';
export function pathToId(path) {
const match = (path || '').match(/^\/story\/(.+)/);

3
lib/csf/README.md Normal file
View File

@ -0,0 +1,3 @@
# Storybook CSF
A minimal set of utility functions for dealing with Storybook [Component Story Format](https://storybook.js.org/docs/formats/component-story-format/).

35
lib/csf/package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "@storybook/csf",
"version": "5.3.0-beta.19",
"description": "",
"keywords": [
"storybook"
],
"homepage": "https://github.com/storybookjs/storybook/tree/master/lib/csf",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "lib/csf"
},
"license": "MIT",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"lodash": "^4.17.15"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,4 +1,4 @@
import { toId, storyNameFromExport } from './utils';
import { toId, storyNameFromExport, isExportStory } from '.';
describe('toId', () => {
[
@ -51,3 +51,41 @@ describe('storyNameFromExport', () => {
Object.entries(testCases).forEach(([key, val]) => expect(storyNameFromExport(key)).toBe(val));
});
});
describe('isExportStory', () => {
it('should exclude __esModule', () => {
expect(isExportStory('__esModule', {})).toBeFalsy();
});
it('should include all stories when there are no filters', () => {
expect(isExportStory('a', {})).toBeTruthy();
});
it('should filter stories by arrays', () => {
expect(isExportStory('a', { includeStories: ['a'] })).toBeTruthy();
expect(isExportStory('a', { includeStories: [] })).toBeFalsy();
expect(isExportStory('a', { includeStories: ['b'] })).toBeFalsy();
expect(isExportStory('a', { excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { excludeStories: [] })).toBeTruthy();
expect(isExportStory('a', { excludeStories: ['b'] })).toBeTruthy();
expect(isExportStory('a', { includeStories: ['a'], excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { includeStories: [], excludeStories: [] })).toBeFalsy();
expect(isExportStory('a', { includeStories: ['a'], excludeStories: ['b'] })).toBeTruthy();
});
it('should filter stories by regex', () => {
expect(isExportStory('a', { includeStories: /a/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /.*/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /b/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /a/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /.*/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /b/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /a/, excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { includeStories: /.*/, excludeStories: /.*/ })).toBeFalsy();
expect(isExportStory('a', { includeStories: /a/, excludeStories: /b/ })).toBeTruthy();
});
});

69
lib/csf/src/index.ts Normal file
View File

@ -0,0 +1,69 @@
import startCase from 'lodash/startCase';
// Remove punctuation https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
export const sanitize = (string: string) => {
return (
string
.toLowerCase()
// eslint-disable-next-line no-useless-escape
.replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-')
.replace(/-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
);
};
const sanitizeSafe = (string: string, part: string) => {
const sanitized = sanitize(string);
if (sanitized === '') {
throw new Error(`Invalid ${part} '${string}', must include alphanumeric characters`);
}
return sanitized;
};
export const toId = (kind: string, name: string) =>
`${sanitizeSafe(kind, 'kind')}--${sanitizeSafe(name, 'name')}`;
// Transform the CSF named export into a readable story name
export const storyNameFromExport = (key: string) => startCase(key);
type StoryDescriptor = string[] | RegExp;
export interface IncludeExcludeOptions {
includeStories?: StoryDescriptor;
excludeStories?: StoryDescriptor;
}
function matches(storyKey: string, arrayOrRegex?: StoryDescriptor) {
if (Array.isArray(arrayOrRegex)) {
return arrayOrRegex.includes(storyKey);
}
return storyKey.match(arrayOrRegex);
}
export function isExportStory(
key: string,
{ includeStories, excludeStories }: IncludeExcludeOptions
) {
return (
// https://babeljs.io/docs/en/babel-plugin-transform-modules-commonjs
key !== '__esModule' &&
(!includeStories || matches(key, includeStories)) &&
(!excludeStories || !matches(key, excludeStories))
);
}
interface SeparatorOptions {
rootSeparator: string | RegExp;
groupSeparator: string | RegExp;
}
export const parseKind = (kind: string, { rootSeparator, groupSeparator }: SeparatorOptions) => {
const [root, remainder] = kind.split(rootSeparator, 2);
const groups = (remainder || kind).split(groupSeparator).filter(i => !!i);
// when there's no remainder, it means the root wasn't found/split
return {
root: remainder ? root : null,
groups,
};
};

8
lib/csf/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["src/**.test.ts"]
}

View File

@ -28,6 +28,7 @@
},
"dependencies": {
"@reach/router": "^1.2.1",
"@storybook/csf": "5.3.0-beta.19",
"@types/reach__router": "^1.2.3",
"core-js": "^3.0.1",
"global": "^4.3.2",

View File

@ -1,43 +1,13 @@
import qs from 'qs';
import memoize from 'memoizerific';
import startCase from 'lodash/startCase';
interface StoryData {
viewMode?: string;
storyId?: string;
}
interface SeparatorOptions {
rootSeparator: string | RegExp;
groupSeparator: string | RegExp;
}
const splitPathRegex = /\/([^/]+)\/([^/]+)?/;
// Remove punctuation https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
export const sanitize = (string: string) => {
return (
string
.toLowerCase()
// eslint-disable-next-line no-useless-escape
.replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-')
.replace(/-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
);
};
const sanitizeSafe = (string: string, part: string) => {
const sanitized = sanitize(string);
if (sanitized === '') {
throw new Error(`Invalid ${part} '${string}', must include alphanumeric characters`);
}
return sanitized;
};
export const toId = (kind: string, name: string) =>
`${sanitizeSafe(kind, 'kind')}--${sanitizeSafe(name, 'name')}`;
export const parsePath: (path?: string) => StoryData = memoize(1000)(
(path: string | undefined | null) => {
const result: StoryData = {
@ -83,17 +53,3 @@ export const getMatch = memoize(1000)((current: string, target: string, startsWi
}
return null;
});
export const parseKind = (kind: string, { rootSeparator, groupSeparator }: SeparatorOptions) => {
const [root, remainder] = kind.split(rootSeparator, 2);
const groups = (remainder || kind).split(groupSeparator).filter(i => !!i);
// when there's no remainder, it means the root wasn't found/split
return {
root: remainder ? root : null,
groups,
};
};
// Transform the CSF named export into a readable story name
export const storyNameFromExport = (key: string) => startCase(key);

View File

@ -30,7 +30,7 @@
"dependencies": {
"@storybook/addons": "5.3.0-beta.19",
"@storybook/client-logger": "5.3.0-beta.19",
"@storybook/router": "5.3.0-beta.19",
"@storybook/csf": "5.3.0-beta.19",
"core-js": "^3.0.1",
"estraverse": "^4.2.0",
"global": "^4.3.2",

View File

@ -1,4 +1,4 @@
const { toId } = require('@storybook/router/utils');
const { toId } = require('@storybook/csf');
const STORIES_OF = 'storiesOf';

View File

@ -1,4 +1,4 @@
import { storyNameFromExport } from '@storybook/router/utils';
import { storyNameFromExport } from '@storybook/csf';
import { handleADD, handleSTORYOF, patchNode, handleExportedName } from './parse-helpers';
const estraverse = require('estraverse');
@ -23,6 +23,8 @@ export function splitSTORYOF(ast, source) {
export function splitExports(ast, source) {
const parts = [];
let lastIndex = 0;
const excludeStories = ['text'];
const includeStories = undefined;
estraverse.traverse(ast, {
fallback: 'iteration',

View File

@ -11,6 +11,7 @@ module.exports = {
'@storybook/channels': dirname(resolve('@storybook/channels/package.json')),
'@storybook/components': dirname(resolve('@storybook/components/package.json')),
'@storybook/core-events': dirname(resolve('@storybook/core-events/package.json')),
'@storybook/csf': dirname(resolve('@storybook/csf/package.json')),
'@storybook/router': dirname(resolve('@storybook/router/package.json')),
'@storybook/theming': dirname(resolve('@storybook/theming/package.json')),
'@storybook/ui': dirname(resolve('@storybook/ui/package.json')),