diff --git a/package.json b/package.json index d51af0e57737..9b8247321892 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "shelljs": "^0.7.3", "style-loader": "^0.13.1", "url-loader": "^0.5.7", + "uuid": "^3.0.1", "webpack": "^1.13.2", "webpack-dev-middleware": "^1.8.3", "webpack-hot-middleware": "^2.12.2" diff --git a/src/bin/storybook-start.js b/src/bin/storybook-start.js index fd9855b32c1a..ff859d45ab3b 100644 --- a/src/bin/storybook-start.js +++ b/src/bin/storybook-start.js @@ -8,7 +8,12 @@ import Server from '../server'; program .option('-h, --host ', 'host to listen on') .option('-p, --port ', 'port to listen on') + .option('-s, --secured', 'whether server is running on https') .option('-c, --config-dir [dir-name]', 'storybook config directory') + .option('-e, --environment [environment]', 'DEVELOPMENT/PRODUCTION environment for webpack') + .option('-r, --reset-cache', 'reset react native packager') + .option('--skip-packager', 'run only storybook server') + .option('-i, --manual-id', 'allow multiple users to work with same storybook') .parse(process.argv); const projectDir = path.resolve(); @@ -18,7 +23,14 @@ if (program.host) { listenAddr.push(program.host); } -const server = new Server({projectDir, configDir}); +const server = new Server({ + projectDir, + configDir, + environment: program.environment, + manualId: program.manualId, + secured: program.secured +}); + server.listen(...listenAddr, function (err) { if (err) { throw err; @@ -27,11 +39,15 @@ server.listen(...listenAddr, function (err) { console.info(`\nReact Native Storybook started on => ${address}\n`); }); -const projectRoots = configDir === projectDir ? [configDir] : [configDir, projectDir]; +if (!program.skipPackager) { + const projectRoots = configDir === projectDir ? [configDir] : [configDir, projectDir]; // RN packager -shelljs.exec([ - 'node node_modules/react-native/local-cli/cli.js start', - `--projectRoots ${projectRoots.join(',')}`, - `--root ${projectDir}`, -].join(' '), {async: true}); + shelljs.exec([ + 'node node_modules/react-native/local-cli/cli.js start', + `--projectRoots ${projectRoots.join(',')}`, + `--root ${projectDir}`, + program.resetCache && '--reset-cache' + ].filter(x => x).join(' '), {async: true}); + +} diff --git a/src/manager/index.js b/src/manager/index.js index c17ed07580d1..7acbd65d470a 100644 --- a/src/manager/index.js +++ b/src/manager/index.js @@ -2,4 +2,4 @@ import renderStorybookUI from '@kadira/storybook-ui'; import Provider from './provider'; const rootEl = document.getElementById('root'); -renderStorybookUI(rootEl, new Provider({ url: `ws://${location.host}` })); +renderStorybookUI(rootEl, new Provider({ url: location.host, options: window.storybookOptions})); diff --git a/src/manager/provider.js b/src/manager/provider.js index f7a7d4c668b5..b21a65dc7ffa 100644 --- a/src/manager/provider.js +++ b/src/manager/provider.js @@ -2,12 +2,25 @@ import React from 'react'; import { Provider } from '@kadira/storybook-ui'; import createChannel from '@kadira/storybook-channel-websocket'; import addons from '@kadira/storybook-addons'; +import uuid from 'uuid'; export default class ReactProvider extends Provider { - constructor({ url }) { + constructor({ url: domain, options }) { super(); + this.options = options; this.selection = null; this.channel = addons.getChannel(); + + const secured = options.secured; + const websocketType = secured ? 'wss' : 'ws'; + let url = websocketType + '://' + domain; + if (options.manualId) { + const pairedId = uuid().substr(-6); + + this.pairedId = pairedId; + url += '/pairedId=' + this.pairedId; + } + if (!this.channel) { this.channel = createChannel({ url }); addons.setChannel(this.channel); @@ -22,10 +35,19 @@ export default class ReactProvider extends Provider { this.selection = { kind, story }; this.channel.emit('setCurrentStory', { kind, story }); const renderPreview = addons.getPreview(); - if (renderPreview) { - return renderPreview(kind, story); + + const innerPreview = renderPreview ? renderPreview(kind, story) : null; + + if (this.options.manualId) { + return ( +
+ Your ID: { this.pairedId } + { innerPreview } +
+ ); } - return null; + + return innerPreview; } handleAPI(api) { diff --git a/src/preview/components/StoryView/index.js b/src/preview/components/StoryView/index.js index df417ca9ac3a..c2ad5f517af2 100644 --- a/src/preview/components/StoryView/index.js +++ b/src/preview/components/StoryView/index.js @@ -6,7 +6,14 @@ export default class StoryView extends Component { constructor(props, ...args) { super(props, ...args); this.state = {storyFn: null, selection: {}}; - this.props.events.on('story', this.selectStory.bind(this)); + + this.storyHandler = this.selectStory.bind(this); + + this.props.events.on('story', this.storyHandler); + } + + componentWillUnmount() { + this.props.events.removeListener('story', this.storyHandler); } selectStory(storyFn, selection) { diff --git a/src/preview/index.js b/src/preview/index.js index db7b65a66a78..6a68c9e7d96a 100644 --- a/src/preview/index.js +++ b/src/preview/index.js @@ -56,11 +56,20 @@ export default class Preview { return () => { let webUrl = null; let channel = addons.getChannel(); - if (!channel) { + if (params.resetStorybook || !channel) { const host = params.host || 'localhost'; - const port = params.port || 7007; - const url = `ws://${host}:${port}`; - webUrl = `http://${host}:${port}`; + + const port = params.port !== false + ? ':' + (params.port || 7007) + : ''; + + const query = params.query || ''; + const secured = params.secured; + const websocketType = secured ? 'wss' : 'ws'; + const httpType = secured ? 'https' : 'http'; + + const url = `${websocketType}://${host}${port}/${query}`; + webUrl = `${httpType}://${host}${port}`; channel = createChannel({ url }); addons.setChannel(channel); } diff --git a/src/server/config/webpack.config.prod.js b/src/server/config/webpack.config.prod.js index f932bfea11a0..b05f154f505e 100644 --- a/src/server/config/webpack.config.prod.js +++ b/src/server/config/webpack.config.prod.js @@ -14,13 +14,14 @@ const config = { devtool: '#cheap-module-source-map', entry: entries, output: { + path: path.join(__dirname, 'dist'), filename: 'static/[name].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: '', + publicPath: '/', }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), diff --git a/src/server/index.html.js b/src/server/index.html.js index e647e7122de1..8b47247ca793 100644 --- a/src/server/index.html.js +++ b/src/server/index.html.js @@ -1,6 +1,6 @@ import url from 'url'; -export default function (publicPath, settings) { +export default function (publicPath, options) { return ` @@ -40,6 +40,9 @@ export default function (publicPath, settings) {
+ diff --git a/src/server/index.js b/src/server/index.js index c14575951f8b..f13e756ed4f6 100755 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,4 +1,5 @@ import express from 'express'; +import querystring from 'querystring'; import http from 'http'; import ws from 'ws'; import storybook from './middleware'; @@ -8,15 +9,30 @@ export default class Server { this.options = options; this.httpServer = http.createServer(); this.expressApp = express(); - this.expressApp.use(storybook(options.projectDir, options.configDir)); + this.expressApp.use(storybook(options)); this.httpServer.on('request', this.expressApp); this.wsServer = ws.Server({server: this.httpServer}); this.wsServer.on('connection', s => this.handleWS(s)); } handleWS(socket) { + + if (this.options.manualId) { + const params = socket.upgradeReq && socket.upgradeReq.url + ? querystring.parse(socket.upgradeReq.url.substr(1)) + : {}; + + if (params.pairedId) { + socket.pairedId = params.pairedId; + } + } + socket.on('message', data => { - this.wsServer.clients.forEach(c => c.send(data)); + this.wsServer.clients.forEach(c => { + if (!this.options.manualId || (socket.pairedId && socket.pairedId === c.pairedId)) { + return c.send(data); + } + }); }); } diff --git a/src/server/middleware.js b/src/server/middleware.js index c7444a048a9e..180ead6e130d 100644 --- a/src/server/middleware.js +++ b/src/server/middleware.js @@ -5,6 +5,7 @@ import webpack from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; import baseConfig from './config/webpack.config'; +import baseProductionConfig from './config/webpack.config.prod'; import loadConfig from './config'; import getIndexHtml from './index.html'; @@ -20,10 +21,13 @@ function getMiddleware(configDir) { return function () {}; } -export default function (projectDir, configDir) { +export default function ({projectDir, configDir, ...options}) { // Build the webpack configuration using the `baseConfig` // custom `.babelrc` file and `webpack.config.js` files - const config = loadConfig('DEVELOPMENT', baseConfig, projectDir, configDir); + const environment = options.environment || 'DEVELOPMENT'; + const isProd = environment === 'PRODUCTION'; + const currentWebpackConfig = isProd ? baseProductionConfig : baseConfig; + const config = loadConfig(environment, currentWebpackConfig, projectDir, configDir); // remove the leading '/' let publicPath = config.output.publicPath; @@ -43,10 +47,16 @@ export default function (projectDir, configDir) { middlewareFn(router); router.use(webpackDevMiddleware(compiler, devMiddlewareOptions)); - router.use(webpackHotMiddleware(compiler)); + + if (!isProd) { + router.use(webpackHotMiddleware(compiler)); + } router.get('/', function (req, res) { - res.send(getIndexHtml(publicPath)); + res.send(getIndexHtml(publicPath, { + manualId: options.manualId, + secured: options.secured, + })); }); return router;