ADD base for app/polymer && ADD base for example/polymer-cli

This commit is contained in:
Norbert de Langen 2017-11-03 09:39:32 +01:00
parent d997c57248
commit f8198397a1
No known key found for this signature in database
GPG Key ID: 976651DA156C2825
46 changed files with 2375 additions and 0 deletions

3
app/polymer/.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["env", "stage-0", "react"]
}

3
app/polymer/.npmignore Normal file
View File

@ -0,0 +1,3 @@
docs
src
.babelrc

43
app/polymer/README.md Normal file
View File

@ -0,0 +1,43 @@
# Storybook for Polymer
[![Build Status on CircleCI](https://circleci.com/gh/storybooks/storybook.svg?style=shield)](https://circleci.com/gh/storybooks/storybook)
[![CodeFactor](https://www.codefactor.io/repository/github/storybooks/storybook/badge)](https://www.codefactor.io/repository/github/storybooks/storybook)
[![Known Vulnerabilities](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847/badge.svg)](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847)
[![BCH compliance](https://bettercodehub.com/edge/badge/storybooks/storybook)](https://bettercodehub.com/results/storybooks/storybook) [![codecov](https://codecov.io/gh/storybooks/storybook/branch/master/graph/badge.svg)](https://codecov.io/gh/storybooks/storybook)
[![Storybook Slack](https://now-examples-slackin-nqnzoygycp.now.sh/badge.svg)](https://now-examples-slackin-nqnzoygycp.now.sh/)
[![Backers on Open Collective](https://opencollective.com/storybook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/storybook/sponsors/badge.svg)](#sponsors)
* * *
Storybook for 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 Screenshot](https://github.com/storybooks/storybook/blob/master/app/vue/docs/demo.gif)
Storybook runs outside of your app.
So you can develop UI components in isolation without worrying about app specific dependencies and requirements.
## Getting Started
> 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!

3
app/polymer/bin/build.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../dist/server/build');

3
app/polymer/bin/index.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../dist/server');

BIN
app/polymer/docs/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

82
app/polymer/package.json Normal file
View File

@ -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": {}
}

View File

@ -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'
// );

View File

@ -0,0 +1,7 @@
/* global document */
import renderStorybookUI from '@storybook/ui';
import Provider from './provider';
const rootEl = document.getElementById('root');
renderStorybookUI(rootEl, new Provider());

View File

@ -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 (
<iframe
id="storybook-preview-iframe"
title="preview"
style={iframeStyle}
src={this.props.url}
allowFullScreen
/>
);
}
}
Preview.propTypes = {
url: PropTypes.string.isRequired,
};
export default Preview;

View File

@ -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 <Preview url={url} />;
}
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);
}
}

View File

@ -0,0 +1,54 @@
<template>
<div class="errordisplay_main">
<div class="errordisplay_heading">{{ message }}</div>
<pre class="errordisplay_code">
<code>
{{ stack }}
</code>
</pre>
</div>
</template>
<script>
export default {
name: 'error-display',
props: {
message: {
type: String,
required: true
},
stack: {
type: String,
required: true
}
}
}
</script>
<style>
.errordisplay_main {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 20;
background-color: rgb(187, 49, 49);
color: #FFF;
webkit-font-smoothing: antialiased;
}
.errordisplay_heading {
font-size: 20;
font-weight: 600;
letter-spacing: 0.2;
margin: 10px 0;
font-family: -apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif;
}
.errordisplay_code {
font-size: 14;
width: 100vw;
overflow: auto;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<div class="nopreview_wrapper">
<div class="nopreview_main">
<h1 class="nopreview_heading">No Preview</h1>
<p>Sorry, but you either have no stories or none are selected somehow.</p>
<ul>
<li>Please check the storybook config.</li>
<li>Try reloading the page.</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'no-preview',
}
</script>
<style>
.nopreview_wrapper {
position: fixed;
display: flex;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 20;
align-content: center;
justify-content: center;
font-family: -apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif;
webkit-font-smoothing: antialiased;
}
.nopreview_main {
margin: auto;
padding: 30px;
border-radius: 10px;
background: rgba(0,0,0,0.03);
}
.nopreview_heading {
font-size: 20;
font-weight: 600;
letter-spacing: 0.2;
margin: 10px 0;
text-align: center;
}
</style>

View File

@ -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,
};
}

View File

@ -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 };
});
}
}

View File

@ -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: `<div>aa${fn().template}</div>` }));
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
expect(storyStore.stories[0].fn().template).toBe('<div>aa<p>hello</p></div>');
});
it('should add global decorators', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
api.addDecorator(fn => ({ template: `<div>bb${fn().template}</div>` }));
const localApi = api.storiesOf('none', module);
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
expect(storyStore.stories[0].fn().template).toBe('<div>bb<p>hello</p></div>');
});
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: `<div>aa${fn().template}</div>` }));
localApi.addDecorator(fn => ({ template: `<div>bb${fn().template}</div>` }));
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
expect(storyStore.stories[0].fn().template).toBe('<div>aa<div>bb<p>hello</p></div></div>');
});
it('should pass the context', () => {
const storyStore = new StoryStore();
const api = new ClientAPI({ storyStore });
const localApi = api.storiesOf('none', module);
localApi.addDecorator(fn => ({ template: `<div>aa${fn().template}</div>` }));
localApi.add('storyName', ({ kind, story }) => ({ template: `<p>${kind}-${story}</p>` }));
const kind = 'dfdfd';
const story = 'ef349ff';
const result = storyStore.stories[0].fn({ kind, story });
expect(result.template).toBe(`<div>aa<p>${kind}-${story}</p></div>`);
});
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: `<div>${kind}-${story}-${fn().template}</div>`,
}));
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
const kind = 'dfdfd';
const story = 'ef349ff';
const result = storyStore.stories[0].fn({ kind, story });
expect(result.template).toBe(`<div>${kind}-${story}-<p>hello</p></div>`);
});
});
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'] },
],
},
]);
});
});
});

View File

@ -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();
}
}
}

View File

@ -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);

View File

@ -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 });
}
};
}

View File

@ -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;
}
}

View File

@ -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: '<my-comp></my-comp>' })" or "() => ({ components: MyComp, template: '<my-comp></my-comp>' })" 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);
// }
}

View File

@ -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]);
}
}

View File

@ -0,0 +1,2 @@
// import '@storybook/addon-actions/register';
// import '@storybook/addon-links/register';

View File

@ -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;
}

View File

@ -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'],
});
});
});

85
app/polymer/src/server/build.js Executable file
View File

@ -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 <dir-names>', '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);
}
});

View File

@ -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),
},
},
};
}

View File

@ -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 <library>` forces a project rebuild.
// Were 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;

View File

@ -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,
},
],
],
};

View File

@ -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,
},
],
],
};

View File

@ -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;
};

View File

@ -0,0 +1,4 @@
/* globals window */
window.STORYBOOK_REACT_CLASSES = {};
window.STORYBOOK_ENV = 'vue';

View File

@ -0,0 +1,3 @@
import 'core-js/es6/symbol';
import 'core-js/fn/array/iterator';
import 'airbnb-js-shims';

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base target="_parent">
<script>
if (window.parent !== window) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__VUE_DEVTOOLS_GLOBAL_HOOK__;
window.parent.__VUE_DEVTOOLS_CONTEXT__ = window.document;
}
</script>
<title>Storybook</title>
<%= htmlWebpackPlugin.options.data.previewHead %>
</head>
<body>
<div id="root"></div>
<div id="error-display"></div>
</body>
</html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="storybook-version" content="<%= htmlWebpackPlugin.options.data.version %>">
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<title>Storybook</title>
<style>
/*
When resizing panels, the drag event breaks if the cursor
moves over the iframe. Add the 'dragging' class to the body
at drag start and remove it when the drag ends.
*/
.dragging iframe {
pointer-events: none;
}
/* Styling the fuzzy search box placeholders */
.searchBox::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: #ddd;
font-size: 16px;
}
.searchBox::-moz-placeholder { /* Firefox 19+ */
color: #ddd;
font-size: 16px;
}
.searchBox:focus{
border-color: #EEE !important;
}
.btn:hover{
background-color: #eee
}
</style>
<%= htmlWebpackPlugin.options.data.managerHead %>
</head>
<body style="margin: 0;">
<div id="root"></div>
</body>
</html>

167
app/polymer/src/server/index.js Executable file
View File

@ -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 <dir-names>', '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 <ca>',
'Provide an SSL certificate authority. (Optional with --https, required if using a self-signed certificate)',
parseList
)
.option('--ssl-cert <cert>', 'Provide an SSL certificate. (Required with --https)')
.option('--ssl-key <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);
}
});

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -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 () => {};
}

View File

@ -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);
});
});

View File

@ -0,0 +1,3 @@
# Polymer kitchen sink example
TODO