mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-28 05:10:17 +08:00
ADD base for app/polymer && ADD base for example/polymer-cli
This commit is contained in:
parent
d997c57248
commit
f8198397a1
3
app/polymer/.babelrc
Normal file
3
app/polymer/.babelrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["env", "stage-0", "react"]
|
||||
}
|
3
app/polymer/.npmignore
Normal file
3
app/polymer/.npmignore
Normal file
@ -0,0 +1,3 @@
|
||||
docs
|
||||
src
|
||||
.babelrc
|
43
app/polymer/README.md
Normal file
43
app/polymer/README.md
Normal file
@ -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!
|
3
app/polymer/bin/build.js
Executable file
3
app/polymer/bin/build.js
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('../dist/server/build');
|
3
app/polymer/bin/index.js
Executable file
3
app/polymer/bin/index.js
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('../dist/server');
|
BIN
app/polymer/docs/demo.gif
Normal file
BIN
app/polymer/docs/demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
BIN
app/polymer/docs/react_storybook_screenshot.png
Normal file
BIN
app/polymer/docs/react_storybook_screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 245 KiB |
BIN
app/polymer/docs/storybooks_io_logo.png
Normal file
BIN
app/polymer/docs/storybooks_io_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
82
app/polymer/package.json
Normal file
82
app/polymer/package.json
Normal 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": {}
|
||||
}
|
17
app/polymer/src/client/index.js
Normal file
17
app/polymer/src/client/index.js
Normal 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'
|
||||
// );
|
7
app/polymer/src/client/manager/index.js
Normal file
7
app/polymer/src/client/manager/index.js
Normal 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());
|
39
app/polymer/src/client/manager/preview.js
Normal file
39
app/polymer/src/client/manager/preview.js
Normal 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;
|
51
app/polymer/src/client/manager/provider.js
Normal file
51
app/polymer/src/client/manager/provider.js
Normal 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);
|
||||
}
|
||||
}
|
54
app/polymer/src/client/preview/ErrorDisplay.vue
Normal file
54
app/polymer/src/client/preview/ErrorDisplay.vue
Normal 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>
|
49
app/polymer/src/client/preview/NoPreview.vue
Normal file
49
app/polymer/src/client/preview/NoPreview.vue
Normal 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>
|
34
app/polymer/src/client/preview/actions.js
Normal file
34
app/polymer/src/client/preview/actions.js
Normal 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,
|
||||
};
|
||||
}
|
119
app/polymer/src/client/preview/client_api.js
Normal file
119
app/polymer/src/client/preview/client_api.js
Normal 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 };
|
||||
});
|
||||
}
|
||||
}
|
295
app/polymer/src/client/preview/client_api.test.js
Normal file
295
app/polymer/src/client/preview/client_api.test.js
Normal 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'] },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
67
app/polymer/src/client/preview/config_api.js
Normal file
67
app/polymer/src/client/preview/config_api.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
55
app/polymer/src/client/preview/index.js
Normal file
55
app/polymer/src/client/preview/index.js
Normal 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);
|
18
app/polymer/src/client/preview/init.js
Normal file
18
app/polymer/src/client/preview/init.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
}
|
40
app/polymer/src/client/preview/reducer.js
Normal file
40
app/polymer/src/client/preview/reducer.js
Normal 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;
|
||||
}
|
||||
}
|
101
app/polymer/src/client/preview/render.js
Normal file
101
app/polymer/src/client/preview/render.js
Normal 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);
|
||||
// }
|
||||
}
|
99
app/polymer/src/client/preview/story_store.js
Normal file
99
app/polymer/src/client/preview/story_store.js
Normal 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]);
|
||||
}
|
||||
}
|
2
app/polymer/src/server/addons.js
Normal file
2
app/polymer/src/server/addons.js
Normal file
@ -0,0 +1,2 @@
|
||||
// import '@storybook/addon-actions/register';
|
||||
// import '@storybook/addon-links/register';
|
84
app/polymer/src/server/babel_config.js
Normal file
84
app/polymer/src/server/babel_config.js
Normal 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;
|
||||
}
|
110
app/polymer/src/server/babel_config.test.js
Normal file
110
app/polymer/src/server/babel_config.test.js
Normal 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
85
app/polymer/src/server/build.js
Executable 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);
|
||||
}
|
||||
});
|
88
app/polymer/src/server/config.js
Normal file
88
app/polymer/src/server/config.js
Normal 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),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
@ -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.
|
||||
// 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;
|
28
app/polymer/src/server/config/babel.js
Normal file
28
app/polymer/src/server/config/babel.js
Normal 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,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
29
app/polymer/src/server/config/babel.prod.js
Normal file
29
app/polymer/src/server/config/babel.prod.js
Normal 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,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
73
app/polymer/src/server/config/defaults/webpack.config.js
Normal file
73
app/polymer/src/server/config/defaults/webpack.config.js
Normal 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;
|
||||
};
|
4
app/polymer/src/server/config/globals.js
Normal file
4
app/polymer/src/server/config/globals.js
Normal file
@ -0,0 +1,4 @@
|
||||
/* globals window */
|
||||
|
||||
window.STORYBOOK_REACT_CLASSES = {};
|
||||
window.STORYBOOK_ENV = 'vue';
|
3
app/polymer/src/server/config/polyfills.js
Normal file
3
app/polymer/src/server/config/polyfills.js
Normal file
@ -0,0 +1,3 @@
|
||||
import 'core-js/es6/symbol';
|
||||
import 'core-js/fn/array/iterator';
|
||||
import 'airbnb-js-shims';
|
37
app/polymer/src/server/config/utils.js
Normal file
37
app/polymer/src/server/config/utils.js
Normal 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';
|
102
app/polymer/src/server/config/webpack.config.js
Normal file
102
app/polymer/src/server/config/webpack.config.js
Normal 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;
|
||||
}
|
91
app/polymer/src/server/config/webpack.config.prod.js
Normal file
91
app/polymer/src/server/config/webpack.config.prod.js
Normal 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;
|
||||
}
|
20
app/polymer/src/server/iframe.html.ejs
Normal file
20
app/polymer/src/server/iframe.html.ejs
Normal 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>
|
44
app/polymer/src/server/index.html.ejs
Normal file
44
app/polymer/src/server/index.html.ejs
Normal 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
167
app/polymer/src/server/index.js
Executable 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);
|
||||
}
|
||||
});
|
64
app/polymer/src/server/middleware.js
Normal file
64
app/polymer/src/server/middleware.js
Normal 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;
|
||||
}
|
BIN
app/polymer/src/server/public/favicon.ico
Executable file
BIN
app/polymer/src/server/public/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
56
app/polymer/src/server/utils.js
Normal file
56
app/polymer/src/server/utils.js
Normal 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 () => {};
|
||||
}
|
69
app/polymer/src/server/utils.test.js
Normal file
69
app/polymer/src/server/utils.test.js
Normal 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);
|
||||
});
|
||||
});
|
3
examples/polymer-cli/README.md
Normal file
3
examples/polymer-cli/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Polymer kitchen sink example
|
||||
|
||||
TODO
|
Loading…
x
Reference in New Issue
Block a user