From f42248157b3cf9b49de98ea9bca1f88495d6d58a Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Fri, 1 Sep 2017 18:29:11 +0200 Subject: [PATCH] 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 390ef2fef27d..f3d93e24ceee 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 884b9a7188a7..99df39a8f57b 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 8cb15c640d93..0236481efd78 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 24eacd0bf24b..d90af4f9c919 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 989b1adca7ad..12a2793137f6 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 000000000000..55723c849e3d --- /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 2c8d390a590c..000000000000 --- 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 000000000000..397aaf41d52e --- /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 dc5b0afc42b8..000000000000 --- 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 7ebeb18be477..e5b0ed9b8e4a 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 000000000000..3ced6fa641bb --- /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; +};