From 9eca8337079d2ef966b559fd3a7236aa9557aa21 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 6 Nov 2017 00:52:36 +0900 Subject: [PATCH 1/7] Refactor StoryStore into @storybook/core --- app/angular/package.json | 1 + app/angular/src/client/preview/index.js | 2 +- app/angular/src/client/preview/story_store.js | 89 ----------------- app/react-native/package.json | 1 + app/react-native/src/preview/index.js | 2 +- app/react-native/src/preview/story_store.js | 98 ------------------ app/react/package.json | 1 + app/react/src/client/preview/index.js | 2 +- app/vue/package.json | 1 + app/vue/src/client/preview/index.js | 2 +- app/vue/src/client/preview/story_store.js | 99 ------------------- lib/core/README.md | 13 +++ lib/core/client.js | 2 + lib/core/package.json | 22 +++++ lib/core/src/client/index.js | 3 + lib/core/src/client/preview/index.js | 3 + .../core}/src/client/preview/story_store.js | 0 17 files changed, 51 insertions(+), 290 deletions(-) delete mode 100644 app/angular/src/client/preview/story_store.js delete mode 100644 app/react-native/src/preview/story_store.js delete mode 100644 app/vue/src/client/preview/story_store.js create mode 100644 lib/core/README.md create mode 100644 lib/core/client.js create mode 100644 lib/core/package.json create mode 100644 lib/core/src/client/index.js create mode 100644 lib/core/src/client/preview/index.js rename {app/react => lib/core}/src/client/preview/story_store.js (100%) diff --git a/app/angular/package.json b/app/angular/package.json index b97c12815ea9..4fe2e5218c45 100644 --- a/app/angular/package.json +++ b/app/angular/package.json @@ -30,6 +30,7 @@ "@storybook/addon-actions": "^3.3.0-alpha.2", "@storybook/addon-links": "^3.3.0-alpha.2", "@storybook/addons": "^3.3.0-alpha.2", + "@storybook/core": "^3.3.0-alpha.2", "@storybook/channel-postmessage": "^3.3.0-alpha.2", "@storybook/ui": "^3.3.0-alpha.2", "airbnb-js-shims": "^1.1.1", diff --git a/app/angular/src/client/preview/index.js b/app/angular/src/client/preview/index.js index 085bd1824aae..75088b24be6c 100644 --- a/app/angular/src/client/preview/index.js +++ b/app/angular/src/client/preview/index.js @@ -3,7 +3,7 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; -import StoryStore from './story_store'; +import { StoryStore } from '@storybook/core/client'; import ClientApi from './client_api'; import ConfigApi from './config_api'; import render from './render'; diff --git a/app/angular/src/client/preview/story_store.js b/app/angular/src/client/preview/story_store.js deleted file mode 100644 index 99ebbc02f740..000000000000 --- a/app/angular/src/client/preview/story_store.js +++ /dev/null @@ -1,89 +0,0 @@ -/* eslint-disable no-underscore-dangle */ - -let count = 0; - -function getId() { - count += 1; - return count; -} - -export default class StoryStore { - constructor() { - this._data = {}; - } - - addStory(kind, name, fn) { - if (!this._data[kind]) { - this._data[kind] = { - kind, - index: getId(), - stories: {}, - }; - } - - this._data[kind].stories[name] = { - name, - index: getId(), - fn, - }; - } - - getStoryKinds() { - return Object.keys(this._data) - .map(key => this._data[key]) - .filter(kind => Object.keys(kind.stories).length > 0) - .sort((info1, info2) => info1.index - info2.index) - .map(info => info.kind); - } - - getStories(kind) { - if (!this._data[kind]) { - return []; - } - - return Object.keys(this._data[kind].stories) - .map(name => this._data[kind].stories[name]) - .sort((info1, info2) => info1.index - info2.index) - .map(info => info.name); - } - - getStory(kind, name) { - const storiesKind = this._data[kind]; - if (!storiesKind) { - return null; - } - - const storyInfo = storiesKind.stories[name]; - if (!storyInfo) { - return null; - } - - return storyInfo.fn; - } - - removeStoryKind(kind) { - this._data[kind].stories = {}; - } - - hasStoryKind(kind) { - return Boolean(this._data[kind]); - } - - hasStory(kind, name) { - return Boolean(this.getStory(kind, name)); - } - - dumpStoryBook() { - const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) })); - - return data; - } - - size() { - return Object.keys(this._data).length; - } - - clean() { - this.getStoryKinds().forEach(kind => delete this._data[kind]); - } -} diff --git a/app/react-native/package.json b/app/react-native/package.json index cdeb682288d2..03281848555e 100644 --- a/app/react-native/package.json +++ b/app/react-native/package.json @@ -28,6 +28,7 @@ "@storybook/addon-links": "^3.3.0-alpha.2", "@storybook/addons": "^3.3.0-alpha.2", "@storybook/channel-websocket": "^3.3.0-alpha.2", + "@storybook/core": "^3.3.0-alpha.2", "@storybook/ui": "^3.3.0-alpha.2", "autoprefixer": "^7.1.6", "babel-core": "^6.26.0", diff --git a/app/react-native/src/preview/index.js b/app/react-native/src/preview/index.js index 2529253e331c..40445fa897b5 100644 --- a/app/react-native/src/preview/index.js +++ b/app/react-native/src/preview/index.js @@ -6,7 +6,7 @@ import parse from 'url-parse'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-websocket'; import { EventEmitter } from 'events'; -import StoryStore from './story_store'; +import { StoryStore } from '@storybook/core/client'; import StoryKindApi from './story_kind'; import OnDeviceUI from './components/OnDeviceUI'; import StoryView from './components/StoryView'; diff --git a/app/react-native/src/preview/story_store.js b/app/react-native/src/preview/story_store.js deleted file mode 100644 index f202ab8c6b2b..000000000000 --- a/app/react-native/src/preview/story_store.js +++ /dev/null @@ -1,98 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ -import { EventEmitter } from 'events'; - -let count = 0; - -export default class StoryStore extends EventEmitter { - constructor() { - super(); - this._data = {}; - } - - addStory(kind, name, fn, fileName) { - count += 1; - if (!this._data[kind]) { - this._data[kind] = { - kind, - fileName, - index: count, - stories: {}, - }; - } - - this._data[kind].stories[name] = { - name, - index: count, - fn, - }; - - this.emit('storyAdded', kind, name, fn); - } - - getStoryKinds() { - return Object.keys(this._data) - .map(key => this._data[key]) - .sort((info1, info2) => info1.index - info2.index) - .map(info => info.kind); - } - - getStories(kind) { - if (!this._data[kind]) { - return []; - } - - return Object.keys(this._data[kind].stories) - .map(name => this._data[kind].stories[name]) - .sort((info1, info2) => info1.index - info2.index) - .map(info => info.name); - } - - getStoryFileName(kind) { - const storiesKind = this._data[kind]; - if (!storiesKind) { - return null; - } - - return storiesKind.fileName; - } - - getStory(kind, name) { - const storiesKind = this._data[kind]; - if (!storiesKind) { - return null; - } - - const storyInfo = storiesKind.stories[name]; - if (!storyInfo) { - return null; - } - - return storyInfo.fn; - } - - removeStoryKind(kind) { - delete this._data[kind]; - } - - hasStoryKind(kind) { - return Boolean(this._data[kind]); - } - - hasStory(kind, name) { - return Boolean(this.getStory(kind, name)); - } - - dumpStoryBook() { - const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) })); - - return data; - } - - size() { - return Object.keys(this._data).length; - } - - clean() { - this.getStoryKinds().forEach(kind => delete this._data[kind]); - } -} diff --git a/app/react/package.json b/app/react/package.json index b60fee00853b..dda6a19f6985 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -26,6 +26,7 @@ "@storybook/addon-links": "^3.3.0-alpha.2", "@storybook/addons": "^3.3.0-alpha.2", "@storybook/channel-postmessage": "^3.3.0-alpha.2", + "@storybook/core": "^3.3.0-alpha.2", "@storybook/ui": "^3.3.0-alpha.2", "airbnb-js-shims": "^1.3.0", "autoprefixer": "^7.1.6", diff --git a/app/react/src/client/preview/index.js b/app/react/src/client/preview/index.js index 60508441eeee..e76364259832 100644 --- a/app/react/src/client/preview/index.js +++ b/app/react/src/client/preview/index.js @@ -4,7 +4,7 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; -import StoryStore from './story_store'; +import { StoryStore } from '@storybook/core/client'; import ClientApi from './client_api'; import ConfigApi from './config_api'; import render from './render'; diff --git a/app/vue/package.json b/app/vue/package.json index dad0b8ce625a..a4ca2adfc76f 100644 --- a/app/vue/package.json +++ b/app/vue/package.json @@ -26,6 +26,7 @@ "@storybook/addon-links": "^3.3.0-alpha.2", "@storybook/addons": "^3.3.0-alpha.2", "@storybook/channel-postmessage": "^3.3.0-alpha.2", + "@storybook/core": "^3.3.0-alpha.2", "@storybook/ui": "^3.3.0-alpha.2", "airbnb-js-shims": "^1.3.0", "autoprefixer": "^7.1.6", diff --git a/app/vue/src/client/preview/index.js b/app/vue/src/client/preview/index.js index 60508441eeee..e76364259832 100644 --- a/app/vue/src/client/preview/index.js +++ b/app/vue/src/client/preview/index.js @@ -4,7 +4,7 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; -import StoryStore from './story_store'; +import { StoryStore } from '@storybook/core/client'; import ClientApi from './client_api'; import ConfigApi from './config_api'; import render from './render'; diff --git a/app/vue/src/client/preview/story_store.js b/app/vue/src/client/preview/story_store.js deleted file mode 100644 index a82bba34d24d..000000000000 --- a/app/vue/src/client/preview/story_store.js +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ - -let count = 0; - -function getId() { - count += 1; - return count; -} - -export default class StoryStore { - constructor() { - this._data = {}; - } - - addStory(kind, name, fn, fileName) { - if (!this._data[kind]) { - this._data[kind] = { - kind, - fileName, - index: getId(), - stories: {}, - }; - } - - this._data[kind].stories[name] = { - name, - index: getId(), - fn, - }; - } - - getStoryKinds() { - return Object.keys(this._data) - .map(key => this._data[key]) - .filter(kind => Object.keys(kind.stories).length > 0) - .sort((info1, info2) => info1.index - info2.index) - .map(info => info.kind); - } - - getStories(kind) { - if (!this._data[kind]) { - return []; - } - - return Object.keys(this._data[kind].stories) - .map(name => this._data[kind].stories[name]) - .sort((info1, info2) => info1.index - info2.index) - .map(info => info.name); - } - - getStoryFileName(kind) { - const storiesKind = this._data[kind]; - if (!storiesKind) { - return null; - } - - return storiesKind.fileName; - } - - getStory(kind, name) { - const storiesKind = this._data[kind]; - if (!storiesKind) { - return null; - } - - const storyInfo = storiesKind.stories[name]; - if (!storyInfo) { - return null; - } - - return storyInfo.fn; - } - - removeStoryKind(kind) { - this._data[kind].stories = {}; - } - - hasStoryKind(kind) { - return Boolean(this._data[kind]); - } - - hasStory(kind, name) { - return Boolean(this.getStory(kind, name)); - } - - dumpStoryBook() { - const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) })); - - return data; - } - - size() { - return Object.keys(this._data).length; - } - - clean() { - this.getStoryKinds().forEach(kind => delete this._data[kind]); - } -} diff --git a/lib/core/README.md b/lib/core/README.md new file mode 100644 index 000000000000..5ae08249a8ee --- /dev/null +++ b/lib/core/README.md @@ -0,0 +1,13 @@ +# Storybook for React + +[![Greenkeeper badge](https://badges.greenkeeper.io/storybooks/storybook.svg)](https://greenkeeper.io/) +[![Build Status](https://travis-ci.org/storybooks/storybook.svg?branch=master)](https://travis-ci.org/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://storybooks-slackin.herokuapp.com/badge.svg)](https://storybooks-slackin.herokuapp.com/) + +This package contains common data structures used among the different frameworks +(React, RN, Vue, Angular, etc). + +FIXME diff --git a/lib/core/client.js b/lib/core/client.js new file mode 100644 index 000000000000..6de6d9128855 --- /dev/null +++ b/lib/core/client.js @@ -0,0 +1,2 @@ +/* eslint-disable global-require */ +module.exports = require('./dist/client').default; diff --git a/lib/core/package.json b/lib/core/package.json new file mode 100644 index 000000000000..70f98d5a6a21 --- /dev/null +++ b/lib/core/package.json @@ -0,0 +1,22 @@ +{ + "name": "@storybook/core", + "version": "3.3.0-alpha.2", + "description": "Storybook framework-agnostic API", + "homepage": "https://github.com/storybooks/storybook/tree/master/lib/core", + "bugs": { + "url": "https://github.com/storybooks/storybook/issues" + }, + "license": "MIT", + "main": "dist/client/index.js", + "repository": { + "type": "git", + "url": "https://github.com/storybooks/storybook.git" + }, + "scripts": { + "dev": "DEV_BUILD=1 nodemon --watch ./src --exec 'yarn prepare'", + "prepare": "node ../../scripts/prepare.js" + }, + "devDependencies": { + "babel-cli": "^6.26.0" + } +} diff --git a/lib/core/src/client/index.js b/lib/core/src/client/index.js new file mode 100644 index 000000000000..7cec7d05ee95 --- /dev/null +++ b/lib/core/src/client/index.js @@ -0,0 +1,3 @@ +import preview from './preview'; + +export default preview; diff --git a/lib/core/src/client/preview/index.js b/lib/core/src/client/preview/index.js new file mode 100644 index 000000000000..8892fdd3d07e --- /dev/null +++ b/lib/core/src/client/preview/index.js @@ -0,0 +1,3 @@ +import StoryStore from './story_store'; + +export default { StoryStore }; diff --git a/app/react/src/client/preview/story_store.js b/lib/core/src/client/preview/story_store.js similarity index 100% rename from app/react/src/client/preview/story_store.js rename to lib/core/src/client/preview/story_store.js From 4573883f70e5acdbfaf8c82894cba6c5c5d7425b Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 6 Nov 2017 19:57:39 +0900 Subject: [PATCH 2/7] Refactor client / config / redux API into core Questionable changes: - ability to pass a story decorator function - pass clearDecorators to ConfigAPI React/Vue/Angular working (apparently). RN still broken. --- app/angular/src/client/preview/client_api.js | 94 ------ .../src/client/preview/client_api.test.js | 250 --------------- app/angular/src/client/preview/config_api.js | 67 ---- app/angular/src/client/preview/index.js | 13 +- app/angular/src/client/preview/init.js | 4 +- app/react/src/client/preview/actions.js | 34 -- app/react/src/client/preview/config_api.js | 67 ---- app/react/src/client/preview/index.js | 15 +- app/react/src/client/preview/init.js | 4 +- app/react/src/client/preview/reducer.js | 40 --- app/vue/src/client/preview/actions.js | 34 -- app/vue/src/client/preview/client_api.js | 119 ------- app/vue/src/client/preview/client_api.test.js | 295 ------------------ app/vue/src/client/preview/index.js | 33 +- app/vue/src/client/preview/init.js | 4 +- app/vue/src/client/preview/reducer.js | 40 --- examples/crna-kitchen-sink/package.json | 1 + examples/react-native-vanilla/package.json | 1 + lib/components/src/highlight_button.js | 2 +- lib/core/package.json | 3 + .../core}/src/client/preview/actions.js | 0 .../core}/src/client/preview/client_api.js | 21 +- .../src/client/preview/client_api.test.js | 0 .../core}/src/client/preview/config_api.js | 6 +- lib/core/src/client/preview/index.js | 6 +- .../core}/src/client/preview/reducer.js | 0 26 files changed, 68 insertions(+), 1085 deletions(-) delete mode 100644 app/angular/src/client/preview/client_api.js delete mode 100644 app/angular/src/client/preview/client_api.test.js delete mode 100644 app/angular/src/client/preview/config_api.js delete mode 100644 app/react/src/client/preview/actions.js delete mode 100644 app/react/src/client/preview/config_api.js delete mode 100644 app/react/src/client/preview/reducer.js delete mode 100644 app/vue/src/client/preview/actions.js delete mode 100644 app/vue/src/client/preview/client_api.js delete mode 100644 app/vue/src/client/preview/client_api.test.js delete mode 100644 app/vue/src/client/preview/reducer.js rename {app/angular => lib/core}/src/client/preview/actions.js (100%) rename {app/react => lib/core}/src/client/preview/client_api.js (85%) rename {app/react => lib/core}/src/client/preview/client_api.test.js (100%) rename {app/vue => lib/core}/src/client/preview/config_api.js (91%) rename {app/angular => lib/core}/src/client/preview/reducer.js (100%) diff --git a/app/angular/src/client/preview/client_api.js b/app/angular/src/client/preview/client_api.js deleted file mode 100644 index 0a17a4d48c5d..000000000000 --- a/app/angular/src/client/preview/client_api.js +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable no-underscore-dangle */ - -export default class ClientApi { - constructor({ channel, storyStore }) { - // channel can be null when running in node - // always check whether channel is available - this._channel = channel; - this._storyStore = storyStore; - this._addons = {}; - this._globalDecorators = []; - } - - setAddon(addon) { - this._addons = { - ...this._addons, - ...addon, - }; - } - - addDecorator(decorator) { - this._globalDecorators.push(decorator); - } - - clearDecorators() { - this._globalDecorators = []; - } - - storiesOf(kind, m) { - if (!kind && typeof kind !== 'string') { - throw new Error('Invalid or missing kind provided for stories, should be a string'); - } - - if (m && m.hot) { - m.hot.dispose(() => { - this._storyStore.removeStoryKind(kind); - }); - } - - const localDecorators = []; - const api = { - kind, - }; - - // apply addons - Object.keys(this._addons).forEach(name => { - const addon = this._addons[name]; - api[name] = (...args) => { - addon.apply(api, args); - return api; - }; - }); - - api.add = (storyName, getStory) => { - if (typeof storyName !== 'string') { - throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`); - } - - if (this._storyStore.hasStory(kind, storyName)) { - throw new Error(`Story of "${kind}" named "${storyName}" already exists`); - } - - // Wrap the getStory function with each decorator. The first - // decorator will wrap the story function. The second will - // wrap the first decorator and so on. - const decorators = [...localDecorators, ...this._globalDecorators]; - - const getDecoratedStory = decorators.reduce( - (decorated, decorator) => context => decorator(() => decorated(context), context), - getStory - ); - - // Add the fully decorated getStory function. - this._storyStore.addStory(kind, storyName, getDecoratedStory); - return api; - }; - - api.addDecorator = decorator => { - localDecorators.push(decorator); - return api; - }; - - return api; - } - - getStorybook() { - return this._storyStore.getStoryKinds().map(kind => { - const stories = this._storyStore.getStories(kind).map(name => { - const render = this._storyStore.getStory(kind, name); - return { name, render }; - }); - return { kind, stories }; - }); - } -} diff --git a/app/angular/src/client/preview/client_api.test.js b/app/angular/src/client/preview/client_api.test.js deleted file mode 100644 index cb67ce2e59ab..000000000000 --- a/app/angular/src/client/preview/client_api.test.js +++ /dev/null @@ -1,250 +0,0 @@ -/* eslint-disable no-underscore-dangle */ - -import ClientAPI from './client_api'; - -class StoryStore { - constructor() { - this.stories = []; - } - - addStory(kind, story, fn) { - this.stories.push({ kind, story, fn }); - } - - getStoryKinds() { - return this.stories.reduce((kinds, info) => { - if (kinds.indexOf(info.kind) === -1) { - kinds.push(info.kind); - } - return kinds; - }, []); - } - - getStories(kind) { - return this.stories.reduce((stories, info) => { - if (info.kind === kind) { - stories.push(info.story); - } - return stories; - }, []); - } - - getStory(kind, name) { - return this.stories.reduce((fn, info) => { - if (!fn && info.kind === kind && info.story === name) { - return info.fn; - } - return fn; - }, null); - } - - hasStory(kind, name) { - return Boolean(this.getStory(kind, name)); - } -} - -describe('preview.client_api', () => { - describe('setAddon', () => { - it('should register addons', () => { - const api = new ClientAPI({}); - let data; - - api.setAddon({ - aa() { - data = 'foo'; - }, - }); - - api.storiesOf('none').aa(); - expect(data).toBe('foo'); - }); - - it('should not remove previous addons', () => { - const api = new ClientAPI({}); - const data = []; - - api.setAddon({ - aa() { - data.push('foo'); - }, - }); - - api.setAddon({ - bb() { - data.push('bar'); - }, - }); - - api - .storiesOf('none') - .aa() - .bb(); - expect(data).toEqual(['foo', 'bar']); - }); - - it('should call with the api context', () => { - const api = new ClientAPI({}); - let data; - - api.setAddon({ - aa() { - data = typeof this.add; - }, - }); - - api.storiesOf('none').aa(); - expect(data).toBe('function'); - }); - - it('should be able to access addons added previously', () => { - const api = new ClientAPI({}); - let data; - - api.setAddon({ - aa() { - data = 'foo'; - }, - }); - - api.setAddon({ - bb() { - this.aa(); - }, - }); - - api.storiesOf('none').bb(); - expect(data).toBe('foo'); - }); - - it('should be able to access the current kind', () => { - const api = new ClientAPI({}); - const kind = 'dfdwf3e3'; - let data; - - api.setAddon({ - aa() { - data = this.kind; - }, - }); - - api.storiesOf(kind).aa(); - expect(data).toBe(kind); - }); - }); - - describe('addDecorator', () => { - it('should add local decorators', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none'); - localApi.addDecorator(fn => `aa-${fn()}`); - - localApi.add('storyName', () => 'Hello'); - expect(storyStore.stories[0].fn()).toBe('aa-Hello'); - }); - - it('should add global decorators', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - api.addDecorator(fn => `bb-${fn()}`); - const localApi = api.storiesOf('none'); - - localApi.add('storyName', () => 'Hello'); - expect(storyStore.stories[0].fn()).toBe('bb-Hello'); - }); - - it('should utilize both decorators at once', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none'); - - api.addDecorator(fn => `aa-${fn()}`); - localApi.addDecorator(fn => `bb-${fn()}`); - - localApi.add('storyName', () => 'Hello'); - expect(storyStore.stories[0].fn()).toBe('aa-bb-Hello'); - }); - - it('should pass the context', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none'); - localApi.addDecorator(fn => `aa-${fn()}`); - - localApi.add('storyName', ({ kind, story }) => `${kind}-${story}`); - - const kind = 'dfdfd'; - const story = 'ef349ff'; - - const result = storyStore.stories[0].fn({ kind, story }); - expect(result).toBe(`aa-${kind}-${story}`); - }); - - it('should have access to the context', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none'); - localApi.addDecorator((fn, { kind, story }) => `${kind}-${story}-${fn()}`); - - localApi.add('storyName', () => 'Hello'); - - const kind = 'dfdfd'; - const story = 'ef349ff'; - - const result = storyStore.stories[0].fn({ kind, story }); - expect(result).toBe(`${kind}-${story}-Hello`); - }); - }); - - describe('clearDecorators', () => { - it('should remove all global decorators', () => { - const api = new ClientAPI({}); - api._globalDecorators = 1234; - api.clearDecorators(); - expect(api._globalDecorators).toEqual([]); - }); - }); - - describe('getStorybook', () => { - it('should return storybook when empty', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const book = api.getStorybook(); - expect(book).toEqual([]); - }); - - it('should return storybook with stories', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const functions = { - 'story-1.1': () => 'story-1.1', - 'story-1.2': () => 'story-1.2', - 'story-2.1': () => 'story-2.1', - 'story-2.2': () => 'story-2.2', - }; - const kind1 = api.storiesOf('kind-1'); - kind1.add('story-1.1', functions['story-1.1']); - kind1.add('story-1.2', functions['story-1.2']); - const kind2 = api.storiesOf('kind-2'); - kind2.add('story-2.1', functions['story-2.1']); - kind2.add('story-2.2', functions['story-2.2']); - const book = api.getStorybook(); - expect(book).toEqual([ - { - kind: 'kind-1', - stories: [ - { name: 'story-1.1', render: functions['story-1.1'] }, - { name: 'story-1.2', render: functions['story-1.2'] }, - ], - }, - { - kind: 'kind-2', - stories: [ - { name: 'story-2.1', render: functions['story-2.1'] }, - { name: 'story-2.2', render: functions['story-2.2'] }, - ], - }, - ]); - }); - }); -}); diff --git a/app/angular/src/client/preview/config_api.js b/app/angular/src/client/preview/config_api.js deleted file mode 100644 index 8d2923e5bfce..000000000000 --- a/app/angular/src/client/preview/config_api.js +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable no-underscore-dangle */ - -import { location } from 'global'; -import { setInitialStory, setError, clearError } from './actions'; -import { clearDecorators } from './'; - -export default class ConfigApi { - constructor({ channel, storyStore, reduxStore }) { - // channel can be null when running in node - // always check whether channel is available - this._channel = channel; - this._storyStore = storyStore; - this._reduxStore = reduxStore; - } - - _renderMain(loaders) { - if (loaders) loaders(); - - const stories = this._storyStore.dumpStoryBook(); - // send to the parent frame. - this._channel.emit('setStories', { stories }); - - // clear the error if exists. - this._reduxStore.dispatch(clearError()); - this._reduxStore.dispatch(setInitialStory(stories)); - } - - _renderError(e) { - const { stack, message } = e; - const error = { stack, message }; - this._reduxStore.dispatch(setError(error)); - } - - configure(loaders, module) { - const render = () => { - try { - this._renderMain(loaders); - } catch (error) { - if (module.hot && module.hot.status() === 'apply') { - // We got this issue, after webpack fixed it and applying it. - // Therefore error message is displayed forever even it's being fixed. - // So, we'll detect it reload the page. - location.reload(); - } else { - // If we are accessing the site, but the error is not fixed yet. - // There we can render the error. - this._renderError(error); - } - } - }; - - if (module.hot) { - module.hot.accept(() => { - setTimeout(render); - }); - module.hot.dispose(() => { - clearDecorators(); - }); - } - - if (this._channel) { - render(); - } else { - loaders(); - } - } -} diff --git a/app/angular/src/client/preview/index.js b/app/angular/src/client/preview/index.js index 75088b24be6c..d2a68d15c8e4 100644 --- a/app/angular/src/client/preview/index.js +++ b/app/angular/src/client/preview/index.js @@ -3,13 +3,9 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; -import { StoryStore } from '@storybook/core/client'; -import ClientApi from './client_api'; -import ConfigApi from './config_api'; -import render from './render'; +import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; import init from './init'; -import { selectStory } from './actions'; -import reducer from './reducer'; +import render from './render'; // check whether we're running on node/browser const isBrowser = @@ -25,7 +21,7 @@ if (isBrowser) { const queryParams = qs.parse(window.location.search.substring(1)); const channel = createChannel({ page: 'preview' }); channel.on('setCurrentStory', data => { - reduxStore.dispatch(selectStory(data.kind, data.story)); + reduxStore.dispatch(Actions.selectStory(data.kind, data.story)); }); Object.assign(context, { channel, window, queryParams }); addons.setChannel(channel); @@ -33,7 +29,6 @@ if (isBrowser) { } const clientApi = new ClientApi(context); -const configApi = new ConfigApi(context); // do exports export const storiesOf = clientApi.storiesOf.bind(clientApi); @@ -41,6 +36,8 @@ export const setAddon = clientApi.setAddon.bind(clientApi); export const addDecorator = clientApi.addDecorator.bind(clientApi); export const clearDecorators = clientApi.clearDecorators.bind(clientApi); export const getStorybook = clientApi.getStorybook.bind(clientApi); + +const configApi = new ConfigApi({ clearDecorators, ...context }); export const configure = configApi.configure.bind(configApi); // initialize the UI diff --git a/app/angular/src/client/preview/init.js b/app/angular/src/client/preview/init.js index a8de2d28f168..138e0a7d27ac 100644 --- a/app/angular/src/client/preview/init.js +++ b/app/angular/src/client/preview/init.js @@ -1,11 +1,11 @@ import keyEvents from '@storybook/ui/dist/libs/key_events'; -import { selectStory } from './actions'; +import { Actions } from '@storybook/core/client'; export default function(context) { const { queryParams, reduxStore, window, channel } = context; // set the story if correct params are loaded via the URL. if (queryParams.selectedKind) { - reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory)); + reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); } // Handle keyEvents and pass them to the parent. diff --git a/app/react/src/client/preview/actions.js b/app/react/src/client/preview/actions.js deleted file mode 100644 index f15e8676a205..000000000000 --- a/app/react/src/client/preview/actions.js +++ /dev/null @@ -1,34 +0,0 @@ -export const types = { - SET_ERROR: 'PREVIEW_SET_ERROR', - CLEAR_ERROR: 'PREVIEW_CLEAR_ERROR', - SELECT_STORY: 'PREVIEW_SELECT_STORY', - SET_INITIAL_STORY: 'PREVIEW_SET_INITIAL_STORY', -}; - -export function setInitialStory(storyKindList) { - return { - type: types.SET_INITIAL_STORY, - storyKindList, - }; -} - -export function setError(error) { - return { - type: types.SET_ERROR, - error, - }; -} - -export function clearError() { - return { - type: types.CLEAR_ERROR, - }; -} - -export function selectStory(kind, story) { - return { - type: types.SELECT_STORY, - kind, - story, - }; -} diff --git a/app/react/src/client/preview/config_api.js b/app/react/src/client/preview/config_api.js deleted file mode 100644 index cc0bbea3af3e..000000000000 --- a/app/react/src/client/preview/config_api.js +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ - -import { location } from 'global'; -import { setInitialStory, setError, clearError } from './actions'; -import { clearDecorators } from './'; - -export default class ConfigApi { - constructor({ channel, storyStore, reduxStore }) { - // channel can be null when running in node - // always check whether channel is available - this._channel = channel; - this._storyStore = storyStore; - this._reduxStore = reduxStore; - } - - _renderMain(loaders) { - if (loaders) loaders(); - - const stories = this._storyStore.dumpStoryBook(); - // send to the parent frame. - this._channel.emit('setStories', { stories }); - - // clear the error if exists. - this._reduxStore.dispatch(clearError()); - this._reduxStore.dispatch(setInitialStory(stories)); - } - - _renderError(e) { - const { stack, message } = e; - const error = { stack, message }; - this._reduxStore.dispatch(setError(error)); - } - - configure(loaders, module) { - const render = () => { - try { - this._renderMain(loaders); - } catch (error) { - if (module.hot && module.hot.status() === 'apply') { - // We got this issue, after webpack fixed it and applying it. - // Therefore error message is displayed forever even it's being fixed. - // So, we'll detect it reload the page. - location.reload(); - } else { - // If we are accessing the site, but the error is not fixed yet. - // There we can render the error. - this._renderError(error); - } - } - }; - - if (module.hot) { - module.hot.accept(() => { - setTimeout(render); - }); - module.hot.dispose(() => { - clearDecorators(); - }); - } - - if (this._channel) { - render(); - } else { - loaders(); - } - } -} diff --git a/app/react/src/client/preview/index.js b/app/react/src/client/preview/index.js index e76364259832..50923195274d 100644 --- a/app/react/src/client/preview/index.js +++ b/app/react/src/client/preview/index.js @@ -4,13 +4,9 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; -import { StoryStore } from '@storybook/core/client'; -import ClientApi from './client_api'; -import ConfigApi from './config_api'; -import render from './render'; +import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; import init from './init'; -import { selectStory } from './actions'; -import reducer from './reducer'; +import render from './render'; // check whether we're running on node/browser const { navigator } = global; @@ -27,7 +23,7 @@ if (isBrowser) { const queryParams = qs.parse(window.location.search.substring(1)); const channel = createChannel({ page: 'preview' }); channel.on('setCurrentStory', data => { - reduxStore.dispatch(selectStory(data.kind, data.story)); + reduxStore.dispatch(Actions.selectStory(data.kind, data.story)); }); Object.assign(context, { channel, window, queryParams }); addons.setChannel(channel); @@ -35,14 +31,13 @@ if (isBrowser) { } const clientApi = new ClientApi(context); -const configApi = new ConfigApi(context); - -// do exports export const storiesOf = clientApi.storiesOf.bind(clientApi); export const setAddon = clientApi.setAddon.bind(clientApi); export const addDecorator = clientApi.addDecorator.bind(clientApi); export const clearDecorators = clientApi.clearDecorators.bind(clientApi); export const getStorybook = clientApi.getStorybook.bind(clientApi); + +const configApi = new ConfigApi({ clearDecorators, ...context }); export const configure = configApi.configure.bind(configApi); // initialize the UI diff --git a/app/react/src/client/preview/init.js b/app/react/src/client/preview/init.js index a8de2d28f168..138e0a7d27ac 100644 --- a/app/react/src/client/preview/init.js +++ b/app/react/src/client/preview/init.js @@ -1,11 +1,11 @@ import keyEvents from '@storybook/ui/dist/libs/key_events'; -import { selectStory } from './actions'; +import { Actions } from '@storybook/core/client'; export default function(context) { const { queryParams, reduxStore, window, channel } = context; // set the story if correct params are loaded via the URL. if (queryParams.selectedKind) { - reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory)); + reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); } // Handle keyEvents and pass them to the parent. diff --git a/app/react/src/client/preview/reducer.js b/app/react/src/client/preview/reducer.js deleted file mode 100644 index 015df6c83915..000000000000 --- a/app/react/src/client/preview/reducer.js +++ /dev/null @@ -1,40 +0,0 @@ -import { types } from './actions'; - -export default function reducer(state = {}, action) { - switch (action.type) { - case types.CLEAR_ERROR: { - return { - ...state, - error: null, - }; - } - - case types.SET_ERROR: { - return { - ...state, - error: action.error, - }; - } - - case types.SELECT_STORY: { - return { - ...state, - selectedKind: action.kind, - selectedStory: action.story, - }; - } - - case types.SET_INITIAL_STORY: { - const newState = { ...state }; - const { storyKindList } = action; - if (!newState.selectedKind && storyKindList.length > 0) { - newState.selectedKind = storyKindList[0].kind; - newState.selectedStory = storyKindList[0].stories[0]; - } - return newState; - } - - default: - return state; - } -} diff --git a/app/vue/src/client/preview/actions.js b/app/vue/src/client/preview/actions.js deleted file mode 100644 index f15e8676a205..000000000000 --- a/app/vue/src/client/preview/actions.js +++ /dev/null @@ -1,34 +0,0 @@ -export const types = { - SET_ERROR: 'PREVIEW_SET_ERROR', - CLEAR_ERROR: 'PREVIEW_CLEAR_ERROR', - SELECT_STORY: 'PREVIEW_SELECT_STORY', - SET_INITIAL_STORY: 'PREVIEW_SET_INITIAL_STORY', -}; - -export function setInitialStory(storyKindList) { - return { - type: types.SET_INITIAL_STORY, - storyKindList, - }; -} - -export function setError(error) { - return { - type: types.SET_ERROR, - error, - }; -} - -export function clearError() { - return { - type: types.CLEAR_ERROR, - }; -} - -export function selectStory(kind, story) { - return { - type: types.SELECT_STORY, - kind, - story, - }; -} diff --git a/app/vue/src/client/preview/client_api.js b/app/vue/src/client/preview/client_api.js deleted file mode 100644 index 4362755997b8..000000000000 --- a/app/vue/src/client/preview/client_api.js +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ - -export default class ClientApi { - constructor({ channel, storyStore }) { - // channel can be null when running in node - // always check whether channel is available - this._channel = channel; - this._storyStore = storyStore; - this._addons = {}; - this._globalDecorators = []; - } - - setAddon(addon) { - this._addons = { - ...this._addons, - ...addon, - }; - } - - addDecorator(decorator) { - this._globalDecorators.push(decorator); - } - - clearDecorators() { - this._globalDecorators = []; - } - - storiesOf(kind, m) { - if (!kind && typeof kind !== 'string') { - throw new Error('Invalid or missing kind provided for stories, should be a string'); - } - - if (!m) { - // eslint-disable-next-line no-console - console.warn( - `Missing 'module' parameter for story with a kind of '${kind}'. It will break your HMR` - ); - } - - if (m && m.hot) { - m.hot.dispose(() => { - this._storyStore.removeStoryKind(kind); - }); - } - - const localDecorators = []; - const api = { - kind, - }; - - // apply addons - Object.keys(this._addons).forEach(name => { - const addon = this._addons[name]; - api[name] = (...args) => { - addon.apply(api, args); - return api; - }; - }); - - const createWrapperComponent = Target => ({ - functional: true, - render(h, c) { - return h(Target, c.data, c.children); - }, - }); - - api.add = (storyName, getStory) => { - if (typeof storyName !== 'string') { - throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`); - } - - if (this._storyStore.hasStory(kind, storyName)) { - throw new Error(`Story of "${kind}" named "${storyName}" already exists`); - } - - // Wrap the getStory function with each decorator. The first - // decorator will wrap the story function. The second will - // wrap the first decorator and so on. - const decorators = [...localDecorators, ...this._globalDecorators]; - - const getDecoratedStory = decorators.reduce( - (decorated, decorator) => context => { - const story = () => decorated(context); - const decoratedStory = decorator(story, context); - decoratedStory.components = decoratedStory.components || {}; - decoratedStory.components.story = createWrapperComponent(story()); - return decoratedStory; - }, - getStory - ); - - const fileName = m ? m.filename : null; - - // Add the fully decorated getStory function. - this._storyStore.addStory(kind, storyName, getDecoratedStory, fileName); - return api; - }; - - api.addDecorator = decorator => { - localDecorators.push(decorator); - return api; - }; - - return api; - } - - getStorybook() { - return this._storyStore.getStoryKinds().map(kind => { - const fileName = this._storyStore.getStoryFileName(kind); - - const stories = this._storyStore.getStories(kind).map(name => { - const render = this._storyStore.getStory(kind, name); - return { name, render }; - }); - - return { kind, fileName, stories }; - }); - } -} diff --git a/app/vue/src/client/preview/client_api.test.js b/app/vue/src/client/preview/client_api.test.js deleted file mode 100644 index b12f63f036a4..000000000000 --- a/app/vue/src/client/preview/client_api.test.js +++ /dev/null @@ -1,295 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ - -import ClientAPI from './client_api'; - -class StoryStore { - constructor() { - this.stories = []; - } - - addStory(kind, story, fn, fileName) { - this.stories.push({ kind, story, fn, fileName }); - } - - getStoryKinds() { - return this.stories.reduce((kinds, info) => { - if (kinds.indexOf(info.kind) === -1) { - kinds.push(info.kind); - } - return kinds; - }, []); - } - - getStories(kind) { - return this.stories.reduce((stories, info) => { - if (info.kind === kind) { - stories.push(info.story); - } - return stories; - }, []); - } - - getStoryFileName(kind) { - const story = this.stories.find(info => info.kind === kind); - return story ? story.fileName : null; - } - - getStory(kind, name) { - return this.stories.reduce((fn, info) => { - if (!fn && info.kind === kind && info.story === name) { - return info.fn; - } - return fn; - }, null); - } - - hasStory(kind, name) { - return Boolean(this.getStory(kind, name)); - } -} - -describe('preview.client_api', () => { - describe('setAddon', () => { - it('should register addons', () => { - const api = new ClientAPI({}); - let data; - - api.setAddon({ - aa() { - data = 'foo'; - }, - }); - - api.storiesOf('none', module).aa(); - expect(data).toBe('foo'); - }); - - it('should not remove previous addons', () => { - const api = new ClientAPI({}); - const data = []; - - api.setAddon({ - aa() { - data.push('foo'); - }, - }); - - api.setAddon({ - bb() { - data.push('bar'); - }, - }); - - api - .storiesOf('none', module) - .aa() - .bb(); - expect(data).toEqual(['foo', 'bar']); - }); - - it('should call with the api context', () => { - const api = new ClientAPI({}); - let data; - - api.setAddon({ - aa() { - data = typeof this.add; - }, - }); - - api.storiesOf('none', module).aa(); - expect(data).toBe('function'); - }); - - it('should be able to access addons added previously', () => { - const api = new ClientAPI({}); - let data; - - api.setAddon({ - aa() { - data = 'foo'; - }, - }); - - api.setAddon({ - bb() { - this.aa(); - }, - }); - - api.storiesOf('none', module).bb(); - expect(data).toBe('foo'); - }); - - it('should be able to access the current kind', () => { - const api = new ClientAPI({}); - const kind = 'dfdwf3e3'; - let data; - - api.setAddon({ - aa() { - data = this.kind; - }, - }); - - api.storiesOf(kind, module).aa(); - expect(data).toBe(kind); - }); - }); - - describe('addDecorator', () => { - it('should add local decorators', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none', module); - localApi.addDecorator(fn => ({ template: `
aa${fn().template}
` })); - - localApi.add('storyName', () => ({ template: '

hello

' })); - expect(storyStore.stories[0].fn().template).toBe('
aa

hello

'); - }); - - it('should add global decorators', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - api.addDecorator(fn => ({ template: `
bb${fn().template}
` })); - const localApi = api.storiesOf('none', module); - - localApi.add('storyName', () => ({ template: '

hello

' })); - expect(storyStore.stories[0].fn().template).toBe('
bb

hello

'); - }); - - it('should utilize both decorators at once', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none', module); - - api.addDecorator(fn => ({ template: `
aa${fn().template}
` })); - localApi.addDecorator(fn => ({ template: `
bb${fn().template}
` })); - - localApi.add('storyName', () => ({ template: '

hello

' })); - expect(storyStore.stories[0].fn().template).toBe('
aa
bb

hello

'); - }); - - it('should pass the context', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none', module); - localApi.addDecorator(fn => ({ template: `
aa${fn().template}
` })); - - localApi.add('storyName', ({ kind, story }) => ({ template: `

${kind}-${story}

` })); - - const kind = 'dfdfd'; - const story = 'ef349ff'; - - const result = storyStore.stories[0].fn({ kind, story }); - expect(result.template).toBe(`
aa

${kind}-${story}

`); - }); - - it('should have access to the context', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const localApi = api.storiesOf('none', module); - localApi.addDecorator((fn, { kind, story }) => ({ - template: `
${kind}-${story}-${fn().template}
`, - })); - - localApi.add('storyName', () => ({ template: '

hello

' })); - - const kind = 'dfdfd'; - const story = 'ef349ff'; - - const result = storyStore.stories[0].fn({ kind, story }); - expect(result.template).toBe(`
${kind}-${story}-

hello

`); - }); - }); - - describe('clearDecorators', () => { - it('should remove all global decorators', () => { - const api = new ClientAPI({}); - api._globalDecorators = 1234; - api.clearDecorators(); - expect(api._globalDecorators).toEqual([]); - }); - }); - - describe('getStorybook', () => { - it('should return storybook when empty', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const book = api.getStorybook(); - expect(book).toEqual([]); - }); - - it('should return storybook with stories', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const functions = { - 'story-1.1': () => 'story-1.1', - 'story-1.2': () => 'story-1.2', - 'story-2.1': () => 'story-2.1', - 'story-2.2': () => 'story-2.2', - }; - const kind1 = api.storiesOf('kind-1', { filename: 'kind1.js' }); - kind1.add('story-1.1', functions['story-1.1']); - kind1.add('story-1.2', functions['story-1.2']); - const kind2 = api.storiesOf('kind-2', { filename: 'kind2.js' }); - kind2.add('story-2.1', functions['story-2.1']); - kind2.add('story-2.2', functions['story-2.2']); - const book = api.getStorybook(); - expect(book).toEqual([ - { - kind: 'kind-1', - fileName: 'kind1.js', - stories: [ - { name: 'story-1.1', render: functions['story-1.1'] }, - { name: 'story-1.2', render: functions['story-1.2'] }, - ], - }, - { - kind: 'kind-2', - fileName: 'kind2.js', - stories: [ - { name: 'story-2.1', render: functions['story-2.1'] }, - { name: 'story-2.2', render: functions['story-2.2'] }, - ], - }, - ]); - }); - - it('should return storybook with file names when module with file name provided', () => { - const storyStore = new StoryStore(); - const api = new ClientAPI({ storyStore }); - const functions = { - 'story-1.1': () => 'story-1.1', - 'story-1.2': () => 'story-1.2', - 'story-2.1': () => 'story-2.1', - 'story-2.2': () => 'story-2.2', - }; - const kind1 = api.storiesOf('kind-1', { filename: 'foo' }); - kind1.add('story-1.1', functions['story-1.1']); - kind1.add('story-1.2', functions['story-1.2']); - const kind2 = api.storiesOf('kind-2', { filename: 'bar' }); - kind2.add('story-2.1', functions['story-2.1']); - kind2.add('story-2.2', functions['story-2.2']); - const book = api.getStorybook(); - expect(book).toEqual([ - { - kind: 'kind-1', - fileName: 'foo', - stories: [ - { name: 'story-1.1', render: functions['story-1.1'] }, - { name: 'story-1.2', render: functions['story-1.2'] }, - ], - }, - { - kind: 'kind-2', - fileName: 'bar', - stories: [ - { name: 'story-2.1', render: functions['story-2.1'] }, - { name: 'story-2.2', render: functions['story-2.2'] }, - ], - }, - ]); - }); - }); -}); diff --git a/app/vue/src/client/preview/index.js b/app/vue/src/client/preview/index.js index e76364259832..00f9d2506dbf 100644 --- a/app/vue/src/client/preview/index.js +++ b/app/vue/src/client/preview/index.js @@ -4,13 +4,9 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; -import { StoryStore } from '@storybook/core/client'; -import ClientApi from './client_api'; -import ConfigApi from './config_api'; -import render from './render'; +import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; import init from './init'; -import { selectStory } from './actions'; -import reducer from './reducer'; +import render from './render'; // check whether we're running on node/browser const { navigator } = global; @@ -21,13 +17,31 @@ const isBrowser = const storyStore = new StoryStore(); const reduxStore = createStore(reducer); -const context = { storyStore, reduxStore }; + +const createWrapperComponent = Target => ({ + functional: true, + render(h, c) { + return h(Target, c.data, c.children); + }, +}); +const decorateStory = (getStory, decorators) => + decorators.reduce( + (decorated, decorator) => context => { + const story = () => decorated(context); + const decoratedStory = decorator(story, context); + decoratedStory.components = decoratedStory.components || {}; + decoratedStory.components.story = createWrapperComponent(story()); + return decoratedStory; + }, + getStory + ); +const context = { storyStore, reduxStore, decorateStory }; if (isBrowser) { const queryParams = qs.parse(window.location.search.substring(1)); const channel = createChannel({ page: 'preview' }); channel.on('setCurrentStory', data => { - reduxStore.dispatch(selectStory(data.kind, data.story)); + reduxStore.dispatch(Actions.selectStory(data.kind, data.story)); }); Object.assign(context, { channel, window, queryParams }); addons.setChannel(channel); @@ -35,7 +49,6 @@ if (isBrowser) { } const clientApi = new ClientApi(context); -const configApi = new ConfigApi(context); // do exports export const storiesOf = clientApi.storiesOf.bind(clientApi); @@ -43,6 +56,8 @@ export const setAddon = clientApi.setAddon.bind(clientApi); export const addDecorator = clientApi.addDecorator.bind(clientApi); export const clearDecorators = clientApi.clearDecorators.bind(clientApi); export const getStorybook = clientApi.getStorybook.bind(clientApi); + +const configApi = new ConfigApi({ ...context, clearDecorators }); export const configure = configApi.configure.bind(configApi); // initialize the UI diff --git a/app/vue/src/client/preview/init.js b/app/vue/src/client/preview/init.js index a8de2d28f168..138e0a7d27ac 100644 --- a/app/vue/src/client/preview/init.js +++ b/app/vue/src/client/preview/init.js @@ -1,11 +1,11 @@ import keyEvents from '@storybook/ui/dist/libs/key_events'; -import { selectStory } from './actions'; +import { Actions } from '@storybook/core/client'; export default function(context) { const { queryParams, reduxStore, window, channel } = context; // set the story if correct params are loaded via the URL. if (queryParams.selectedKind) { - reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory)); + reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); } // Handle keyEvents and pass them to the parent. diff --git a/app/vue/src/client/preview/reducer.js b/app/vue/src/client/preview/reducer.js deleted file mode 100644 index 015df6c83915..000000000000 --- a/app/vue/src/client/preview/reducer.js +++ /dev/null @@ -1,40 +0,0 @@ -import { types } from './actions'; - -export default function reducer(state = {}, action) { - switch (action.type) { - case types.CLEAR_ERROR: { - return { - ...state, - error: null, - }; - } - - case types.SET_ERROR: { - return { - ...state, - error: action.error, - }; - } - - case types.SELECT_STORY: { - return { - ...state, - selectedKind: action.kind, - selectedStory: action.story, - }; - } - - case types.SET_INITIAL_STORY: { - const newState = { ...state }; - const { storyKindList } = action; - if (!newState.selectedKind && storyKindList.length > 0) { - newState.selectedKind = storyKindList[0].kind; - newState.selectedStory = storyKindList[0].stories[0]; - } - return newState; - } - - default: - return state; - } -} diff --git a/examples/crna-kitchen-sink/package.json b/examples/crna-kitchen-sink/package.json index 7553a83afc05..31783fc57c9d 100644 --- a/examples/crna-kitchen-sink/package.json +++ b/examples/crna-kitchen-sink/package.json @@ -12,6 +12,7 @@ "@storybook/channels": "file:../../packs/storybook-channels.tgz", "@storybook/channel-postmessage": "file:../../packs/storybook-channel-postmessage.tgz", "@storybook/components": "file:../../packs/storybook-components.tgz", + "@storybook/core": "file:../../packs/storybook-core.tgz", "@storybook/react-native": "file:../../packs/storybook-react-native.tgz", "@storybook/ui": "file:../../packs/storybook-ui.tgz", "react-native-scripts": "1.1.0", diff --git a/examples/react-native-vanilla/package.json b/examples/react-native-vanilla/package.json index 4f7befdc0822..c2c70b4a5add 100644 --- a/examples/react-native-vanilla/package.json +++ b/examples/react-native-vanilla/package.json @@ -26,6 +26,7 @@ "@storybook/channels": "file:../../packs/storybook-channels.tgz", "@storybook/channel-postmessage": "file:../../packs/storybook-channel-postmessage.tgz", "@storybook/components": "file:../../packs/storybook-components.tgz", + "@storybook/core": "file:../../packs/storybook-core.tgz", "@storybook/react-native": "file:../../packs/storybook-react-native.tgz", "@storybook/ui": "file:../../packs/storybook-ui.tgz", "react-dom": "^16.0.0" diff --git a/lib/components/src/highlight_button.js b/lib/components/src/highlight_button.js index 609bdc5aa419..fb26f0bc0f90 100644 --- a/lib/components/src/highlight_button.js +++ b/lib/components/src/highlight_button.js @@ -5,7 +5,7 @@ export default glamorous.button( border: '1px solid rgba(0, 0, 0, 0)', font: 'inherit', background: 'none', - 'box-shadow': 'none', + boxShadow: 'none', padding: 0, ':hover': { backgroundColor: 'rgba(0, 0, 0, 0.05)', diff --git a/lib/core/package.json b/lib/core/package.json index 70f98d5a6a21..fb8b33f2d02f 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -16,6 +16,9 @@ "dev": "DEV_BUILD=1 nodemon --watch ./src --exec 'yarn prepare'", "prepare": "node ../../scripts/prepare.js" }, + "dependencies": { + "global": "^4.3.2" + }, "devDependencies": { "babel-cli": "^6.26.0" } diff --git a/app/angular/src/client/preview/actions.js b/lib/core/src/client/preview/actions.js similarity index 100% rename from app/angular/src/client/preview/actions.js rename to lib/core/src/client/preview/actions.js diff --git a/app/react/src/client/preview/client_api.js b/lib/core/src/client/preview/client_api.js similarity index 85% rename from app/react/src/client/preview/client_api.js rename to lib/core/src/client/preview/client_api.js index 85d448683ff9..80ea2a473ae8 100644 --- a/app/react/src/client/preview/client_api.js +++ b/lib/core/src/client/preview/client_api.js @@ -1,13 +1,20 @@ /* eslint no-underscore-dangle: 0 */ +const defaultDecorateStory = (getStory, decorators) => + decorators.reduce( + (decorated, decorator) => context => decorator(() => decorated(context), context), + getStory + ); + export default class ClientApi { - constructor({ channel, storyStore }) { + constructor({ channel, storyStore, decorateStory = defaultDecorateStory }) { // channel can be null when running in node // always check whether channel is available this._channel = channel; this._storyStore = storyStore; this._addons = {}; this._globalDecorators = []; + this._decorateStory = decorateStory; } setAddon(addon) { @@ -71,15 +78,15 @@ export default class ClientApi { // wrap the first decorator and so on. const decorators = [...localDecorators, ...this._globalDecorators]; - const fn = decorators.reduce( - (decorated, decorator) => context => decorator(() => decorated(context), context), - getStory - ); - const fileName = m ? m.filename : null; // Add the fully decorated getStory function. - this._storyStore.addStory(kind, storyName, fn, fileName); + this._storyStore.addStory( + kind, + storyName, + this._decorateStory(getStory, decorators), + fileName + ); return api; }; diff --git a/app/react/src/client/preview/client_api.test.js b/lib/core/src/client/preview/client_api.test.js similarity index 100% rename from app/react/src/client/preview/client_api.test.js rename to lib/core/src/client/preview/client_api.test.js diff --git a/app/vue/src/client/preview/config_api.js b/lib/core/src/client/preview/config_api.js similarity index 91% rename from app/vue/src/client/preview/config_api.js rename to lib/core/src/client/preview/config_api.js index cc0bbea3af3e..e250938e51bc 100644 --- a/app/vue/src/client/preview/config_api.js +++ b/lib/core/src/client/preview/config_api.js @@ -2,15 +2,15 @@ import { location } from 'global'; import { setInitialStory, setError, clearError } from './actions'; -import { clearDecorators } from './'; export default class ConfigApi { - constructor({ channel, storyStore, reduxStore }) { + constructor({ channel, storyStore, reduxStore, clearDecorators }) { // channel can be null when running in node // always check whether channel is available this._channel = channel; this._storyStore = storyStore; this._reduxStore = reduxStore; + this._clearDecorators = clearDecorators; } _renderMain(loaders) { @@ -54,7 +54,7 @@ export default class ConfigApi { setTimeout(render); }); module.hot.dispose(() => { - clearDecorators(); + this._clearDecorators(); }); } diff --git a/lib/core/src/client/preview/index.js b/lib/core/src/client/preview/index.js index 8892fdd3d07e..bf73c0d74093 100644 --- a/lib/core/src/client/preview/index.js +++ b/lib/core/src/client/preview/index.js @@ -1,3 +1,7 @@ +import * as Actions from './actions'; +import ClientApi from './client_api'; +import ConfigApi from './config_api'; import StoryStore from './story_store'; +import reducer from './reducer'; -export default { StoryStore }; +export default { Actions, ClientApi, ConfigApi, StoryStore, reducer }; diff --git a/app/angular/src/client/preview/reducer.js b/lib/core/src/client/preview/reducer.js similarity index 100% rename from app/angular/src/client/preview/reducer.js rename to lib/core/src/client/preview/reducer.js From e8f76ceed5ef424012bc566f5d6d424340d93f2a Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Wed, 8 Nov 2017 15:41:41 +0900 Subject: [PATCH 3/7] Self-binding core client/config API --- app/angular/src/client/preview/index.js | 12 +++--------- app/react/src/client/preview/index.js | 8 ++------ app/vue/src/client/preview/index.js | 10 ++-------- lib/core/README.md | 2 +- lib/core/src/client/preview/client_api.js | 21 ++++++++++----------- lib/core/src/client/preview/config_api.js | 4 ++-- 6 files changed, 20 insertions(+), 37 deletions(-) diff --git a/app/angular/src/client/preview/index.js b/app/angular/src/client/preview/index.js index d2a68d15c8e4..df6cc71b3154 100644 --- a/app/angular/src/client/preview/index.js +++ b/app/angular/src/client/preview/index.js @@ -29,16 +29,10 @@ if (isBrowser) { } const clientApi = new ClientApi(context); +export const { storiesOf, setAddon, addDecorator, clearDecorators } = clientApi; -// do exports -export const storiesOf = clientApi.storiesOf.bind(clientApi); -export const setAddon = clientApi.setAddon.bind(clientApi); -export const addDecorator = clientApi.addDecorator.bind(clientApi); -export const clearDecorators = clientApi.clearDecorators.bind(clientApi); -export const getStorybook = clientApi.getStorybook.bind(clientApi); - -const configApi = new ConfigApi({ clearDecorators, ...context }); -export const configure = configApi.configure.bind(configApi); +const configApi = new ConfigApi({ ...context, clearDecorators }); +export const { configure } = configApi; // initialize the UI const renderUI = () => { diff --git a/app/react/src/client/preview/index.js b/app/react/src/client/preview/index.js index 50923195274d..683928904193 100644 --- a/app/react/src/client/preview/index.js +++ b/app/react/src/client/preview/index.js @@ -31,14 +31,10 @@ if (isBrowser) { } const clientApi = new ClientApi(context); -export const storiesOf = clientApi.storiesOf.bind(clientApi); -export const setAddon = clientApi.setAddon.bind(clientApi); -export const addDecorator = clientApi.addDecorator.bind(clientApi); -export const clearDecorators = clientApi.clearDecorators.bind(clientApi); -export const getStorybook = clientApi.getStorybook.bind(clientApi); +export const { storiesOf, setAddon, addDecorator, clearDecorators, getStorybook } = clientApi; const configApi = new ConfigApi({ clearDecorators, ...context }); -export const configure = configApi.configure.bind(configApi); +export const { configure } = configApi; // initialize the UI const renderUI = () => { diff --git a/app/vue/src/client/preview/index.js b/app/vue/src/client/preview/index.js index 00f9d2506dbf..f5b0d3092329 100644 --- a/app/vue/src/client/preview/index.js +++ b/app/vue/src/client/preview/index.js @@ -49,16 +49,10 @@ if (isBrowser) { } const clientApi = new ClientApi(context); - -// do exports -export const storiesOf = clientApi.storiesOf.bind(clientApi); -export const setAddon = clientApi.setAddon.bind(clientApi); -export const addDecorator = clientApi.addDecorator.bind(clientApi); -export const clearDecorators = clientApi.clearDecorators.bind(clientApi); -export const getStorybook = clientApi.getStorybook.bind(clientApi); +export const { storiesOf, setAddon, addDecorator, clearDecorators } = clientApi; const configApi = new ConfigApi({ ...context, clearDecorators }); -export const configure = configApi.configure.bind(configApi); +export const { configure } = configApi; // initialize the UI const renderUI = () => { diff --git a/lib/core/README.md b/lib/core/README.md index 5ae08249a8ee..829c447cb283 100644 --- a/lib/core/README.md +++ b/lib/core/README.md @@ -1,4 +1,4 @@ -# Storybook for React +# Storybook Core [![Greenkeeper badge](https://badges.greenkeeper.io/storybooks/storybook.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.org/storybooks/storybook.svg?branch=master)](https://travis-ci.org/storybooks/storybook) diff --git a/lib/core/src/client/preview/client_api.js b/lib/core/src/client/preview/client_api.js index 80ea2a473ae8..73e074b345ab 100644 --- a/lib/core/src/client/preview/client_api.js +++ b/lib/core/src/client/preview/client_api.js @@ -17,22 +17,22 @@ export default class ClientApi { this._decorateStory = decorateStory; } - setAddon(addon) { + setAddon = addon => { this._addons = { ...this._addons, ...addon, }; - } + }; - addDecorator(decorator) { + addDecorator = decorator => { this._globalDecorators.push(decorator); - } + }; - clearDecorators() { + clearDecorators = () => { this._globalDecorators = []; - } + }; - storiesOf(kind, m) { + storiesOf = (kind, m) => { if (!kind && typeof kind !== 'string') { throw new Error('Invalid or missing kind provided for stories, should be a string'); } @@ -96,10 +96,10 @@ export default class ClientApi { }; return api; - } + }; - getStorybook() { - return this._storyStore.getStoryKinds().map(kind => { + getStorybook = () => + this._storyStore.getStoryKinds().map(kind => { const fileName = this._storyStore.getStoryFileName(kind); const stories = this._storyStore.getStories(kind).map(name => { @@ -109,5 +109,4 @@ export default class ClientApi { return { kind, fileName, stories }; }); - } } diff --git a/lib/core/src/client/preview/config_api.js b/lib/core/src/client/preview/config_api.js index e250938e51bc..cfa3f2d930b3 100644 --- a/lib/core/src/client/preview/config_api.js +++ b/lib/core/src/client/preview/config_api.js @@ -31,7 +31,7 @@ export default class ConfigApi { this._reduxStore.dispatch(setError(error)); } - configure(loaders, module) { + configure = (loaders, module) => { const render = () => { try { this._renderMain(loaders); @@ -63,5 +63,5 @@ export default class ConfigApi { } else { loaders(); } - } + }; } From dd4e81336ab1f22d621bfab11b3ef45589dd4299 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 9 Nov 2017 14:20:42 +0900 Subject: [PATCH 4/7] Remove framework init.js - move keyboard handling to the UI library, rather than introducing new deps - keep QS handling in each framework (for now) --- app/angular/src/client/preview/index.js | 18 +++++++++++++----- app/angular/src/client/preview/init.js | 18 ------------------ app/react/src/client/preview/index.js | 18 +++++++++++++----- app/react/src/client/preview/init.js | 18 ------------------ app/vue/src/client/preview/index.js | 18 +++++++++++++----- app/vue/src/client/preview/init.js | 18 ------------------ lib/ui/src/libs/key_events.js | 10 ++++++++++ 7 files changed, 49 insertions(+), 69 deletions(-) delete mode 100644 app/angular/src/client/preview/init.js delete mode 100644 app/react/src/client/preview/init.js delete mode 100644 app/vue/src/client/preview/init.js diff --git a/app/angular/src/client/preview/index.js b/app/angular/src/client/preview/index.js index df6cc71b3154..4c14c6fd0abd 100644 --- a/app/angular/src/client/preview/index.js +++ b/app/angular/src/client/preview/index.js @@ -1,10 +1,10 @@ import { window, navigator } from 'global'; import { createStore } from 'redux'; +import qs from 'qs'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; -import qs from 'qs'; +import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events'; import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; -import init from './init'; import render from './render'; // check whether we're running on node/browser @@ -18,14 +18,22 @@ const reduxStore = createStore(reducer); const context = { storyStore, reduxStore }; if (isBrowser) { - const queryParams = qs.parse(window.location.search.substring(1)); + // create preview channel const channel = createChannel({ page: 'preview' }); channel.on('setCurrentStory', data => { reduxStore.dispatch(Actions.selectStory(data.kind, data.story)); }); - Object.assign(context, { channel, window, queryParams }); addons.setChannel(channel); - init(context); + Object.assign(context, { channel }); + + // handle query params + const queryParams = qs.parse(window.location.search.substring(1)); + if (queryParams.selectedKind) { + reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); + } + + // Handle keyboard shortcuts + window.onkeydown = handleKeyboardShortcuts(channel); } const clientApi = new ClientApi(context); diff --git a/app/angular/src/client/preview/init.js b/app/angular/src/client/preview/init.js deleted file mode 100644 index 138e0a7d27ac..000000000000 --- a/app/angular/src/client/preview/init.js +++ /dev/null @@ -1,18 +0,0 @@ -import keyEvents from '@storybook/ui/dist/libs/key_events'; -import { Actions } from '@storybook/core/client'; - -export default function(context) { - const { queryParams, reduxStore, window, channel } = context; - // set the story if correct params are loaded via the URL. - if (queryParams.selectedKind) { - reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); - } - - // Handle keyEvents and pass them to the parent. - window.onkeydown = e => { - const parsedEvent = keyEvents(e); - if (parsedEvent) { - channel.emit('applyShortcut', { event: parsedEvent }); - } - }; -} diff --git a/app/react/src/client/preview/index.js b/app/react/src/client/preview/index.js index 683928904193..08b816d2bc0e 100644 --- a/app/react/src/client/preview/index.js +++ b/app/react/src/client/preview/index.js @@ -2,10 +2,10 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; -import createChannel from '@storybook/channel-postmessage'; import qs from 'qs'; +import createChannel from '@storybook/channel-postmessage'; +import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events'; import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; -import init from './init'; import render from './render'; // check whether we're running on node/browser @@ -20,14 +20,22 @@ const reduxStore = createStore(reducer); const context = { storyStore, reduxStore }; if (isBrowser) { - const queryParams = qs.parse(window.location.search.substring(1)); + // setup preview channel const channel = createChannel({ page: 'preview' }); channel.on('setCurrentStory', data => { reduxStore.dispatch(Actions.selectStory(data.kind, data.story)); }); - Object.assign(context, { channel, window, queryParams }); addons.setChannel(channel); - init(context); + Object.assign(context, { channel }); + + // handle query params + const queryParams = qs.parse(window.location.search.substring(1)); + if (queryParams.selectedKind) { + reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); + } + + // Handle keyboard shortcuts + window.onkeydown = handleKeyboardShortcuts(channel); } const clientApi = new ClientApi(context); diff --git a/app/react/src/client/preview/init.js b/app/react/src/client/preview/init.js deleted file mode 100644 index 138e0a7d27ac..000000000000 --- a/app/react/src/client/preview/init.js +++ /dev/null @@ -1,18 +0,0 @@ -import keyEvents from '@storybook/ui/dist/libs/key_events'; -import { Actions } from '@storybook/core/client'; - -export default function(context) { - const { queryParams, reduxStore, window, channel } = context; - // set the story if correct params are loaded via the URL. - if (queryParams.selectedKind) { - reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); - } - - // Handle keyEvents and pass them to the parent. - window.onkeydown = e => { - const parsedEvent = keyEvents(e); - if (parsedEvent) { - channel.emit('applyShortcut', { event: parsedEvent }); - } - }; -} diff --git a/app/vue/src/client/preview/index.js b/app/vue/src/client/preview/index.js index f5b0d3092329..20850365c082 100644 --- a/app/vue/src/client/preview/index.js +++ b/app/vue/src/client/preview/index.js @@ -1,11 +1,11 @@ /* global window */ import { createStore } from 'redux'; +import qs from 'qs'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; -import qs from 'qs'; +import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events'; import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; -import init from './init'; import render from './render'; // check whether we're running on node/browser @@ -38,14 +38,22 @@ const decorateStory = (getStory, decorators) => const context = { storyStore, reduxStore, decorateStory }; if (isBrowser) { - const queryParams = qs.parse(window.location.search.substring(1)); + // create preview channel const channel = createChannel({ page: 'preview' }); channel.on('setCurrentStory', data => { reduxStore.dispatch(Actions.selectStory(data.kind, data.story)); }); - Object.assign(context, { channel, window, queryParams }); addons.setChannel(channel); - init(context); + Object.assign(context, { channel }); + + // handle query params + const queryParams = qs.parse(window.location.search.substring(1)); + if (queryParams.selectedKind) { + reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); + } + + // Handle keyboard shortcuts + window.onkeydown = handleKeyboardShortcuts(channel); } const clientApi = new ClientApi(context); diff --git a/app/vue/src/client/preview/init.js b/app/vue/src/client/preview/init.js deleted file mode 100644 index 138e0a7d27ac..000000000000 --- a/app/vue/src/client/preview/init.js +++ /dev/null @@ -1,18 +0,0 @@ -import keyEvents from '@storybook/ui/dist/libs/key_events'; -import { Actions } from '@storybook/core/client'; - -export default function(context) { - const { queryParams, reduxStore, window, channel } = context; - // set the story if correct params are loaded via the URL. - if (queryParams.selectedKind) { - reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); - } - - // Handle keyEvents and pass them to the parent. - window.onkeydown = e => { - const parsedEvent = keyEvents(e); - if (parsedEvent) { - channel.emit('applyShortcut', { event: parsedEvent }); - } - }; -} diff --git a/lib/ui/src/libs/key_events.js b/lib/ui/src/libs/key_events.js index c1cd9765b67d..395361def51e 100755 --- a/lib/ui/src/libs/key_events.js +++ b/lib/ui/src/libs/key_events.js @@ -62,3 +62,13 @@ export default function handle(e) { return false; } } + +// window.keydown handler to dispatch a key event to the preview channel +export function handleKeyboardShortcuts(channel) { + return event => { + const parsedEvent = handle(event); + if (parsedEvent) { + channel.emit('applyShortcut', { event: parsedEvent }); + } + }; +} From 10ed61dd9e82ad00bfa31872ffc140dfeebe9d42 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 28 Dec 2017 11:33:41 +1100 Subject: [PATCH 5/7] Use store revisions to ensure that stories re-render on HMR. For #2587 --- app/react/src/client/preview/render.js | 27 +++++++++++++++------- lib/core/src/client/preview/client_api.js | 1 + lib/core/src/client/preview/story_store.js | 14 ++++++++++- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/react/src/client/preview/render.js b/app/react/src/client/preview/render.js index 4795dffc5ede..b53dbd4c68cb 100644 --- a/app/react/src/client/preview/render.js +++ b/app/react/src/client/preview/render.js @@ -14,6 +14,7 @@ const logger = console; let rootEl = null; let previousKind = ''; let previousStory = ''; +let previousRevision = -1; if (isBrowser) { rootEl = document.getElementById('root'); @@ -46,6 +47,7 @@ export function renderMain(data, storyStore) { const noPreview = ; const { selectedKind, selectedStory } = data; + const revision = storyStore.getRevision(); const story = storyStore.getStory(selectedKind, selectedStory); if (!story) { ReactDOM.render(noPreview, rootEl); @@ -56,16 +58,25 @@ export function renderMain(data, storyStore) { // renderMain() gets executed after each action. Actions will cause the whole // story to re-render without this check. // https://github.com/storybooks/react-storybook/issues/116 - if (selectedKind !== previousKind || previousStory !== selectedStory) { - // We need to unmount the existing set of components in the DOM node. - // Otherwise, React may not recrease instances for every story run. - // This could leads to issues like below: - // https://github.com/storybooks/react-storybook/issues/81 - previousKind = selectedKind; - previousStory = selectedStory; - ReactDOM.unmountComponentAtNode(rootEl); + // However, we do want the story to re-render if the store itself has changed + // (which happens at the moment when HMR occurs) + if ( + revision === previousRevision && + selectedKind === previousKind && + previousStory === selectedStory + ) { + return null; } + // We need to unmount the existing set of components in the DOM node. + // Otherwise, React may not recrease instances for every story run. + // This could leads to issues like below: + // https://github.com/storybooks/react-storybook/issues/81 + previousRevision = revision; + previousKind = selectedKind; + previousStory = selectedStory; + ReactDOM.unmountComponentAtNode(rootEl); + const context = { kind: selectedKind, story: selectedStory, diff --git a/lib/core/src/client/preview/client_api.js b/lib/core/src/client/preview/client_api.js index 73e074b345ab..f40c68e56562 100644 --- a/lib/core/src/client/preview/client_api.js +++ b/lib/core/src/client/preview/client_api.js @@ -47,6 +47,7 @@ export default class ClientApi { if (m && m.hot) { m.hot.dispose(() => { this._storyStore.removeStoryKind(kind); + this._storyStore.incrementRevision(); }); } diff --git a/lib/core/src/client/preview/story_store.js b/lib/core/src/client/preview/story_store.js index 0f7fcff98705..33424250ccfe 100644 --- a/lib/core/src/client/preview/story_store.js +++ b/lib/core/src/client/preview/story_store.js @@ -12,6 +12,15 @@ export default class StoryStore extends EventEmitter { constructor() { super(); this._data = {}; + this._revision = 0; + } + + getRevision() { + return this._revision; + } + + incrementRevision() { + this._revision += 1; } addStory(kind, name, fn, fileName) { @@ -88,7 +97,10 @@ export default class StoryStore extends EventEmitter { } dumpStoryBook() { - const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) })); + const data = this.getStoryKinds().map(kind => ({ + kind, + stories: this.getStories(kind), + })); return data; } From ad97f325cfabc760219edf6833c4e2fa35c5c6f3 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 3 Jan 2018 17:53:05 +1100 Subject: [PATCH 6/7] Re-apply #2349 to shilman/refactor-core This didn't apply at all cleanly as it involved adding code to each framework's `init.js`, which was removed in the refactor. Here we do better; we simply abstract the job of URL<->redux store syncing to the core library. --- app/angular/package.json | 2 +- app/angular/src/client/preview/index.js | 16 +++++++----- app/react/src/client/preview/index.js | 16 +++++++----- app/vue/src/client/preview/index.js | 16 +++++++----- lib/core/package.json | 3 ++- lib/core/src/client/preview/index.js | 3 ++- .../src/client/preview/syncUrlWithStore.js | 26 +++++++++++++++++++ yarn.lock | 2 +- 8 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 lib/core/src/client/preview/syncUrlWithStore.js diff --git a/app/angular/package.json b/app/angular/package.json index ae9975845567..cdffb0f86179 100644 --- a/app/angular/package.json +++ b/app/angular/package.json @@ -60,7 +60,7 @@ "postcss-flexbugs-fixes": "^3.0.0", "postcss-loader": "^2.0.5", "prop-types": "^15.5.10", - "qs": "^6.4.0", + "qs": "^6.5.1", "raw-loader": "^0.5.1", "react": "^16.0.0", "react-dom": "^16.0.0", diff --git a/app/angular/src/client/preview/index.js b/app/angular/src/client/preview/index.js index 0520fc8b4bfe..020b415bd80f 100644 --- a/app/angular/src/client/preview/index.js +++ b/app/angular/src/client/preview/index.js @@ -1,10 +1,16 @@ import { window, navigator } from 'global'; import { createStore } from 'redux'; -import qs from 'qs'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events'; -import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; +import { + StoryStore, + ClientApi, + ConfigApi, + Actions, + reducer, + syncUrlWithStore, +} from '@storybook/core/client'; import render from './render'; // check whether we're running on node/browser @@ -28,11 +34,7 @@ if (isBrowser) { addons.setChannel(channel); Object.assign(context, { channel }); - // handle query params - const queryParams = qs.parse(window.location.search.substring(1)); - if (queryParams.selectedKind) { - reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); - } + syncUrlWithStore(reduxStore); // Handle keyboard shortcuts window.onkeydown = handleKeyboardShortcuts(channel); diff --git a/app/react/src/client/preview/index.js b/app/react/src/client/preview/index.js index c491451c2d53..2d7bfe85f327 100644 --- a/app/react/src/client/preview/index.js +++ b/app/react/src/client/preview/index.js @@ -1,10 +1,16 @@ import { createStore } from 'redux'; import addons from '@storybook/addons'; -import qs from 'qs'; import { navigator, window } from 'global'; import createChannel from '@storybook/channel-postmessage'; import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events'; -import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; +import { + StoryStore, + ClientApi, + ConfigApi, + Actions, + reducer, + syncUrlWithStore, +} from '@storybook/core/client'; import render from './render'; @@ -31,11 +37,7 @@ if (isBrowser) { addons.setChannel(channel); Object.assign(context, { channel }); - // handle query params - const queryParams = qs.parse(window.location.search.substring(1)); - if (queryParams.selectedKind) { - reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); - } + syncUrlWithStore(reduxStore); // Handle keyboard shortcuts window.onkeydown = handleKeyboardShortcuts(channel); diff --git a/app/vue/src/client/preview/index.js b/app/vue/src/client/preview/index.js index ba7ad887d5c4..744ce99f6792 100644 --- a/app/vue/src/client/preview/index.js +++ b/app/vue/src/client/preview/index.js @@ -1,10 +1,16 @@ import { createStore } from 'redux'; -import qs from 'qs'; import addons from '@storybook/addons'; import createChannel from '@storybook/channel-postmessage'; import { navigator, window } from 'global'; import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events'; -import { StoryStore, ClientApi, ConfigApi, Actions, reducer } from '@storybook/core/client'; +import { + StoryStore, + ClientApi, + ConfigApi, + Actions, + reducer, + syncUrlWithStore, +} from '@storybook/core/client'; import render from './render'; @@ -47,11 +53,7 @@ if (isBrowser) { addons.setChannel(channel); Object.assign(context, { channel }); - // handle query params - const queryParams = qs.parse(window.location.search.substring(1)); - if (queryParams.selectedKind) { - reduxStore.dispatch(Actions.selectStory(queryParams.selectedKind, queryParams.selectedStory)); - } + syncUrlWithStore(reduxStore); // Handle keyboard shortcuts window.onkeydown = handleKeyboardShortcuts(channel); diff --git a/lib/core/package.json b/lib/core/package.json index 64708cb4d244..eb97d9ffa0de 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -19,7 +19,8 @@ "dependencies": { "@storybook/client-logger": "^3.3.3", "events": "^1.1.1", - "global": "^4.3.2" + "global": "^4.3.2", + "qs": "^6.5.1" }, "devDependencies": { "babel-cli": "^6.26.0" diff --git a/lib/core/src/client/preview/index.js b/lib/core/src/client/preview/index.js index bf73c0d74093..f7597eec5627 100644 --- a/lib/core/src/client/preview/index.js +++ b/lib/core/src/client/preview/index.js @@ -3,5 +3,6 @@ import ClientApi from './client_api'; import ConfigApi from './config_api'; import StoryStore from './story_store'; import reducer from './reducer'; +import syncUrlWithStore from './syncUrlWithStore'; -export default { Actions, ClientApi, ConfigApi, StoryStore, reducer }; +export default { Actions, ClientApi, ConfigApi, StoryStore, reducer, syncUrlWithStore }; diff --git a/lib/core/src/client/preview/syncUrlWithStore.js b/lib/core/src/client/preview/syncUrlWithStore.js new file mode 100644 index 000000000000..d5f192b3b7d2 --- /dev/null +++ b/lib/core/src/client/preview/syncUrlWithStore.js @@ -0,0 +1,26 @@ +import qs from 'qs'; +import { window } from 'global'; +import { selectStory } from './actions'; + +// Ensure the story in the redux store and on the preview URL are in sync. +// In theory we should listen to pushState events but given it's an iframe +// the user can't actually change the URL. +// We should change this if we support a "preview only" mode in the future. +export default function syncUrlToStore(reduxStore) { + // handle query params + const queryParams = qs.parse(window.location.search.substring(1)); + if (queryParams.selectedKind) { + reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory)); + } + + reduxStore.subscribe(() => { + const { selectedKind, selectedStory } = reduxStore.getState(); + + const queryString = qs.stringify({ + ...queryParams, + selectedKind, + selectedStory, + }); + window.history.pushState({}, '', `?${queryString}`); + }); +} diff --git a/yarn.lock b/yarn.lock index 3c7a8e1f3aa0..176863c0eded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10939,7 +10939,7 @@ qs@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607" -qs@6.5.1, qs@^6.4.0, qs@^6.5.1, qs@~6.5.1: +qs@6.5.1, qs@^6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" From 5fe02c189c2901dc4867cc03933e0c4402d762b3 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 3 Jan 2018 22:43:34 +1100 Subject: [PATCH 7/7] Also export getStorybook from angular/vue --- app/angular/src/client/preview/index.js | 2 +- app/vue/src/client/preview/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/angular/src/client/preview/index.js b/app/angular/src/client/preview/index.js index 020b415bd80f..e7380797df95 100644 --- a/app/angular/src/client/preview/index.js +++ b/app/angular/src/client/preview/index.js @@ -41,7 +41,7 @@ if (isBrowser) { } const clientApi = new ClientApi(context); -export const { storiesOf, setAddon, addDecorator, clearDecorators } = clientApi; +export const { storiesOf, setAddon, addDecorator, clearDecorators, getStorybook } = clientApi; const configApi = new ConfigApi({ ...context, clearDecorators }); export const { configure } = configApi; diff --git a/app/vue/src/client/preview/index.js b/app/vue/src/client/preview/index.js index 744ce99f6792..9db85853c61d 100644 --- a/app/vue/src/client/preview/index.js +++ b/app/vue/src/client/preview/index.js @@ -60,7 +60,7 @@ if (isBrowser) { } const clientApi = new ClientApi(context); -export const { storiesOf, setAddon, addDecorator, clearDecorators } = clientApi; +export const { storiesOf, setAddon, addDecorator, clearDecorators, getStorybook } = clientApi; const configApi = new ConfigApi({ ...context, clearDecorators }); export const { configure } = configApi;