diff --git a/packages/babel-preset-react-app/README.md b/packages/babel-preset-react-app/README.md index 3470ea0a6f3..12f223e1584 100644 --- a/packages/babel-preset-react-app/README.md +++ b/packages/babel-preset-react-app/README.md @@ -51,3 +51,47 @@ Make sure you have a `tsconfig.json` file at the root directory. You can also us "presets": [["react-app", { "flow": false, "typescript": true }]] } ``` + +## Usage within NPM packages + +If you are creating an NPM package that contains a React component you can use the options for `commonjs` and `esmodules` to create proper builds for `lib`, `es` and `dist` folders. The configuration example below will work for most common cases but will not be suitable to all projects. Similar setups are used by popular NPM packages such as [react-redux](https://github.com/reduxjs/react-redux) and [react-router](https://github.com/ReactTraining/react-router/tree/master/packages/react-router). + +### `babel.config.js` + +When building for `lib`, `es` folders you want to set the `absoluteRuntime` to false. When building for the `dist` folder, you also want to disable helpers (because Rollup manages helpers automatically). + +Note that it is recommended to set `NODE_ENV` environment variable to "production" when building an NPM package. Setting `NODE_ENV` to "development" will put the `@babel/preset-react` plugin into development mode, which is undesirable for a published NPM package. + +```js +const { NODE_ENV, MODULES_ENV } = process.env; + +const isEnvTest = NODE_ENV === 'test'; +if (!isEnvTest) { + // force production mode for package builds + process.env.NODE_ENV = 'production'; +} + +const useCommonJS = isEnvTest || MODULES_ENV === 'commonjs'; +const useESModules = MODULES_ENV === 'esmodules'; + +module.exports = { + presets: [ + // for testing with jest/jsdom + useCommonJS && isEnvTest && 'babel-preset-react-app/test', + // building for lib folder + useCommonJS && + !isEnvTest && [ + 'babel-preset-react-app/commonjs', + { absoluteRuntime: false }, + ], + // building for es folder + useESModules && [ + 'babel-preset-react-app/esmodules', + { absoluteRuntime: false }, + ], + // building for dist folder + !useCommonJS && + !useESModules && ['babel-preset-react-app', { helpers: false }], + ].filter(Boolean), +}; +``` diff --git a/packages/babel-preset-react-app/commonjs.js b/packages/babel-preset-react-app/commonjs.js new file mode 100644 index 00000000000..0a7b4975eeb --- /dev/null +++ b/packages/babel-preset-react-app/commonjs.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const create = require('./create'); + +module.exports = function(api, opts) { + // This is similar to how `env` works in Babel: + // https://babeljs.io/docs/usage/babelrc/#env-option + // We are not using `env` because it’s ignored in versions > babel-core@6.10.4: + // https://github.com/babel/babel/issues/4539 + // https://github.com/facebook/create-react-app/issues/720 + // It’s also nice that we can enforce `NODE_ENV` being specified. + const env = process.env.BABEL_ENV || process.env.NODE_ENV; + return create( + api, + Object.assign({ helpers: false }, opts, { useCommonJS: true }), + env + ); +}; diff --git a/packages/babel-preset-react-app/create.js b/packages/babel-preset-react-app/create.js index a0423f2551c..8bfffed2871 100644 --- a/packages/babel-preset-react-app/create.js +++ b/packages/babel-preset-react-app/create.js @@ -29,10 +29,15 @@ module.exports = function(api, opts, env) { var isEnvProduction = env === 'production'; var isEnvTest = env === 'test'; + var useCommonJS = validateBoolOption( + 'useCommonJS', + opts.useCommonJS, + isEnvTest + ); var useESModules = validateBoolOption( 'useESModules', opts.useESModules, - isEnvDevelopment || isEnvProduction + false ); var isFlowEnabled = validateBoolOption('flow', opts.flow, true); var isTypeScriptEnabled = validateBoolOption( @@ -54,6 +59,24 @@ module.exports = function(api, opts, env) { ); } + // When building for commonjs or esmodules environments we need to choose + // different targets. + let targets; + if (useCommonJS) { + // For commonjs we target the oldest supported version of node. + // For tests we target the installed version of node. + // https://babeljs.io/docs/en/babel-preset-env#targetsnode + targets = { node: isEnvTest ? 'current' : 6 }; + } else if (useESModules) { + // For esmodules we use the special esmodules target. + // https://babeljs.io/docs/en/babel-preset-env#targetsesmodules + targets = { esmodules: true }; + } else { + // We want Create React App to be IE 9 compatible until React itself + // no longer works with IE 9 + targets = { ie: 9 }; + } + if (!isEnvDevelopment && !isEnvProduction && !isEnvTest) { throw new Error( 'Using `babel-preset-react-app` requires that you specify `NODE_ENV` or ' + @@ -63,6 +86,11 @@ module.exports = function(api, opts, env) { '.' ); } + if (useESModules && useCommonJS) { + throw new Error( + '`babel-preset-react-app` does not support setting both useESModules and useCommonJS to true.' + ); + } return { presets: [ @@ -70,28 +98,22 @@ module.exports = function(api, opts, env) { // ES features necessary for user's Node version require('@babel/preset-env').default, { - targets: { - node: 'current', - }, + targets, }, ], (isEnvProduction || isEnvDevelopment) && [ // Latest stable ECMAScript features require('@babel/preset-env').default, { - // We want Create React App to be IE 9 compatible until React itself - // no longer works with IE 9 - targets: { - ie: 9, - }, + targets, // Users cannot override this behavior because this Babel // configuration is highly tuned for ES5 support ignoreBrowserslistConfig: true, // If users import all core-js they're probably not concerned with // bundle size. We shouldn't rely on magic to try and shrink it. useBuiltIns: false, - // Do not transform modules to CJS - modules: false, + // Do not transform modules to CJS (unless we're targeting commonJS) + modules: useCommonJS ? 'cjs' : false, // Exclude transforms that make all code slower exclude: ['transform-typeof-symbol'], }, @@ -157,11 +179,12 @@ module.exports = function(api, opts, env) { { corejs: false, helpers: areHelpersEnabled, - regenerator: true, + // We only need to use regenerator in environments that don't support generators. + regenerator: !(useESModules || useCommonJS), // https://babeljs.io/docs/en/babel-plugin-transform-runtime#useesmodules - // We should turn this on once the lowest version of Node LTS - // supports ES Modules. - useESModules, + // We want to enable this for all builds except commonjs. This allows for smaller + // builds since it doesn't need to preserve commonjs semantics. + useESModules: !useCommonJS, // Undocumented option that lets us encapsulate our runtime, ensuring // the correct version is used // https://github.com/babel/babel/blob/090c364a90fe73d36a30707fc612ce037bdbbb24/packages/babel-plugin-transform-runtime/src/index.js#L35-L42 @@ -177,7 +200,7 @@ module.exports = function(api, opts, env) { ], // Adds syntax support for import() require('@babel/plugin-syntax-dynamic-import').default, - isEnvTest && + useCommonJS && // Transform dynamic import to require require('babel-plugin-dynamic-import-node'), ].filter(Boolean), diff --git a/packages/babel-preset-react-app/dependencies.js b/packages/babel-preset-react-app/dependencies.js index 60c76fb5b3a..15c99f8ca1a 100644 --- a/packages/babel-preset-react-app/dependencies.js +++ b/packages/babel-preset-react-app/dependencies.js @@ -36,6 +36,16 @@ module.exports = function(api, opts) { var isEnvProduction = env === 'production'; var isEnvTest = env === 'test'; + var useCommonJS = validateBoolOption( + 'useCommonJS', + opts.useCommonJS, + isEnvTest + ); + var useESModules = validateBoolOption( + 'useESModules', + opts.useESModules, + false + ); var areHelpersEnabled = validateBoolOption('helpers', opts.helpers, false); var useAbsoluteRuntime = validateBoolOption( 'absoluteRuntime', @@ -50,6 +60,24 @@ module.exports = function(api, opts) { ); } + // When building for commonjs or esmodules environments we need to choose + // different targets. + let targets; + if (useCommonJS) { + // For commonjs we target the oldest supported version of node. + // For tests we target the installed version of node. + // https://babeljs.io/docs/en/babel-preset-env#targetsnode + targets = { node: isEnvTest ? 'current' : 6 }; + } else if (useESModules) { + // For esmodules we use the special esmodules target. + // https://babeljs.io/docs/en/babel-preset-env#targetsesmodules + targets = { esmodules: true }; + } else { + // We want Create React App to be IE 9 compatible until React itself + // no longer works with IE 9 + targets = { ie: 9 }; + } + if (!isEnvDevelopment && !isEnvProduction && !isEnvTest) { throw new Error( 'Using `babel-preset-react-app` requires that you specify `NODE_ENV` or ' + @@ -59,6 +87,11 @@ module.exports = function(api, opts) { '.' ); } + if (useESModules && useCommonJS) { + throw new Error( + '`babel-preset-react-app` does not support setting both useESModules and useCommonJS to true.' + ); + } return { // Babel assumes ES Modules, which isn't safe until CommonJS @@ -71,9 +104,7 @@ module.exports = function(api, opts) { // ES features necessary for user's Node version require('@babel/preset-env').default, { - targets: { - node: 'current', - }, + targets, // Do not transform modules to CJS modules: false, // Exclude transforms that make all code slower @@ -84,19 +115,15 @@ module.exports = function(api, opts) { // Latest stable ECMAScript features require('@babel/preset-env').default, { - // We want Create React App to be IE 9 compatible until React itself - // no longer works with IE 9 - targets: { - ie: 9, - }, + targets, // Users cannot override this behavior because this Babel // configuration is highly tuned for ES5 support ignoreBrowserslistConfig: true, // If users import all core-js they're probably not concerned with // bundle size. We shouldn't rely on magic to try and shrink it. useBuiltIns: false, - // Do not transform modules to CJS - modules: false, + // Do not transform modules to CJS (unless we're targeting commonJS) + modules: useCommonJS ? 'cjs' : false, // Exclude transforms that make all code slower exclude: ['transform-typeof-symbol'], }, @@ -110,11 +137,12 @@ module.exports = function(api, opts) { { corejs: false, helpers: areHelpersEnabled, - regenerator: true, + // We only need to use regenerator in environments that don't support generators. + regenerator: !(useESModules || useCommonJS), // https://babeljs.io/docs/en/babel-plugin-transform-runtime#useesmodules - // We should turn this on once the lowest version of Node LTS - // supports ES Modules. - useESModules: isEnvDevelopment || isEnvProduction, + // We want to enable this for all builds except commonjs. This allows for smaller + // builds since it doesn't need to preserve commonjs semantics. + useESModules: !useCommonJS, // Undocumented option that lets us encapsulate our runtime, ensuring // the correct version is used // https://github.com/babel/babel/blob/090c364a90fe73d36a30707fc612ce037bdbbb24/packages/babel-plugin-transform-runtime/src/index.js#L35-L42 @@ -123,7 +151,7 @@ module.exports = function(api, opts) { ], // Adds syntax support for import() require('@babel/plugin-syntax-dynamic-import').default, - isEnvTest && + useCommonJS && // Transform dynamic import to require require('babel-plugin-transform-dynamic-import').default, ].filter(Boolean), diff --git a/packages/babel-preset-react-app/esmodules.js b/packages/babel-preset-react-app/esmodules.js new file mode 100644 index 00000000000..b9584b8c320 --- /dev/null +++ b/packages/babel-preset-react-app/esmodules.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const create = require('./create'); + +module.exports = function(api, opts) { + // This is similar to how `env` works in Babel: + // https://babeljs.io/docs/usage/babelrc/#env-option + // We are not using `env` because it’s ignored in versions > babel-core@6.10.4: + // https://github.com/babel/babel/issues/4539 + // https://github.com/facebook/create-react-app/issues/720 + // It’s also nice that we can enforce `NODE_ENV` being specified. + const env = process.env.BABEL_ENV || process.env.NODE_ENV; + return create( + api, + Object.assign({ helpers: false }, opts, { useESModules: true }), + env + ); +}; diff --git a/packages/babel-preset-react-app/package.json b/packages/babel-preset-react-app/package.json index c6b180fb1ab..ccc08758144 100644 --- a/packages/babel-preset-react-app/package.json +++ b/packages/babel-preset-react-app/package.json @@ -8,9 +8,11 @@ "url": "https://github.com/facebook/create-react-app/issues" }, "files": [ + "commonjs.js", "create.js", "dependencies.js", "dev.js", + "esmodules.js", "index.js", "webpack-overrides.js", "prod.js", @@ -35,6 +37,7 @@ "babel-loader": "8.0.5", "babel-plugin-dynamic-import-node": "2.2.0", "babel-plugin-macros": "2.5.0", + "babel-plugin-transform-dynamic-import": "2.1.0", "babel-plugin-transform-react-remove-prop-types": "0.4.24" } }