diff --git a/app/react/package.json b/app/react/package.json index 09ff8eb3b30b..e58239f6b1f6 100644 --- a/app/react/package.json +++ b/app/react/package.json @@ -33,12 +33,15 @@ "@babel/runtime": "^7.1.2", "@emotion/styled": "^0.10.6", "@storybook/core": "4.0.0-rc.1", + "@storybook/node-logger": "^3.4.11", "babel-plugin-react-docgen": "^2.0.0", "common-tags": "^1.8.0", "global": "^4.3.2", "lodash": "^4.17.11", "prop-types": "^15.6.2", - "react-dev-utils": "^6.0.5" + "react-dev-utils": "^6.0.5", + "semver": "^5.6.0", + "webpack": "^4.21.0" }, "peerDependencies": { "babel-loader": "^7.0.0 || ^8.0.0", diff --git a/app/react/src/server/cra_config.js b/app/react/src/server/cra_config.js new file mode 100644 index 000000000000..07a38175cccf --- /dev/null +++ b/app/react/src/server/cra_config.js @@ -0,0 +1,85 @@ +import semver from 'semver'; +import { normalizeCondition } from 'webpack/lib/RuleSet'; + +export function isReactScriptsInstalled() { + try { + // eslint-disable-next-line global-require, import/no-extraneous-dependencies + const reactScriptsJson = require('react-scripts/package.json'); + if (semver.lt(reactScriptsJson.version, '2.0.0')) return false; + return true; + } catch (e) { + return false; + } +} + +export function getStyleRules(rules) { + // Extensions of style rules we're interested in + const extensions = ['.css', '.scss', '.sass', '.module.css', '.module.scss', '.module.sass']; + + return rules.reduce((styleRules, rule) => { + // If at least one style extension satisfies the rule test, the rule is one + // we want to extract + if (rule.test && extensions.some(normalizeCondition(rule.test))) { + // If the base test is for styles, return early + return styleRules.concat(rule); + } + + // Get any style rules contained in rule.oneOf + if (!rule.test && rule.oneOf) { + styleRules.push(...getStyleRules(rule.oneOf)); + } + + // Get any style rules contained in rule.rules + if (!rule.test && rule.rules) { + styleRules.push(...getStyleRules(rule.rules)); + } + + return styleRules; + }, []); +} + +export function getCraWebpackConfig(mode) { + if (mode === 'production') { + // eslint-disable-next-line global-require, import/no-extraneous-dependencies + return require('react-scripts/config/webpack.config.prod'); + } + + // eslint-disable-next-line global-require, import/no-extraneous-dependencies + return require('react-scripts/config/webpack.config.dev'); +} + +export function applyCRAWebpackConfig(baseConfig) { + // Remove any rules from baseConfig that test true for any one of the extensions + const baseRulesExcludingStyles = baseConfig.module.rules.filter( + rule => !rule.test || !['.css', '.scss', '.sass'].some(normalizeCondition(rule.test)) + ); + + // Load create-react-app config + const craWebpackConfig = getCraWebpackConfig(baseConfig.mode); + + const craStyleRules = getStyleRules(craWebpackConfig.module.rules); + + // Add css minification for production + const plugins = [...baseConfig.plugins]; + if (baseConfig.mode === 'production') { + // eslint-disable-next-line global-require, import/no-extraneous-dependencies + const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + plugins.push( + new MiniCssExtractPlugin({ + // Options similar to the same options in webpackOptions.output + // both options are optional + filename: 'static/css/[name].[contenthash:8].css', + chunkFilename: 'static/css/[name].[contenthash:8].chunk.css', + }) + ); + } + + return { + ...baseConfig, + module: { + ...baseConfig.module, + rules: [...baseRulesExcludingStyles, ...craStyleRules], + }, + plugins, + }; +} diff --git a/app/react/src/server/framework-preset-cra-styles.js b/app/react/src/server/framework-preset-cra-styles.js new file mode 100644 index 000000000000..ad1ce7a27142 --- /dev/null +++ b/app/react/src/server/framework-preset-cra-styles.js @@ -0,0 +1,13 @@ +import { logger } from '@storybook/node-logger'; +import { applyCRAWebpackConfig, isReactScriptsInstalled } from './cra_config'; + +export function webpackFinal(config) { + if (!isReactScriptsInstalled()) { + logger.info('=> Using base config because react-scripts is not installed.'); + return config; + } + + logger.info('=> Loading create-react-app config.'); + + return applyCRAWebpackConfig(config); +} diff --git a/app/react/src/server/options.js b/app/react/src/server/options.js index 1dda06fe1e3c..643114ff9ac6 100644 --- a/app/react/src/server/options.js +++ b/app/react/src/server/options.js @@ -6,5 +6,6 @@ export default { frameworkPresets: [ require.resolve('./framework-preset-react.js'), require.resolve('./framework-preset-react-docgen.js'), + require.resolve('./framework-preset-cra-styles.js'), ], }; diff --git a/lib/core/src/server/config.js b/lib/core/src/server/config.js index 05f6fb563168..83ac5604ad19 100644 --- a/lib/core/src/server/config.js +++ b/lib/core/src/server/config.js @@ -7,7 +7,6 @@ function wrapCorePresets(presets) { return { babel: async (config, args) => presets.apply('babel', config, args), webpack: async (config, args) => presets.apply('webpack', config, args), - webpackFinal: async (config, args) => presets.apply('webpackFinal', config, args), preview: async (config, args) => presets.apply('preview', config, args), manager: async (config, args) => presets.apply('manager', config, args), }; @@ -35,9 +34,7 @@ async function getWebpackConfig(options, presets) { manager: await presets.manager([], options), }; - const webpackConfig = await presets.webpack({}, { ...options, babelOptions, entries }); - - return presets.webpackFinal(webpackConfig, options); + return presets.webpack({}, { ...options, babelOptions, entries }); } export default async options => { diff --git a/lib/core/src/server/core-preset-webpack-custom.js b/lib/core/src/server/core-preset-webpack-custom.js index 5c14ec084cf3..22458e39ca54 100644 --- a/lib/core/src/server/core-preset-webpack-custom.js +++ b/lib/core/src/server/core-preset-webpack-custom.js @@ -12,8 +12,22 @@ function informAboutCustomConfig(defaultConfigName) { logger.info(`=> Using default webpack setup based on "${defaultConfigName}".`); } -export function webpack(config, { configDir, configType, 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); +} + +export async function webpack(config, options) { + const { configDir, configType, defaultConfigName } = options; + const presets = wrapPresets(options.presets); + + const finalConfig = await presets.webpackFinal(config, options); // Check whether user has a custom webpack config file and // return the (extended) base configuration if it's not available. @@ -21,15 +35,16 @@ export function webpack(config, { configDir, configType, defaultConfigName }) { if (customConfig === null) { informAboutCustomConfig(defaultConfigName); - return defaultConfig; + return createFinalDefaultConfig(presets, config, options); } if (typeof customConfig === 'function') { logger.info('=> Loading custom webpack config (full-control mode).'); - return customConfig(config, configType, defaultConfig); + const finalDefaultConfig = await createFinalDefaultConfig(presets, config, options); + return customConfig(finalConfig, configType, finalDefaultConfig); } logger.info('=> Loading custom webpack config (extending mode).'); - return mergeConfigs(config, customConfig); + return mergeConfigs(finalConfig, customConfig); } diff --git a/yarn.lock b/yarn.lock index 043d4127aa76..a245e8a837e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1880,6 +1880,13 @@ "@storybook/react-simple-di" "^1.2.1" babel-runtime "6.x.x" +"@storybook/node-logger@^3.4.11": + version "3.4.11" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-3.4.11.tgz#a6684a4c21f74dae937cd9f202deec0932d3f3b5" + integrity sha512-eCjvZsCwZTcjDOeG7JDEVs5bugyybpAFu/4+X3hfikxGBBjnx2NtjJIfIsriUKa1O559+aFGUG73wogYAjudhg== + dependencies: + npmlog "^4.1.2" + "@storybook/podda@^1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@storybook/podda/-/podda-1.2.3.tgz#53c4a1a3f8c7bbd5755dff5c34576fd1af9d38ba" @@ -5753,12 +5760,12 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000864, caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000887, caniuse-lite@^1.0.30000889, caniuse-lite@^1.0.30000890: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000864, caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000887, caniuse-lite@^1.0.30000889: version "1.0.30000890" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000890.tgz#86a18ffcc65d79ec6a437e985761b8bf1c4efeaf" integrity sha512-4NI3s4Y6ROm+SgZN5sLUG4k7nVWQnedis3c/RWkynV5G6cHSY7+a8fwFyn2yoBDE3E6VswhTNNwR3PvzGqlTkg== -caniuse-lite@^1.0.30000892: +caniuse-lite@^1.0.30000890, caniuse-lite@^1.0.30000892: version "1.0.30000892" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000892.tgz#344d2b51ee3ff5977537da4aa449c90eec40b759" integrity sha512-X9rxMaWZNbJB5qjkDqPtNv/yfViTeUL6ILk0QJNxLV3OhKC5Acn5vxsuUvllR6B48mog8lmS+whwHq/QIYSL9w==