Skip to content

Commit

Permalink
Merge pull request #1 from storybookjs/migrate-from-monorepo
Browse files Browse the repository at this point in the history
Extract @storybook/csf from monorepo
  • Loading branch information
shilman authored Dec 9, 2019
2 parents 675a788 + 5b90772 commit 943c35e
Show file tree
Hide file tree
Showing 9 changed files with 6,588 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
],
};
19 changes: 19 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2

jobs:
test:
docker:
- image: circleci/node:10
steps:
- checkout
- run: yarn install
- run: yarn build
- run: yarn lint
- run: yarn test

workflows:
version: 2

build_and_test:
jobs:
- test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
*.log
dist
.idea
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
# Storybook Component Story Format (CSF)

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

## Install

```sh
yarn add @storybook/csf
```

## API

See package source for function definitions and types:

- `isExportStory(key, { includeStories, excludeStories })` - Does a named export match CSF inclusion/exclusion options?

- `parseKind(kind, { rootSeparator, groupSeparator })` - Parse out the component/kind name from a path, using the given separator config.

- `sanitize(string)` - Remove punctuation and illegal characters from a story ID.

- `toId(kind, name)` - Generate a storybook ID from a component/kind and story name.

## Contributing

If you have any suggestions, please open an issue or a PR.

All contributions are welcome!

### run tests:

```sh
yarn test
```
65 changes: 65 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@storybook/csf",
"version": "0.0.1",
"description": "Storybook Component Story Format (CSF) utilities",
"keywords": [
"storybook",
"component story format",
"csf",
"stories"
],
"homepage": "https://github.com/storybookjs/csf",
"bugs": {
"url": "https://github.com/storybookjs/csf/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/csf.git"
},
"license": "MIT",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "babel src --out-dir dist --extensions \".ts\" && tsc --emitDeclarationOnly",
"lint": "eslint src --ext .js,.ts",
"prepublish": "yarn build",
"test": "jest"
},
"eslintConfig": {
"extends": [
"@storybook/eslint-config-storybook"
]
},
"prettier": "@storybook/linter-config/prettier.config",
"jest": {
"testEnvironment": "node"
},
"dependencies": {
"lodash": "^4.17.15"
},
"devDependencies": {
"@babel/cli": "^7.7.4",
"@babel/core": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"@babel/preset-typescript": "^7.7.4",
"@storybook/eslint-config-storybook": "^2.1.0",
"@types/jest": "^24.0.23",
"@types/lodash": "^4.14.149",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^24.9.0",
"common-tags": "^1.8.0",
"eslint": "^6.7.1",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"typescript": "^3.7.2"
},
"publishConfig": {
"access": "public"
}
}
91 changes: 91 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { toId, storyNameFromExport, isExportStory } from '.';

describe('toId', () => {
[
// name, kind, story, output
['handles simple cases', 'kind', 'story', 'kind--story'],
['handles basic substitution', 'a b$c?d😀e', '1-2:3', 'a-b-c-d😀e--1-2-3'],
['handles runs of non-url chars', 'a?&*b', 'story', 'a-b--story'],
['removes non-url chars from start and end', '?ab-', 'story', 'ab--story'],
['downcases', 'KIND', 'STORY', 'kind--story'],
['non-latin', 'Кнопки', 'нормальный', 'кнопки--нормальный'],
['korean', 'kind', '바보 (babo)', 'kind--바보-babo'],
['all punctuation', 'kind', 'unicorns,’–—―′¿`"<>()!.!!!{}[]%^&$*#&', 'kind--unicorns'],
].forEach(([name, kind, story, output]) => {
it(name, () => {
expect(toId(kind, story)).toBe(output);
});
});

it('does not allow kind with *no* url chars', () => {
expect(() => toId('?', 'asdf')).toThrow(
`Invalid kind '?', must include alphanumeric characters`
);
});

it('does not allow empty kind', () => {
expect(() => toId('', 'asdf')).toThrow(`Invalid kind '', must include alphanumeric characters`);
});

it('does not allow story with *no* url chars', () => {
expect(() => toId('kind', '?')).toThrow(
`Invalid name '?', must include alphanumeric characters`
);
});

it('does not allow empty story', () => {
expect(() => toId('kind', '')).toThrow(`Invalid name '', must include alphanumeric characters`);
});
});

describe('storyNameFromExport', () => {
it('should format CSF exports with sensible defaults', () => {
const testCases = {
name: 'Name',
someName: 'Some Name',
someNAME: 'Some NAME',
some_custom_NAME: 'Some Custom NAME',
someName1234: 'Some Name 1234',
someName1_2_3_4: 'Some Name 1 2 3 4',
};
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();
});
});
84 changes: 84 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import startCase from 'lodash/startCase';

/**
* Remove punctuation and illegal characters from a story ID.
*
* See 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;
};

/**
* Generate a storybook ID from a component/kind and story name.
*/
export const toId = (kind: string, name: string) =>
`${sanitizeSafe(kind, 'kind')}--${sanitizeSafe(name, 'name')}`;

/**
* Transform a 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);
}

/**
* Does a named export match CSF inclusion/exclusion options?
*/
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))
);
}

export interface SeparatorOptions {
rootSeparator: string | RegExp;
groupSeparator: string | RegExp;
}

/**
* Parse out the component/kind name from a path, using the given separator config.
*/
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,
};
};
16 changes: 16 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es2015", "es2017", "dom"],
"module": "commonjs",
"declaration": true,
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"types": ["jest"],
"outDir": "dist"
},
"include": ["src"],
"exclude": ["dist"]
}
Loading

0 comments on commit 943c35e

Please sign in to comment.