diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 9731c81d6927..000000000000 --- a/.babelrc +++ /dev/null @@ -1,53 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - "@babel/preset-react", - "@babel/preset-flow" - ], - "plugins": [ - "babel-plugin-emotion", - "babel-plugin-macros", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-export-default-from", - [ - "@babel/plugin-transform-runtime", - { - "regenerator": true - } - ] - ], - "env": { - "test": { - "plugins": ["babel-plugin-require-context-hook"] - } - }, - "overrides": [ - { - "test": "./examples/vue-kitchen-sink", - "presets": [ - "@babel/preset-env", - "babel-preset-vue" - ] - }, - { - "test": [ - "./lib/core/src/server", - "./lib/node-logger", - "./lib/codemod", - "./addons/storyshots", - "./addons/storysource/src/loader", - "./app/**/src/server/**" - ], - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "8.11" - } - } - ] - ] - } - ] -} diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 000000000000..8db0608b032f --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,46 @@ +module.exports = { + presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-flow'], + plugins: [ + 'babel-plugin-emotion', + 'babel-plugin-macros', + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-export-default-from', + [ + '@babel/plugin-transform-runtime', + { + regenerator: true, + }, + ], + ], + env: { + test: { + plugins: ['babel-plugin-require-context-hook'], + }, + }, + overrides: [ + { + test: './examples/vue-kitchen-sink', + presets: ['@babel/preset-env', 'babel-preset-vue'], + }, + { + test: [ + './lib/core/src/server', + './lib/node-logger', + './lib/codemod', + './addons/storyshots', + './addons/storysource/src/loader', + './app/**/src/server/**', + ], + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: '8.11', + }, + }, + ], + ], + }, + ], +}; diff --git a/app/angular/src/server/angular-cli_config.js b/app/angular/src/server/angular-cli_config.js index 049167745352..c5b25b4710f1 100644 --- a/app/angular/src/server/angular-cli_config.js +++ b/app/angular/src/server/angular-cli_config.js @@ -103,12 +103,10 @@ export function applyAngularCliWebpackConfig(baseConfig, cliWebpackConfigOptions const rulesExcludingStyles = filterOutStylingRules(baseConfig); // cliStyleConfig.entry adds global style files to the webpack context - const entry = { + const entry = [ ...baseConfig.entry, - iframe: [] - .concat(baseConfig.entry.iframe) - .concat(Object.values(cliStyleConfig.entry).reduce((acc, item) => acc.concat(item), [])), - }; + ...Object.values(cliStyleConfig.entry).reduce((acc, item) => acc.concat(item), []), + ]; const module = { ...baseConfig.module, diff --git a/app/angular/src/server/framework-preset-angular.js b/app/angular/src/server/framework-preset-angular.js index 882e212caecb..714d9fb5a20f 100644 --- a/app/angular/src/server/framework-preset-angular.js +++ b/app/angular/src/server/framework-preset-angular.js @@ -38,13 +38,13 @@ export function webpack(config, { configDir }) { }, resolve: { ...config.resolve, - extensions: [...config.resolve.extensions, '.ts', '.tsx'], + extensions: ['.ts', '.tsx', ...config.resolve.extensions], }, plugins: [ ...config.plugins, // See https://github.com/angular/angular/issues/11580#issuecomment-401127742 new ContextReplacementPlugin( - /@angular(\\|\/)core(\\|\/)fesm5/, + /@angular(\\|\/)core(\\|\/)(fesm5|bundles)/, path.resolve(__dirname, '..') ), createForkTsCheckerInstance(tsLoaderOptions), 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 09720b78ff2b..77da36c090b2 100644 --- a/app/react-native/src/server/config/webpack.config.prod.js +++ b/app/react-native/src/server/config/webpack.config.prod.js @@ -3,7 +3,7 @@ import webpack from 'webpack'; import Dotenv from 'dotenv-webpack'; import HtmlWebpackPlugin from 'html-webpack-plugin'; -import { getManagerHeadHtml } from '@storybook/core/dist/server/utils'; +import { getManagerHeadHtml } from '@storybook/core/dist/server/utils/template'; import { version } from '../../../package.json'; import { includePaths, excludePaths, loadEnv } from './utils'; diff --git a/app/riot/src/server/framework-preset-riot.js b/app/riot/src/server/framework-preset-riot.js index d7a5786502da..820e02e015d2 100644 --- a/app/riot/src/server/framework-preset-riot.js +++ b/app/riot/src/server/framework-preset-riot.js @@ -15,5 +15,12 @@ export function webpack(config) { }, ], }, + resolve: { + ...config.resolve, + alias: { + ...config.resolve.alias, + 'riot-compiler': 'riot-compiler/dist/es6.compiler', + }, + }, }; } diff --git a/examples/angular-cli/package.json b/examples/angular-cli/package.json index 1bf055909cee..906cf911eea9 100644 --- a/examples/angular-cli/package.json +++ b/examples/angular-cli/package.json @@ -5,7 +5,7 @@ "license": "MIT", "scripts": { "build": "ng build", - "build-storybook": "npm run storybook:prebuild && build-storybook -s src", + "build-storybook": "npm run storybook:prebuild && build-storybook -s src/assets", "e2e": "ng e2e", "ng": "ng", "start": "ng serve", diff --git a/examples/angular-cli/src/favicon.ico b/examples/angular-cli/src/assets/favicon.ico similarity index 100% rename from examples/angular-cli/src/favicon.ico rename to examples/angular-cli/src/assets/favicon.ico diff --git a/examples/cra-kitchen-sink/public/index.html b/examples/cra-kitchen-sink/public/index.html deleted file mode 100644 index aab5e3b00ce4..000000000000 --- a/examples/cra-kitchen-sink/public/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - React App - - -
- - - diff --git a/examples/svelte-kitchen-sink/public/index.html b/examples/svelte-kitchen-sink/public/index.html deleted file mode 100644 index 605ba2f12207..000000000000 --- a/examples/svelte-kitchen-sink/public/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - Svelte App - - -
- - - diff --git a/examples/vue-kitchen-sink/public/index.html b/examples/vue-kitchen-sink/public/index.html deleted file mode 100644 index aab5e3b00ce4..000000000000 --- a/examples/vue-kitchen-sink/public/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - React App - - -
- - - diff --git a/lib/core/package.json b/lib/core/package.json index 35423deff122..f5f607bd3a9c 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -30,6 +30,7 @@ "@emotion/core": "^0.13.1", "@emotion/provider": "^0.11.2", "@emotion/styled": "^0.10.6", + "@ndelangen/html-webpack-harddisk-plugin": "^0.2.0", "@storybook/addons": "4.1.0-alpha.1", "@storybook/channel-postmessage": "4.1.0-alpha.1", "@storybook/client-logger": "4.1.0-alpha.1", @@ -43,6 +44,7 @@ "boxen": "^2.0.0", "case-sensitive-paths-webpack-plugin": "^2.1.2", "chalk": "^2.4.1", + "child-process-promise": "^2.2.1", "cli-table3": "0.5.1", "commander": "^2.19.0", "common-tags": "^1.8.0", @@ -75,6 +77,7 @@ "semver": "^5.6.0", "serve-favicon": "^2.5.0", "shelljs": "^0.8.2", + "spawn-promise": "^0.1.8", "style-loader": "^0.23.1", "svg-url-loader": "^2.3.2", "url-loader": "^1.1.2", diff --git a/lib/core/server.js b/lib/core/server.js index b851b3b0ba2c..95d61363f855 100644 --- a/lib/core/server.js +++ b/lib/core/server.js @@ -1,5 +1,5 @@ -const defaultWebpackConfig = require('./dist/server/config/webpack.config.default.js'); -const serverUtils = require('./dist/server/utils'); +const defaultWebpackConfig = require('./dist/server/preview/base-webpack.config'); +const serverUtils = require('./dist/server/utils/template'); const buildStatic = require('./dist/server/build-static'); const buildDev = require('./dist/server/build-dev'); diff --git a/lib/core/src/server/build-dev.js b/lib/core/src/server/build-dev.js index 25f3d2abf3b0..daddfc5bed2a 100644 --- a/lib/core/src/server/build-dev.js +++ b/lib/core/src/server/build-dev.js @@ -14,7 +14,7 @@ import semver from 'semver'; import { stripIndents } from 'common-tags'; import Table from 'cli-table3'; -import storybook, { webpackValid } from './middleware'; +import storybook, { webpackValid } from './dev-server'; import { getDevCli } from './cli'; const defaultFavIcon = require.resolve('./public/favicon.ico'); diff --git a/lib/core/src/server/build-static.js b/lib/core/src/server/build-static.js index 3176fd2ae9a7..573a21b0202f 100644 --- a/lib/core/src/server/build-static.js +++ b/lib/core/src/server/build-static.js @@ -1,15 +1,20 @@ -import webpack from 'webpack'; -import path from 'path'; import fs from 'fs'; +import path from 'path'; +import webpack from 'webpack'; import shelljs from 'shelljs'; +import childProcess from 'child-process-promise'; + import { logger } from '@storybook/node-logger'; + import { getProdCli } from './cli'; import loadConfig from './config'; +import { loadEnv } from './config/utils'; const defaultFavIcon = require.resolve('./public/favicon.ico'); export async function buildStaticStandalone(options) { - const { outputDir, staticDir, watch } = options; + const { outputDir, staticDir, watch, configDir, packageJson } = options; + const environment = loadEnv(); // create output directory if not exists shelljs.mkdir('-p', path.resolve(outputDir)); @@ -17,16 +22,38 @@ export async function buildStaticStandalone(options) { shelljs.rm('-rf', path.resolve(outputDir, 'static')); shelljs.cp(defaultFavIcon, outputDir); + logger.info('building manager..'); + const managerStartTime = process.hrtime(); + await childProcess + .exec( + `node ${path.join(__dirname, 'manager/webpack.js')} dir=${configDir} out=${path.resolve( + outputDir + )}`, + { + env: { + NODE_ENV: 'production', + ...environment, + }, + } + ) + .then(() => { + const managerTotalTime = process.hrtime(managerStartTime); + logger.trace({ message: 'manager built', time: managerTotalTime }); + }); + // 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 = await loadConfig({ configType: 'PRODUCTION', - corePresets: [require.resolve('./core-preset-prod.js')], + outputDir, + packageJson, + corePresets: [require.resolve('./preview/preview-preset.js')], + overridePresets: [require.resolve('./preview/custom-webpack-preset.js')], ...options, }); - config.output.path = path.resolve(outputDir); + // config.output.path = path.resolve(outputDir); // copy all static files if (staticDir) { @@ -42,6 +69,8 @@ export async function buildStaticStandalone(options) { // compile all resources with webpack and write them to the disk. return new Promise((resolve, reject) => { + const previewStartTime = process.hrtime(); + const webpackCb = (err, stats) => { if (err || stats.hasErrors()) { logger.error('Failed to build the storybook'); @@ -52,11 +81,14 @@ export async function buildStaticStandalone(options) { process.exitCode = 1; return reject(err); } - logger.info('Building storybook completed.'); + + const previewTotalTime = process.hrtime(previewStartTime); + logger.trace({ message: 'preview built', time: previewTotalTime }); + return resolve(stats); }; - logger.info('Building storybook ...'); + logger.info('building preview..'); const compiler = webpack(config); if (watch) { @@ -73,6 +105,7 @@ export async function buildStatic({ packageJson, ...loadOptions }) { await buildStaticStandalone({ ...cliOptions, ...loadOptions, + packageJson, configDir: cliOptions.configDir || './.storybook', outputDir: cliOptions.outputDir || './storybook-static', }); diff --git a/lib/core/src/server/common/babel-cache-preset.js b/lib/core/src/server/common/babel-cache-preset.js new file mode 100644 index 000000000000..a5b798468132 --- /dev/null +++ b/lib/core/src/server/common/babel-cache-preset.js @@ -0,0 +1,11 @@ +import findCacheDir from 'find-cache-dir'; + +const extend = babelConfig => ({ + // 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: 'storybook' }), + ...babelConfig, +}); + +export { extend as babel, extend as managerBabel }; diff --git a/lib/core/src/server/common/babel.js b/lib/core/src/server/common/babel.js new file mode 100644 index 000000000000..c0ed09f6e925 --- /dev/null +++ b/lib/core/src/server/common/babel.js @@ -0,0 +1,32 @@ +function createProdPresets() { + return [ + [ + require.resolve('babel-preset-minify'), + { + builtIns: false, + mangle: false, + }, + ], + ]; +} + +export default ({ configType }) => { + const isProd = configType === 'PRODUCTION'; + const prodPresets = isProd ? createProdPresets() : []; + + return { + presets: [require.resolve('@babel/preset-env'), ...prodPresets], + plugins: [ + require.resolve('babel-plugin-macros'), + require.resolve('@babel/plugin-transform-regenerator'), + require.resolve('@babel/plugin-proposal-class-properties'), + [ + require.resolve('@babel/plugin-transform-runtime'), + { + helpers: true, + regenerator: true, + }, + ], + ], + }; +}; diff --git a/lib/core/src/server/common/custom-presets.js b/lib/core/src/server/common/custom-presets.js new file mode 100644 index 000000000000..aeabe5b9cb5a --- /dev/null +++ b/lib/core/src/server/common/custom-presets.js @@ -0,0 +1,17 @@ +import path from 'path'; +import { logger } from '@storybook/node-logger'; +import serverRequire from '../utils/server-require'; + +export default function loadCustomPresets({ configDir }) { + const presets = serverRequire(path.resolve(configDir, 'presets')); + + if (presets) { + logger.warn( + '"Custom presets" is an experimental and undocumented feature that will be changed or deprecated soon. Use it on your own risk.' + ); + + return presets; + } + + return []; +} diff --git a/lib/core/src/server/config/polyfills.js b/lib/core/src/server/common/polyfills.js similarity index 100% rename from lib/core/src/server/config/polyfills.js rename to lib/core/src/server/common/polyfills.js diff --git a/lib/core/src/server/config.js b/lib/core/src/server/config.js index 83ac5604ad19..2c67e1fffcf4 100644 --- a/lib/core/src/server/config.js +++ b/lib/core/src/server/config.js @@ -1,54 +1,25 @@ -import path from 'path'; -import { logger } from '@storybook/node-logger'; import loadPresets from './presets'; -import serverRequire from './serverRequire'; +import loadCustomPresets from './common/custom-presets'; -function wrapCorePresets(presets) { - return { - babel: async (config, args) => presets.apply('babel', config, args), - webpack: async (config, args) => presets.apply('webpack', config, args), - preview: async (config, args) => presets.apply('preview', config, args), - manager: async (config, args) => presets.apply('manager', config, args), - }; -} - -function customPreset({ configDir }) { - const presets = serverRequire(path.resolve(configDir, 'presets')); - - if (presets) { - logger.warn( - '"Custom presets" is an experimental and undocumented feature that will be changed or deprecated soon. Use it on your own risk.' - ); - - return presets; - } - - return []; -} - -async function getWebpackConfig(options, presets) { - const babelOptions = await presets.babel({}, options); - - const entries = { - iframe: await presets.preview([], options), - manager: await presets.manager([], options), - }; +async function getPreviewWebpackConfig(options, presets) { + const babelOptions = await presets.apply('babel', {}, options); + const entries = await presets.apply('entries', [], options); - return presets.webpack({}, { ...options, babelOptions, entries }); + return presets.apply('webpack', {}, { ...options, babelOptions, entries }); } export default async options => { - const { corePresets = [], frameworkPresets = [], ...restOptions } = options; + const { corePresets = [], frameworkPresets = [], overridePresets = [], ...restOptions } = options; const presetsConfig = [ ...corePresets, - require.resolve('./core-preset-babel-cache.js'), + require.resolve('./common/babel-cache-preset.js'), ...frameworkPresets, - ...customPreset(options), - require.resolve('./core-preset-webpack-custom.js'), + ...loadCustomPresets(options), + ...overridePresets, ]; const presets = loadPresets(presetsConfig); - return getWebpackConfig({ ...restOptions, presets }, wrapCorePresets(presets)); + return getPreviewWebpackConfig({ ...restOptions, presets }, presets); }; diff --git a/lib/core/src/server/config/babel.dev.js b/lib/core/src/server/config/babel.dev.js deleted file mode 100644 index 0b6e5dbd19f6..000000000000 --- a/lib/core/src/server/config/babel.dev.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - presets: [[require.resolve('@babel/preset-env')]], - plugins: [ - require.resolve('babel-plugin-macros'), - require.resolve('@babel/plugin-transform-regenerator'), - require.resolve('@babel/plugin-proposal-class-properties'), - [ - require.resolve('@babel/plugin-transform-runtime'), - { - helpers: true, - regenerator: true, - }, - ], - ], -}; diff --git a/lib/core/src/server/config/babel.prod.js b/lib/core/src/server/config/babel.prod.js deleted file mode 100644 index 8bc12a641e12..000000000000 --- a/lib/core/src/server/config/babel.prod.js +++ /dev/null @@ -1,15 +0,0 @@ -import baseConfig from './babel.dev'; - -export default { - ...baseConfig, - presets: [ - ...baseConfig.presets, - [ - require.resolve('babel-preset-minify'), - { - builtIns: false, - mangle: false, - }, - ], - ], -}; diff --git a/lib/core/src/server/config/entries.js b/lib/core/src/server/config/entries.js deleted file mode 100644 index 262e0eb39b3b..000000000000 --- a/lib/core/src/server/config/entries.js +++ /dev/null @@ -1,29 +0,0 @@ -export async function createPreviewEntry(options) { - const { configDir, presets } = options; - const preview = [require.resolve('./polyfills'), require.resolve('./globals')]; - - const configs = await presets.apply('config', [], options); - - if (!configs || !configs.length) { - throw new Error(`=> Create a storybook config file in "${configDir}/config.{ext}".`); - } - - preview.push(...configs); - - return preview; -} - -export async function createManagerEntry(options) { - const { presets } = options; - const manager = [require.resolve('./polyfills')]; - - const addons = await presets.apply('addons', [], options); - - if (addons && addons.length) { - manager.push(...addons); - } - - manager.push(require.resolve('../../client/manager')); - - return manager; -} diff --git a/lib/core/src/server/config/utils.js b/lib/core/src/server/config/utils.js index bc91c80b47e7..402305b875ec 100644 --- a/lib/core/src/server/config/utils.js +++ b/lib/core/src/server/config/utils.js @@ -2,10 +2,8 @@ import path from 'path'; import { getEnvironment } from 'lazy-universal-dotenv'; export const includePaths = [path.resolve('./')]; - -export const excludePaths = [path.resolve('node_modules')]; - export const nodeModulesPaths = path.resolve('./node_modules'); +export const excludePaths = [nodeModulesPaths]; const nodePathsToArray = nodePath => nodePath @@ -21,7 +19,7 @@ export function loadEnv(options = {}) { NODE_ENV: process.env.NODE_ENV || defaultNodeEnv, NODE_PATH: process.env.NODE_PATH || '', // This is to support CRA's public folder feature. - // In production we set this to dot(.) to allow the browser to access these assests + // In production we set this to dot(.) to allow the browser to access these assets // 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: options.production ? '.' : '', diff --git a/lib/core/src/server/config/webpack.config.dev.js b/lib/core/src/server/config/webpack.config.dev.js deleted file mode 100644 index e090c92fbf47..000000000000 --- a/lib/core/src/server/config/webpack.config.dev.js +++ /dev/null @@ -1,108 +0,0 @@ -import path from 'path'; -import webpack from 'webpack'; -import Dotenv from 'dotenv-webpack'; -import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'; -import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; - -import { - includePaths, - excludePaths, - nodeModulesPaths, - loadEnv, - getBabelRuntimePath, -} from './utils'; -import { getPreviewHeadHtml, getManagerHeadHtml, getPreviewBodyHtml } from '../utils'; -import { version } from '../../../package.json'; - -export default ({ configDir, quiet, babelOptions, entries }) => { - const { raw, stringified } = loadEnv(); - const entriesMeta = { - iframe: { - headHtmlSnippet: getPreviewHeadHtml(configDir, process.env), - bodyHtmlSnippet: getPreviewBodyHtml(), - }, - manager: { - headHtmlSnippet: getManagerHeadHtml(configDir, process.env), - }, - }; - - return { - mode: 'development', - 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: '', - }, - plugins: [ - ...Object.keys(entries).map( - e => - new HtmlWebpackPlugin({ - filename: `${e === 'manager' ? 'index' : e}.html`, - excludeChunks: Object.keys(entries).filter(i => i !== e), - chunksSortMode: 'none', - alwaysWriteToDisk: true, - inject: false, - templateParameters: (compilation, files, options) => ({ - compilation, - files, - options, - version, - ...entriesMeta[e], - }), - template: require.resolve(`../templates/index.ejs`), - }) - ), - new webpack.DefinePlugin({ 'process.env': stringified }), - new webpack.HotModuleReplacementPlugin(), - new CaseSensitivePathsPlugin(), - new WatchMissingNodeModulesPlugin(nodeModulesPaths), - quiet ? null : new webpack.ProgressPlugin(), - new Dotenv({ silent: true }), - ].filter(Boolean), - module: { - rules: [ - { - test: /\.(mjs|jsx?)$/, - use: [ - { - loader: 'babel-loader', - options: babelOptions, - }, - ], - include: includePaths, - exclude: excludePaths, - }, - { - test: /\.md$/, - use: [ - { - loader: require.resolve('raw-loader'), - }, - ], - }, - ], - }, - resolve: { - // Since we ship with json-loader always, it's better to move extensions to here - // from the default config. - extensions: ['.mjs', '.js', '.jsx', '.json'], - // 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(raw.NODE_PATH || []), - alias: { - '@babel/runtime': getBabelRuntimePath(), - }, - }, - performance: { - hints: false, - }, - }; -}; diff --git a/lib/core/src/server/config/webpack.config.prod.js b/lib/core/src/server/config/webpack.config.prod.js deleted file mode 100644 index 58a3c08619cb..000000000000 --- a/lib/core/src/server/config/webpack.config.prod.js +++ /dev/null @@ -1,103 +0,0 @@ -import webpack from 'webpack'; -import Dotenv from 'dotenv-webpack'; -import HtmlWebpackPlugin from 'html-webpack-plugin'; - -import { version } from '../../../package.json'; -import { includePaths, excludePaths, loadEnv, getBabelRuntimePath } from './utils'; -import { getPreviewHeadHtml, getManagerHeadHtml, getPreviewBodyHtml } from '../utils'; - -export default ({ configDir, babelOptions, entries }) => { - const { stringified, raw } = loadEnv({ production: true }); - - const entriesMeta = { - iframe: { - headHtmlSnippet: getPreviewHeadHtml(configDir, process.env), - bodyHtmlSnippet: getPreviewBodyHtml(), - }, - manager: { - headHtmlSnippet: getManagerHeadHtml(configDir, process.env), - }, - }; - - return { - mode: 'production', - 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: [ - ...Object.keys(entries).map( - e => - new HtmlWebpackPlugin({ - filename: `${e === 'manager' ? 'index' : e}.html`, - excludeChunks: Object.keys(entries).filter(i => i !== e), - chunksSortMode: 'none', - alwaysWriteToDisk: true, - inject: false, - templateParameters: (compilation, files, options) => ({ - compilation, - files, - options, - version, - ...entriesMeta[e], - }), - template: require.resolve(`../templates/index.ejs`), - }) - ), - new webpack.DefinePlugin({ 'process.env': stringified }), - new Dotenv({ silent: true }), - ], - module: { - rules: [ - { - test: /\.(mjs|jsx?)$/, - use: [ - { - loader: 'babel-loader', - options: babelOptions, - }, - ], - include: includePaths, - exclude: excludePaths, - }, - { - test: /\.md$/, - use: [ - { - loader: require.resolve('raw-loader'), - }, - ], - }, - ], - }, - resolve: { - // Since we ship with json-loader always, it's better to move extensions to here - // from the default config. - extensions: ['.mjs', '.js', '.jsx', '.json'], - // 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(raw.NODE_PATH || []), - alias: { - '@babel/runtime': getBabelRuntimePath(), - }, - }, - optimization: { - // Automatically split vendor and commons for preview bundle - // https://twitter.com/wSokra/status/969633336732905474 - splitChunks: { - chunks: chunk => chunk.name !== 'manager', - }, - // Keep the runtime chunk seperated to enable long term caching - // https://twitter.com/wSokra/status/969679223278505985 - runtimeChunk: true, - }, - }; -}; diff --git a/lib/core/src/server/core-preset-babel-cache.js b/lib/core/src/server/core-preset-babel-cache.js deleted file mode 100644 index 4abb4082c8d4..000000000000 --- a/lib/core/src/server/core-preset-babel-cache.js +++ /dev/null @@ -1,11 +0,0 @@ -import findCacheDir from 'find-cache-dir'; - -export function babel(babelConfig) { - return { - // 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, - }; -} diff --git a/lib/core/src/server/core-preset-dev.js b/lib/core/src/server/core-preset-dev.js deleted file mode 100644 index 420b0c4619d5..000000000000 --- a/lib/core/src/server/core-preset-dev.js +++ /dev/null @@ -1,36 +0,0 @@ -import loadCustomBabelConfig from './loadCustomBabelConfig'; -import loadCustomAddons from './loadCustomAddonsFile'; -import loadCustomConfig from './loadCustomConfigFile'; -import createDevConfig from './config/webpack.config.dev'; -import defaultBabelConfig from './config/babel.dev'; -import { createManagerEntry, createPreviewEntry } from './config/entries'; - -export async function webpack(_, options) { - return createDevConfig(options); -} - -export async function babel(_, options) { - const { configDir, presets } = options; - - return loadCustomBabelConfig(configDir, () => - presets.apply('babelDefault', defaultBabelConfig, options) - ); -} - -export async function manager(_, options) { - return createManagerEntry(options); -} - -export async function preview(_, options) { - const entry = await createPreviewEntry(options); - - return [...entry, `${require.resolve('webpack-hot-middleware/client')}?reload=true`]; -} - -export async function addons(_, options) { - return loadCustomAddons(options); -} - -export async function config(_, options) { - return loadCustomConfig(options); -} diff --git a/lib/core/src/server/core-preset-prod.js b/lib/core/src/server/core-preset-prod.js deleted file mode 100644 index ac50f2f2d224..000000000000 --- a/lib/core/src/server/core-preset-prod.js +++ /dev/null @@ -1,34 +0,0 @@ -import loadCustomBabelConfig from './loadCustomBabelConfig'; -import loadCustomAddons from './loadCustomAddonsFile'; -import loadCustomConfig from './loadCustomConfigFile'; -import createProdConfig from './config/webpack.config.prod'; -import defaultBabelConfig from './config/babel.prod'; -import { createManagerEntry, createPreviewEntry } from './config/entries'; - -export async function webpack(_, options) { - return createProdConfig(options); -} - -export async function babel(_, options) { - const { configDir, presets } = options; - - return loadCustomBabelConfig(configDir, () => - presets.apply('babelDefault', defaultBabelConfig, options) - ); -} - -export async function manager(_, options) { - return createManagerEntry(options); -} - -export async function preview(_, options) { - return createPreviewEntry(options); -} - -export async function addons(_, options) { - return loadCustomAddons(options); -} - -export async function config(_, options) { - return loadCustomConfig(options); -} diff --git a/lib/core/src/server/dev-server.js b/lib/core/src/server/dev-server.js new file mode 100644 index 000000000000..a06b2a0db59a --- /dev/null +++ b/lib/core/src/server/dev-server.js @@ -0,0 +1,108 @@ +import path from 'path'; +import { Router } from 'express'; +import webpack from 'webpack'; +import childProcess from 'child-process-promise'; +import { logger } from '@storybook/node-logger'; + +import webpackDevMiddleware from 'webpack-dev-middleware'; +import webpackHotMiddleware from 'webpack-hot-middleware'; + +import { getMiddleware } from './utils/middleware'; +import loadConfig from './config'; + +import { loadEnv } from './config/utils'; + +let webpackResolve = () => {}; +let webpackReject = () => {}; + +export const webpackValid = new Promise((resolve, reject) => { + webpackResolve = resolve; + webpackReject = reject; +}); + +export default async function(options) { + const environment = loadEnv(); + const configDir = path.resolve(options.configDir); + const outputDir = path.resolve(options.outputDir || path.join(__dirname, '..', 'public')); + const configType = 'DEVELOPMENT'; + + const managerStartTime = process.hrtime(); + + // TODO: can we pass here the JSON.stringify(option) + const managerPromise = childProcess + .exec(`node ${path.join(__dirname, 'manager/webpack.js')} dir=${configDir} out=${outputDir}`, { + env: { + ...environment, + NODE_ENV: 'production', + }, + }) + .then(a => { + const managerTotalTime = process.hrtime(managerStartTime); + logger.trace({ message: 'manager built', time: managerTotalTime }); + + return a; + }); + + const iframeConfig = await loadConfig({ + configType, + outputDir, + corePresets: [require.resolve('./preview/preview-preset.js')], + overridePresets: [require.resolve('./preview/custom-webpack-preset.js')], + ...options, + }); + + const middlewareFn = getMiddleware(configDir); + + // remove the leading '/' + let { publicPath } = iframeConfig.output; + if (publicPath[0] === '/') { + publicPath = publicPath.slice(1); + } + + const iframeStartTime = process.hrtime(); + const iframeCompiler = webpack(iframeConfig); + const devMiddlewareOptions = { + noInfo: true, + publicPath: iframeConfig.output.publicPath, + watchOptions: iframeConfig.watchOptions || {}, + ...iframeConfig.devServer, + }; + + const router = new Router(); + const webpackDevMiddlewareInstance = webpackDevMiddleware(iframeCompiler, devMiddlewareOptions); + router.use(webpackDevMiddlewareInstance); + router.use(webpackHotMiddleware(iframeCompiler)); + + // custom middleware + middlewareFn(router); + + const iframePromise = new Promise((res, rej) => { + webpackDevMiddlewareInstance.waitUntilValid(stats => { + const iframeTotalTime = process.hrtime(iframeStartTime); + logger.trace({ message: 'iframe built', time: iframeTotalTime }); + + if (stats.hasErrors()) { + rej(stats); + } else { + res(stats); + } + }); + }); + + Promise.all([managerPromise, iframePromise]) + .then(([, iframeStats]) => { + router.get('/', (request, response) => { + response.set('Content-Type', 'text/html'); + response.sendFile(path.join(`${outputDir}/index.html`)); + }); + router.get(/(.+\.js)$/, (request, response) => { + response.set('Content-Type', 'text/javascript '); + response.sendFile(path.join(`${outputDir}/${request.params[0]}`)); + }); + + webpackResolve(iframeStats); + }) + .catch(e => webpackReject(e)); + + return router; +} diff --git a/lib/core/src/server/manager/manager-config.js b/lib/core/src/server/manager/manager-config.js new file mode 100644 index 000000000000..50e4b6510d7b --- /dev/null +++ b/lib/core/src/server/manager/manager-config.js @@ -0,0 +1,23 @@ +import loadPresets from '../presets'; +import loadCustomPresets from '../common/custom-presets'; + +async function getManagerWebpackConfig(options, presets) { + const entries = await presets.apply('managerEntries', [], options); + + return presets.apply('managerWebpack', {}, { ...options, entries }); +} + +export default async options => { + const { corePresets = [], overridePresets = [], ...restOptions } = options; + + const presetsConfig = [ + ...corePresets, + require.resolve('../common/babel-cache-preset.js'), + ...loadCustomPresets(options), + ...overridePresets, + ]; + + const presets = loadPresets(presetsConfig); + + return getManagerWebpackConfig({ ...restOptions, presets }, presets); +}; diff --git a/lib/core/src/server/manager/manager-preset.js b/lib/core/src/server/manager/manager-preset.js new file mode 100644 index 000000000000..91956f8b1531 --- /dev/null +++ b/lib/core/src/server/manager/manager-preset.js @@ -0,0 +1,25 @@ +import loadCustomAddons from '../utils/load-custom-addons-file'; +import createDevConfig from './manager-webpack.config'; + +export async function managerWebpack(_, options) { + return createDevConfig(options); +} + +export async function managerEntries(_, options) { + const { presets } = options; + const entries = [require.resolve('../common/polyfills')]; + + const installedAddons = await presets.apply('addons', [], options); + + if (installedAddons && installedAddons.length) { + entries.push(...installedAddons); + } + + entries.push(require.resolve('../../client/manager')); + + return entries; +} + +export async function addons(_, options) { + return loadCustomAddons(options); +} diff --git a/lib/core/src/server/manager/manager-webpack.config.js b/lib/core/src/server/manager/manager-webpack.config.js new file mode 100644 index 000000000000..aef6d3e7584c --- /dev/null +++ b/lib/core/src/server/manager/manager-webpack.config.js @@ -0,0 +1,82 @@ +import webpack from 'webpack'; +import Dotenv from 'dotenv-webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import HtmlWebpackHarddiskPlugin from '@ndelangen/html-webpack-harddisk-plugin'; +import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; + +import { version } from '../../../package.json'; +import { getManagerHeadHtml } from '../utils/template'; +import { loadEnv, getBabelRuntimePath } from '../config/utils'; + +export default ({ configDir, entries, outputDir }) => { + const { raw, stringified } = loadEnv(); + + const exclude = /node_modules/; + + return { + name: 'manager', + mode: 'production', + bail: true, + devtool: 'none', + entry: entries, + output: { + path: outputDir, + filename: '[name].[chunkhash].bundle.js', + publicPath: '', + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: `index.html`, + chunksSortMode: 'none', + alwaysWriteToDisk: true, + inject: false, + templateParameters: (compilation, files, options) => ({ + compilation, + files, + options, + version, + headHtmlSnippet: getManagerHeadHtml(configDir, process.env), + }), + template: require.resolve(`../templates/index.ejs`), + }), + new HtmlWebpackHarddiskPlugin(), + new webpack.DefinePlugin({ 'process.env': stringified }), + new CaseSensitivePathsPlugin(), + new Dotenv({ silent: true }), + ], + module: { + rules: [ + { + test: /\.jsx?$/, + use: [ + { + loader: 'babel-loader', + }, + ], + exclude, + }, + { + test: /\.md$/, + use: [ + { + loader: require.resolve('raw-loader'), + }, + ], + }, + ], + }, + resolve: { + extensions: ['.mjs', '.js', '.jsx', '.json'], + modules: ['node_modules'].concat(raw.NODE_PATH || []), + alias: { + '@babel/runtime': getBabelRuntimePath(), + }, + }, + optimization: { + splitChunks: { + chunks: 'all', + }, + runtimeChunk: true, + }, + }; +}; diff --git a/lib/core/src/server/manager/webpack.js b/lib/core/src/server/manager/webpack.js new file mode 100644 index 000000000000..2fcd1fea4d3b --- /dev/null +++ b/lib/core/src/server/manager/webpack.js @@ -0,0 +1,47 @@ +import console from 'console'; +import webpack from 'webpack'; + +import loadManagerConfig from './manager-config'; + +const { Console } = console; + +const configDir = process.argv.reduce( + (acc, i) => (i.includes('dir=') ? i.replace('dir=', '') : acc), + './.storybook' +); +const outputDir = process.argv.reduce( + (acc, i) => (i.includes('out=') ? i.replace('out=', '') : acc), + undefined +); + +const bad = new Console(process.stderr); +const good = new Console(process.stdout); + +loadManagerConfig({ + outputDir, + configDir, + corePresets: [require.resolve('./manager-preset.js')], +}) + .then(config => webpack(config)) + .then( + compiler => + new Promise((res, rej) => { + try { + compiler.run((err, stats) => { + if (err || stats.hasErrors()) { + bad.log(JSON.stringify({ err, data: stats.toJson() }, null, 2)); + rej(err); + } else { + res(`success! ${process.env.NODE_ENV}`); + } + }); + } catch (e) { + rej(e); + } + }) + ) + .then(data => good.log(data)) + .catch(err => { + bad.log(err); + process.exit(1); + }); diff --git a/lib/core/src/server/middleware.js b/lib/core/src/server/middleware.js deleted file mode 100644 index dd91e765a59b..000000000000 --- a/lib/core/src/server/middleware.js +++ /dev/null @@ -1,70 +0,0 @@ -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 { getMiddleware } from './utils'; -import loadConfig from './config'; - -let webpackResolve = () => {}; -let webpackReject = () => {}; - -export const webpackValid = new Promise((resolve, reject) => { - webpackResolve = resolve; - webpackReject = reject; -}); - -export default async function(options) { - const { configDir } = options; - - // Build the webpack configuration using the `getBaseConfig` - // custom `.babelrc` file and `webpack.config.js` files - const config = await loadConfig({ - configType: 'DEVELOPMENT', - corePresets: [require.resolve('./core-preset-dev.js')], - ...options, - }); - 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; -} diff --git a/lib/core/src/server/presets.js b/lib/core/src/server/presets.js index 9e8489fb6b75..68666de79734 100644 --- a/lib/core/src/server/presets.js +++ b/lib/core/src/server/presets.js @@ -2,7 +2,7 @@ import { logger } from '@storybook/node-logger'; function interopRequireDefault(filePath) { // eslint-disable-next-line global-require,import/no-dynamic-require - const result = require(filePath); + const result = require(`${filePath}`); const isES6DefaultExported = typeof result === 'object' && result !== null && typeof result.default !== 'undefined'; @@ -27,6 +27,7 @@ function loadPreset(preset) { }; } catch (e) { logger.warn(` Failed to load preset: ${JSON.stringify(preset)}`); + logger.error(e); return false; } } @@ -38,7 +39,9 @@ function loadPresets(presets) { logger.info('=> Loading presets'); - return presets.map(loadPreset).filter(preset => preset); + const result = presets.map(loadPreset).filter(preset => preset); + + return result; } function applyPresets(presets, config, args = {}, extension) { diff --git a/lib/core/src/server/config/webpack.config.default.js b/lib/core/src/server/preview/base-webpack.config.js similarity index 93% rename from lib/core/src/server/config/webpack.config.default.js rename to lib/core/src/server/preview/base-webpack.config.js index ff4e7a18f60f..80be3b1c5de5 100644 --- a/lib/core/src/server/config/webpack.config.default.js +++ b/lib/core/src/server/preview/base-webpack.config.js @@ -20,10 +20,10 @@ export function createDefaultWebpackConfig(storybookBaseConfig) { { loader: require.resolve('postcss-loader'), options: { - ident: 'postcss', // https://webpack.js.org/guides/migrating/#complex-options + ident: 'postcss', postcss: {}, plugins: () => [ - require('postcss-flexbugs-fixes'), // eslint-disable-line + require('postcss-flexbugs-fixes'), // eslint-disable-line global-require autoprefixer({ flexbox: 'no-2009', }), diff --git a/lib/core/src/server/core-preset-webpack-custom.js b/lib/core/src/server/preview/custom-webpack-preset.js similarity index 57% rename from lib/core/src/server/core-preset-webpack-custom.js rename to lib/core/src/server/preview/custom-webpack-preset.js index 22458e39ca54..467d268902e7 100644 --- a/lib/core/src/server/core-preset-webpack-custom.js +++ b/lib/core/src/server/preview/custom-webpack-preset.js @@ -1,40 +1,32 @@ import { logger } from '@storybook/node-logger'; -import loadCustomWebpackConfig from './loadCustomWebpackConfig'; -import mergeConfigs from './mergeConfigs'; -import { createDefaultWebpackConfig } from './config/webpack.config.default'; +import loadCustomWebpackConfig from '../utils/load-custom-webpack-config'; +import mergeConfigs from '../utils/merge-webpack-config'; +import { createDefaultWebpackConfig } from './base-webpack.config'; -function informAboutCustomConfig(defaultConfigName) { +function logConfigName(defaultConfigName) { if (!defaultConfigName) { logger.info('=> Using default webpack setup.'); - return; + } else { + logger.info(`=> Using default webpack setup based on "${defaultConfigName}".`); } - - logger.info(`=> Using default webpack setup based on "${defaultConfigName}".`); -} - -function wrapPresets(presets) { - return { - webpackFinal: async (config, args) => presets.apply('webpackFinal', config, args), - }; } async function createFinalDefaultConfig(presets, config, options) { const defaultConfig = createDefaultWebpackConfig(config); - return presets.webpackFinal(defaultConfig, options); + return presets.apply('webpackFinal', defaultConfig, options); } export async function webpack(config, options) { - const { configDir, configType, defaultConfigName } = options; - const presets = wrapPresets(options.presets); + const { configDir, configType, defaultConfigName, presets } = options; - const finalConfig = await presets.webpackFinal(config, options); + const finalConfig = await presets.apply('webpackFinal', config, options); // Check whether user has a custom webpack config file and // return the (extended) base configuration if it's not available. const customConfig = loadCustomWebpackConfig(configDir); if (customConfig === null) { - informAboutCustomConfig(defaultConfigName); + logConfigName(defaultConfigName); return createFinalDefaultConfig(presets, config, options); } diff --git a/lib/core/src/server/preview/entries.js b/lib/core/src/server/preview/entries.js new file mode 100644 index 000000000000..124bd5b0a014 --- /dev/null +++ b/lib/core/src/server/preview/entries.js @@ -0,0 +1,14 @@ +export async function createPreviewEntry(options) { + const { configDir, presets } = options; + const entries = [require.resolve('../common/polyfills'), require.resolve('./globals')]; + + const configs = await presets.apply('config', [], options); + + if (!configs || !configs.length) { + throw new Error(`=> Create a storybook config file in "${configDir}/config.{ext}".`); + } + + entries.push(...configs); + + return entries; +} diff --git a/lib/core/src/server/config/globals.js b/lib/core/src/server/preview/globals.js similarity index 100% rename from lib/core/src/server/config/globals.js rename to lib/core/src/server/preview/globals.js diff --git a/lib/core/src/server/preview/iframe-webpack.config.js b/lib/core/src/server/preview/iframe-webpack.config.js new file mode 100644 index 000000000000..b1408691f834 --- /dev/null +++ b/lib/core/src/server/preview/iframe-webpack.config.js @@ -0,0 +1,103 @@ +import path from 'path'; +import webpack from 'webpack'; +import Dotenv from 'dotenv-webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; +import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'; + +import { + includePaths, + excludePaths, + nodeModulesPaths, + loadEnv, + getBabelRuntimePath, +} from '../config/utils'; +import { getPreviewHeadHtml, getPreviewBodyHtml } from '../utils/template'; + +export default ({ + configDir, + babelOptions, + entries, + outputDir = path.join('.', 'public'), + quiet, + packageJson, + configType, +}) => { + const { raw, stringified } = loadEnv({ production: true }); + const isProd = configType === 'PRODUCTION'; + + return { + mode: isProd ? 'production' : 'development', + bail: isProd, + devtool: '#cheap-module-source-map', + entry: entries, + output: { + path: path.join(process.cwd(), outputDir), + filename: '[name].[hash].bundle.js', + publicPath: '', + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: `iframe.html`, + chunksSortMode: 'none', + alwaysWriteToDisk: true, + inject: false, + templateParameters: (compilation, files, options) => ({ + compilation, + files, + options, + version: packageJson.version, + headHtmlSnippet: getPreviewHeadHtml(configDir, process.env), + bodyHtmlSnippet: getPreviewBodyHtml(), + }), + template: require.resolve(`../templates/index.ejs`), + }), + new webpack.DefinePlugin({ 'process.env': stringified }), + isProd ? null : new WatchMissingNodeModulesPlugin(nodeModulesPaths), + isProd ? null : new webpack.HotModuleReplacementPlugin(), + new CaseSensitivePathsPlugin(), + quiet ? null : new webpack.ProgressPlugin(), + new Dotenv({ silent: true }), + ].filter(Boolean), + module: { + rules: [ + { + test: /\.(mjs|jsx?)$/, + use: [ + { + loader: 'babel-loader', + options: babelOptions, + }, + ], + include: includePaths, + exclude: excludePaths, + }, + { + test: /\.md$/, + use: [ + { + loader: require.resolve('raw-loader'), + }, + ], + }, + ], + }, + resolve: { + extensions: ['.mjs', '.js', '.jsx', '.json'], + modules: ['node_modules'].concat(raw.NODE_PATH || []), + mainFields: ['browser', 'main', 'module'], + alias: { + '@babel/runtime': getBabelRuntimePath(), + }, + }, + optimization: { + splitChunks: { + chunks: 'all', + }, + runtimeChunk: true, + }, + performance: { + hints: isProd ? 'warning' : false, + }, + }; +}; diff --git a/lib/core/src/server/preview/preview-preset.js b/lib/core/src/server/preview/preview-preset.js new file mode 100644 index 000000000000..d2c56e182579 --- /dev/null +++ b/lib/core/src/server/preview/preview-preset.js @@ -0,0 +1,30 @@ +import loadCustomBabelConfig from '../utils/load-custom-babel-config'; +import loadCustomConfig from '../utils/load-custom-config-file'; +import babelConfig from '../common/babel'; + +import webpackConfig from './iframe-webpack.config'; +import { createPreviewEntry } from './entries'; + +export const webpack = async (_, options) => webpackConfig(options); + +export const babel = async (_, options) => { + const { configDir, presets } = options; + + return loadCustomBabelConfig(configDir, () => + presets.apply('babelDefault', babelConfig(options), options) + ); +}; + +export const entries = async (_, options) => { + let result = []; + + result = result.concat(await createPreviewEntry(options)); + + if (options.configType === 'DEVELOPMENT') { + result = result.concat(`${require.resolve('webpack-hot-middleware/client')}?reload=true`); + } + + return result; +}; + +export const config = async (_, options) => loadCustomConfig(options); diff --git a/lib/core/src/server/__snapshots__/mergeConfigs.test.js.snap b/lib/core/src/server/utils/__snapshots__/merge-webpack-config.test.js.snap similarity index 100% rename from lib/core/src/server/__snapshots__/mergeConfigs.test.js.snap rename to lib/core/src/server/utils/__snapshots__/merge-webpack-config.test.js.snap diff --git a/lib/core/src/server/interpret-files.js b/lib/core/src/server/utils/interpret-files.js similarity index 100% rename from lib/core/src/server/interpret-files.js rename to lib/core/src/server/utils/interpret-files.js diff --git a/lib/core/src/server/interpret-files.test.js b/lib/core/src/server/utils/interpret-files.test.js similarity index 100% rename from lib/core/src/server/interpret-files.test.js rename to lib/core/src/server/utils/interpret-files.test.js diff --git a/lib/core/src/server/loadCustomAddonsFile.js b/lib/core/src/server/utils/load-custom-addons-file.js similarity index 99% rename from lib/core/src/server/loadCustomAddonsFile.js rename to lib/core/src/server/utils/load-custom-addons-file.js index 90211dd5a954..b17f37c9c5ba 100644 --- a/lib/core/src/server/loadCustomAddonsFile.js +++ b/lib/core/src/server/utils/load-custom-addons-file.js @@ -1,5 +1,6 @@ import path from 'path'; import { logger } from '@storybook/node-logger'; + import { getInterpretedFile } from './interpret-files'; function loadCustomAddons({ configDir }) { diff --git a/lib/core/src/server/loadCustomBabelConfig.js b/lib/core/src/server/utils/load-custom-babel-config.js similarity index 99% rename from lib/core/src/server/loadCustomBabelConfig.js rename to lib/core/src/server/utils/load-custom-babel-config.js index 417ebcd5591f..6ba6a94ce333 100644 --- a/lib/core/src/server/loadCustomBabelConfig.js +++ b/lib/core/src/server/utils/load-custom-babel-config.js @@ -1,8 +1,9 @@ import fs from 'fs'; import path from 'path'; import JSON5 from 'json5'; -import { sync as resolveSync } from 'resolve'; import { satisfies } from 'semver'; +import { sync as resolveSync } from 'resolve'; + import { logger } from '@storybook/node-logger'; function removeReactHmre(presets) { diff --git a/lib/core/src/server/loadCustomConfigFile.js b/lib/core/src/server/utils/load-custom-config-file.js similarity index 100% rename from lib/core/src/server/loadCustomConfigFile.js rename to lib/core/src/server/utils/load-custom-config-file.js diff --git a/lib/core/src/server/loadCustomWebpackConfig.js b/lib/core/src/server/utils/load-custom-webpack-config.js similarity index 81% rename from lib/core/src/server/loadCustomWebpackConfig.js rename to lib/core/src/server/utils/load-custom-webpack-config.js index 20c2ab8856ed..73dcc0aa9476 100644 --- a/lib/core/src/server/loadCustomWebpackConfig.js +++ b/lib/core/src/server/utils/load-custom-webpack-config.js @@ -1,5 +1,5 @@ import path from 'path'; -import serverRequire from './serverRequire'; +import serverRequire from './server-require'; const webpackConfigs = ['webpack.config', 'webpackfile']; diff --git a/lib/core/src/server/mergeConfigs.js b/lib/core/src/server/utils/merge-webpack-config.js similarity index 100% rename from lib/core/src/server/mergeConfigs.js rename to lib/core/src/server/utils/merge-webpack-config.js diff --git a/lib/core/src/server/mergeConfigs.test.js b/lib/core/src/server/utils/merge-webpack-config.test.js similarity index 96% rename from lib/core/src/server/mergeConfigs.test.js rename to lib/core/src/server/utils/merge-webpack-config.test.js index da2962bcb8e6..4a5c78aaf952 100644 --- a/lib/core/src/server/mergeConfigs.test.js +++ b/lib/core/src/server/utils/merge-webpack-config.test.js @@ -1,4 +1,4 @@ -import mergeConfigs from './mergeConfigs'; +import mergeConfigs from './merge-webpack-config'; const config = { devtool: 'source-maps', diff --git a/lib/core/src/server/utils/middleware.js b/lib/core/src/server/utils/middleware.js new file mode 100644 index 000000000000..74928bc7a5c8 --- /dev/null +++ b/lib/core/src/server/utils/middleware.js @@ -0,0 +1,14 @@ +import path from 'path'; +import fs from 'fs'; + +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 () => {}; +} diff --git a/lib/core/src/server/serverRequire.js b/lib/core/src/server/utils/server-require.js similarity index 100% rename from lib/core/src/server/serverRequire.js rename to lib/core/src/server/utils/server-require.js diff --git a/lib/core/src/server/utils.js b/lib/core/src/server/utils/template.js similarity index 54% rename from lib/core/src/server/utils.js rename to lib/core/src/server/utils/template.js index 985cd4483969..7b2bc51be54f 100644 --- a/lib/core/src/server/utils.js +++ b/lib/core/src/server/utils/template.js @@ -1,27 +1,18 @@ import path from 'path'; import fs from 'fs'; -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 () => {}; -} - const interpolate = (string, data = {}) => Object.entries(data).reduce((acc, [k, v]) => acc.replace(new RegExp(`%${k}%`, 'g'), v), string); export function getPreviewBodyHtml() { - return fs.readFileSync(path.resolve(__dirname, 'templates/base-preview-body.html'), 'utf8'); + return fs.readFileSync(path.resolve(__dirname, '../templates/base-preview-body.html'), 'utf8'); } export function getPreviewHeadHtml(configDirPath, interpolations) { - const base = fs.readFileSync(path.resolve(__dirname, 'templates/base-preview-head.html'), 'utf8'); + const base = fs.readFileSync( + path.resolve(__dirname, '../templates/base-preview-head.html'), + 'utf8' + ); const headHtmlPath = path.resolve(configDirPath, 'preview-head.html'); let result = base; @@ -34,7 +25,10 @@ export function getPreviewHeadHtml(configDirPath, interpolations) { } export function getManagerHeadHtml(configDirPath, interpolations) { - const base = fs.readFileSync(path.resolve(__dirname, 'templates/base-manager-head.html'), 'utf8'); + const base = fs.readFileSync( + path.resolve(__dirname, '../templates/base-manager-head.html'), + 'utf8' + ); const scriptPath = path.resolve(configDirPath, 'manager-head.html'); let result = base; diff --git a/lib/core/src/server/utils.test.js b/lib/core/src/server/utils/template.test.js similarity index 82% rename from lib/core/src/server/utils.test.js rename to lib/core/src/server/utils/template.test.js index e5e6ad6445c2..debc72a0fbe3 100644 --- a/lib/core/src/server/utils.test.js +++ b/lib/core/src/server/utils/template.test.js @@ -1,5 +1,5 @@ import mock from 'mock-fs'; -import { getPreviewHeadHtml } from './utils'; +import { getPreviewHeadHtml } from './template'; const HEAD_HTML_CONTENTS = ''; const BASE_HTML_CONTENTS = ''; @@ -8,7 +8,7 @@ describe('server.getPreviewHeadHtml', () => { describe('when .storybook/preview-head.html does not exist', () => { beforeEach(() => { mock({ - [`${__dirname}/templates/base-preview-head.html`]: BASE_HTML_CONTENTS, + [`${__dirname}/../templates/base-preview-head.html`]: BASE_HTML_CONTENTS, config: {}, }); }); @@ -26,7 +26,7 @@ describe('server.getPreviewHeadHtml', () => { describe('when .storybook/preview-head.html exists', () => { beforeEach(() => { mock({ - [`${__dirname}/templates/base-preview-head.html`]: BASE_HTML_CONTENTS, + [`${__dirname}/../templates/base-preview-head.html`]: BASE_HTML_CONTENTS, config: { 'preview-head.html': HEAD_HTML_CONTENTS, }, diff --git a/lib/node-logger/package.json b/lib/node-logger/package.json index b5d207f35e26..816506d14647 100644 --- a/lib/node-logger/package.json +++ b/lib/node-logger/package.json @@ -24,6 +24,8 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "npmlog": "^4.1.2" + "chalk": "^2.4.1", + "npmlog": "^4.1.2", + "pretty-hrtime": "^1.0.3" } } diff --git a/lib/node-logger/src/index.js b/lib/node-logger/src/index.js index b1b5784eedcc..bd166aebf037 100644 --- a/lib/node-logger/src/index.js +++ b/lib/node-logger/src/index.js @@ -1,7 +1,20 @@ import npmLog from 'npmlog'; +import prettyTime from 'pretty-hrtime'; +import chalk from 'chalk'; + +export const colors = { + pink: chalk.hex('F1618C'), + purple: chalk.hex('B57EE5'), + orange: chalk.hex('F3AD38'), + green: chalk.hex('A2E05E'), + blue: chalk.hex('6DABF5'), + red: chalk.hex('F16161'), + gray: chalk.gray, +}; export const logger = { info: message => npmLog.info('', message), warn: message => npmLog.warn('', message), error: message => npmLog.error('', message), + trace: ({ message, time }) => npmLog.info(`${message} (${colors.purple(prettyTime(time))})`), }; diff --git a/package.json b/package.json index 8f1d8226304b..8563f769d313 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "repo-dirty-check": "node ./scripts/repo-dirty-check", "start": "yarn --cwd examples/official-storybook storybook", "test": "node ./scripts/test.js", - "test-latest-cra": "yarn --cwd lib/cli run test-latest-cra" + "test-latest-cra": "yarn --prefix --cwd lib/cli run test-latest-cra", + "test:cli": "npm --prefix lib/cli run test" }, "devDependencies": { "@angular/common": "^7.0.1", diff --git a/yarn.lock b/yarn.lock index 4877a9565d42..6759645ddb6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1818,11 +1818,18 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@ndelangen/html-webpack-harddisk-plugin@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@ndelangen/html-webpack-harddisk-plugin/-/html-webpack-harddisk-plugin-0.2.0.tgz#d2eb570597c83c1aa93d1f2fcb3d874a5855de07" + integrity sha512-55Mo2b5WtIT0l653y6ocu7C6QzznbasEnlixGzA26WK8Fj81wuEY3xf5N5bNAvDVfrwTLIPTXdEUGgPdrPLszw== + dependencies: + mkdirp "^0.5.1" "@ngrx/store@^6.1.2": version "6.1.2" resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-6.1.2.tgz#20fb5ab4d79571b804a348093aa11a167fe2946f" integrity sha512-W9MbXrwhIRmN1BlINF9BT+rHR046e1HNk7GqykcDJrK9wW74PJW3aE5iuPb2sTPipBMjPHsXzc73E4U/+OTAyw== + "@ngtools/webpack@7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-7.0.3.tgz#96bc0d94e9a8ac84eb34cf81c59fdd21bfbd18e3" @@ -17502,6 +17509,11 @@ pretty-format@^4.2.1: resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-4.3.1.tgz#530be5c42b3c05b36414a7a2a4337aa80acd0e8d" integrity sha1-UwvlxCs8BbNkFKeipDN6qArNDo0= +pretty-hrtime@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" + integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= + pretty-ms@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-3.2.0.tgz#87a8feaf27fc18414d75441467d411d6e6098a25" @@ -20638,6 +20650,13 @@ spawn-command@^0.0.2-1: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= +spawn-promise@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/spawn-promise/-/spawn-promise-0.1.8.tgz#a5bea98814c48f52cbe02720e7fe2d6fc3b5119a" + integrity sha512-pTkEOFxvYLq9SaI1d8bwepj0yD9Yyz65+4e979YZLv/L3oYPxZpDTabcm6e+KIZniGK9mQ+LGrwB5s1v2z67nQ== + dependencies: + co "^4.6.0" + spawn-sync@^1.0.11, spawn-sync@^1.0.15: version "1.0.15" resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"