diff --git a/.circleci/config.yml b/.circleci/config.yml index b4e68b5a7786..46e95d6f1484 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,7 @@ jobs: - node_modules - examples/angular-cli/node_modules - examples/cra-kitchen-sink/node_modules + - examples/mithril-kitchen-sink/node_modules - examples/official-storybook/node_modules - examples/polymer-cli/node_modules - examples/vue-kitchen-sink/node_modules @@ -100,6 +101,11 @@ jobs: command: | cd examples/official-storybook yarn build-storybook + - run: + name: "Build mithril kitchen-sink" + command: | + cd examples/mithril-kitchen-sink + yarn build-storybook - run: name: "Visually test storybook" command: | @@ -148,6 +154,11 @@ jobs: command: | cd examples/official-storybook yarn storybook --smoke-test + - run: + name: "Run mithril kitchen-sink (smoke test)" + command: | + cd examples/mithril-kitchen-sink + yarn storybook --smoke-test react-native: <<: *defaults steps: diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index 956c243397a9..d89887889513 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -17,6 +17,7 @@ 'app: react-native': ["app/react-native/**"] 'app: react': ["app/react/**"] 'app: vue': ["app/vue/**"] +'app: mithril': ["app/mithril/**"] 'babel / webpack': ["webpack", "babel"] 'cli': ["lib/cli/**"] 'compatibility with other tools': [] diff --git a/ADDONS_SUPPORT.md b/ADDONS_SUPPORT.md index 19303b2ae306..148b756ac9f0 100644 --- a/ADDONS_SUPPORT.md +++ b/ADDONS_SUPPORT.md @@ -1,19 +1,19 @@ ## Addon / Framework Support Table -| |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| -| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:| -|[a11y](addons/a11y) |+| | | | | -|[actions](addons/actions) |+|+|+|+|+| -|[background](addons/background) |+| | | | | -|[centered](addons/centered) |+| |+| | | -|[events](addons/events) |+| | | | | -|[graphql](addons/graphql) |+| | | | | -|[info](addons/info) |+| | | | | -|[jest](addons/jest) |+| | | | | -|[knobs](addons/knobs) |+|+|+|+|+| -|[links](addons/links) |+|+|+|+|+| -|[notes](addons/notes) |+| |+|+|+| -|[options](addons/options) |+|+|+|+|+| -|[storyshots](addons/storyshots) |+|+|+|+| | -|[storysource](addons/storysource)|+| |+|+|+| -|[viewport](addons/viewport) |+| |+|+|+| +| |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| +| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| +|[a11y](addons/a11y) |+| | | | | | +|[actions](addons/actions) |+|+|+|+|+|+| +|[background](addons/background) |+| | | | |+| +|[centered](addons/centered) |+| |+| | |+| +|[events](addons/events) |+| | | | | | +|[graphql](addons/graphql) |+| | | | | | +|[info](addons/info) |+| | | | | | +|[jest](addons/jest) |+| | | | | | +|[knobs](addons/knobs) |+|+|+|+|+|+| +|[links](addons/links) |+|+|+|+|+|+| +|[notes](addons/notes) |+| |+|+|+|+| +|[options](addons/options) |+|+|+|+|+|+| +|[storyshots](addons/storyshots) |+|+|+|+| | | +|[storysource](addons/storysource)|+| |+|+|+|+| +|[viewport](addons/viewport) |+| |+|+|+|+| diff --git a/README.md b/README.md index ac0ca350409e..07d611abcb3c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ For additional help, join us [in our Slack](https://now-examples-slackin-rrirkqo - [Vue](app/vue) - [Angular](app/angular) - [Polymer](app/polymer) alpha +- [Mithril](app/mithril) alpha ### Sub Projects @@ -107,6 +108,7 @@ See [Addon / Framework Support Table](ADDONS_SUPPORT.md) - [Vue](https://storybooks-vue.netlify.com/) - [Angular](https://storybooks-angular.netlify.com/) - [Polymer](https://storybooks-polymer.netlify.com/) +- [Mithril](https://storybooks-mithril.netlify.com/) ### 3.4 - [React Official](https://release-3-4--storybooks-official.netlify.com) diff --git a/addons/background/README.md b/addons/background/README.md index 0dae593a512e..55f0146e95b4 100644 --- a/addons/background/README.md +++ b/addons/background/README.md @@ -69,3 +69,10 @@ storiesOf("Button", module) .addDecorator(backgrounds) .add("with text", () => ); ``` + +> In the case of Mithril, use these imports: +> +> ```js +> import { storiesOf } from '@storybook/mithril'; +> import backgrounds from "@storybook/addon-backgrounds/mithril"; +> ``` diff --git a/addons/background/mithril.js b/addons/background/mithril.js new file mode 100644 index 000000000000..884a541476ec --- /dev/null +++ b/addons/background/mithril.js @@ -0,0 +1 @@ +module.exports = require('./dist/mithril'); diff --git a/addons/background/src/mithril.js b/addons/background/src/mithril.js new file mode 100644 index 000000000000..a9fd92f71e79 --- /dev/null +++ b/addons/background/src/mithril.js @@ -0,0 +1,39 @@ +/** @jsx m */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import m from 'mithril'; + +import addons from '@storybook/addons'; + +export class BackgroundDecorator { + constructor(vnode) { + this.props = vnode.attrs; + + const { channel, story } = vnode.attrs; + + // A channel is explicitly passed in for testing + if (channel) { + this.channel = channel; + } else { + this.channel = addons.getChannel(); + } + + this.story = story(); + } + + oncreate() { + this.channel.emit('background-set', this.props.backgrounds); + } + + onremove() { + this.channel.emit('background-unset'); + } + + view() { + return m(this.story); + } +} + +export default backgrounds => story => ({ + view: () => , +}); diff --git a/addons/centered/README.md b/addons/centered/README.md index f7298db32882..b24f3819ee28 100644 --- a/addons/centered/README.md +++ b/addons/centered/README.md @@ -49,11 +49,29 @@ storiesOf('MyComponent', module) .add('without props', () => ({ components: { MyComponent }, template: '' - }) + })) .add('with some props', () => ({ components: { MyComponent }, template: '' - }); + })); +``` + +example for Mithril: + +```js +import { storiesOf } from '@storybook/mithril'; +import centered from '@storybook/addon-centered/mithril'; + +import MyComponent from '../Component'; + +storiesOf('MyComponent', module) + .addDecorator(centered) + .add('without props', () => ({ + view: () => + })) + .add('with some props', () => ({ + view: () => + })); ``` Also, you can also add this decorator globally @@ -84,6 +102,19 @@ configure(function () { }, module); ``` +example for Mithril: + +```js +import { configure, addDecorator } from '@storybook/mithril'; +import centered from '@storybook/addon-centered/mithril'; + +addDecorator(centered); + +configure(function () { + //... +}, module); +``` + #### As an extension ##### 1 - Configure the extension diff --git a/addons/centered/mithril.js b/addons/centered/mithril.js new file mode 100644 index 000000000000..884a541476ec --- /dev/null +++ b/addons/centered/mithril.js @@ -0,0 +1 @@ +module.exports = require('./dist/mithril'); diff --git a/addons/centered/src/mithril.js b/addons/centered/src/mithril.js new file mode 100644 index 000000000000..682d947e8307 --- /dev/null +++ b/addons/centered/src/mithril.js @@ -0,0 +1,30 @@ +/** @jsx m */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import m from 'mithril'; + +const style = { + position: 'fixed', + top: 0, + left: 0, + bottom: 0, + right: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + overflow: 'auto', +}; + +const innerStyle = { + margin: 'auto', +}; + +export default function(storyFn) { + return { + view: () => ( +
+
{m(storyFn())}
+
+ ), + }; +} diff --git a/addons/knobs/README.md b/addons/knobs/README.md index 9a4b8e6912c9..7a98a6331c8a 100644 --- a/addons/knobs/README.md +++ b/addons/knobs/README.md @@ -118,6 +118,13 @@ stories.add('as dynamic variables', () => { > import { storiesOf } from '@storybook/angular'; > import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/angular'; > ``` +> +> In the case of Mithril, use these imports: +> +> ```js +> import { storiesOf } from '@storybook/mithril'; +> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/mithril'; +> ``` You can see your Knobs in a Storybook panel as shown below. diff --git a/addons/knobs/mithril.js b/addons/knobs/mithril.js new file mode 100644 index 000000000000..884a541476ec --- /dev/null +++ b/addons/knobs/mithril.js @@ -0,0 +1 @@ +module.exports = require('./dist/mithril'); diff --git a/addons/knobs/src/mithril/WrapStory.js b/addons/knobs/src/mithril/WrapStory.js new file mode 100644 index 000000000000..dd0ff093e754 --- /dev/null +++ b/addons/knobs/src/mithril/WrapStory.js @@ -0,0 +1,68 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import m from 'mithril'; + +export default class WrapStory { + constructor(vnode) { + this.knobChanged = this.knobChanged.bind(this); + this.knobClicked = this.knobClicked.bind(this); + this.resetKnobs = this.resetKnobs.bind(this); + this.setPaneKnobs = this.setPaneKnobs.bind(this); + this.props = vnode.attrs; + this.storyContent = vnode.attrs.initialContent; + } + + oncreate() { + // Watch for changes in knob editor. + this.props.channel.on('addon:knobs:knobChange', this.knobChanged); + // Watch for clicks in knob editor. + this.props.channel.on('addon:knobs:knobClick', this.knobClicked); + // Watch for the reset event and reset knobs. + this.props.channel.on('addon:knobs:reset', this.resetKnobs); + // Watch for any change in the knobStore and set the panel again for those + // changes. + this.props.knobStore.subscribe(this.setPaneKnobs); + // Set knobs in the panel for the first time. + this.setPaneKnobs(); + } + + onremove() { + this.props.channel.removeListener('addon:knobs:knobChange', this.knobChanged); + this.props.channel.removeListener('addon:knobs:knobClick', this.knobClicked); + this.props.channel.removeListener('addon:knobs:reset', this.resetKnobs); + this.props.knobStore.unsubscribe(this.setPaneKnobs); + } + + setPaneKnobs(timestamp = +new Date()) { + const { channel, knobStore } = this.props; + channel.emit('addon:knobs:setKnobs', { knobs: knobStore.getAll(), timestamp }); + } + + knobChanged(change) { + const { name, value } = change; + const { knobStore, storyFn, context } = this.props; + // Update the related knob and it's value. + const knobOptions = knobStore.get(name); + + knobOptions.value = value; + knobStore.markAllUnused(); + this.storyContent = storyFn(context); + m.redraw(); + } + + knobClicked(clicked) { + const knobOptions = this.props.knobStore.get(clicked.name); + knobOptions.callback(); + } + + resetKnobs() { + const { knobStore, storyFn, context } = this.props; + knobStore.reset(); + this.storyContent = storyFn(context); + m.redraw(); + this.setPaneKnobs(false); + } + + view() { + return m(this.storyContent); + } +} diff --git a/addons/knobs/src/mithril/index.js b/addons/knobs/src/mithril/index.js new file mode 100644 index 000000000000..f2dbdd29c2ae --- /dev/null +++ b/addons/knobs/src/mithril/index.js @@ -0,0 +1,49 @@ +/** @jsx m */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import m from 'mithril'; +import addons from '@storybook/addons'; + +import WrapStory from './WrapStory'; + +import { + knob, + text, + boolean, + number, + color, + object, + array, + date, + select, + selectV2, + button, + manager, +} from '../base'; + +export { knob, text, boolean, number, color, object, array, date, select, selectV2, button }; + +export const mithrilHandler = (channel, knobStore) => getStory => context => { + const initialContent = getStory(context); + const props = { context, storyFn: getStory, channel, knobStore, initialContent }; + return { + view: () => , + }; +}; + +function wrapperKnobs(options) { + const channel = addons.getChannel(); + manager.setChannel(channel); + + if (options) channel.emit('addon:knobs:setOptions', options); + + return mithrilHandler(channel, manager.knobStore); +} + +export function withKnobs(storyFn, context) { + return wrapperKnobs()(storyFn)(context); +} + +export function withKnobsOptions(options = {}) { + return (storyFn, context) => wrapperKnobs(options)(storyFn)(context); +} diff --git a/app/mithril/.npmignore b/app/mithril/.npmignore new file mode 100644 index 000000000000..6d236e2ff68d --- /dev/null +++ b/app/mithril/.npmignore @@ -0,0 +1,2 @@ +docs +.babelrc diff --git a/app/mithril/README.md b/app/mithril/README.md new file mode 100644 index 000000000000..a373ca63ffb9 --- /dev/null +++ b/app/mithril/README.md @@ -0,0 +1,33 @@ +# Storybook for Mithril + +[![Build Status on CircleCI](https://circleci.com/gh/storybooks/storybook.svg?style=shield)](https://circleci.com/gh/storybooks/storybook) +[![CodeFactor](https://www.codefactor.io/repository/github/storybooks/storybook/badge)](https://www.codefactor.io/repository/github/storybooks/storybook) +[![Known Vulnerabilities](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847/badge.svg)](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847) +[![BCH compliance](https://bettercodehub.com/edge/badge/storybooks/storybook)](https://bettercodehub.com/results/storybooks/storybook) [![codecov](https://codecov.io/gh/storybooks/storybook/branch/master/graph/badge.svg)](https://codecov.io/gh/storybooks/storybook) +[![Storybook Slack](https://now-examples-slackin-rrirkqohko.now.sh/badge.svg)](https://now-examples-slackin-rrirkqohko.now.sh/) +[![Backers on Open Collective](https://opencollective.com/storybook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/storybook/sponsors/badge.svg)](#sponsors) + +* * * + +Storybook for Mithril is a UI development environment for your Mithril components. +With it, you can visualize different states of your UI components and develop them interactively. + +![Storybook Screenshot](docs/demo.gif) + +Storybook runs outside of your app. +So you can develop UI components in isolation without worrying about app specific dependencies and requirements. + +## Getting Started + +```sh +npm i -g @storybook/cli +cd my-mithril-app +getstorybook +``` + +For more information visit: [storybook.js.org](https://storybook.js.org) + +* * * + +Storybook also comes with a lot of [addons](https://storybook.js.org/addons/introduction) and a great API to customize as you wish. +You can also build a [static version](https://storybook.js.org/basics/exporting-storybook) of your storybook and deploy it anywhere you want. diff --git a/app/mithril/bin/build.js b/app/mithril/bin/build.js new file mode 100755 index 000000000000..780773c6cd31 --- /dev/null +++ b/app/mithril/bin/build.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../dist/server/build'); diff --git a/app/mithril/bin/index.js b/app/mithril/bin/index.js new file mode 100755 index 000000000000..2e96258ce63d --- /dev/null +++ b/app/mithril/bin/index.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../dist/server'); diff --git a/app/mithril/docs/demo.gif b/app/mithril/docs/demo.gif new file mode 100644 index 000000000000..c8366f8534a6 Binary files /dev/null and b/app/mithril/docs/demo.gif differ diff --git a/app/mithril/docs/react_storybook_screenshot.png b/app/mithril/docs/react_storybook_screenshot.png new file mode 100644 index 000000000000..9763382042b2 Binary files /dev/null and b/app/mithril/docs/react_storybook_screenshot.png differ diff --git a/app/mithril/docs/storybooks_io_logo.png b/app/mithril/docs/storybooks_io_logo.png new file mode 100644 index 000000000000..3dd9b09f3a95 Binary files /dev/null and b/app/mithril/docs/storybooks_io_logo.png differ diff --git a/app/mithril/package.json b/app/mithril/package.json new file mode 100644 index 000000000000..839d89004fd3 --- /dev/null +++ b/app/mithril/package.json @@ -0,0 +1,67 @@ +{ + "name": "@storybook/mithril", + "version": "3.4.0-rc.3", + "description": "Storybook for Mithril: Develop Mithril Component in isolation.", + "homepage": "https://github.com/storybooks/storybook/tree/master/app/mithril", + "bugs": { + "url": "https://github.com/storybooks/storybook/issues" + }, + "license": "MIT", + "main": "dist/client/index.js", + "jsnext:main": "src/client/index.js", + "bin": { + "build-storybook": "./bin/build.js", + "start-storybook": "./bin/index.js", + "storybook-server": "./bin/index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/storybooks/storybook.git" + }, + "scripts": { + "dev": "cross-env DEV_BUILD=1 nodemon --watch ./src --exec \"yarn prepare\"", + "prepare": "node ../../scripts/prepare.js" + }, + "dependencies": { + "@storybook/addons": "3.4.0-rc.3", + "@storybook/channel-postmessage": "3.4.0-rc.3", + "@storybook/client-logger": "3.4.0-rc.3", + "@storybook/core": "3.4.0-rc.3", + "@storybook/node-logger": "3.4.0-rc.3", + "@storybook/ui": "3.4.0-rc.3", + "airbnb-js-shims": "^1.4.1", + "babel-loader": "^7.1.4", + "babel-plugin-macros": "^2.2.0", + "babel-plugin-transform-regenerator": "^6.26.0", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-preset-env": "^1.6.1", + "babel-preset-minify": "^0.3.0", + "babel-preset-stage-0": "^6.24.1", + "babel-runtime": "^6.26.0", + "case-sensitive-paths-webpack-plugin": "^2.1.2", + "common-tags": "^1.7.2", + "core-js": "^2.5.4", + "dotenv-webpack": "^1.5.5", + "find-cache-dir": "^1.0.0", + "global": "^4.3.2", + "html-loader": "^0.5.5", + "html-webpack-plugin": "^3.1.0", + "json5": "^0.5.1", + "markdown-loader": "^2.0.2", + "react": "^16.2.0", + "react-dev-utils": "^5.0.0", + "react-dom": "^16.2.0", + "util-deprecate": "^1.0.2", + "webpack": "^4.3.0", + "webpack-hot-middleware": "^2.21.2" + }, + "devDependencies": { + "mithril": "^1.1.6", + "nodemon": "^1.17.2" + }, + "peerDependencies": { + "babel-core": "^6.26.0 || ^7.0.0-0", + "babel-runtime": ">=6.0.0", + "mithril": "^1.1.6" + } +} diff --git a/app/mithril/src/client/index.js b/app/mithril/src/client/index.js new file mode 100644 index 000000000000..308ceea8889f --- /dev/null +++ b/app/mithril/src/client/index.js @@ -0,0 +1,8 @@ +export { + storiesOf, + setAddon, + addDecorator, + configure, + getStorybook, + forceReRender, +} from './preview'; diff --git a/app/mithril/src/client/preview/error_display.js b/app/mithril/src/client/preview/error_display.js new file mode 100644 index 000000000000..aa50e814985d --- /dev/null +++ b/app/mithril/src/client/preview/error_display.js @@ -0,0 +1,45 @@ +/** @jsx m */ + +import m from 'mithril'; + +const mainStyle = { + position: 'fixed', + top: 0, + bottom: 0, + left: 0, + right: 0, + padding: 20, + backgroundColor: 'rgb(187, 49, 49)', + color: '#FFF', + WebkitFontSmoothing: 'antialiased', +}; + +const headingStyle = { + fontSize: 20, + fontWeight: 600, + letterSpacing: 0.2, + margin: '10px 0', + fontFamily: ` + -apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", + "Helvetica Neue", "Lucida Grande", sans-serif + `, +}; + +const codeStyle = { + fontSize: 14, + width: '100vw', + overflow: 'auto', +}; + +const ErrorDisplay = { + view: vnode => ( +
+
{vnode.attrs.error.message}
+
+        {vnode.attrs.error.stack}
+      
+
+ ), +}; + +export default ErrorDisplay; diff --git a/app/mithril/src/client/preview/index.js b/app/mithril/src/client/preview/index.js new file mode 100644 index 000000000000..b04f8439b607 --- /dev/null +++ b/app/mithril/src/client/preview/index.js @@ -0,0 +1,17 @@ +import { start } from '@storybook/core/client'; + +import render from './render'; + +const { clientApi, configApi, forceReRender } = start(render); + +export const { + storiesOf, + setAddon, + addDecorator, + addParameters, + clearDecorators, + getStorybook, +} = clientApi; + +export const { configure } = configApi; +export { forceReRender }; diff --git a/app/mithril/src/client/preview/render.js b/app/mithril/src/client/preview/render.js new file mode 100644 index 000000000000..1c6272a64110 --- /dev/null +++ b/app/mithril/src/client/preview/render.js @@ -0,0 +1,93 @@ +/* global document */ +/** @jsx m */ + +import m from 'mithril'; +import { stripIndents } from 'common-tags'; +import { logger } from '@storybook/client-logger'; +import ErrorDisplay from './error_display'; + +// check whether we're running on node/browser +const isBrowser = typeof window !== 'undefined'; + +let rootEl = null; +let previousKind = ''; +let previousStory = ''; + +if (isBrowser) { + rootEl = document.getElementById('root'); +} + +export function renderError(error) { + const properError = new Error(error.title); + properError.stack = error.description; + + const redBox = ; + m.mount(rootEl, { view: () => redBox }); +} + +export function renderException(error) { + // We always need to render redbox in the mainPage if we get an error. + // Since this is an error, this affects to the main page as well. + const realError = new Error(error.message); + realError.stack = error.stack; + const redBox = ; + m.mount(rootEl, { view: () => redBox }); + + // Log the stack to the console. So, user could check the source code. + logger.error(error.stack); +} + +export function renderMain(data, storyStore, forceRender) { + if (storyStore.size() === 0) return; + + const NoPreview = { view: () =>

No Preview Available!

}; + const noPreview = ; + const { selectedKind, selectedStory } = data; + + const story = storyStore.getStory(selectedKind, selectedStory); + if (!story) { + m.mount(rootEl, { view: () => noPreview }); + return; + } + + if (!forceRender && selectedKind === previousKind && previousStory === selectedStory) { + return; + } + + previousKind = selectedKind; + previousStory = selectedStory; + + const context = { + kind: selectedKind, + story: selectedStory, + }; + + const element = story(context); + + if (!element) { + const error = { + title: `Expecting a Mithril element from the story: "${selectedStory}" of "${selectedKind}".`, + description: stripIndents` + Did you forget to return the Mithril element from the story? + Use "() => MyComp" or "() => { return MyComp; }" when defining the story. + `, + }; + renderError(error); + return; + } + + m.mount(rootEl, { view: () => m(element) }); +} + +export default function renderPreview({ reduxStore, storyStore }, forceRender = false) { + const state = reduxStore.getState(); + if (state.error) { + return renderException(state.error); + } + + try { + return renderMain(state, storyStore, forceRender); + } catch (ex) { + return renderException(ex); + } +} diff --git a/app/mithril/src/server/babel_config.js b/app/mithril/src/server/babel_config.js new file mode 100644 index 000000000000..7758d5279985 --- /dev/null +++ b/app/mithril/src/server/babel_config.js @@ -0,0 +1,68 @@ +import fs from 'fs'; +import path from 'path'; +import JSON5 from 'json5'; +import { logger } from '@storybook/node-logger'; +import defaultConfig from './config/babel'; + +function removeReactHmre(presets) { + const index = presets.indexOf('react-hmre'); + if (index > -1) { + presets.splice(index, 1); + } +} + +// Tries to load a .babelrc and returns the parsed object if successful +function loadFromPath(babelConfigPath) { + let config; + if (fs.existsSync(babelConfigPath)) { + const content = fs.readFileSync(babelConfigPath, 'utf-8'); + try { + config = JSON5.parse(content); + config.babelrc = false; + logger.info('=> Loading custom .babelrc'); + } catch (e) { + logger.error(`=> Error parsing .babelrc file: ${e.message}`); + throw e; + } + } + + if (!config) return null; + + // Remove react-hmre preset. + // It causes issues with react-storybook. + // We don't really need it. + // Earlier, we fix this by running storybook in the production mode. + // But, that hide some useful debug messages. + if (config.presets) { + removeReactHmre(config.presets); + } + + if (config.env && config.env.development && config.env.development.presets) { + removeReactHmre(config.env.development.presets); + } + + return config; +} + +export default function(configDir) { + let babelConfig = loadFromPath(path.resolve(configDir, '.babelrc')); + let inConfigDir = true; + + if (!babelConfig) { + babelConfig = loadFromPath('.babelrc'); + inConfigDir = false; + } + + if (babelConfig) { + // If the custom config uses babel's `extends` clause, then replace it with + // an absolute path. `extends` will not work unless we do this. + if (babelConfig.extends) { + babelConfig.extends = inConfigDir + ? path.resolve(configDir, babelConfig.extends) + : path.resolve(babelConfig.extends); + } + } + + const finalConfig = babelConfig || defaultConfig; + return finalConfig; +} diff --git a/app/mithril/src/server/babel_config.test.js b/app/mithril/src/server/babel_config.test.js new file mode 100644 index 000000000000..8c7c0acd2f4c --- /dev/null +++ b/app/mithril/src/server/babel_config.test.js @@ -0,0 +1,84 @@ +import loadBabelConfig from './babel_config'; + +// eslint-disable-next-line global-require +jest.mock('fs', () => require('../../../../__mocks__/fs')); +jest.mock('path', () => ({ + resolve: () => '.babelrc', +})); + +const setup = ({ files }) => { + // eslint-disable-next-line no-underscore-dangle, global-require + require('fs').__setMockFiles(files); +}; + +describe('babel_config', () => { + // As the 'fs' is going to be mocked, let's call require.resolve + // so the require.cache has the correct route to the file. + // In fact let's use it in the tests :) + it('should return the config with the extra plugins when `plugins` is an array.', () => { + setup({ + files: { + '.babelrc': `{ + "presets": [ + "env", + "foo-preset" + ], + "plugins": [ + "foo-plugin" + ] + }`, + }, + }); + + const config = loadBabelConfig('.foo'); + + expect(config).toEqual({ + babelrc: false, + plugins: ['foo-plugin'], + presets: ['env', 'foo-preset'], + }); + }); + + it('should return the config with the extra plugins when `plugins` is not an array.', () => { + setup({ + files: { + '.babelrc': `{ + "presets": [ + "env", + "foo-preset" + ], + "plugins": "bar-plugin" + }`, + }, + }); + + const config = loadBabelConfig('.bar'); + + expect(config).toEqual({ + babelrc: false, + plugins: 'bar-plugin', + presets: ['env', 'foo-preset'], + }); + }); + + it('should return the config only with the extra plugins when `plugins` is not present.', () => { + // Mock a `.babelrc` config file with no plugins key. + setup({ + files: { + '.babelrc': `{ + "presets": [ + "env", + "foo-preset" + ] + }`, + }, + }); + + const config = loadBabelConfig('.biz'); + + expect(config).toEqual({ + babelrc: false, + presets: ['env', 'foo-preset'], + }); + }); +}); diff --git a/app/mithril/src/server/build.js b/app/mithril/src/server/build.js new file mode 100755 index 000000000000..804f258a0ba4 --- /dev/null +++ b/app/mithril/src/server/build.js @@ -0,0 +1,12 @@ +import { buildStatic } from '@storybook/core/server'; +import path from 'path'; +import packageJson from '../../package.json'; +import getBaseConfig from './config/webpack.config.prod'; +import loadConfig from './config'; + +buildStatic({ + packageJson, + getBaseConfig, + loadConfig, + defaultFavIcon: path.resolve(__dirname, 'public/favicon.ico'), +}); diff --git a/app/mithril/src/server/config.js b/app/mithril/src/server/config.js new file mode 100644 index 000000000000..25f113cd195b --- /dev/null +++ b/app/mithril/src/server/config.js @@ -0,0 +1,86 @@ +/* eslint-disable global-require, import/no-dynamic-require */ +import fs from 'fs'; +import path from 'path'; +import findCacheDir from 'find-cache-dir'; +import { logger } from '@storybook/node-logger'; +import { createDefaultWebpackConfig } from '@storybook/core/server'; +import loadBabelConfig from './babel_config'; + +// `baseConfig` is a webpack configuration bundled with storybook. +// Storybook will look in the `configDir` directory +// (inside working directory) if a config path is not provided. +export default function(configType, baseConfig, configDir) { + const config = baseConfig; + + const babelConfig = loadBabelConfig(configDir); + config.module.rules[0].query = { + // This is a feature of `babel-loader` for webpack (not Babel itself). + // It enables a cache directory for faster-rebuilds + // `find-cache-dir` will create the cache directory under the node_modules directory. + cacheDirectory: findCacheDir({ name: 'react-storybook' }), + ...babelConfig, + }; + + // Check whether a config.js file exists inside the storybook + // config directory and throw an error if it's not. + const storybookConfigPath = path.resolve(configDir, 'config.js'); + if (!fs.existsSync(storybookConfigPath)) { + const err = new Error(`=> Create a storybook config file in "${configDir}/config.js".`); + throw err; + } + config.entry.preview.push(require.resolve(storybookConfigPath)); + + // Check whether addons.js file exists inside the storybook. + // Load the default addons.js file if it's missing. + // Insert it after polyfills.js, but before client/manager. + const storybookCustomAddonsPath = path.resolve(configDir, 'addons.js'); + if (fs.existsSync(storybookCustomAddonsPath)) { + logger.info('=> Loading custom addons config.'); + config.entry.manager.splice(1, 0, storybookCustomAddonsPath); + } + + const defaultConfig = createDefaultWebpackConfig(config); + + // Check whether user has a custom webpack config file and + // return the (extended) base configuration if it's not available. + const customConfigPath = path.resolve(configDir, 'webpack.config.js'); + + if (!fs.existsSync(customConfigPath)) { + logger.info('=> Using default webpack setup.'); + return defaultConfig; + } + const customConfig = require(customConfigPath); + + if (typeof customConfig === 'function') { + logger.info('=> Loading custom webpack config (full-control mode).'); + return customConfig(config, configType, defaultConfig); + } + logger.info('=> Loading custom webpack config (extending mode).'); + return { + ...customConfig, + // We'll always load our configurations after the custom config. + // So, we'll always load the stuff we need. + ...config, + // Override with custom devtool if provided + devtool: customConfig.devtool || config.devtool, + // We need to use our and custom plugins. + plugins: [...config.plugins, ...(customConfig.plugins || [])], + module: { + ...config.module, + // We need to use our and custom rules. + ...customConfig.module, + rules: [ + ...config.module.rules, + ...((customConfig.module && customConfig.module.rules) || []), + ], + }, + resolve: { + ...config.resolve, + ...customConfig.resolve, + alias: { + ...config.alias, + ...(customConfig.resolve && customConfig.resolve.alias), + }, + }, + }; +} diff --git a/app/mithril/src/server/config/babel.js b/app/mithril/src/server/config/babel.js new file mode 100644 index 000000000000..77a413a7cb48 --- /dev/null +++ b/app/mithril/src/server/config/babel.js @@ -0,0 +1,28 @@ +module.exports = { + // Don't try to find .babelrc because we want to force this configuration. + babelrc: false, + presets: [ + [ + require.resolve('babel-preset-env'), + { + targets: { + browsers: ['last 2 versions', 'safari >= 7'], + }, + modules: process.env.NODE_ENV === 'test' ? 'commonjs' : false, + }, + ], + require.resolve('babel-preset-stage-0'), + ], + plugins: [ + require.resolve('babel-plugin-macros'), + require.resolve('babel-plugin-transform-regenerator'), + [ + require.resolve('babel-plugin-transform-runtime'), + { + helpers: true, + polyfill: true, + regenerator: true, + }, + ], + ], +}; diff --git a/app/mithril/src/server/config/babel.prod.js b/app/mithril/src/server/config/babel.prod.js new file mode 100644 index 000000000000..0418f1a1dc3b --- /dev/null +++ b/app/mithril/src/server/config/babel.prod.js @@ -0,0 +1,28 @@ +module.exports = { + // Don't try to find .babelrc because we want to force this configuration. + babelrc: false, + presets: [ + [ + require.resolve('babel-preset-env'), + { + targets: { + browsers: ['last 2 versions', 'safari >= 7'], + }, + modules: false, + }, + ], + require.resolve('babel-preset-stage-0'), + require.resolve('babel-preset-minify'), + ], + plugins: [ + require.resolve('babel-plugin-transform-regenerator'), + [ + require.resolve('babel-plugin-transform-runtime'), + { + helpers: true, + polyfill: true, + regenerator: true, + }, + ], + ], +}; diff --git a/app/mithril/src/server/config/globals.js b/app/mithril/src/server/config/globals.js new file mode 100644 index 000000000000..defcc85dc655 --- /dev/null +++ b/app/mithril/src/server/config/globals.js @@ -0,0 +1,4 @@ +/* globals window */ + +window.STORYBOOK_REACT_CLASSES = {}; +window.STORYBOOK_ENV = 'mithril'; diff --git a/app/mithril/src/server/config/polyfills.js b/app/mithril/src/server/config/polyfills.js new file mode 100644 index 000000000000..869b6824b5ff --- /dev/null +++ b/app/mithril/src/server/config/polyfills.js @@ -0,0 +1,3 @@ +import 'core-js/es6/symbol'; +import 'core-js/fn/array/iterator'; +import 'airbnb-js-shims'; diff --git a/app/mithril/src/server/config/utils.js b/app/mithril/src/server/config/utils.js new file mode 100644 index 000000000000..fc73c38a37f0 --- /dev/null +++ b/app/mithril/src/server/config/utils.js @@ -0,0 +1,35 @@ +import path from 'path'; + +export const includePaths = [path.resolve('./')]; + +export const excludePaths = [path.resolve('node_modules')]; + +export const nodeModulesPaths = path.resolve('./node_modules'); + +export const nodePaths = (process.env.NODE_PATH || '') + .split(process.platform === 'win32' ? ';' : ':') + .filter(Boolean) + .map(p => path.resolve('./', p)); + +// Load environment variables starts with STORYBOOK_ to the client side. +export function loadEnv(options = {}) { + const defaultNodeEnv = options.production ? 'production' : 'development'; + const env = { + NODE_ENV: JSON.stringify(process.env.NODE_ENV || defaultNodeEnv), + // This is to support CRA's public folder feature. + // In production we set this to dot(.) to allow the browser to access these assests + // even when deployed inside a subpath. (like in GitHub pages) + // In development this is just empty as we always serves from the root. + PUBLIC_URL: JSON.stringify(options.production ? '.' : ''), + }; + + Object.keys(process.env) + .filter(name => /^STORYBOOK_/.test(name)) + .forEach(name => { + env[name] = JSON.stringify(process.env[name]); + }); + + return { + 'process.env': env, + }; +} diff --git a/app/mithril/src/server/config/webpack.config.js b/app/mithril/src/server/config/webpack.config.js new file mode 100644 index 000000000000..056565dc92bc --- /dev/null +++ b/app/mithril/src/server/config/webpack.config.js @@ -0,0 +1,94 @@ +import path from 'path'; +import webpack from 'webpack'; +import Dotenv from 'dotenv-webpack'; +import InterpolateHtmlPlugin from 'react-dev-utils/InterpolateHtmlPlugin'; +import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'; +import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import { managerPath } from '@storybook/core/server'; + +import { includePaths, excludePaths, nodeModulesPaths, loadEnv, nodePaths } from './utils'; +import babelLoaderConfig from './babel'; +import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils'; +import { version } from '../../../package.json'; + +export default function(configDir) { + const config = { + mode: 'development', + devtool: 'cheap-module-source-map', + entry: { + manager: [require.resolve('./polyfills'), managerPath], + preview: [ + require.resolve('./polyfills'), + require.resolve('./globals'), + `${require.resolve('webpack-hot-middleware/client')}?reload=true`, + ], + }, + output: { + path: path.join(__dirname, 'dist'), + filename: 'static/[name].bundle.js', + publicPath: '/', + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + chunks: ['manager'], + data: { + managerHead: getManagerHeadHtml(configDir), + version, + }, + template: require.resolve('../index.html.ejs'), + }), + new HtmlWebpackPlugin({ + filename: 'iframe.html', + excludeChunks: ['manager'], + data: { + previewHead: getPreviewHeadHtml(configDir), + }, + template: require.resolve('../iframe.html.ejs'), + }), + new InterpolateHtmlPlugin(process.env), + new webpack.DefinePlugin(loadEnv()), + new webpack.HotModuleReplacementPlugin(), + new CaseSensitivePathsPlugin(), + new WatchMissingNodeModulesPlugin(nodeModulesPaths), + new webpack.ProgressPlugin(), + new Dotenv({ silent: true }), + ], + module: { + rules: [ + { + test: /\.jsx?$/, + loader: require.resolve('babel-loader'), + query: babelLoaderConfig, + include: includePaths, + exclude: excludePaths, + }, + { + test: /\.md$/, + use: [ + { + loader: require.resolve('html-loader'), + }, + { + loader: require.resolve('markdown-loader'), + }, + ], + }, + ], + }, + resolve: { + // Since we ship with json-loader always, it's better to move extensions to here + // from the default config. + extensions: ['.js', '.json', '.jsx'], + // Add support to NODE_PATH. With this we could avoid relative path imports. + // Based on this CRA feature: https://github.com/facebookincubator/create-react-app/issues/253 + modules: ['node_modules'].concat(nodePaths), + }, + performance: { + hints: false, + }, + }; + + return config; +} diff --git a/app/mithril/src/server/config/webpack.config.prod.js b/app/mithril/src/server/config/webpack.config.prod.js new file mode 100644 index 000000000000..cf4a52274623 --- /dev/null +++ b/app/mithril/src/server/config/webpack.config.prod.js @@ -0,0 +1,86 @@ +import webpack from 'webpack'; +import Dotenv from 'dotenv-webpack'; +import InterpolateHtmlPlugin from 'react-dev-utils/InterpolateHtmlPlugin'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import { managerPath } from '@storybook/core/server'; +import babelLoaderConfig from './babel.prod'; +import { includePaths, excludePaths, loadEnv, nodePaths } from './utils'; +import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils'; +import { version } from '../../../package.json'; + +export default function(configDir) { + const entries = { + preview: [require.resolve('./polyfills'), require.resolve('./globals')], + manager: [require.resolve('./polyfills'), managerPath], + }; + + const config = { + mode: 'production', + bail: true, + devtool: '#cheap-module-source-map', + entry: entries, + output: { + filename: 'static/[name].[chunkhash].bundle.js', + // Here we set the publicPath to ''. + // This allows us to deploy storybook into subpaths like GitHub pages. + // This works with css and image loaders too. + // This is working for storybook since, we don't use pushState urls and + // relative URLs works always. + publicPath: '', + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + chunks: ['manager'], + data: { + managerHead: getManagerHeadHtml(configDir), + version, + }, + template: require.resolve('../index.html.ejs'), + }), + new HtmlWebpackPlugin({ + filename: 'iframe.html', + excludeChunks: ['manager'], + data: { + previewHead: getPreviewHeadHtml(configDir), + }, + template: require.resolve('../iframe.html.ejs'), + }), + new InterpolateHtmlPlugin(process.env), + new webpack.DefinePlugin(loadEnv({ production: true })), + new Dotenv({ silent: true }), + ], + module: { + rules: [ + { + test: /\.jsx?$/, + loader: require.resolve('babel-loader'), + query: babelLoaderConfig, + include: includePaths, + exclude: excludePaths, + }, + { + test: /\.md$/, + use: [ + { + loader: require.resolve('html-loader'), + }, + { + loader: require.resolve('markdown-loader'), + }, + ], + }, + ], + }, + resolve: { + // Since we ship with json-loader always, it's better to move extensions to here + // from the default config. + extensions: ['.js', '.json', '.jsx'], + // Add support to NODE_PATH. With this we could avoid relative path imports. + // Based on this CRA feature: https://github.com/facebookincubator/create-react-app/issues/253 + modules: ['node_modules'].concat(nodePaths), + }, + }; + + return config; +} diff --git a/app/mithril/src/server/iframe.html.ejs b/app/mithril/src/server/iframe.html.ejs new file mode 100644 index 000000000000..8d0c1baa34fd --- /dev/null +++ b/app/mithril/src/server/iframe.html.ejs @@ -0,0 +1,15 @@ + + + + + + + + Storybook + <%= htmlWebpackPlugin.options.data.previewHead %> + + +
+
+ + diff --git a/app/mithril/src/server/index.html.ejs b/app/mithril/src/server/index.html.ejs new file mode 100644 index 000000000000..dfcb56218d36 --- /dev/null +++ b/app/mithril/src/server/index.html.ejs @@ -0,0 +1,44 @@ + + + + + + + + Storybook + + <%= htmlWebpackPlugin.options.data.managerHead %> + + + +
+ + diff --git a/app/mithril/src/server/index.js b/app/mithril/src/server/index.js new file mode 100755 index 000000000000..69df1fe59e54 --- /dev/null +++ b/app/mithril/src/server/index.js @@ -0,0 +1,12 @@ +import { buildDev } from '@storybook/core/server'; +import path from 'path'; +import packageJson from '../../package.json'; +import getBaseConfig from './config/webpack.config'; +import loadConfig from './config'; + +buildDev({ + packageJson, + getBaseConfig, + loadConfig, + defaultFavIcon: path.resolve(__dirname, 'public/favicon.ico'), +}); diff --git a/app/mithril/src/server/public/favicon.ico b/app/mithril/src/server/public/favicon.ico new file mode 100755 index 000000000000..e1cf7f1c59fd Binary files /dev/null and b/app/mithril/src/server/public/favicon.ico differ diff --git a/app/mithril/src/server/utils.js b/app/mithril/src/server/utils.js new file mode 100644 index 000000000000..913f4c3219ea --- /dev/null +++ b/app/mithril/src/server/utils.js @@ -0,0 +1,30 @@ +import path from 'path'; +import fs from 'fs'; +import deprecate from 'util-deprecate'; + +const fallbackHeadUsage = deprecate(() => {}, +'Usage of head.html has been deprecated. Please rename head.html to preview-head.html'); + +export function getPreviewHeadHtml(configDirPath) { + const headHtmlPath = path.resolve(configDirPath, 'preview-head.html'); + const fallbackHtmlPath = path.resolve(configDirPath, 'head.html'); + let headHtml = ''; + if (fs.existsSync(headHtmlPath)) { + headHtml = fs.readFileSync(headHtmlPath, 'utf8'); + } else if (fs.existsSync(fallbackHtmlPath)) { + headHtml = fs.readFileSync(fallbackHtmlPath, 'utf8'); + fallbackHeadUsage(); + } + + return headHtml; +} + +export function getManagerHeadHtml(configDirPath) { + const scriptPath = path.resolve(configDirPath, 'manager-head.html'); + let scriptHtml = ''; + if (fs.existsSync(scriptPath)) { + scriptHtml = fs.readFileSync(scriptPath, 'utf8'); + } + + return scriptHtml; +} diff --git a/app/mithril/src/server/utils.test.js b/app/mithril/src/server/utils.test.js new file mode 100644 index 000000000000..a8db3b65a210 --- /dev/null +++ b/app/mithril/src/server/utils.test.js @@ -0,0 +1,69 @@ +import { getPreviewHeadHtml, getManagerHeadHtml } from './utils'; + +// eslint-disable-next-line global-require +jest.mock('fs', () => require('../../../../__mocks__/fs')); +jest.mock('path', () => ({ + resolve: (a, p) => p, +})); + +const setup = ({ files }) => { + // eslint-disable-next-line no-underscore-dangle, global-require + require('fs').__setMockFiles(files); +}; + +const HEAD_HTML_CONTENTS = 'UNITTEST_HEAD_HTML_CONTENTS'; + +describe('getPreviewHeadHtml', () => { + it('returns an empty string without head.html present', () => { + setup({ + files: {}, + }); + + const result = getPreviewHeadHtml('first'); + expect(result).toEqual(''); + }); + + it('return contents of head.html when present', () => { + setup({ + files: { + 'head.html': HEAD_HTML_CONTENTS, + }, + }); + + const result = getPreviewHeadHtml('second'); + expect(result).toEqual(HEAD_HTML_CONTENTS); + }); + + it('returns contents of preview-head.html when present', () => { + setup({ + files: { + 'preview-head.html': HEAD_HTML_CONTENTS, + }, + }); + + const result = getPreviewHeadHtml('second'); + expect(result).toEqual(HEAD_HTML_CONTENTS); + }); +}); + +describe('getManagerHeadHtml', () => { + it('returns an empty string without manager-head.html present', () => { + setup({ + files: {}, + }); + + const result = getManagerHeadHtml('first'); + expect(result).toEqual(''); + }); + + it('returns contents of manager-head.html when present', () => { + setup({ + files: { + 'manager-head.html': HEAD_HTML_CONTENTS, + }, + }); + + const result = getManagerHeadHtml('second'); + expect(result).toEqual(HEAD_HTML_CONTENTS); + }); +}); diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js index a4eaeba5257f..ba05f36ff20f 100644 --- a/docs/gatsby-config.js +++ b/docs/gatsby-config.js @@ -12,6 +12,7 @@ module.exports = { '/basics/guide-react/', '/basics/guide-vue/', '/basics/guide-angular/', + '/basics/guide-mithril/', '/basics/writing-stories/', '/basics/exporting-storybook/', '/basics/faq/', diff --git a/docs/src/pages/basics/guide-mithril/index.md b/docs/src/pages/basics/guide-mithril/index.md new file mode 100644 index 000000000000..9f75c147564b --- /dev/null +++ b/docs/src/pages/basics/guide-mithril/index.md @@ -0,0 +1,108 @@ +--- +id: 'guide-mithril' +title: 'Storybook for Mithril' +--- + +You may have tried to use our quick start guide to setup your project for Storybook. If you want to set up Storybook manually, this is the guide for you. + +> This will also help you to understand how Storybook works. + +## Starter Guide Mithril + +Storybook has its own Webpack setup and a dev server. + +In this guide, we will set up Storybook for your Mithril project. + +## Table of contents + +- [Add @storybook/mithril](#add-storybookmithril) +- [Add mithril and babel-core](#add-mithril-and-babel-core) +- [Create the config file](#create-the-config-file) +- [Write your stories](#write-your-stories) +- [Run your Storybook](#run-your-storybook) + +## Add @storybook/mithril + +First of all, you need to add `@storybook/mithril` to your project. To do that, simply run: + +```sh +npm i --save-dev @storybook/mithril +``` + +## Add mithril and babel-core + +Make sure that you have `mithril` and `babel-core` in your dependencies as well because we list these as a peerDependency: + +```sh +npm i --save mithril +npm i --save-dev babel-core +``` + +Then add the following NPM script to your package json in order to start the storybook later in this guide: + +```json +{ + "scripts": { + "storybook": "start-storybook -p 9001 -c .storybook" + } +} +``` + +## Create the config file + +Storybook can be configured in several different ways. +Thatโ€™s why we need a config directory. We've added a `-c` option to the above NPM script mentioning `.storybook` as the config directory. + +For the basic Storybook configuration file, you don't need to do much, but simply tell Storybook where to find stories. + +To do that, simply create a file at `.storybook/config.js` with the following content: + +```js +import { configure } from '@storybook/mithril'; + +function loadStories() { + require('../stories/index.js'); + // You can require as many stories as you need. +} + +configure(loadStories, module); +``` + +That'll load stories in `../stories/index.js`. + +## Write your stories + +Now you can write some stories inside the `../stories/index.js` file, like this: + +```js +/** @jsx m */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import { action } from '@storybook/addon-actions'; +import Button from '../components/Button'; + +storiesOf('Button', module) + .add('with text', () => ({ + view: () => + })) + .add('with some emoji', () => ({ + view: () => + })); +``` + +Story is a single state of your component. In the above case, there are two stories for the native button component: + +1. with text +2. with some emoji + +## Run your Storybook + +Now everything is ready. Simply run your storybook with: + +```sh +npm run storybook +``` + +Now you can change components and write stories whenever you need to. diff --git a/docs/src/pages/basics/live-examples/index.md b/docs/src/pages/basics/live-examples/index.md index 18ec48ece6a5..425630295b10 100644 --- a/docs/src/pages/basics/live-examples/index.md +++ b/docs/src/pages/basics/live-examples/index.md @@ -10,6 +10,7 @@ title: 'Live Examples' - [Vue](https://storybooks-vue.netlify.com/) - [Angular](https://storybooks-angular.netlify.com/) - [Polymer](https://storybooks-polymer.netlify.com/) +- [Mithril](https://storybooks-mithril.netlify.com/) ### 3.4 - [React Official](https://release-3-4--storybooks-official.netlify.com) diff --git a/docs/src/pages/basics/quick-start-guide/index.md b/docs/src/pages/basics/quick-start-guide/index.md index 71ee3d4157ad..c46b998e5220 100644 --- a/docs/src/pages/basics/quick-start-guide/index.md +++ b/docs/src/pages/basics/quick-start-guide/index.md @@ -3,7 +3,7 @@ id: 'quick-start-guide' title: 'Quick Start Guide' --- -Storybook is very easy to use. You can use it with any kind of React or Vue or Angular project. +Storybook is very easy to use. You can use it with any kind of React or Vue or Angular or Mithril project. Follow these steps to get started with Storybook. ```sh @@ -23,4 +23,4 @@ Then you can access your storybook from the browser. * * * -To learn more about what `getstorybook` command does, have a look at our [Start Guide for React](/basics/guide-react/) or [Start Guide for Vue](/basics/guide-vue/) or [Start Guide for Angular](/basics/guide-angular/). +To learn more about what `getstorybook` command does, have a look at our [Start Guide for React](/basics/guide-react/) or [Start Guide for Vue](/basics/guide-vue/) or [Start Guide for Angular](/basics/guide-angular/) or [Start Guide for Mithril](/basics/guide-mithril/). diff --git a/docs/src/pages/basics/slow-start-guide/index.md b/docs/src/pages/basics/slow-start-guide/index.md index a49d890f60c0..ce7882948d81 100644 --- a/docs/src/pages/basics/slow-start-guide/index.md +++ b/docs/src/pages/basics/slow-start-guide/index.md @@ -8,3 +8,4 @@ Storybook supports multiple UI libraries, the manual setup for each is different - [Storybook for React](/basics/guide-react/) - [Storybook for Vue](/basics/guide-vue/) - [Storybook for Angular](/basics/guide-angular/) +- [Storybook for Mithril](/basics/guide-mithril/) diff --git a/examples/mithril-kitchen-sink/.babelrc b/examples/mithril-kitchen-sink/.babelrc new file mode 100644 index 000000000000..5e1d82404dd3 --- /dev/null +++ b/examples/mithril-kitchen-sink/.babelrc @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-react-jsx" + ] +} diff --git a/examples/mithril-kitchen-sink/.gitignore b/examples/mithril-kitchen-sink/.gitignore new file mode 100644 index 000000000000..6c96c5cff124 --- /dev/null +++ b/examples/mithril-kitchen-sink/.gitignore @@ -0,0 +1,15 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# testing +coverage + +# production +build + +# misc +.DS_Store +.env +npm-debug.log diff --git a/examples/mithril-kitchen-sink/.storybook/addons.js b/examples/mithril-kitchen-sink/.storybook/addons.js new file mode 100644 index 000000000000..ff96ea46a3bd --- /dev/null +++ b/examples/mithril-kitchen-sink/.storybook/addons.js @@ -0,0 +1,8 @@ +import '@storybook/addon-storysource/register'; +import '@storybook/addon-actions/register'; +import '@storybook/addon-links/register'; +import '@storybook/addon-notes/register'; +import '@storybook/addon-knobs/register'; +import '@storybook/addon-viewport/register'; +import '@storybook/addon-options/register'; +import '@storybook/addon-backgrounds/register'; diff --git a/examples/mithril-kitchen-sink/.storybook/config.js b/examples/mithril-kitchen-sink/.storybook/config.js new file mode 100644 index 000000000000..aff56383c053 --- /dev/null +++ b/examples/mithril-kitchen-sink/.storybook/config.js @@ -0,0 +1,13 @@ +import { configure } from '@storybook/mithril'; +import { setOptions } from '@storybook/addon-options'; + +setOptions({ + hierarchyRootSeparator: /\|/, +}); + +function loadStories() { + const req = require.context('../src/stories', true, /\.stories\.js$/); + req.keys().forEach(filename => req(filename)); +} + +configure(loadStories, module); diff --git a/examples/mithril-kitchen-sink/.storybook/webpack.config.js b/examples/mithril-kitchen-sink/.storybook/webpack.config.js new file mode 100644 index 000000000000..692ad69caccc --- /dev/null +++ b/examples/mithril-kitchen-sink/.storybook/webpack.config.js @@ -0,0 +1,12 @@ +const path = require('path'); + +module.exports = (storybookBaseConfig, configType, defaultConfig) => { + defaultConfig.module.rules.push({ + test: [/\.stories\.js$/], + loaders: [require.resolve('@storybook/addon-storysource/loader')], + include: [path.resolve(__dirname, '../src')], + enforce: 'pre', + }); + + return defaultConfig; +}; diff --git a/examples/mithril-kitchen-sink/README.md b/examples/mithril-kitchen-sink/README.md new file mode 100644 index 000000000000..765012ecfa46 --- /dev/null +++ b/examples/mithril-kitchen-sink/README.md @@ -0,0 +1,3 @@ +# Storybook Demo + +This is a demo app to test Mithril integration with Storybook. Run `npm install` or `yarn install` to sync Storybook module with the source code and run `npm run storybook` or `yarn storybook` to start the Storybook. diff --git a/examples/mithril-kitchen-sink/package.json b/examples/mithril-kitchen-sink/package.json new file mode 100644 index 000000000000..21ff6e0650be --- /dev/null +++ b/examples/mithril-kitchen-sink/package.json @@ -0,0 +1,29 @@ +{ + "name": "mithril-example", + "version": "3.4.0-rc.3", + "private": true, + "scripts": { + "build-storybook": "build-storybook", + "storybook": "start-storybook -p 9007" + }, + "dependencies": { + "mithril": "^1.1.6" + }, + "devDependencies": { + "@storybook/addon-actions": "3.4.0-rc.3", + "@storybook/addon-backgrounds": "3.4.0-rc.3", + "@storybook/addon-centered": "3.4.0-rc.3", + "@storybook/addon-knobs": "3.4.0-rc.3", + "@storybook/addon-links": "3.4.0-rc.3", + "@storybook/addon-notes": "3.4.0-rc.3", + "@storybook/addon-options": "3.4.0-rc.3", + "@storybook/addon-storyshots": "3.4.0-rc.3", + "@storybook/addon-storysource": "3.4.0-rc.3", + "@storybook/addon-viewport": "3.4.0-rc.3", + "@storybook/addons": "3.4.0-rc.3", + "@storybook/mithril": "3.4.0-rc.3", + "babel-core": "^6.26.0", + "babel-plugin-transform-react-jsx": "^6.24.1", + "webpack": "^4.3.0" + } +} diff --git a/examples/mithril-kitchen-sink/src/BaseButton.js b/examples/mithril-kitchen-sink/src/BaseButton.js new file mode 100644 index 000000000000..ea451b9569d0 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/BaseButton.js @@ -0,0 +1,12 @@ +import m from 'mithril'; + +const BaseButton = { + view: ({ attrs }) => + m( + 'button', + { disabled: attrs.disabled, onclick: attrs.onclick, style: attrs.style }, + attrs.label + ), +}; + +export default BaseButton; diff --git a/examples/mithril-kitchen-sink/src/Button.js b/examples/mithril-kitchen-sink/src/Button.js new file mode 100644 index 000000000000..8fcb462dd537 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/Button.js @@ -0,0 +1,23 @@ +/** @jsx m */ + +import m from 'mithril'; + +const style = { + border: '1px solid #eee', + borderRadius: '3px', + backgroundColor: '#FFFFFF', + cursor: 'pointer', + fontSize: '15px', + padding: '3px 10px', + margin: '10px', +}; + +const Button = { + view: vnode => ( + + ), +}; + +export default Button; diff --git a/examples/mithril-kitchen-sink/src/Welcome.js b/examples/mithril-kitchen-sink/src/Welcome.js new file mode 100644 index 000000000000..c24a34d9c152 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/Welcome.js @@ -0,0 +1,137 @@ +/** @jsx m */ + +import m from 'mithril'; + +const Main = { + view: vnode => ( +
+ {vnode.children} +
+ ), +}; + +const Title = { + view: vnode =>

{vnode.children}

, +}; + +const Note = { + view: vnode => ( +

+ {vnode.children} +

+ ), +}; + +const InlineCode = { + view: vnode => ( + + {vnode.children} + + ), +}; + +const Link = { + view: vnode => ( + + {vnode.children} + + ), +}; + +const NavButton = { + view: vnode => ( + + ), +}; + +const Welcome = { + view: vnode => ( +
+ 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 stories located at{' '} + src/stories/index.js.) +

+

+ Usually we create stories with smaller UI components in the app.
+ Have a look at the{' '} + + Writing Stories + {' '} + section in our documentation. +

+ + NOTE: +
+ Have a look at the .storybook/webpack.config.js to add webpack + loaders and plugins you are using in this project. +
+
+ ), +}; + +export default Welcome; diff --git a/examples/mithril-kitchen-sink/src/stories/addon-actions.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-actions.stories.js new file mode 100644 index 000000000000..6ec373132e4d --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/addon-actions.stories.js @@ -0,0 +1,24 @@ +/** @jsx m */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import { action } from '@storybook/addon-actions'; +import Button from '../Button'; + +storiesOf('Addons|Actions', module) + .add('Action only', () => ({ + view: () => , + })) + .add('Action and method', () => ({ + view: () => ( + + ), + })); diff --git a/examples/mithril-kitchen-sink/src/stories/addon-backgrounds.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-backgrounds.stories.js new file mode 100644 index 000000000000..373a25de5bb6 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/addon-backgrounds.stories.js @@ -0,0 +1,22 @@ +/** @jsx m */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; + +import backgrounds from '@storybook/addon-backgrounds/mithril'; +import BaseButton from '../BaseButton'; + +storiesOf('Addons|Backgrounds', module) + .addDecorator( + backgrounds([ + { name: 'twitter', value: '#00aced' }, + { name: 'facebook', value: '#3b5998', default: true }, + ]) + ) + .add('story 1', () => ({ + view: () => , + })) + .add('story 2', () => ({ + view: () => , + })); diff --git a/examples/mithril-kitchen-sink/src/stories/addon-centered.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-centered.stories.js new file mode 100644 index 000000000000..e97deabdef51 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/addon-centered.stories.js @@ -0,0 +1,13 @@ +/** @jsx m */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import Centered from '@storybook/addon-centered/mithril'; +import Button from '../Button'; + +storiesOf('Addons|Centered', module) + .addDecorator(Centered) + .add('button', () => ({ + view: () => , + })); diff --git a/examples/mithril-kitchen-sink/src/stories/addon-knobs.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-knobs.stories.js new file mode 100644 index 000000000000..c4246eee5005 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/addon-knobs.stories.js @@ -0,0 +1,71 @@ +/** @jsx m */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import { action } from '@storybook/addon-actions'; +import { + withKnobs, + text, + number, + boolean, + array, + select, + color, + date, + button, +} from '@storybook/addon-knobs/mithril'; + +storiesOf('Addons|Knobs', module) + .addDecorator(withKnobs) + .add('Simple', () => { + const name = text('Name', 'John Doe'); + const age = number('Age', 44); + const content = `I am ${name} and I'm ${age} years old.`; + + return { + view: () =>
{content}
, + }; + }) + .add('All knobs', () => { + const name = text('Name', 'Jane'); + const stock = number('Stock', 20, { + range: true, + min: 0, + max: 30, + step: 5, + }); + const fruits = { + apples: 'Apple', + bananas: 'Banana', + cherries: 'Cherry', + }; + const fruit = select('Fruit', fruits, 'apple'); + const price = number('Price', 2.25); + + const colour = color('Border', 'deeppink'); + const today = date('Today', new Date('Jan 20 2017 GMT+0')); + const items = array('Items', ['Laptop', 'Book', 'Whiskey']); + const nice = boolean('Nice', true); + + const stockMessage = stock + ? `I have a stock of ${stock} ${fruit}, costing $${price} each.` + : `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`; + const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!'; + const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }; + + button('Arbitrary action', action('You clicked it!')); + + return { + view: () => ( +
+

My name is {name},

+

today is {new Date(today).toLocaleDateString('en-US', dateOptions)}

+

{stockMessage}

+

Also, I have:

+
    {items.map(item => `
  • ${item}
  • `).join('')}
+

{salutation}

+
+ ), + }; + }); diff --git a/examples/mithril-kitchen-sink/src/stories/addon-links.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-links.stories.js new file mode 100644 index 000000000000..51e786293367 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/addon-links.stories.js @@ -0,0 +1,11 @@ +/** @jsx m */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import { linkTo } from '@storybook/addon-links'; +import Button from '../Button'; + +storiesOf('Addons|Links', module).add('Go to welcome', () => ({ + view: () => , +})); diff --git a/examples/mithril-kitchen-sink/src/stories/addon-notes.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-notes.stories.js new file mode 100644 index 000000000000..ba1edfa822f1 --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/addon-notes.stories.js @@ -0,0 +1,48 @@ +/** @jsx m */ +/* eslint-disable jsx-a11y/accessible-emoji */ + +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import { withNotes } from '@storybook/addon-notes'; + +storiesOf('Addons|Notes', module) + .add( + 'Simple note', + withNotes({ text: 'My notes on some bold text' })(() => ({ + view: () => ( +

+ + Etiam vulputate elit eu venenatis eleifend. Duis nec lectus augue. Morbi egestas diam + sed vulputate mollis. Fusce egestas pretium vehicula. Integer sed neque diam. Donec + consectetur velit vitae enim varius, ut placerat arcu imperdiet. Praesent sed faucibus + arcu. Nullam sit amet nibh a enim eleifend rhoncus. Donec pretium elementum leo at + fermentum. Nulla sollicitudin, mauris quis semper tempus, sem metus tristique diam, + efficitur pulvinar mi urna id urna. + +

+ ), + })) + ) + .add( + 'Note with HTML', + withNotes({ + text: ` +

My notes on emojies

+ + It's not all that important to be honest, but.. + + Emojis are great, I love emojis, in fact I like using them in my Component notes too! ๐Ÿ˜‡ + `, + })(() => ({ + view: () => ( +

+ ๐Ÿค”๐Ÿ˜ณ๐Ÿ˜ฏ๐Ÿ˜ฎ +
+ ๐Ÿ˜„๐Ÿ˜ฉ๐Ÿ˜“๐Ÿ˜ฑ +
+ ๐Ÿค“๐Ÿ˜‘๐Ÿ˜ถ๐Ÿ˜Š +

+ ), + })) + ); diff --git a/examples/mithril-kitchen-sink/src/stories/index.stories.js b/examples/mithril-kitchen-sink/src/stories/index.stories.js new file mode 100644 index 000000000000..a0952e562bfd --- /dev/null +++ b/examples/mithril-kitchen-sink/src/stories/index.stories.js @@ -0,0 +1,25 @@ +import m from 'mithril'; + +import { storiesOf } from '@storybook/mithril'; +import { action } from '@storybook/addon-actions'; +import { linkTo } from '@storybook/addon-links'; + +import Button from '../Button'; +import Welcome from '../Welcome'; + +storiesOf('Welcome', module).add('to Storybook', () => ({ + view: () => m(Welcome, { showApp: linkTo('Button') }), +})); + +storiesOf('Button', module) + .add('with text', () => ({ + view: () => m(Button, { onclick: action('clicked') }, 'Hello Button'), + })) + .add('with some emoji', () => ({ + view: () => + m( + Button, + { onclick: action('clicked') }, + m('span', { role: 'img', ariaLabel: 'so cool' }, '๐Ÿ˜€ ๐Ÿ˜Ž ๐Ÿ‘ ๐Ÿ’ฏ') + ), + })); diff --git a/examples/official-storybook/built-storybooks/mithril-kitchen-sink b/examples/official-storybook/built-storybooks/mithril-kitchen-sink new file mode 120000 index 000000000000..c027b899fc0d --- /dev/null +++ b/examples/official-storybook/built-storybooks/mithril-kitchen-sink @@ -0,0 +1 @@ +../../mithril-kitchen-sink/storybook-static \ No newline at end of file diff --git a/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot b/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot index 6ebeacf6f19b..c3321d85cc11 100644 --- a/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot +++ b/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot @@ -16,6 +16,14 @@ exports[`Storyshots App|acceptance cra-kitchen-sink 1`] = ` /> `; +exports[`Storyshots App|acceptance mithril-kitchen-sink 1`] = ` +