diff --git a/.babelrc b/.babelrc index cbd3ac60a229..9b84f85f78e7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "presets": [ - "es2015" + "es2015", "react" ], "plugins": [ "transform-runtime" diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 000000000000..425739ff73fa --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,10 @@ +import { configure } from '@kadira/storybook'; + +const req = require.context('../stories/required_with_context', true, /.stories.js$/) + +function loadStories() { + req.keys().forEach((filename) => req(filename)) + require('../stories/directly_required') +} + +configure(loadStories, module); \ No newline at end of file diff --git a/package.json b/package.json index 98be5c92eade..2cc6d035708d 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,23 @@ "scripts": { "prepublish": "babel ./src --out-dir ./dist", "lint": "standard", - "test": "npm run lint" + "test": "npm run lint", + "jest": "jest", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" }, "devDependencies": { + "@kadira/storybook": "^2.21.0", "babel-cli": "^6.14.0", + "babel-jest": "^18.0.0", "babel-plugin-transform-runtime": "^6.15.0", "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "jest": "^18.0.0", "standard": "^8.6.0" }, "dependencies": { + "react": "^15.4.1", "babel-runtime": "^6.20.0", "react-test-renderer": "^15.3.1", "read-pkg-up": "^2.0.0" diff --git a/src/index.js b/src/index.js index c5073f5aa15b..fefda44d2bed 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,14 @@ import renderer from 'react-test-renderer' import path from 'path' import readPkgUp from 'read-pkg-up' - +import runWithRequireContext from './require_context' const { describe, it, expect } = global let storybook let configPath +const babel = require('babel-core') + const pkg = readPkgUp.sync().pkg const isStorybook = (pkg.devDependencies && pkg.devDependencies['@kadira/storybook']) || @@ -18,21 +20,26 @@ const isRNStorybook = export default function testStorySnapshots (options = {}) { if (isStorybook) { storybook = require.requireActual('@kadira/storybook') + const loadBabelConfig = require('@kadira/storybook/dist/server/babel_config').default const configDirPath = path.resolve(options.configPath || '.storybook') configPath = path.join(configDirPath, 'config.js') + + const content = babel.transformFileSync(configPath, babelConfig).code + const contextOpts = { + filename: configPath, + dirname: configDirPath + } + const babelConfig = loadBabelConfig(configDirPath) + + runWithRequireContext(content, contextOpts) } else if (isRNStorybook) { storybook = require.requireActual('@kadira/react-native-storybook') configPath = path.resolve(options.configPath || 'storybook') + require.requireActual(configPath) } else { throw new Error('\'storyshots\' is intended only to be used with react storybook or react native storybook') } - try { - require.requireActual(configPath) - } catch (e) { - throw new Error(`Could not load stories from ${configPath}. Check 'configPath' option`) - } - if (typeof describe !== 'function') { throw new Error('\'testStorySnapshots\' is intended only to be used inside jest') } diff --git a/src/require_context.js b/src/require_context.js new file mode 100644 index 000000000000..b67537f657da --- /dev/null +++ b/src/require_context.js @@ -0,0 +1,72 @@ +import vm from 'vm' +import fs from 'fs' +import path from 'path' +import moduleSystem from 'module' + +function requireModules (keys, root, directory, regExp, recursive) { + const files = fs.readdirSync(path.join(root, directory)) + + files.forEach((filename) => { + // webpack adds a './' to the begining of the key + // TODO: Check this in windows + const entryKey = `./${path.join(directory, filename)}` + if (regExp.test(entryKey)) { + // eslint-disable-next-line no-param-reassign, global-require, import/no-dynamic-require + keys[entryKey] = require(path.join(root, directory, filename)) + return + } + + if (!recursive) { + return + } + + if (fs.statSync(path.join(root, directory, filename)).isDirectory()) { + requireModules(keys, root, path.join(directory, filename), regExp, recursive) + } + }) +} + +function isRelativeRequest (request) { + if (request.charCodeAt(0) !== 46/* . */) { + return false + } + + if (request === '.' || '..') { + return true + } + + return request.charCodeAt(1) === 47/* / */ || ( + request.charCodeAt(1) === 46/* . */ && request.charCodeAt(2) === 47/* / */) +} + +export default function runWithRequireContext (content, options) { + const { filename, dirname } = options + + const newRequire = (request) => { + if (isRelativeRequest(request)) { + // eslint-disable-next-line global-require, import/no-dynamic-require + return require(path.resolve(dirname, request)) + } + + // eslint-disable-next-line global-require, import/no-dynamic-require + return require(request) + } + + newRequire.resolve = require.resolve + newRequire.extensions = require.extensions + newRequire.main = require.main + newRequire.cache = require.cache + + newRequire.context = (directory, useSubdirectories = false, regExp = /^\.\//) => { + const fullPath = path.resolve(dirname, directory) + const keys = {} + requireModules(keys, fullPath, '.', regExp, useSubdirectories) + + const req = f => (keys[f]) + req.keys = () => (Object.keys(keys)) + return req + } + + const compiledModule = vm.runInThisContext(moduleSystem.wrap(content)) + compiledModule(module.exports, newRequire, module, filename, dirname) +} diff --git a/stories/__test__/__snapshots__/storyshots.test.js.snap b/stories/__test__/__snapshots__/storyshots.test.js.snap new file mode 100644 index 000000000000..c02481b10f6e --- /dev/null +++ b/stories/__test__/__snapshots__/storyshots.test.js.snap @@ -0,0 +1,200 @@ +exports[`Storyshots Another Button with some emoji 1`] = ` + +`; + +exports[`Storyshots Another Button with text 1`] = ` + +`; + +exports[`Storyshots Button with some emoji 1`] = ` + +`; + +exports[`Storyshots Button with text 1`] = ` + +`; + +exports[`Storyshots Welcome to Storybook 1`] = ` +
+

+ Welcome to STORYBOOK +

+

+ This is a UI component dev environment for your app. +

+

+ We\'ve added some basic stories inside the + + src/stories + + directory. +
+ A story is a single state of one or more UI components. You can have as many stories as you want. +
+ (Basically a story is like a visual test case.) +

+

+ See these sample + + stories + + for a component called + + Button + + . +

+

+ Just like that, you can add your own components as stories. +
+ You can also edit those components and see changes right away. +
+ (Try editing the + + Button + + component located at + + src/stories/Button.js + + .) +

+

+ This is just one thing you can do with Storybook. +
+ Have a look at the + + React Storybook + + repo for more information. +

+
+`; diff --git a/stories/__test__/storyshots.test.js b/stories/__test__/storyshots.test.js new file mode 100644 index 000000000000..0da7c69f7060 --- /dev/null +++ b/stories/__test__/storyshots.test.js @@ -0,0 +1,2 @@ +import initStoryshots from '../../src' +initStoryshots() diff --git a/stories/directly_required/Button.js b/stories/directly_required/Button.js new file mode 100644 index 000000000000..af3307f4f0f6 --- /dev/null +++ b/stories/directly_required/Button.js @@ -0,0 +1,27 @@ +import React from 'react' + +const buttonStyles = { + border: '1px solid #eee', + borderRadius: 3, + backgroundColor: '#FFFFFF', + cursor: 'pointer', + fontSize: 15, + padding: '3px 10px', + margin: 10 +} + +const Button = ({ children, onClick }) => ( + +) + +Button.propTypes = { + children: React.PropTypes.string.isRequired, + onClick: React.PropTypes.func +} + +export default Button diff --git a/stories/directly_required/index.js b/stories/directly_required/index.js new file mode 100644 index 000000000000..64bf4f18e695 --- /dev/null +++ b/stories/directly_required/index.js @@ -0,0 +1,11 @@ +import React from 'react' +import { storiesOf, action } from '@kadira/storybook' +import Button from './Button' + +storiesOf('Another Button', module) + .add('with text', () => ( + + )) + .add('with some emoji', () => ( + + )) diff --git a/stories/required_with_context/Button.js b/stories/required_with_context/Button.js new file mode 100644 index 000000000000..af3307f4f0f6 --- /dev/null +++ b/stories/required_with_context/Button.js @@ -0,0 +1,27 @@ +import React from 'react' + +const buttonStyles = { + border: '1px solid #eee', + borderRadius: 3, + backgroundColor: '#FFFFFF', + cursor: 'pointer', + fontSize: 15, + padding: '3px 10px', + margin: 10 +} + +const Button = ({ children, onClick }) => ( + +) + +Button.propTypes = { + children: React.PropTypes.string.isRequired, + onClick: React.PropTypes.func +} + +export default Button diff --git a/stories/required_with_context/Button.stories.js b/stories/required_with_context/Button.stories.js new file mode 100644 index 000000000000..dc1398ca774e --- /dev/null +++ b/stories/required_with_context/Button.stories.js @@ -0,0 +1,17 @@ +import React from 'react' +import { storiesOf, action, linkTo } from '@kadira/storybook' +import Button from './Button' +import Welcome from './Welcome' + +storiesOf('Welcome', module) + .add('to Storybook', () => ( + + )) + +storiesOf('Button', module) + .add('with text', () => ( + + )) + .add('with some emoji', () => ( + + )) diff --git a/stories/required_with_context/Welcome.js b/stories/required_with_context/Welcome.js new file mode 100644 index 000000000000..84d9e63798e1 --- /dev/null +++ b/stories/required_with_context/Welcome.js @@ -0,0 +1,72 @@ +import React from 'react' + +const styles = { + main: { + margin: 15, + maxWidth: 600, + lineHeight: 1.4, + fontFamily: '"Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif' + }, + + logo: { + width: 200 + }, + + link: { + color: '#1474f3', + textDecoration: 'none', + borderBottom: '1px solid #1474f3', + paddingBottom: 2 + }, + + code: { + fontSize: 15, + fontWeight: 600, + padding: '2px 5px', + border: '1px solid #eae9e9', + borderRadius: 4, + backgroundColor: '#f3f2f2', + color: '#3a3a3a' + } +} + +export default class Welcome extends React.Component { + showApp (e) { + e.preventDefault() + if (this.props.showApp) this.props.showApp() + } + + render () { + return ( +
+

Welcome to STORYBOOK

+

+ This is a UI component dev environment for your app. +

+

+ We've added some basic stories inside the src/stories directory. +
+ A story is a single state of one or more UI components. You can have as many stories as you want. +
+ (Basically a story is like a visual test case.) +

+

+ See these sample stories for a component called Button. +

+

+ Just like that, you can add your own components as stories. +
+ You can also edit those components and see changes right away. +
+ (Try editing the Button component + located at src/stories/Button.js.) +

+

+ This is just one thing you can do with Storybook. +
+ Have a look at the React Storybook repo for more information. +

+
+ ) + } +} diff --git a/stories/required_with_context/Welcome.stories.js b/stories/required_with_context/Welcome.stories.js new file mode 100644 index 000000000000..c8e25ada01dc --- /dev/null +++ b/stories/required_with_context/Welcome.stories.js @@ -0,0 +1,8 @@ +import React from 'react' +import { storiesOf, linkTo } from '@kadira/storybook' +import Welcome from './Welcome' + +storiesOf('Welcome', module) + .add('to Storybook', () => ( + + ))