diff --git a/app/polymer/.babelrc b/app/polymer/.babelrc
new file mode 100644
index 00000000000..845c3cf4d75
--- /dev/null
+++ b/app/polymer/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env", "stage-0", "react"]
+}
diff --git a/app/polymer/.npmignore b/app/polymer/.npmignore
new file mode 100644
index 00000000000..329fc8d67ad
--- /dev/null
+++ b/app/polymer/.npmignore
@@ -0,0 +1,3 @@
+docs
+src
+.babelrc
diff --git a/app/polymer/README.md b/app/polymer/README.md
new file mode 100644
index 00000000000..9b8712c29eb
--- /dev/null
+++ b/app/polymer/README.md
@@ -0,0 +1,43 @@
+# Storybook for Polymer
+
+[](https://circleci.com/gh/storybooks/storybook)
+[](https://www.codefactor.io/repository/github/storybooks/storybook)
+[](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847)
+[](https://bettercodehub.com/results/storybooks/storybook) [](https://codecov.io/gh/storybooks/storybook)
+[](https://now-examples-slackin-nqnzoygycp.now.sh/)
+[](#backers) [](#sponsors)
+
+* * *
+
+Storybook for polymer is a UI development environment for your Polymer components.
+With it, you can visualize different states of your UI components and develop them interactively.
+
+> Storybook for Polymer is at the **EXPERIMENTAL** stage!
+
+
+
+Storybook runs outside of your app.
+So you can develop UI components in isolation without worrying about app specific dependencies and requirements.
+
+## Getting Started
+
+> This is currently not yet implemented, sorry!
+
+```sh
+npm i -g @storybook/cli
+cd my-polymer-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.
+
+## Polymer Notes
+
+- This is super super experimental, if you want to use this, expect some bugs, and missing features.
+- We're looking for help to support this. If you're a member of the Polymer community and like this projet, please help us!
+ If you need any onboarding from us, we're happy to help you in any way!
diff --git a/app/polymer/bin/build.js b/app/polymer/bin/build.js
new file mode 100755
index 00000000000..780773c6cd3
--- /dev/null
+++ b/app/polymer/bin/build.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+require('../dist/server/build');
diff --git a/app/polymer/bin/index.js b/app/polymer/bin/index.js
new file mode 100755
index 00000000000..2e96258ce63
--- /dev/null
+++ b/app/polymer/bin/index.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+require('../dist/server');
diff --git a/app/polymer/docs/demo.gif b/app/polymer/docs/demo.gif
new file mode 100644
index 00000000000..c8366f8534a
Binary files /dev/null and b/app/polymer/docs/demo.gif differ
diff --git a/app/polymer/docs/react_storybook_screenshot.png b/app/polymer/docs/react_storybook_screenshot.png
new file mode 100644
index 00000000000..9763382042b
Binary files /dev/null and b/app/polymer/docs/react_storybook_screenshot.png differ
diff --git a/app/polymer/docs/storybooks_io_logo.png b/app/polymer/docs/storybooks_io_logo.png
new file mode 100644
index 00000000000..3dd9b09f3a9
Binary files /dev/null and b/app/polymer/docs/storybooks_io_logo.png differ
diff --git a/app/polymer/package.json b/app/polymer/package.json
new file mode 100644
index 00000000000..47592da2d05
--- /dev/null
+++ b/app/polymer/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "@storybook/polymer",
+ "version": "3.3.0-alpha.2",
+ "description": "Storybook for Polymer: Develop Vue Component in isolation with Hot Reloading.",
+ "homepage": "https://github.com/storybooks/storybook/tree/master/apps/polymer",
+ "bugs": {
+ "url": "https://github.com/storybooks/storybook/issues"
+ },
+ "license": "MIT",
+ "main": "dist/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": "DEV_BUILD=1 nodemon --watch ./src --exec 'yarn prepare'",
+ "prepare": "node ../../scripts/prepare.js"
+ },
+ "dependencies": {
+ "@storybook/addons": "^3.3.0-alpha.2",
+ "@storybook/channel-postmessage": "^3.3.0-alpha.2",
+ "@storybook/ui": "^3.3.0-alpha.2",
+ "airbnb-js-shims": "^1.3.0",
+ "autoprefixer": "^7.1.6",
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-plugin-react-docgen": "^1.8.0",
+ "babel-plugin-transform-regenerator": "^6.26.0",
+ "babel-plugin-transform-runtime": "^6.23.0",
+ "babel-preset-env": "^1.6.0",
+ "babel-preset-minify": "^0.2.0",
+ "babel-preset-react": "^6.24.1",
+ "babel-preset-react-app": "^3.1.0",
+ "babel-preset-stage-0": "^6.24.1",
+ "babel-runtime": "^6.26.0",
+ "case-sensitive-paths-webpack-plugin": "^2.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.11.0",
+ "common-tags": "^1.4.0",
+ "configstore": "^3.1.1",
+ "core-js": "^2.5.1",
+ "css-loader": "^0.28.7",
+ "express": "^4.16.2",
+ "file-loader": "^0.11.2",
+ "find-cache-dir": "^1.0.0",
+ "global": "^4.3.2",
+ "html-webpack-plugin": "^2.30.1",
+ "json-loader": "^0.5.7",
+ "json-stringify-safe": "^5.0.1",
+ "json5": "^0.5.1",
+ "lodash.pick": "^4.4.0",
+ "polymer-webpack-loader": "2.0.0",
+ "postcss-flexbugs-fixes": "^3.2.0",
+ "postcss-loader": "^2.0.8",
+ "prop-types": "^15.6.0",
+ "qs": "^6.5.1",
+ "react": "^16.0.0",
+ "react-dom": "^16.0.0",
+ "react-modal": "^2.4.1",
+ "redux": "^3.7.2",
+ "request": "^2.83.0",
+ "serve-favicon": "^2.4.5",
+ "shelljs": "^0.7.8",
+ "style-loader": "^0.18.2",
+ "url-loader": "^0.6.2",
+ "util-deprecate": "^1.0.2",
+ "uuid": "^3.1.0",
+ "webpack": "^3.6.0",
+ "webpack-dev-middleware": "^1.12.0",
+ "webpack-hot-middleware": "^2.20.0"
+ },
+ "devDependencies": {
+ "babel-cli": "^6.26.0",
+ "nodemon": "^1.12.1"
+ },
+ "peerDependencies": {}
+}
diff --git a/app/polymer/src/client/index.js b/app/polymer/src/client/index.js
new file mode 100644
index 00000000000..4d19c3fb217
--- /dev/null
+++ b/app/polymer/src/client/index.js
@@ -0,0 +1,17 @@
+// import deprecate from 'util-deprecate';
+
+// NOTE export these to keep backwards compatibility
+// import { action as deprecatedAction } from '@storybook/addon-actions';
+// import { linkTo as deprecatedLinkTo } from '@storybook/addon-links';
+
+export { storiesOf, setAddon, addDecorator, configure, getStorybook } from './preview';
+
+// export const action = deprecate(
+// deprecatedAction,
+// '@storybook/react action is deprecated. See: https://github.com/storybooks/storybook/tree/master/addons/actions'
+// );
+
+// export const linkTo = deprecate(
+// deprecatedLinkTo,
+// '@storybook/react linkTo is deprecated. See: https://github.com/storybooks/storybook/tree/master/addons/links'
+// );
diff --git a/app/polymer/src/client/manager/index.js b/app/polymer/src/client/manager/index.js
new file mode 100644
index 00000000000..24082de7ca5
--- /dev/null
+++ b/app/polymer/src/client/manager/index.js
@@ -0,0 +1,7 @@
+/* global document */
+
+import renderStorybookUI from '@storybook/ui';
+import Provider from './provider';
+
+const rootEl = document.getElementById('root');
+renderStorybookUI(rootEl, new Provider());
diff --git a/app/polymer/src/client/manager/preview.js b/app/polymer/src/client/manager/preview.js
new file mode 100644
index 00000000000..4f8e792d964
--- /dev/null
+++ b/app/polymer/src/client/manager/preview.js
@@ -0,0 +1,39 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+
+const iframeStyle = {
+ width: '100%',
+ height: '100%',
+ border: 0,
+ margin: 0,
+ padding: 0,
+};
+
+class Preview extends Component {
+ shouldComponentUpdate() {
+ // When the manager is re-rendered, due to changes in the layout (going full screen / changing
+ // addon panel to right) Preview section will update. If its re-rendered the whole html page
+ // inside the html is re-rendered making the story to re-mount.
+ // We dont have to re-render this component for any reason since changes are communicated to
+ // story using the channel and necessary changes are done by it.
+ return false;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+Preview.propTypes = {
+ url: PropTypes.string.isRequired,
+};
+
+export default Preview;
diff --git a/app/polymer/src/client/manager/provider.js b/app/polymer/src/client/manager/provider.js
new file mode 100644
index 00000000000..84e75d88f88
--- /dev/null
+++ b/app/polymer/src/client/manager/provider.js
@@ -0,0 +1,51 @@
+import { location } from 'global';
+import qs from 'qs';
+import React from 'react';
+import { Provider } from '@storybook/ui';
+import addons from '@storybook/addons';
+import createChannel from '@storybook/channel-postmessage';
+import Preview from './preview';
+
+export default class ReactProvider extends Provider {
+ constructor() {
+ super();
+ this.channel = createChannel({ page: 'manager' });
+ addons.setChannel(this.channel);
+ }
+
+ getPanels() {
+ return addons.getPanels();
+ }
+
+ renderPreview(selectedKind, selectedStory) {
+ const queryParams = {
+ selectedKind,
+ selectedStory,
+ };
+
+ // Add the react-perf query string to the iframe if that present.
+ if (/react_perf/.test(location.search)) {
+ queryParams.react_perf = '1';
+ }
+
+ const queryString = qs.stringify(queryParams);
+ const url = `iframe.html?${queryString}`;
+ return ;
+ }
+
+ handleAPI(api) {
+ api.onStory((kind, story) => {
+ this.channel.emit('setCurrentStory', { kind, story });
+ });
+ this.channel.on('setStories', data => {
+ api.setStories(data.stories);
+ });
+ this.channel.on('selectStory', data => {
+ api.selectStory(data.kind, data.story);
+ });
+ this.channel.on('applyShortcut', data => {
+ api.handleShortcut(data.event);
+ });
+ addons.loadAddons(api);
+ }
+}
diff --git a/app/polymer/src/client/preview/ErrorDisplay.vue b/app/polymer/src/client/preview/ErrorDisplay.vue
new file mode 100644
index 00000000000..6d2cbbbd30d
--- /dev/null
+++ b/app/polymer/src/client/preview/ErrorDisplay.vue
@@ -0,0 +1,54 @@
+
+
+
{{ message }}
+
+
+ {{ stack }}
+
+
+
+
+
+
+
+
diff --git a/app/polymer/src/client/preview/NoPreview.vue b/app/polymer/src/client/preview/NoPreview.vue
new file mode 100644
index 00000000000..5fa988c32c1
--- /dev/null
+++ b/app/polymer/src/client/preview/NoPreview.vue
@@ -0,0 +1,49 @@
+
+
+
+
No Preview
+
Sorry, but you either have no stories or none are selected somehow.
+
+ - Please check the storybook config.
+ - Try reloading the page.
+
+
+
+
+
+
+
+
diff --git a/app/polymer/src/client/preview/actions.js b/app/polymer/src/client/preview/actions.js
new file mode 100644
index 00000000000..f15e8676a20
--- /dev/null
+++ b/app/polymer/src/client/preview/actions.js
@@ -0,0 +1,34 @@
+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/polymer/src/client/preview/client_api.js b/app/polymer/src/client/preview/client_api.js
new file mode 100644
index 00000000000..4362755997b
--- /dev/null
+++ b/app/polymer/src/client/preview/client_api.js
@@ -0,0 +1,119 @@
+/* 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/polymer/src/client/preview/client_api.test.js b/app/polymer/src/client/preview/client_api.test.js
new file mode 100644
index 00000000000..b12f63f036a
--- /dev/null
+++ b/app/polymer/src/client/preview/client_api.test.js
@@ -0,0 +1,295 @@
+/* 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('');
+ });
+
+ 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('');
+ });
+
+ 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('');
+ });
+
+ 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(``);
+ });
+
+ 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(``);
+ });
+ });
+
+ 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/polymer/src/client/preview/config_api.js b/app/polymer/src/client/preview/config_api.js
new file mode 100644
index 00000000000..cc0bbea3af3
--- /dev/null
+++ b/app/polymer/src/client/preview/config_api.js
@@ -0,0 +1,67 @@
+/* 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/polymer/src/client/preview/index.js b/app/polymer/src/client/preview/index.js
new file mode 100644
index 00000000000..60508441eee
--- /dev/null
+++ b/app/polymer/src/client/preview/index.js
@@ -0,0 +1,55 @@
+/* global window */
+
+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 ClientApi from './client_api';
+import ConfigApi from './config_api';
+import render from './render';
+import init from './init';
+import { selectStory } from './actions';
+import reducer from './reducer';
+
+// check whether we're running on node/browser
+const { navigator } = global;
+const isBrowser =
+ navigator &&
+ navigator.userAgent !== 'storyshots' &&
+ !(navigator.userAgent.indexOf('Node.js') > -1);
+
+const storyStore = new StoryStore();
+const reduxStore = createStore(reducer);
+const context = { storyStore, reduxStore };
+
+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));
+ });
+ Object.assign(context, { channel, window, queryParams });
+ addons.setChannel(channel);
+ init(context);
+}
+
+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);
+export const configure = configApi.configure.bind(configApi);
+
+// initialize the UI
+const renderUI = () => {
+ if (isBrowser) {
+ render(context);
+ }
+};
+
+reduxStore.subscribe(renderUI);
diff --git a/app/polymer/src/client/preview/init.js b/app/polymer/src/client/preview/init.js
new file mode 100644
index 00000000000..a8de2d28f16
--- /dev/null
+++ b/app/polymer/src/client/preview/init.js
@@ -0,0 +1,18 @@
+import keyEvents from '@storybook/ui/dist/libs/key_events';
+import { selectStory } from './actions';
+
+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));
+ }
+
+ // 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/polymer/src/client/preview/reducer.js b/app/polymer/src/client/preview/reducer.js
new file mode 100644
index 00000000000..06e60d94968
--- /dev/null
+++ b/app/polymer/src/client/preview/reducer.js
@@ -0,0 +1,40 @@
+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;
+ }
+ return newState;
+ }
+
+ default:
+ return state;
+ }
+}
diff --git a/app/polymer/src/client/preview/render.js b/app/polymer/src/client/preview/render.js
new file mode 100644
index 00000000000..a86c1c1e4cc
--- /dev/null
+++ b/app/polymer/src/client/preview/render.js
@@ -0,0 +1,101 @@
+/* eslint-disable no-unused-vars */
+
+import { stripIndents } from 'common-tags';
+// import Vue from 'vue';
+
+// import ErrorDisplay from './ErrorDisplay.vue';
+// import NoPreview from './NoPreview.vue';
+
+// const logger = console;
+// let previousKind = '';
+// let previousStory = '';
+// let app = null;
+// let err = null;
+
+function renderErrorDisplay(error) {
+ // if (err) err.$destroy();
+ // err = new Vue({
+ // el: '#error-display',
+ // render(h) {
+ // return h(
+ // 'div',
+ // { attrs: { id: 'error-display' } },
+ // error ? [h(ErrorDisplay, { props: { message: error.message, stack: error.stack } })] : []
+ // );
+ // },
+ // });
+}
+
+export function renderError(error) {
+ // renderErrorDisplay(error);
+}
+
+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.
+ // renderErrorDisplay(error);
+ // // Log the stack to the console. So, user could check the source code.
+ // logger.error(error.stack);
+}
+
+function renderRoot(options) {
+ // if (err) {
+ // renderErrorDisplay(null); // clear
+ // err = null;
+ // }
+ // if (app) app.$destroy();
+ // app = new Vue(options);
+}
+
+export function renderMain(data, storyStore) {
+ // if (storyStore.size() === 0) return;
+ // const { selectedKind, selectedStory } = data;
+ // const story = storyStore.getStory(selectedKind, selectedStory);
+ // // Unmount the previous story only if selectedKind or selectedStory has changed.
+ // // 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;
+ // } else {
+ // return;
+ // }
+ // const context = {
+ // kind: selectedKind,
+ // story: selectedStory,
+ // };
+ // const component = story ? story(context) : NoPreview;
+ // if (!component) {
+ // const error = {
+ // message: `Expecting a Vue component from the story: "${selectedStory}" of "${selectedKind}".`,
+ // stack: stripIndents`
+ // Did you forget to return the Vue component from the story?
+ // Use "() => ({ template: '' })" or "() => ({ components: MyComp, template: '' })" when defining the story.
+ // `,
+ // };
+ // renderError(error);
+ // }
+ // renderRoot({
+ // el: '#root',
+ // render(h) {
+ // return h('div', { attrs: { id: 'root' } }, [h(component)]);
+ // },
+ // });
+}
+
+export default function renderPreview({ reduxStore, storyStore }) {
+ // const state = reduxStore.getState();
+ // if (state.error) {
+ // return renderException(state.error);
+ // }
+ // try {
+ // return renderMain(state, storyStore);
+ // } catch (ex) {
+ // return renderException(ex);
+ // }
+}
diff --git a/app/polymer/src/client/preview/story_store.js b/app/polymer/src/client/preview/story_store.js
new file mode 100644
index 00000000000..a82bba34d24
--- /dev/null
+++ b/app/polymer/src/client/preview/story_store.js
@@ -0,0 +1,99 @@
+/* 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/app/polymer/src/server/addons.js b/app/polymer/src/server/addons.js
new file mode 100644
index 00000000000..280e2ac1365
--- /dev/null
+++ b/app/polymer/src/server/addons.js
@@ -0,0 +1,2 @@
+// import '@storybook/addon-actions/register';
+// import '@storybook/addon-links/register';
diff --git a/app/polymer/src/server/babel_config.js b/app/polymer/src/server/babel_config.js
new file mode 100644
index 00000000000..8eab74ed428
--- /dev/null
+++ b/app/polymer/src/server/babel_config.js
@@ -0,0 +1,84 @@
+import fs from 'fs';
+import path from 'path';
+import JSON5 from 'json5';
+import defaultConfig from './config/babel';
+
+// avoid ESLint errors
+const logger = console;
+
+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;
+ // Ensure plugins are defined or fallback to an array to avoid empty values.
+ const babelConfigPlugins = finalConfig.plugins || [];
+ const extraPlugins = [
+ [
+ require.resolve('babel-plugin-react-docgen'),
+ {
+ DOC_GEN_COLLECTION_NAME: 'STORYBOOK_REACT_CLASSES',
+ },
+ ],
+ ];
+ // If `babelConfigPlugins` is not an `Array`, calling `concat` will inject it
+ // as a single value, if it is an `Array` it will be spreaded.
+ finalConfig.plugins = [].concat(babelConfigPlugins, extraPlugins);
+
+ return finalConfig;
+}
diff --git a/app/polymer/src/server/babel_config.test.js b/app/polymer/src/server/babel_config.test.js
new file mode 100644
index 00000000000..ba991adcd4e
--- /dev/null
+++ b/app/polymer/src/server/babel_config.test.js
@@ -0,0 +1,110 @@
+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 :)
+ const babelPluginReactDocgenPath = require.resolve('babel-plugin-react-docgen');
+
+ 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',
+ [
+ babelPluginReactDocgenPath,
+ {
+ DOC_GEN_COLLECTION_NAME: 'STORYBOOK_REACT_CLASSES',
+ },
+ ],
+ ],
+ 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',
+ [
+ babelPluginReactDocgenPath,
+ {
+ DOC_GEN_COLLECTION_NAME: 'STORYBOOK_REACT_CLASSES',
+ },
+ ],
+ ],
+ 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,
+ plugins: [
+ [
+ babelPluginReactDocgenPath,
+ {
+ DOC_GEN_COLLECTION_NAME: 'STORYBOOK_REACT_CLASSES',
+ },
+ ],
+ ],
+ presets: ['env', 'foo-preset'],
+ });
+ });
+});
diff --git a/app/polymer/src/server/build.js b/app/polymer/src/server/build.js
new file mode 100755
index 00000000000..74fc769d1a5
--- /dev/null
+++ b/app/polymer/src/server/build.js
@@ -0,0 +1,85 @@
+import webpack from 'webpack';
+import program from 'commander';
+import path from 'path';
+import fs from 'fs';
+import chalk from 'chalk';
+import shelljs from 'shelljs';
+import packageJson from '../../package.json';
+import getBaseConfig from './config/webpack.config.prod';
+import loadConfig from './config';
+import { parseList, getEnvConfig } from './utils';
+
+process.env.NODE_ENV = process.env.NODE_ENV || 'production';
+
+// avoid ESLint errors
+const logger = console;
+
+program
+ .version(packageJson.version)
+ .option('-s, --static-dir ', 'Directory where to load static files from', parseList)
+ .option('-o, --output-dir [dir-name]', 'Directory where to store built files')
+ .option('-c, --config-dir [dir-name]', 'Directory where to load Storybook configurations from')
+ .option('-d, --db-path [db-file]', 'DEPRECATED!')
+ .option('--enable-db', 'DEPRECATED!')
+ .parse(process.argv);
+
+logger.info(chalk.bold(`${packageJson.name} v${packageJson.version}\n`));
+
+if (program.enableDb || program.dbPath) {
+ logger.error(
+ [
+ 'Error: the experimental local database addon is no longer bundled with',
+ 'react-storybook. Please remove these flags (-d,--db-path,--enable-db)',
+ 'from the command or npm script and try again.',
+ ].join(' ')
+ );
+ process.exit(1);
+}
+
+// The key is the field created in `program` variable for
+// each command line argument. Value is the env variable.
+getEnvConfig(program, {
+ staticDir: 'SBCONFIG_STATIC_DIR',
+ outputDir: 'SBCONFIG_OUTPUT_DIR',
+ configDir: 'SBCONFIG_CONFIG_DIR',
+});
+
+const configDir = program.configDir || './.storybook';
+const outputDir = program.outputDir || './storybook-static';
+
+// create output directory if not exists
+shelljs.mkdir('-p', path.resolve(outputDir));
+// clear the static dir
+shelljs.rm('-rf', path.resolve(outputDir, 'static'));
+shelljs.cp(path.resolve(__dirname, 'public/favicon.ico'), outputDir);
+
+// Build the webpack configuration using the `baseConfig`
+// custom `.babelrc` file and `webpack.config.js` files
+// NOTE changes to env should be done before calling `getBaseConfig`
+const config = loadConfig('PRODUCTION', getBaseConfig(), configDir);
+config.output.path = path.resolve(outputDir);
+
+// copy all static files
+if (program.staticDir) {
+ program.staticDir.forEach(dir => {
+ if (!fs.existsSync(dir)) {
+ logger.error(`Error: no such directory to load static files: ${dir}`);
+ process.exit(-1);
+ }
+ logger.log(`=> Copying static files from: ${dir}`);
+ shelljs.cp('-r', `${dir}/*`, outputDir);
+ });
+}
+
+// compile all resources with webpack and write them to the disk.
+logger.log('Building storybook ...');
+webpack(config).run((err, stats) => {
+ if (err || stats.hasErrors()) {
+ logger.error('Failed to build the storybook');
+ // eslint-disable-next-line no-unused-expressions
+ err && logger.error(err.message);
+ // eslint-disable-next-line no-unused-expressions
+ stats.hasErrors() && stats.toJson().errors.forEach(e => logger.error(e));
+ process.exit(1);
+ }
+});
diff --git a/app/polymer/src/server/config.js b/app/polymer/src/server/config.js
new file mode 100644
index 00000000000..9b8c0755891
--- /dev/null
+++ b/app/polymer/src/server/config.js
@@ -0,0 +1,88 @@
+/* eslint-disable global-require, import/no-dynamic-require */
+import fs from 'fs';
+import path from 'path';
+import findCacheDir from 'find-cache-dir';
+import loadBabelConfig from './babel_config';
+
+// avoid ESLint errors
+const logger = console;
+
+// `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 storybookDefaultAddonsPath = path.resolve(__dirname, 'addons.js');
+ const storybookCustomAddonsPath = path.resolve(configDir, 'addons.js');
+ if (fs.existsSync(storybookCustomAddonsPath)) {
+ logger.info('=> Loading custom addons config.');
+ config.entry.manager.splice(1, 0, storybookCustomAddonsPath);
+ } else {
+ config.entry.manager.splice(1, 0, storybookDefaultAddonsPath);
+ }
+
+ // 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 based on "vue-cli".');
+ const configPath = path.resolve(__dirname, './config/defaults/webpack.config.js');
+ const customConfig = require(configPath);
+
+ return customConfig(config);
+ }
+ const customConfig = require(customConfigPath);
+
+ if (typeof customConfig === 'function') {
+ logger.info('=> Loading custom webpack config (full-control mode).');
+ return customConfig(config, configType);
+ }
+ 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.rules || [])],
+ },
+ resolve: {
+ ...config.resolve,
+ ...customConfig.resolve,
+ alias: {
+ ...config.alias,
+ ...(customConfig.resolve && customConfig.resolve.alias),
+ },
+ },
+ };
+}
diff --git a/app/polymer/src/server/config/WatchMissingNodeModulesPlugin.js b/app/polymer/src/server/config/WatchMissingNodeModulesPlugin.js
new file mode 100644
index 00000000000..962bbb97ff4
--- /dev/null
+++ b/app/polymer/src/server/config/WatchMissingNodeModulesPlugin.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+// @remove-on-eject-end
+
+// This Webpack plugin ensures `npm install ` forces a project rebuild.
+// We’re not sure why this isn't Webpack's default behavior.
+// See https://github.com/facebookincubator/create-react-app/issues/186.
+
+function WatchMissingNodeModulesPlugin(nodeModulesPath) {
+ this.nodeModulesPath = nodeModulesPath;
+}
+
+WatchMissingNodeModulesPlugin.prototype.apply = function apply(compiler) {
+ compiler.plugin('emit', (compilation, callback) => {
+ const missingDeps = compilation.missingDependencies;
+ const { nodeModulesPath } = this;
+
+ // If any missing files are expected to appear in node_modules...
+ if (missingDeps.some(file => file.indexOf(nodeModulesPath) !== -1)) {
+ // ...tell webpack to watch node_modules recursively until they appear.
+ compilation.contextDependencies.push(nodeModulesPath);
+ }
+
+ callback();
+ });
+};
+
+module.exports = WatchMissingNodeModulesPlugin;
diff --git a/app/polymer/src/server/config/babel.js b/app/polymer/src/server/config/babel.js
new file mode 100644
index 00000000000..2f904b2e2fd
--- /dev/null
+++ b/app/polymer/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'),
+ require.resolve('babel-preset-react'),
+ ],
+ plugins: [
+ require.resolve('babel-plugin-transform-regenerator'),
+ [
+ require.resolve('babel-plugin-transform-runtime'),
+ {
+ helpers: true,
+ polyfill: true,
+ regenerator: true,
+ },
+ ],
+ ],
+};
diff --git a/app/polymer/src/server/config/babel.prod.js b/app/polymer/src/server/config/babel.prod.js
new file mode 100644
index 00000000000..59a87fe49bc
--- /dev/null
+++ b/app/polymer/src/server/config/babel.prod.js
@@ -0,0 +1,29 @@
+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-react'),
+ 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/polymer/src/server/config/defaults/webpack.config.js b/app/polymer/src/server/config/defaults/webpack.config.js
new file mode 100644
index 00000000000..477d1c4c62b
--- /dev/null
+++ b/app/polymer/src/server/config/defaults/webpack.config.js
@@ -0,0 +1,73 @@
+// import webpack from 'webpack';
+import autoprefixer from 'autoprefixer';
+import { includePaths } from '../utils';
+
+// Add a default custom config which is similar to what React Create App does.
+module.exports = storybookBaseConfig => {
+ const newConfig = { ...storybookBaseConfig };
+
+ newConfig.module.rules = [
+ ...storybookBaseConfig.module.rules,
+ {
+ test: /\.css$/,
+ use: [
+ require.resolve('style-loader'),
+ {
+ loader: require.resolve('css-loader'),
+ options: {
+ importLoaders: 1,
+ },
+ },
+ {
+ loader: require.resolve('postcss-loader'),
+ options: {
+ ident: 'postcss', // https://webpack.js.org/guides/migrating/#complex-options
+ plugins: () => [
+ require('postcss-flexbugs-fixes'), // eslint-disable-line
+ autoprefixer({
+ browsers: [
+ '>1%',
+ 'last 4 versions',
+ 'Firefox ESR',
+ 'not ie < 9', // React doesn't support IE8 anyway
+ ],
+ flexbox: 'no-2009',
+ }),
+ ],
+ },
+ },
+ ],
+ },
+ {
+ test: /\.json$/,
+ include: includePaths,
+ loader: require.resolve('json-loader'),
+ },
+ {
+ test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
+ include: includePaths,
+ loader: require.resolve('file-loader'),
+ query: {
+ name: 'static/media/[name].[hash:8].[ext]',
+ },
+ },
+ {
+ test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
+ include: includePaths,
+ loader: require.resolve('url-loader'),
+ query: {
+ limit: 10000,
+ name: 'static/media/[name].[hash:8].[ext]',
+ },
+ },
+ ];
+
+ newConfig.resolve.alias = {
+ ...storybookBaseConfig.resolve.alias,
+ // This is to support NPM2
+ 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator'),
+ };
+
+ // Return the altered config
+ return newConfig;
+};
diff --git a/app/polymer/src/server/config/globals.js b/app/polymer/src/server/config/globals.js
new file mode 100644
index 00000000000..061e9c46525
--- /dev/null
+++ b/app/polymer/src/server/config/globals.js
@@ -0,0 +1,4 @@
+/* globals window */
+
+window.STORYBOOK_REACT_CLASSES = {};
+window.STORYBOOK_ENV = 'vue';
diff --git a/app/polymer/src/server/config/polyfills.js b/app/polymer/src/server/config/polyfills.js
new file mode 100644
index 00000000000..869b6824b5f
--- /dev/null
+++ b/app/polymer/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/polymer/src/server/config/utils.js b/app/polymer/src/server/config/utils.js
new file mode 100644
index 00000000000..0236481efd7
--- /dev/null
+++ b/app/polymer/src/server/config/utils.js
@@ -0,0 +1,37 @@
+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,
+ };
+}
+
+export const getConfigDir = () => process.env.SBCONFIG_CONFIG_DIR || './.storybook';
diff --git a/app/polymer/src/server/config/webpack.config.js b/app/polymer/src/server/config/webpack.config.js
new file mode 100644
index 00000000000..08f3a0a11b0
--- /dev/null
+++ b/app/polymer/src/server/config/webpack.config.js
@@ -0,0 +1,102 @@
+import path from 'path';
+import webpack from 'webpack';
+import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import WatchMissingNodeModulesPlugin from './WatchMissingNodeModulesPlugin';
+import {
+ getConfigDir,
+ includePaths,
+ excludePaths,
+ nodeModulesPaths,
+ loadEnv,
+ nodePaths,
+} from './utils';
+import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils';
+import babelLoaderConfig from './babel';
+import { version } from '../../../package.json';
+
+export default function() {
+ const config = {
+ devtool: 'cheap-module-source-map',
+ entry: {
+ manager: [require.resolve('./polyfills'), require.resolve('../../client/manager')],
+ 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(getConfigDir()),
+ version,
+ },
+ template: require.resolve('../index.html.ejs'),
+ }),
+ new HtmlWebpackPlugin({
+ filename: 'iframe.html',
+ excludeChunks: ['manager'],
+ data: {
+ previewHead: getPreviewHeadHtml(getConfigDir()),
+ },
+ template: require.resolve('../iframe.html.ejs'),
+ }),
+ new webpack.DefinePlugin(loadEnv()),
+ new webpack.HotModuleReplacementPlugin(),
+ new CaseSensitivePathsPlugin(),
+ new WatchMissingNodeModulesPlugin(nodeModulesPaths),
+ new webpack.ProgressPlugin(),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ loader: require.resolve('babel-loader'),
+ query: babelLoaderConfig,
+ include: includePaths,
+ exclude: excludePaths,
+ },
+ {
+ test: /\.html$/,
+ exclude: /node_modules\/(?!(polymer-redux|polymer-webpack-loader)\/).*/,
+ use: [
+ {
+ loader: require.resolve('babel-loader'),
+ options: { cacheDirectory: '.babel-cache' },
+ },
+ {
+ loader: require.resolve('polymer-webpack-loader'),
+ options: { processStyleLinks: true },
+ },
+ ],
+ },
+ ],
+ },
+ 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),
+ alias: {
+ vue$: require.resolve('vue/dist/vue.esm.js'),
+ react$: require.resolve('react'),
+ 'react-dom$': require.resolve('react-dom'),
+ },
+ },
+ performance: {
+ hints: false,
+ },
+ };
+
+ return config;
+}
diff --git a/app/polymer/src/server/config/webpack.config.prod.js b/app/polymer/src/server/config/webpack.config.prod.js
new file mode 100644
index 00000000000..f4fca7240d7
--- /dev/null
+++ b/app/polymer/src/server/config/webpack.config.prod.js
@@ -0,0 +1,91 @@
+import path from 'path';
+import webpack from 'webpack';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import babelLoaderConfig from './babel.prod';
+import { getConfigDir, includePaths, excludePaths, loadEnv, nodePaths } from './utils';
+import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils';
+import { version } from '../../../package.json';
+
+export default function() {
+ const entries = {
+ preview: [require.resolve('./polyfills'), require.resolve('./globals')],
+ manager: [require.resolve('./polyfills'), path.resolve(__dirname, '../../client/manager')],
+ };
+
+ const config = {
+ 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(getConfigDir()),
+ version,
+ },
+ template: require.resolve('../index.html.ejs'),
+ }),
+ new HtmlWebpackPlugin({
+ filename: 'iframe.html',
+ excludeChunks: ['manager'],
+ data: {
+ previewHead: getPreviewHeadHtml(getConfigDir()),
+ },
+ template: require.resolve('../iframe.html.ejs'),
+ }),
+ new webpack.DefinePlugin(loadEnv({ production: true })),
+ new webpack.optimize.UglifyJsPlugin({
+ compress: {
+ screw_ie8: true,
+ warnings: false,
+ },
+ mangle: false,
+ output: {
+ comments: false,
+ screw_ie8: true,
+ },
+ }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ loader: require.resolve('babel-loader'),
+ query: babelLoaderConfig,
+ include: includePaths,
+ exclude: excludePaths,
+ },
+ {
+ test: /\.vue$/,
+ loader: require.resolve('vue-loader'),
+ options: {},
+ },
+ ],
+ },
+ 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),
+ alias: {
+ vue$: require.resolve('vue/dist/vue.esm.js'),
+ react$: require.resolve('react'),
+ 'react-dom$': require.resolve('react-dom'),
+ },
+ },
+ };
+
+ return config;
+}
diff --git a/app/polymer/src/server/iframe.html.ejs b/app/polymer/src/server/iframe.html.ejs
new file mode 100644
index 00000000000..32318e29e95
--- /dev/null
+++ b/app/polymer/src/server/iframe.html.ejs
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ Storybook
+ <%= htmlWebpackPlugin.options.data.previewHead %>
+
+
+
+
+
+
diff --git a/app/polymer/src/server/index.html.ejs b/app/polymer/src/server/index.html.ejs
new file mode 100644
index 00000000000..397aaf41d52
--- /dev/null
+++ b/app/polymer/src/server/index.html.ejs
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+ Storybook
+
+ <%= htmlWebpackPlugin.options.data.managerHead %>
+
+
+
+
+
+
diff --git a/app/polymer/src/server/index.js b/app/polymer/src/server/index.js
new file mode 100755
index 00000000000..5871f7bf70e
--- /dev/null
+++ b/app/polymer/src/server/index.js
@@ -0,0 +1,167 @@
+import express from 'express';
+import https from 'https';
+import favicon from 'serve-favicon';
+import program from 'commander';
+import path from 'path';
+import fs from 'fs';
+import chalk from 'chalk';
+import shelljs from 'shelljs';
+import storybook, { webpackValid } from './middleware';
+import packageJson from '../../package.json';
+import { parseList, getEnvConfig } from './utils';
+
+process.env.NODE_ENV = process.env.NODE_ENV || 'development';
+
+const logger = console;
+
+program
+ .version(packageJson.version)
+ .option('-p, --port [number]', 'Port to run Storybook (Required)', str => parseInt(str, 10))
+ .option('-h, --host [string]', 'Host to run Storybook')
+ .option('-s, --static-dir ', 'Directory where to load static files from')
+ .option('-c, --config-dir [dir-name]', 'Directory where to load Storybook configurations from')
+ .option(
+ '--https',
+ 'Serve Storybook over HTTPS. Note: You must provide your own certificate information.'
+ )
+ .option(
+ '--ssl-ca ',
+ 'Provide an SSL certificate authority. (Optional with --https, required if using a self-signed certificate)',
+ parseList
+ )
+ .option('--ssl-cert ', 'Provide an SSL certificate. (Required with --https)')
+ .option('--ssl-key ', 'Provide an SSL key. (Required with --https)')
+ .option('--smoke-test', 'Exit after successful start')
+ .option('-d, --db-path [db-file]', 'DEPRECATED!')
+ .option('--enable-db', 'DEPRECATED!')
+ .parse(process.argv);
+
+logger.info(chalk.bold(`${packageJson.name} v${packageJson.version}`) + chalk.reset('\n'));
+
+if (program.enableDb || program.dbPath) {
+ logger.error(
+ [
+ 'Error: the experimental local database addon is no longer bundled with',
+ 'react-storybook. Please remove these flags (-d,--db-path,--enable-db)',
+ 'from the command or npm script and try again.',
+ ].join(' ')
+ );
+ process.exit(1);
+}
+
+// The key is the field created in `program` variable for
+// each command line argument. Value is the env variable.
+getEnvConfig(program, {
+ port: 'SBCONFIG_PORT',
+ host: 'SBCONFIG_HOSTNAME',
+ staticDir: 'SBCONFIG_STATIC_DIR',
+ configDir: 'SBCONFIG_CONFIG_DIR',
+});
+
+if (!program.port) {
+ logger.error('Error: port to run Storybook is required!\n');
+ program.help();
+ process.exit(-1);
+}
+
+// Used with `app.listen` below
+const listenAddr = [program.port];
+
+if (program.host) {
+ listenAddr.push(program.host);
+}
+
+const app = express();
+let server = app;
+
+if (program.https) {
+ if (!program.sslCert) {
+ logger.error('Error: --ssl-cert is required with --https');
+ process.exit(-1);
+ }
+ if (!program.sslKey) {
+ logger.error('Error: --ssl-key is required with --https');
+ process.exit(-1);
+ }
+
+ const sslOptions = {
+ ca: (program.sslCa || []).map(ca => fs.readFileSync(ca, 'utf-8')),
+ cert: fs.readFileSync(program.sslCert, 'utf-8'),
+ key: fs.readFileSync(program.sslKey, 'utf-8'),
+ };
+
+ server = https.createServer(sslOptions, app);
+}
+
+let hasCustomFavicon = false;
+
+if (program.staticDir) {
+ program.staticDir = parseList(program.staticDir);
+ program.staticDir.forEach(dir => {
+ const staticPath = path.resolve(dir);
+ if (!fs.existsSync(staticPath)) {
+ logger.error(`Error: no such directory to load static files: ${staticPath}`);
+ process.exit(-1);
+ }
+ logger.log(`=> Loading static files from: ${staticPath} .`);
+ app.use(express.static(staticPath, { index: false }));
+
+ const faviconPath = path.resolve(staticPath, 'favicon.ico');
+ if (fs.existsSync(faviconPath)) {
+ hasCustomFavicon = true;
+ app.use(favicon(faviconPath));
+ }
+ });
+}
+
+if (!hasCustomFavicon) {
+ app.use(favicon(path.resolve(__dirname, 'public/favicon.ico')));
+}
+
+// Build the webpack configuration using the `baseConfig`
+// custom `.babelrc` file and `webpack.config.js` files
+const configDir = program.configDir || './.storybook';
+
+// The repository info is sent to the storybook while running on
+// development mode so it'll be easier for tools to integrate.
+const exec = cmd => shelljs.exec(cmd, { silent: true }).stdout.trim();
+process.env.STORYBOOK_GIT_ORIGIN =
+ process.env.STORYBOOK_GIT_ORIGIN || exec('git remote get-url origin');
+process.env.STORYBOOK_GIT_BRANCH =
+ process.env.STORYBOOK_GIT_BRANCH || exec('git symbolic-ref HEAD --short');
+
+// NOTE changes to env should be done before calling `getBaseConfig`
+// `getBaseConfig` function which is called inside the middleware
+app.use(storybook(configDir));
+
+let serverResolve = () => {};
+let serverReject = () => {};
+const serverListening = new Promise((resolve, reject) => {
+ serverResolve = resolve;
+ serverReject = reject;
+});
+server.listen(...listenAddr, error => {
+ if (error) {
+ serverReject(error);
+ } else {
+ serverResolve();
+ }
+});
+
+Promise.all([webpackValid, serverListening])
+ .then(() => {
+ const proto = program.https ? 'https' : 'http';
+ const address = `${proto}://${program.host || 'localhost'}:${program.port}/`;
+ logger.info(`Storybook started on => ${chalk.cyan(address)}\n`);
+ if (program.smokeTest) {
+ process.exit(0);
+ }
+ })
+ .catch(error => {
+ if (error instanceof Error) {
+ logger.error(error);
+ }
+ if (program.smokeTest) {
+ process.exit(1);
+ }
+ });
diff --git a/app/polymer/src/server/middleware.js b/app/polymer/src/server/middleware.js
new file mode 100644
index 00000000000..1dd6fa775b1
--- /dev/null
+++ b/app/polymer/src/server/middleware.js
@@ -0,0 +1,64 @@
+import { Router } from 'express';
+import webpack from 'webpack';
+import path from 'path';
+import webpackDevMiddleware from 'webpack-dev-middleware';
+import webpackHotMiddleware from 'webpack-hot-middleware';
+import getBaseConfig from './config/webpack.config';
+import loadConfig from './config';
+import { getMiddleware } from './utils';
+
+let webpackResolve = () => {};
+let webpackReject = () => {};
+export const webpackValid = new Promise((resolve, reject) => {
+ webpackResolve = resolve;
+ webpackReject = reject;
+});
+
+export default function(configDir) {
+ // Build the webpack configuration using the `getBaseConfig`
+ // custom `.babelrc` file and `webpack.config.js` files
+ const config = loadConfig('DEVELOPMENT', getBaseConfig(), configDir);
+ const middlewareFn = getMiddleware(configDir);
+
+ // remove the leading '/'
+ let { publicPath } = config.output;
+ if (publicPath[0] === '/') {
+ publicPath = publicPath.slice(1);
+ }
+
+ const compiler = webpack(config);
+ const devMiddlewareOptions = {
+ noInfo: true,
+ publicPath: config.output.publicPath,
+ watchOptions: config.watchOptions || {},
+ ...config.devServer,
+ };
+
+ const router = new Router();
+ const webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, devMiddlewareOptions);
+ router.use(webpackDevMiddlewareInstance);
+ router.use(webpackHotMiddleware(compiler));
+
+ // custom middleware
+ middlewareFn(router);
+
+ webpackDevMiddlewareInstance.waitUntilValid(stats => {
+ router.get('/', (req, res) => {
+ res.set('Content-Type', 'text/html');
+ res.sendFile(path.join(`${__dirname}/public/index.html`));
+ });
+
+ router.get('/iframe.html', (req, res) => {
+ res.set('Content-Type', 'text/html');
+ res.sendFile(path.join(`${__dirname}/public/iframe.html`));
+ });
+
+ if (stats.toJson().errors.length) {
+ webpackReject(stats);
+ } else {
+ webpackResolve(stats);
+ }
+ });
+
+ return router;
+}
diff --git a/app/polymer/src/server/public/favicon.ico b/app/polymer/src/server/public/favicon.ico
new file mode 100755
index 00000000000..e1cf7f1c59f
Binary files /dev/null and b/app/polymer/src/server/public/favicon.ico differ
diff --git a/app/polymer/src/server/utils.js b/app/polymer/src/server/utils.js
new file mode 100644
index 00000000000..71396437a03
--- /dev/null
+++ b/app/polymer/src/server/utils.js
@@ -0,0 +1,56 @@
+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 parseList(str) {
+ return str.split(',');
+}
+
+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;
+}
+
+export function getEnvConfig(program, configEnv) {
+ Object.keys(configEnv).forEach(fieldName => {
+ const envVarName = configEnv[fieldName];
+ const envVarValue = process.env[envVarName];
+ if (envVarValue) {
+ program[fieldName] = envVarValue; // eslint-disable-line
+ }
+ });
+}
+
+export function getMiddleware(configDir) {
+ const middlewarePath = path.resolve(configDir, 'middleware.js');
+ if (fs.existsSync(middlewarePath)) {
+ let middlewareModule = require(middlewarePath); // eslint-disable-line
+ if (middlewareModule.__esModule) { // eslint-disable-line
+ middlewareModule = middlewareModule.default;
+ }
+ return middlewareModule;
+ }
+ return () => {};
+}
diff --git a/app/polymer/src/server/utils.test.js b/app/polymer/src/server/utils.test.js
new file mode 100644
index 00000000000..a8db3b65a21
--- /dev/null
+++ b/app/polymer/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/examples/polymer-cli/README.md b/examples/polymer-cli/README.md
new file mode 100644
index 00000000000..d5f5236d262
--- /dev/null
+++ b/examples/polymer-cli/README.md
@@ -0,0 +1,3 @@
+# Polymer kitchen sink example
+
+TODO