From df1fa10a02c848c8ccb90e2edeeac69e7ddf253e Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Thu, 31 Aug 2017 08:40:35 +0200 Subject: [PATCH 1/8] load chunks after the preview bundle --- app/react/src/server/iframe.html.js | 5 ++- app/react/src/server/iframe.html.test.js | 39 +++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/react/src/server/iframe.html.js b/app/react/src/server/iframe.html.js index 9b9fdc020f5..2c8d390a590 100644 --- a/app/react/src/server/iframe.html.js +++ b/app/react/src/server/iframe.html.js @@ -2,6 +2,8 @@ import url from 'url'; const getExtensionForFilename = filename => /.+\.(\w+)$/.exec(filename)[1]; +export const isPreviewAsset = filename => filename.indexOf('preview.bundle.js') >= 0; + // assets.preview will be: // - undefined // - string e.g. 'static/preview.9adbb5ef965106be1cc3.bundle.js' @@ -39,7 +41,8 @@ export const urlsFromAssets = assets => { return isSupportedExtension && !isMap; }) .forEach(assetUrl => { - urls[getExtensionForFilename(assetUrl)].push(assetUrl); + const method = isPreviewAsset(assetUrl) ? 'unshift' : 'push'; + urls[getExtensionForFilename(assetUrl)][method](assetUrl); }); }); diff --git a/app/react/src/server/iframe.html.test.js b/app/react/src/server/iframe.html.test.js index b57e2b9bba0..66a097d6620 100644 --- a/app/react/src/server/iframe.html.test.js +++ b/app/react/src/server/iframe.html.test.js @@ -1,4 +1,4 @@ -import { urlsFromAssets } from './iframe.html'; +import { urlsFromAssets, isPreviewAsset } from './iframe.html'; describe('server.urlsFromAssets', () => { it('should return the default when there are no assets', () => { @@ -28,4 +28,41 @@ describe('server.urlsFromAssets', () => { css: [], }); }); + + it('should put the JS preview bundle first, before other chunks', () => { + const fixture = { + manager: 'static/manager.a.bundle.js', + preview: [ + 'static/0.bundle.js', + 'static/2.bundle.js', + 'static/3.bundle.js', + 'static/4.bundle.js', + 'static/preview.bundle.js', + 'static/5.bundle.js', + 'static/6.bundle.js', + ], + }; + expect(urlsFromAssets(fixture)).toEqual({ + js: [ + 'static/preview.bundle.js', + 'static/0.bundle.js', + 'static/2.bundle.js', + 'static/3.bundle.js', + 'static/4.bundle.js', + 'static/5.bundle.js', + 'static/6.bundle.js', + ], + css: [], + }); + }); +}); + +describe('server.isPreviewAsset', () => { + it('should return true when this is the preview bundle', () => { + expect(isPreviewAsset('static/preview.bundle.js')).toBe(true); + }); + + it('should return false when this is NOT the preview bundle', () => { + expect(isPreviewAsset('static/some.other.bundle.js')).toBe(false); + }); }); From f42248157b3cf9b49de98ea9bca1f88495d6d58a Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Fri, 1 Sep 2017 18:29:11 +0200 Subject: [PATCH 2/8] use HtmlWebpackPlugin & custom webpack.config.js in cra-kitchen-sink --- app/react/package.json | 1 + app/react/src/server/build.js | 19 +---- app/react/src/server/config/utils.js | 10 ++- app/react/src/server/config/webpack.config.js | 30 ++++++- .../src/server/config/webpack.config.prod.js | 22 ++++- app/react/src/server/iframe.html.ejs | 19 +++++ app/react/src/server/iframe.html.js | 85 ------------------- app/react/src/server/index.html.ejs | 44 ++++++++++ app/react/src/server/index.html.js | 79 ----------------- app/react/src/server/middleware.js | 18 ++-- .../.storybook/webpack.config.js | 29 +++++++ 11 files changed, 157 insertions(+), 199 deletions(-) create mode 100644 app/react/src/server/iframe.html.ejs delete mode 100644 app/react/src/server/iframe.html.js create mode 100644 app/react/src/server/index.html.ejs delete mode 100644 app/react/src/server/index.html.js create mode 100644 examples/cra-kitchen-sink/.storybook/webpack.config.js diff --git a/app/react/package.json b/app/react/package.json index 390ef2fef27..f3d93e24cee 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -50,6 +50,7 @@ "glamor": "^2.20.40", "glamorous": "^4.1.2", "global": "^4.3.2", + "html-webpack-plugin": "^2.30.1", "json-loader": "^0.5.4", "json-stringify-safe": "^5.0.1", "json5": "^0.5.1", diff --git a/app/react/src/server/build.js b/app/react/src/server/build.js index 884b9a7188a..99df39a8f57 100755 --- a/app/react/src/server/build.js +++ b/app/react/src/server/build.js @@ -9,9 +9,7 @@ import shelljs from 'shelljs'; import packageJson from '../../package.json'; import getBaseConfig from './config/webpack.config.prod'; import loadConfig from './config'; -import getIndexHtml from './index.html'; -import getIframeHtml from './iframe.html'; -import { getPreviewHeadHtml, getManagerHeadHtml, parseList, getEnvConfig } from './utils'; +import { parseList, getEnvConfig } from './utils'; process.env.NODE_ENV = process.env.NODE_ENV || 'production'; @@ -86,19 +84,4 @@ webpack(config).run((err, stats) => { stats.hasErrors() && stats.toJson().errors.forEach(e => logger.error(e)); process.exit(1); } - - const data = { - publicPath: config.output.publicPath, - assets: stats.toJson().assetsByChunkName, - }; - - // Write both the storybook UI and IFRAME HTML files to destination path. - fs.writeFileSync( - path.resolve(outputDir, 'index.html'), - getIndexHtml({ ...data, headHtml: getManagerHeadHtml(configDir) }) - ); - fs.writeFileSync( - path.resolve(outputDir, 'iframe.html'), - getIframeHtml({ ...data, headHtml: getPreviewHeadHtml(configDir) }) - ); }); diff --git a/app/react/src/server/config/utils.js b/app/react/src/server/config/utils.js index 8cb15c640d9..0236481efd7 100644 --- a/app/react/src/server/config/utils.js +++ b/app/react/src/server/config/utils.js @@ -23,11 +23,15 @@ export function loadEnv(options = {}) { PUBLIC_URL: JSON.stringify(options.production ? '.' : ''), }; - Object.keys(process.env).filter(name => /^STORYBOOK_/.test(name)).forEach(name => { - env[name] = JSON.stringify(process.env[name]); - }); + Object.keys(process.env) + .filter(name => /^STORYBOOK_/.test(name)) + .forEach(name => { + env[name] = JSON.stringify(process.env[name]); + }); return { 'process.env': env, }; } + +export const getConfigDir = () => process.env.SBCONFIG_CONFIG_DIR || './.storybook'; diff --git a/app/react/src/server/config/webpack.config.js b/app/react/src/server/config/webpack.config.js index 24eacd0bf24..d90af4f9c91 100644 --- a/app/react/src/server/config/webpack.config.js +++ b/app/react/src/server/config/webpack.config.js @@ -1,9 +1,20 @@ 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 { includePaths, excludePaths, nodeModulesPaths, loadEnv, nodePaths } from './utils'; + +import { + getConfigDir, + includePaths, + excludePaths, + nodeModulesPaths, + loadEnv, + nodePaths, +} from './utils'; import babelLoaderConfig from './babel'; +import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils'; +import { version } from '../../../package.json'; export default function() { const config = { @@ -22,6 +33,23 @@ export default function() { 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(), diff --git a/app/react/src/server/config/webpack.config.prod.js b/app/react/src/server/config/webpack.config.prod.js index 989b1adca7a..12a2793137f 100644 --- a/app/react/src/server/config/webpack.config.prod.js +++ b/app/react/src/server/config/webpack.config.prod.js @@ -1,7 +1,10 @@ import path from 'path'; import webpack from 'webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; import babelLoaderConfig from './babel.prod'; -import { includePaths, excludePaths, loadEnv, nodePaths } from './utils'; +import { getConfigDir, includePaths, excludePaths, loadEnv, nodePaths } from './utils'; +import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils'; +import { version } from '../../../package.json'; export default function() { const entries = { @@ -23,6 +26,23 @@ export default function() { 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: { diff --git a/app/react/src/server/iframe.html.ejs b/app/react/src/server/iframe.html.ejs new file mode 100644 index 00000000000..55723c849e3 --- /dev/null +++ b/app/react/src/server/iframe.html.ejs @@ -0,0 +1,19 @@ + + + + + + + + Storybook + <%= htmlWebpackPlugin.options.data.previewHead %> + + +
+
+ + diff --git a/app/react/src/server/iframe.html.js b/app/react/src/server/iframe.html.js deleted file mode 100644 index 2c8d390a590..00000000000 --- a/app/react/src/server/iframe.html.js +++ /dev/null @@ -1,85 +0,0 @@ -import url from 'url'; - -const getExtensionForFilename = filename => /.+\.(\w+)$/.exec(filename)[1]; - -export const isPreviewAsset = filename => filename.indexOf('preview.bundle.js') >= 0; - -// assets.preview will be: -// - undefined -// - string e.g. 'static/preview.9adbb5ef965106be1cc3.bundle.js' -// - array of strings e.g. -// [ 'static/preview.9adbb5ef965106be1cc3.bundle.js', -// 'preview.0d2d3d845f78399fd6d5e859daa152a9.css', -// 'static/preview.9adbb5ef965106be1cc3.bundle.js.map', -// 'preview.0d2d3d845f78399fd6d5e859daa152a9.css.map' ] -export const urlsFromAssets = assets => { - if (!assets) { - return { - js: ['static/preview.bundle.js'], - css: [], - }; - } - - const urls = { - js: [], - css: [], - }; - - Object.keys(assets) - // Don't load the manager script in the iframe - .filter(key => key !== 'manager') - .forEach(key => { - let assetList = assets[key]; - if (!Array.isArray(assetList)) { - assetList = [assetList]; - } - assetList - .filter(assetUrl => { - const extension = getExtensionForFilename(assetUrl); - const isMap = extension === 'map'; - const isSupportedExtension = Boolean(urls[extension]); - return isSupportedExtension && !isMap; - }) - .forEach(assetUrl => { - const method = isPreviewAsset(assetUrl) ? 'unshift' : 'push'; - urls[getExtensionForFilename(assetUrl)][method](assetUrl); - }); - }); - - return urls; -}; - -export default function({ assets, publicPath, headHtml }) { - const urls = urlsFromAssets(assets); - - const cssTags = urls.css - .map(u => ``) - .join('\n'); - const scriptTags = urls.js - .map(u => ``) - .join('\n'); - - return ` - - - - - - - - Storybook - ${headHtml} - ${cssTags} - - -
-
- ${scriptTags} - - - `; -} diff --git a/app/react/src/server/index.html.ejs b/app/react/src/server/index.html.ejs new file mode 100644 index 00000000000..397aaf41d52 --- /dev/null +++ b/app/react/src/server/index.html.ejs @@ -0,0 +1,44 @@ + + + + + + + + Storybook + + <%= htmlWebpackPlugin.options.data.managerHead %> + + + +
+ + diff --git a/app/react/src/server/index.html.js b/app/react/src/server/index.html.js deleted file mode 100644 index dc5b0afc42b..00000000000 --- a/app/react/src/server/index.html.js +++ /dev/null @@ -1,79 +0,0 @@ -import url from 'url'; -import { version } from '../../package.json'; - -// assets.manager will be: -// - undefined -// - string e.g. 'static/manager.9adbb5ef965106be1cc3.bundle.js' -// - array of strings e.g. -// assets.manager will be something like: -// [ 'static/manager.c6e6350b6eb01fff8bad.bundle.js', -// 'static/manager.c6e6350b6eb01fff8bad.bundle.js.map' ] -const managerUrlsFromAssets = assets => { - if (!assets || !assets.manager) { - return { - js: 'static/manager.bundle.js', - }; - } - - if (typeof assets.manager === 'string') { - return { - js: assets.manager, - }; - } - - return { - js: assets.manager.find(filename => filename.match(/\.js$/)), - css: assets.manager.find(filename => filename.match(/\.css$/)), - }; -}; - -export default function({ assets, publicPath, headHtml }) { - const managerUrls = managerUrlsFromAssets(assets); - - return ` - - - - - - - - Storybook - - ${headHtml} - - -
- - - - `; -} diff --git a/app/react/src/server/middleware.js b/app/react/src/server/middleware.js index 7ebeb18be47..e5b0ed9b8e4 100644 --- a/app/react/src/server/middleware.js +++ b/app/react/src/server/middleware.js @@ -1,12 +1,11 @@ +import path from 'path'; import { Router } from 'express'; import webpack from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; import getBaseConfig from './config/webpack.config'; import loadConfig from './config'; -import getIndexHtml from './index.html'; -import getIframeHtml from './iframe.html'; -import { getPreviewHeadHtml, getManagerHeadHtml, getMiddleware } from './utils'; +import { getMiddleware } from './utils'; let webpackResolve = () => {}; let webpackReject = () => {}; @@ -44,19 +43,14 @@ export default function(configDir) { middlewareFn(router); webpackDevMiddlewareInstance.waitUntilValid(stats => { - const data = { - publicPath: config.output.publicPath, - assets: stats.toJson().assetsByChunkName, - }; - router.get('/', (req, res) => { - const headHtml = getManagerHeadHtml(configDir); - res.send(getIndexHtml({ publicPath, headHtml })); + res.set('Content-Type', 'text/html'); + res.sendFile(path.join(`${__dirname}/public/index.html`)); }); router.get('/iframe.html', (req, res) => { - const headHtml = getPreviewHeadHtml(configDir); - res.send(getIframeHtml({ ...data, headHtml, publicPath })); + res.set('Content-Type', 'text/html'); + res.sendFile(path.join(`${__dirname}/public/iframe.html`)); }); if (stats.toJson().errors.length) { diff --git a/examples/cra-kitchen-sink/.storybook/webpack.config.js b/examples/cra-kitchen-sink/.storybook/webpack.config.js new file mode 100644 index 00000000000..3ced6fa641b --- /dev/null +++ b/examples/cra-kitchen-sink/.storybook/webpack.config.js @@ -0,0 +1,29 @@ +const path = require('path'); +const webpack = require('webpack'); + +// load the default config generator. +const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js'); + +// Export a function. Accept the base config as the only param. +module.exports = (storybookBaseConfig, configType) => { + // configType has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + const config = genDefaultConfig(storybookBaseConfig, configType); + + // Make whatever fine-grained changes you need + config.plugins.push( + new webpack.optimize.CommonsChunkPlugin({ + name: "vendor", + chunks: ['preview'], + minChunks: function (module) { + // this assumes your vendor imports exist in the node_modules directory + return module.context && module.context.indexOf("node_modules") !== -1; + }, + }) + ); + + // Return the altered config + return config; +}; From 174db1aa0bb2965e7d268e6e85eb8ff323f55a52 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Fri, 1 Sep 2017 18:35:05 +0200 Subject: [PATCH 3/8] remove useless test file since HtmlWebpackPlugin is taking over --- app/react/src/server/iframe.html.test.js | 68 ------------------------ 1 file changed, 68 deletions(-) delete mode 100644 app/react/src/server/iframe.html.test.js diff --git a/app/react/src/server/iframe.html.test.js b/app/react/src/server/iframe.html.test.js deleted file mode 100644 index 66a097d6620..00000000000 --- a/app/react/src/server/iframe.html.test.js +++ /dev/null @@ -1,68 +0,0 @@ -import { urlsFromAssets, isPreviewAsset } from './iframe.html'; - -describe('server.urlsFromAssets', () => { - it('should return the default when there are no assets', () => { - expect(urlsFromAssets()).toEqual({ - js: ['static/preview.bundle.js'], - css: [], - }); - }); - - it('should return multiple assets', () => { - const fixture = { - manager: 'static/manager.a.bundle.js', - preview: ['static/preview.x.bundle.js', 'static/preview.y.css', 'static/preview.y.css.map'], - }; - expect(urlsFromAssets(fixture)).toEqual({ - js: ['static/preview.x.bundle.js'], - css: ['static/preview.y.css'], - }); - }); - - it('should not return non-js or non-css assets', () => { - const fixture = { - 'some-thing.svg': 'some-thing.svg', - }; - expect(urlsFromAssets(fixture)).toEqual({ - js: [], - css: [], - }); - }); - - it('should put the JS preview bundle first, before other chunks', () => { - const fixture = { - manager: 'static/manager.a.bundle.js', - preview: [ - 'static/0.bundle.js', - 'static/2.bundle.js', - 'static/3.bundle.js', - 'static/4.bundle.js', - 'static/preview.bundle.js', - 'static/5.bundle.js', - 'static/6.bundle.js', - ], - }; - expect(urlsFromAssets(fixture)).toEqual({ - js: [ - 'static/preview.bundle.js', - 'static/0.bundle.js', - 'static/2.bundle.js', - 'static/3.bundle.js', - 'static/4.bundle.js', - 'static/5.bundle.js', - 'static/6.bundle.js', - ], - css: [], - }); - }); -}); - -describe('server.isPreviewAsset', () => { - it('should return true when this is the preview bundle', () => { - expect(isPreviewAsset('static/preview.bundle.js')).toBe(true); - }); - - it('should return false when this is NOT the preview bundle', () => { - expect(isPreviewAsset('static/some.other.bundle.js')).toBe(false); - }); -}); From e3020ce291fdc8263b47546d2b55211e0341d274 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Mon, 4 Sep 2017 09:01:28 +0200 Subject: [PATCH 4/8] use HtmlWebpackPlugin for vue --- app/vue/package.json | 1 + app/vue/src/server/build.js | 26 ++---- app/vue/src/server/config/utils.js | 10 ++- app/vue/src/server/config/webpack.config.js | 29 ++++++- .../src/server/config/webpack.config.prod.js | 22 ++++- app/vue/src/server/iframe.html.ejs | 20 +++++ app/vue/src/server/iframe.html.js | 87 ------------------- app/vue/src/server/index.html.ejs | 44 ++++++++++ app/vue/src/server/index.html.js | 79 ----------------- app/vue/src/server/middleware.js | 18 ++-- 10 files changed, 133 insertions(+), 203 deletions(-) create mode 100644 app/vue/src/server/iframe.html.ejs delete mode 100644 app/vue/src/server/iframe.html.js create mode 100644 app/vue/src/server/index.html.ejs delete mode 100644 app/vue/src/server/index.html.js diff --git a/app/vue/package.json b/app/vue/package.json index 3bec1d4a754..d82d06d0779 100644 --- a/app/vue/package.json +++ b/app/vue/package.json @@ -48,6 +48,7 @@ "file-loader": "^0.11.1", "find-cache-dir": "^1.0.0", "global": "^4.3.2", + "html-webpack-plugin": "^2.30.1", "json-loader": "^0.5.4", "json-stringify-safe": "^5.0.1", "json5": "^0.5.1", diff --git a/app/vue/src/server/build.js b/app/vue/src/server/build.js index b2a598b3e80..99df39a8f57 100755 --- a/app/vue/src/server/build.js +++ b/app/vue/src/server/build.js @@ -9,9 +9,7 @@ import shelljs from 'shelljs'; import packageJson from '../../package.json'; import getBaseConfig from './config/webpack.config.prod'; import loadConfig from './config'; -import getIndexHtml from './index.html'; -import getIframeHtml from './iframe.html'; -import { getPreviewHeadHtml, getManagerHeadHtml, parseList, getEnvConfig } from './utils'; +import { parseList, getEnvConfig } from './utils'; process.env.NODE_ENV = process.env.NODE_ENV || 'production'; @@ -78,24 +76,12 @@ if (program.staticDir) { // compile all resources with webpack and write them to the disk. logger.log('Building storybook ...'); webpack(config).run((err, stats) => { - if (err) { + if (err || stats.hasErrors()) { logger.error('Failed to build the storybook'); - logger.error(err.message); + // 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); } - - const data = { - publicPath: config.output.publicPath, - assets: stats.toJson().assetsByChunkName, - }; - - // Write both the storybook UI and IFRAME HTML files to destination path. - fs.writeFileSync( - path.resolve(outputDir, 'index.html'), - getIndexHtml({ ...data, headHtml: getManagerHeadHtml(configDir) }) - ); - fs.writeFileSync( - path.resolve(outputDir, 'iframe.html'), - getIframeHtml({ ...data, headHtml: getPreviewHeadHtml(configDir) }) - ); }); diff --git a/app/vue/src/server/config/utils.js b/app/vue/src/server/config/utils.js index 8cb15c640d9..0236481efd7 100644 --- a/app/vue/src/server/config/utils.js +++ b/app/vue/src/server/config/utils.js @@ -23,11 +23,15 @@ export function loadEnv(options = {}) { PUBLIC_URL: JSON.stringify(options.production ? '.' : ''), }; - Object.keys(process.env).filter(name => /^STORYBOOK_/.test(name)).forEach(name => { - env[name] = JSON.stringify(process.env[name]); - }); + Object.keys(process.env) + .filter(name => /^STORYBOOK_/.test(name)) + .forEach(name => { + env[name] = JSON.stringify(process.env[name]); + }); return { 'process.env': env, }; } + +export const getConfigDir = () => process.env.SBCONFIG_CONFIG_DIR || './.storybook'; diff --git a/app/vue/src/server/config/webpack.config.js b/app/vue/src/server/config/webpack.config.js index 10d1a6c9c1b..c5fe4cdd631 100644 --- a/app/vue/src/server/config/webpack.config.js +++ b/app/vue/src/server/config/webpack.config.js @@ -1,9 +1,19 @@ 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 { includePaths, excludePaths, nodeModulesPaths, loadEnv, nodePaths } from './utils'; +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 = { @@ -22,6 +32,23 @@ export default function() { 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(), diff --git a/app/vue/src/server/config/webpack.config.prod.js b/app/vue/src/server/config/webpack.config.prod.js index 1260b9de6dc..f4fca7240d7 100644 --- a/app/vue/src/server/config/webpack.config.prod.js +++ b/app/vue/src/server/config/webpack.config.prod.js @@ -1,7 +1,10 @@ import path from 'path'; import webpack from 'webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; import babelLoaderConfig from './babel.prod'; -import { includePaths, excludePaths, loadEnv, nodePaths } from './utils'; +import { getConfigDir, includePaths, excludePaths, loadEnv, nodePaths } from './utils'; +import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils'; +import { version } from '../../../package.json'; export default function() { const entries = { @@ -23,6 +26,23 @@ export default function() { 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: { diff --git a/app/vue/src/server/iframe.html.ejs b/app/vue/src/server/iframe.html.ejs new file mode 100644 index 00000000000..32318e29e95 --- /dev/null +++ b/app/vue/src/server/iframe.html.ejs @@ -0,0 +1,20 @@ + + + + + + + + Storybook + <%= htmlWebpackPlugin.options.data.previewHead %> + + +
+
+ + diff --git a/app/vue/src/server/iframe.html.js b/app/vue/src/server/iframe.html.js deleted file mode 100644 index bfcc2db58ee..00000000000 --- a/app/vue/src/server/iframe.html.js +++ /dev/null @@ -1,87 +0,0 @@ -import url from 'url'; - -const getExtensionForFilename = filename => /.+\.(\w+)$/.exec(filename)[1]; - -// assets.preview will be: -// - undefined -// - string e.g. 'static/preview.9adbb5ef965106be1cc3.bundle.js' -// - array of strings e.g. -// [ 'static/preview.9adbb5ef965106be1cc3.bundle.js', -// 'preview.0d2d3d845f78399fd6d5e859daa152a9.css', -// 'static/preview.9adbb5ef965106be1cc3.bundle.js.map', -// 'preview.0d2d3d845f78399fd6d5e859daa152a9.css.map' ] -const urlsFromAssets = assets => { - if (!assets) { - return { - js: ['static/preview.bundle.js'], - css: [], - }; - } - - const urls = { - js: [], - css: [], - }; - - Object.keys(assets) - // Don't load the manager script in the iframe - .filter(key => key !== 'manager') - .forEach(key => { - let asset = assets[key]; - if (typeof asset === 'string') { - urls[getExtensionForFilename(asset)].push(asset); - } else { - if (!Array.isArray(asset)) { - asset = [asset]; - } - asset - .filter(assetUrl => { - const extension = getExtensionForFilename(assetUrl); - const isMap = extension === 'map'; - const isSupportedExtension = Boolean(urls[extension]); - return isSupportedExtension && !isMap; - }) - .forEach(assetUrl => { - urls[getExtensionForFilename(assetUrl)].push(assetUrl); - }); - } - }); - - return urls; -}; - -export default function({ assets, publicPath, headHtml }) { - const urls = urlsFromAssets(assets); - - const cssTags = urls.css - .map(u => ``) - .join('\n'); - const scriptTags = urls.js - .map(u => ``) - .join('\n'); - - return ` - - - - - - - - Storybook - ${headHtml} - ${cssTags} - - -
-
- ${scriptTags} - - - `; -} diff --git a/app/vue/src/server/index.html.ejs b/app/vue/src/server/index.html.ejs new file mode 100644 index 00000000000..397aaf41d52 --- /dev/null +++ b/app/vue/src/server/index.html.ejs @@ -0,0 +1,44 @@ + + + + + + + + Storybook + + <%= htmlWebpackPlugin.options.data.managerHead %> + + + +
+ + diff --git a/app/vue/src/server/index.html.js b/app/vue/src/server/index.html.js deleted file mode 100644 index dc5b0afc42b..00000000000 --- a/app/vue/src/server/index.html.js +++ /dev/null @@ -1,79 +0,0 @@ -import url from 'url'; -import { version } from '../../package.json'; - -// assets.manager will be: -// - undefined -// - string e.g. 'static/manager.9adbb5ef965106be1cc3.bundle.js' -// - array of strings e.g. -// assets.manager will be something like: -// [ 'static/manager.c6e6350b6eb01fff8bad.bundle.js', -// 'static/manager.c6e6350b6eb01fff8bad.bundle.js.map' ] -const managerUrlsFromAssets = assets => { - if (!assets || !assets.manager) { - return { - js: 'static/manager.bundle.js', - }; - } - - if (typeof assets.manager === 'string') { - return { - js: assets.manager, - }; - } - - return { - js: assets.manager.find(filename => filename.match(/\.js$/)), - css: assets.manager.find(filename => filename.match(/\.css$/)), - }; -}; - -export default function({ assets, publicPath, headHtml }) { - const managerUrls = managerUrlsFromAssets(assets); - - return ` - - - - - - - - Storybook - - ${headHtml} - - -
- - - - `; -} diff --git a/app/vue/src/server/middleware.js b/app/vue/src/server/middleware.js index 7ebeb18be47..cf03d060a59 100644 --- a/app/vue/src/server/middleware.js +++ b/app/vue/src/server/middleware.js @@ -1,12 +1,11 @@ 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 getIndexHtml from './index.html'; -import getIframeHtml from './iframe.html'; -import { getPreviewHeadHtml, getManagerHeadHtml, getMiddleware } from './utils'; +import { getMiddleware } from './utils'; let webpackResolve = () => {}; let webpackReject = () => {}; @@ -44,19 +43,14 @@ export default function(configDir) { middlewareFn(router); webpackDevMiddlewareInstance.waitUntilValid(stats => { - const data = { - publicPath: config.output.publicPath, - assets: stats.toJson().assetsByChunkName, - }; - router.get('/', (req, res) => { - const headHtml = getManagerHeadHtml(configDir); - res.send(getIndexHtml({ publicPath, headHtml })); + res.set('Content-Type', 'text/html'); + res.sendFile(path.join(`${__dirname}/public/index.html`)); }); router.get('/iframe.html', (req, res) => { - const headHtml = getPreviewHeadHtml(configDir); - res.send(getIframeHtml({ ...data, headHtml, publicPath })); + res.set('Content-Type', 'text/html'); + res.sendFile(path.join(`${__dirname}/public/iframe.html`)); }); if (stats.toJson().errors.length) { From 5438db41863805066b2f14585d58f3237c0aad03 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 4 Sep 2017 23:52:22 +0200 Subject: [PATCH 5/8] ADD custom webpack config for vue kitchen sink as with cra-kitchen-sink --- .../.storybook/webpack.config.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 examples/vue-kitchen-sink/.storybook/webpack.config.js diff --git a/examples/vue-kitchen-sink/.storybook/webpack.config.js b/examples/vue-kitchen-sink/.storybook/webpack.config.js new file mode 100644 index 00000000000..f7cbac74a0b --- /dev/null +++ b/examples/vue-kitchen-sink/.storybook/webpack.config.js @@ -0,0 +1,29 @@ +const path = require('path'); +const webpack = require('webpack'); + +// load the default config generator. +const genDefaultConfig = require('@storybook/vue/dist/server/config/defaults/webpack.config.js'); + +// Export a function. Accept the base config as the only param. +module.exports = (storybookBaseConfig, configType) => { + // configType has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + const config = genDefaultConfig(storybookBaseConfig, configType); + + // Make whatever fine-grained changes you need + config.plugins.push( + new webpack.optimize.CommonsChunkPlugin({ + name: "vendor", + chunks: ['preview'], + minChunks: function (module) { + // this assumes your vendor imports exist in the node_modules directory + return module.context && module.context.indexOf("node_modules") !== -1; + }, + }) + ); + + // Return the altered config + return config; +}; From 2b0644c1e7b72d6242f803322740b30b8f95b71d Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 4 Sep 2017 23:58:06 +0200 Subject: [PATCH 6/8] Fix for lint-staged see: https://github.com/okonet/lint-staged/issues/225#issuecomment-327032465 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 7fe0a0cc6ae..088d3c4821b 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,7 @@ "git add" ] }, - "verbose": true, - "concurrent": false + "verbose": true }, "pr-log": { "skipLabels": [ From faa4803ca23a1c2ec40631555e410ca34a76d770 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 6 Sep 2017 08:41:47 +0200 Subject: [PATCH 7/8] FIX snapshots --- .../src/stories/__snapshots__/index.storyshot | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/cra-kitchen-sink/src/stories/__snapshots__/index.storyshot b/examples/cra-kitchen-sink/src/stories/__snapshots__/index.storyshot index a862be90e2b..76bd3f3d9be 100644 --- a/examples/cra-kitchen-sink/src/stories/__snapshots__/index.storyshot +++ b/examples/cra-kitchen-sink/src/stories/__snapshots__/index.storyshot @@ -150,7 +150,7 @@ exports[`Storyshots AddonInfo.DocgenButton DocgenButton 1`] = ` } } > - Button with PropTypes and doc comments + Some Description

@@ -618,7 +618,7 @@ exports[`Storyshots AddonInfo.FlowTypeButton FlowTypeButton 1`] = ` } } > - Button with Flow type documentation comments + Some Description

@@ -3118,9 +3118,9 @@ exports[`Storyshots WithEvents Logger 1`] = ` Object { "color": "rgb(51, 51, 51)", "fontFamily": " - -apple-system, \\".SFNSText-Regular\\", \\"San Francisco\\", \\"Roboto\\", - \\"Segoe UI\\", \\"Helvetica Neue\\", \\"Lucida Grande\\", sans-serif - ", + -apple-system, \\".SFNSText-Regular\\", \\"San Francisco\\", \\"Roboto\\", + \\"Segoe UI\\", \\"Helvetica Neue\\", \\"Lucida Grande\\", sans-serif + ", "padding": 20, } } From bd6d8a64e62c60f34a632a83b5050e93e2a228f6 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 6 Sep 2017 08:41:47 +0200 Subject: [PATCH 8/8] use HtmlWebpackPlugin for RN & CRNA --- app/react-native/package.json | 1 + .../src/server/config/webpack.config.js | 14 ++- .../src/server/config/webpack.config.prod.js | 112 ++++++++++-------- app/react-native/src/server/index.html.ejs | 34 ++++++ app/react-native/src/server/index.html.js | 41 ------- app/react-native/src/server/middleware.js | 11 +- .../.storybook/webpack.config.js | 1 - examples/crna-kitchen-sink/package.json | 3 +- .../storybook/webpack.config.js | 27 +++++ .../.storybook/webpack.config.js | 1 - 10 files changed, 140 insertions(+), 105 deletions(-) create mode 100644 app/react-native/src/server/index.html.ejs delete mode 100644 app/react-native/src/server/index.html.js create mode 100644 examples/crna-kitchen-sink/storybook/webpack.config.js diff --git a/app/react-native/package.json b/app/react-native/package.json index d2d78b903fb..5716932f1b3 100644 --- a/app/react-native/package.json +++ b/app/react-native/package.json @@ -53,6 +53,7 @@ "file-loader": "^0.11.1", "find-cache-dir": "^1.0.0", "global": "^4.3.2", + "html-webpack-plugin": "^2.30.1", "json-loader": "^0.5.4", "json5": "^0.5.1", "postcss-loader": "^2.0.5", diff --git a/app/react-native/src/server/config/webpack.config.js b/app/react-native/src/server/config/webpack.config.js index ddd38a20d00..0e2f5c2ce09 100644 --- a/app/react-native/src/server/config/webpack.config.js +++ b/app/react-native/src/server/config/webpack.config.js @@ -1,9 +1,10 @@ import path from 'path'; import webpack from 'webpack'; import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; import { OccurenceOrderPlugin, includePaths, excludePaths } from './utils'; -const config = { +const getConfig = options => ({ devtool: '#cheap-module-eval-source-map', entry: { manager: [require.resolve('../../manager')], @@ -14,6 +15,13 @@ const config = { publicPath: '/', }, plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + data: { + options: JSON.stringify(options), + }, + template: require.resolve('../index.html.ejs'), + }), new OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new CaseSensitivePathsPlugin(), @@ -29,6 +37,6 @@ const config = { }, ], }, -}; +}); -export default config; +export default getConfig; diff --git a/app/react-native/src/server/config/webpack.config.prod.js b/app/react-native/src/server/config/webpack.config.prod.js index 841459f0c83..b7c15794ef1 100644 --- a/app/react-native/src/server/config/webpack.config.prod.js +++ b/app/react-native/src/server/config/webpack.config.prod.js @@ -1,57 +1,69 @@ import path from 'path'; import webpack from 'webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; import { OccurenceOrderPlugin, includePaths, excludePaths } from './utils'; -const config = { - bail: true, - devtool: '#cheap-module-source-map', - entry: { - manager: [path.resolve(__dirname, '../../manager')], - }, - 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: '/', - }, - plugins: [ - new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), - new webpack.optimize.DedupePlugin(), - new webpack.optimize.UglifyJsPlugin({ - compress: { - screw_ie8: true, - warnings: false, - }, - mangle: { - screw_ie8: true, - }, - output: { - comments: false, - screw_ie8: true, - }, - }), - ], - module: { - loaders: [ - { - test: /\.jsx?$/, - loader: require.resolve('babel-loader'), - query: require('./babel.prod.js'), // eslint-disable-line - include: includePaths, - exclude: excludePaths, - }, +const getConfig = options => { + const config = { + bail: true, + devtool: '#cheap-module-source-map', + entry: { + manager: [path.resolve(__dirname, '../../manager')], + }, + 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: '/', + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + data: { + options: JSON.stringify(options), + }, + template: require.resolve('../index.html.ejs'), + }), + new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.UglifyJsPlugin({ + compress: { + screw_ie8: true, + warnings: false, + }, + mangle: { + screw_ie8: true, + }, + output: { + comments: false, + screw_ie8: true, + }, + }), ], - }, + module: { + loaders: [ + { + test: /\.jsx?$/, + loader: require.resolve('babel-loader'), + query: require('./babel.prod.js'), // eslint-disable-line + include: includePaths, + exclude: excludePaths, + }, + ], + }, + }; + + // Webpack 2 doesn't have a OccurenceOrderPlugin plugin in the production mode. + // But webpack 1 has it. That's why we do this. + if (OccurenceOrderPlugin) { + config.plugins.unshift(new OccurenceOrderPlugin()); + } + + return config; }; -// Webpack 2 doesn't have a OccurenceOrderPlugin plugin in the production mode. -// But webpack 1 has it. That's why we do this. -if (OccurenceOrderPlugin) { - config.plugins.unshift(new OccurenceOrderPlugin()); -} - -export default config; +export default getConfig; diff --git a/app/react-native/src/server/index.html.ejs b/app/react-native/src/server/index.html.ejs new file mode 100644 index 00000000000..84b46693e47 --- /dev/null +++ b/app/react-native/src/server/index.html.ejs @@ -0,0 +1,34 @@ + + + + + + Storybook for React + + + +
+ + + diff --git a/app/react-native/src/server/index.html.js b/app/react-native/src/server/index.html.js deleted file mode 100644 index e3bc622c4e2..00000000000 --- a/app/react-native/src/server/index.html.js +++ /dev/null @@ -1,41 +0,0 @@ -import url from 'url'; - -export default function(publicPath, options) { - return ` - - - - - - Storybook for React - - - -
- - - - - `; -} diff --git a/app/react-native/src/server/middleware.js b/app/react-native/src/server/middleware.js index 10ec0c03c14..3aa404d17d2 100644 --- a/app/react-native/src/server/middleware.js +++ b/app/react-native/src/server/middleware.js @@ -7,7 +7,6 @@ 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'; function getMiddleware(configDir) { const middlewarePath = path.resolve(configDir, 'middleware.js'); @@ -26,7 +25,7 @@ export default function({ projectDir, configDir, ...options }) { // custom `.babelrc` file and `webpack.config.js` files const environment = options.environment || 'DEVELOPMENT'; const isProd = environment === 'PRODUCTION'; - const currentWebpackConfig = isProd ? baseProductionConfig : baseConfig; + const currentWebpackConfig = isProd ? baseProductionConfig(options) : baseConfig(options); const config = loadConfig(environment, currentWebpackConfig, projectDir, configDir); // remove the leading '/' @@ -53,12 +52,8 @@ export default function({ projectDir, configDir, ...options }) { } router.get('/', (req, res) => { - res.send( - getIndexHtml(publicPath, { - manualId: options.manualId, - secured: options.secured, - }) - ); + res.set('Content-Type', 'text/html'); + res.sendFile(path.join(`${__dirname}/public/index.html`)); }); return router; diff --git a/examples/cra-kitchen-sink/.storybook/webpack.config.js b/examples/cra-kitchen-sink/.storybook/webpack.config.js index 3ced6fa641b..001da2406dd 100644 --- a/examples/cra-kitchen-sink/.storybook/webpack.config.js +++ b/examples/cra-kitchen-sink/.storybook/webpack.config.js @@ -1,4 +1,3 @@ -const path = require('path'); const webpack = require('webpack'); // load the default config generator. diff --git a/examples/crna-kitchen-sink/package.json b/examples/crna-kitchen-sink/package.json index 4126c6c8233..996107a5571 100644 --- a/examples/crna-kitchen-sink/package.json +++ b/examples/crna-kitchen-sink/package.json @@ -34,6 +34,7 @@ "expo": "19.0.0", "prop-types": "15.5.10", "react": "16.0.0-alpha.12", - "react-native": "0.46.1" + "react-native": "0.46.1", + "webpack": "^2.5.1 || ^3.0.0" } } diff --git a/examples/crna-kitchen-sink/storybook/webpack.config.js b/examples/crna-kitchen-sink/storybook/webpack.config.js new file mode 100644 index 00000000000..a2daea72816 --- /dev/null +++ b/examples/crna-kitchen-sink/storybook/webpack.config.js @@ -0,0 +1,27 @@ +const webpack = require('webpack'); + +// load the default config generator. +const genDefaultConfig = require('@storybook/react-native/dist/server/config/defaults/webpack.config.js'); + +// Export a function. Accept the base config as the only param. +module.exports = (storybookBaseConfig, configType) => { + // configType has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + const config = genDefaultConfig(storybookBaseConfig, configType); + + // Make whatever fine-grained changes you need + config.plugins.push( + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks(module) { + // this assumes your vendor imports exist in the node_modules directory + return module.context && module.context.indexOf('node_modules') !== -1; + }, + }) + ); + + // Return the altered config + return config; +}; diff --git a/examples/vue-kitchen-sink/.storybook/webpack.config.js b/examples/vue-kitchen-sink/.storybook/webpack.config.js index f7cbac74a0b..416bb092edd 100644 --- a/examples/vue-kitchen-sink/.storybook/webpack.config.js +++ b/examples/vue-kitchen-sink/.storybook/webpack.config.js @@ -1,4 +1,3 @@ -const path = require('path'); const webpack = require('webpack'); // load the default config generator.