mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-02 05:03:44 +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