diff --git a/.babelrc b/.babelrc
index 2880552916426..a976990ee9e0e 100644
--- a/.babelrc
+++ b/.babelrc
@@ -2,7 +2,6 @@
"presets": ["react"],
"ignore": ["third_party"],
"plugins": [
- "fbjs-scripts/babel-6/dev-expression",
"syntax-trailing-function-commas",
"babel-plugin-transform-object-rest-spread",
"transform-es2015-template-literals",
diff --git a/gulpfile.js b/gulpfile.js
index cb287ff2aa6fd..103f608e208f9 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -16,6 +16,7 @@ var del = require('del');
var babelPluginModules = require('fbjs-scripts/babel-6/rewrite-modules');
var extractErrors = require('./scripts/error-codes/gulp-extract-errors');
+var devExpressionWithCodes = require('./scripts/error-codes/dev-expression-with-codes');
var paths = {
react: {
@@ -53,6 +54,7 @@ var errorCodeOpts = {
var babelOpts = {
plugins: [
+ devExpressionWithCodes, // this pass has to run before `rewrite-modules`
[babelPluginModules, {map: moduleMap}],
],
};
diff --git a/scripts/error-codes/__tests__/dev-expression-with-codes-test.js b/scripts/error-codes/__tests__/dev-expression-with-codes-test.js
new file mode 100644
index 0000000000000..fcc1a31513ee2
--- /dev/null
+++ b/scripts/error-codes/__tests__/dev-expression-with-codes-test.js
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+/* eslint-disable quotes */
+'use strict';
+
+let babel = require('babel-core');
+let devExpressionWithCodes = require('../dev-expression-with-codes');
+
+function transform(input) {
+ return babel.transform(input, {
+ plugins: [devExpressionWithCodes],
+ }).code;
+}
+
+function compare(input, output) {
+ var compiled = transform(input);
+ expect(compiled).toEqual(output);
+}
+
+var oldEnv;
+
+describe('dev-expression', function() {
+ beforeEach(() => {
+ oldEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = '';
+ });
+
+ afterEach(() => {
+ process.env.NODE_ENV = oldEnv;
+ });
+
+ it('should replace __DEV__ in if', () => {
+ compare(
+`
+if (__DEV__) {
+ console.log('foo')
+}`,
+`
+if (process.env.NODE_ENV !== 'production') {
+ console.log('foo');
+}`
+ );
+ });
+
+ it('should replace warning calls', () => {
+ compare(
+ "warning(condition, 'a %s b', 'c');",
+ "process.env.NODE_ENV !== 'production' ? warning(condition, 'a %s b', 'c') : void 0;"
+ );
+ });
+
+ it("should add `reactProdInvariant` when it finds `require('invariant')`", () => {
+ compare(
+"var invariant = require('invariant');",
+
+`var _prodInvariant = require('reactProdInvariant');
+
+var invariant = require('invariant');`
+ );
+ });
+
+ it('should replace simple invariant calls', () => {
+ compare(
+ "invariant(condition, 'Do not override existing functions.');",
+ "var _prodInvariant = require('reactProdInvariant');\n\n" +
+ "!condition ? " +
+ "process.env.NODE_ENV !== 'production' ? " +
+ "invariant(false, 'Do not override existing functions.') : " +
+ `_prodInvariant('16') : void 0;`
+ );
+ });
+
+ it("should only add `reactProdInvariant` once", () => {
+ var expectedInvariantTransformResult = (
+ "!condition ? " +
+ "process.env.NODE_ENV !== 'production' ? " +
+ "invariant(false, 'Do not override existing functions.') : " +
+ `_prodInvariant('16') : void 0;`
+ );
+
+ compare(
+`var invariant = require('invariant');
+invariant(condition, 'Do not override existing functions.');
+invariant(condition, 'Do not override existing functions.');`,
+
+`var _prodInvariant = require('reactProdInvariant');
+
+var invariant = require('invariant');
+${expectedInvariantTransformResult}
+${expectedInvariantTransformResult}`
+ );
+ });
+
+ it('should support invariant calls with args', () => {
+ compare(
+ "invariant(condition, 'Expected %s target to be an array; got %s', 'foo', 'bar');",
+ "var _prodInvariant = require('reactProdInvariant');\n\n" +
+ "!condition ? " +
+ "process.env.NODE_ENV !== 'production' ? " +
+ "invariant(false, 'Expected %s target to be an array; got %s', 'foo', 'bar') : " +
+ `_prodInvariant('7', 'foo', 'bar') : void 0;`
+ );
+ });
+
+ it('should support invariant calls with a concatenated template string and args', () => {
+ compare(
+ "invariant(condition, 'Expected a component class, ' + 'got %s.' + '%s', 'Foo', 'Bar');",
+ "var _prodInvariant = require('reactProdInvariant');\n\n" +
+ "!condition ? " +
+ "process.env.NODE_ENV !== 'production' ? " +
+ "invariant(false, 'Expected a component class, got %s.%s', 'Foo', 'Bar') : " +
+ `_prodInvariant('18', 'Foo', 'Bar') : void 0;`
+ );
+ });
+
+ it('should warn in non-test envs if the error message cannot be found', () => {
+ spyOn(console, 'warn');
+ transform("invariant(condition, 'a %s b', 'c');");
+
+ expect(console.warn.calls.count()).toBe(1);
+ expect(console.warn.calls.argsFor(0)[0]).toBe(
+ 'Error message "a %s b" ' +
+ 'cannot be found. The current React version ' +
+ 'and the error map are probably out of sync. ' +
+ 'Please run `gulp react:extract-errors` before building React.'
+ );
+ });
+
+ it('should not warn in test env if the error message cannot be found', () => {
+ process.env.NODE_ENV = 'test';
+
+ spyOn(console, 'warn');
+ transform("invariant(condition, 'a %s b', 'c');");
+
+ expect(console.warn.calls.count()).toBe(0);
+
+ process.env.NODE_ENV = '';
+ });
+});
diff --git a/scripts/error-codes/dev-expression-with-codes.js b/scripts/error-codes/dev-expression-with-codes.js
new file mode 100644
index 0000000000000..9862a20f51a7b
--- /dev/null
+++ b/scripts/error-codes/dev-expression-with-codes.js
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+var evalToString = require('./evalToString');
+var existingErrorMap = require('./codes.json');
+var invertObject = require('./invertObject');
+
+var errorMap = invertObject(existingErrorMap);
+
+module.exports = function(babel) {
+ var t = babel.types;
+
+ var SEEN_SYMBOL = Symbol('dev-expression-with-codes.seen');
+
+ // Generate a hygienic identifier
+ function getProdInvariantIdentifier(path, localState) {
+ if (!localState.prodInvariantIdentifier) {
+ localState.prodInvariantIdentifier = path.scope.generateUidIdentifier('prodInvariant');
+ path.scope.getProgramParent().push({
+ id: localState.prodInvariantIdentifier,
+ init: t.callExpression(
+ t.identifier('require'),
+ [t.stringLiteral('reactProdInvariant')]
+ ),
+ });
+ }
+ return localState.prodInvariantIdentifier;
+ }
+
+ var DEV_EXPRESSION = t.binaryExpression(
+ '!==',
+ t.memberExpression(
+ t.memberExpression(
+ t.identifier('process'),
+ t.identifier('env'),
+ false
+ ),
+ t.identifier('NODE_ENV'),
+ false
+ ),
+ t.stringLiteral('production')
+ );
+
+ return {
+ pre: function() {
+ this.prodInvariantIdentifier = null;
+ },
+
+ visitor: {
+ Identifier: {
+ enter: function(path) {
+ // Do nothing when testing
+ if (process.env.NODE_ENV === 'test') {
+ return;
+ }
+ // Replace __DEV__ with process.env.NODE_ENV !== 'production'
+ if (path.isIdentifier({name: '__DEV__'})) {
+ path.replaceWith(DEV_EXPRESSION);
+ }
+ },
+ },
+ CallExpression: {
+ exit: function(path) {
+ var node = path.node;
+ // Ignore if it's already been processed
+ if (node[SEEN_SYMBOL]) {
+ return;
+ }
+ // Insert `var PROD_INVARIANT = require('reactProdInvariant');`
+ // before all `require('invariant')`s.
+ // NOTE it doesn't support ES6 imports yet.
+ if (
+ path.get('callee').isIdentifier({name: 'require'}) &&
+ path.get('arguments')[0] &&
+ path.get('arguments')[0].isStringLiteral({value: 'invariant'})
+ ) {
+ node[SEEN_SYMBOL] = true;
+ getProdInvariantIdentifier(path, this);
+ } else if (path.get('callee').isIdentifier({name: 'invariant'})) {
+ // Turns this code:
+ //
+ // invariant(condition, argument, 'foo', 'bar');
+ //
+ // into this:
+ //
+ // if (!condition) {
+ // if ("production" !== process.env.NODE_ENV) {
+ // invariant(false, argument, 'foo', 'bar');
+ // } else {
+ // PROD_INVARIANT('XYZ', 'foo', 'bar');
+ // }
+ // }
+ //
+ // where
+ // - `XYZ` is an error code: a unique identifier (a number string)
+ // that references a verbose error message.
+ // The mapping is stored in `scripts/error-codes/codes.json`.
+ // - `PROD_INVARIANT` is the `reactProdInvariant` function that always throws with an error URL like
+ // http://facebook.github.io/react/docs/error-decoder.html?invariant=XYZ&args[]=foo&args[]=bar
+ //
+ // Specifically this does 3 things:
+ // 1. Checks the condition first, preventing an extra function call.
+ // 2. Adds an environment check so that verbose error messages aren't
+ // shipped to production.
+ // 3. Rewrites the call to `invariant` in production to `reactProdInvariant`
+ // - `reactProdInvariant` is always renamed to avoid shadowing
+ // The generated code is longer than the original code but will dead
+ // code removal in a minifier will strip that out.
+ var condition = node.arguments[0];
+ var errorMsgLiteral = evalToString(node.arguments[1]);
+
+ var prodErrorId = errorMap[errorMsgLiteral];
+ if (prodErrorId === undefined) {
+ // The error cannot be found in the map.
+ node[SEEN_SYMBOL] = true;
+ if (process.env.NODE_ENV !== 'test') {
+ console.warn(
+ 'Error message "' + errorMsgLiteral +
+ '" cannot be found. The current React version ' +
+ 'and the error map are probably out of sync. ' +
+ 'Please run `gulp react:extract-errors` before building React.'
+ );
+ }
+ return;
+ }
+
+ var devInvariant = t.callExpression(node.callee, [
+ t.booleanLiteral(false),
+ t.stringLiteral(errorMsgLiteral),
+ ].concat(node.arguments.slice(2)));
+
+ devInvariant[SEEN_SYMBOL] = true;
+
+ var localInvariantId = getProdInvariantIdentifier(path, this);
+ var prodInvariant = t.callExpression(localInvariantId, [
+ t.stringLiteral(prodErrorId),
+ ].concat(node.arguments.slice(2)));
+
+ prodInvariant[SEEN_SYMBOL] = true;
+ path.replaceWith(t.ifStatement(
+ t.unaryExpression('!', condition),
+ t.blockStatement([
+ t.ifStatement(
+ DEV_EXPRESSION,
+ t.blockStatement([
+ t.expressionStatement(devInvariant),
+ ]),
+ t.blockStatement([
+ t.expressionStatement(prodInvariant),
+ ])
+ ),
+ ])
+ ));
+ } else if (path.get('callee').isIdentifier({name: 'warning'})) {
+ // Turns this code:
+ //
+ // warning(condition, argument, argument);
+ //
+ // into this:
+ //
+ // if ("production" !== process.env.NODE_ENV) {
+ // warning(condition, argument, argument);
+ // }
+ //
+ // The goal is to strip out warning calls entirely in production. We
+ // don't need the same optimizations for conditions that we use for
+ // invariant because we don't care about an extra call in __DEV__
+
+ node[SEEN_SYMBOL] = true;
+ path.replaceWith(t.ifStatement(
+ DEV_EXPRESSION,
+ t.blockStatement([
+ t.expressionStatement(
+ node
+ ),
+ ])
+ ));
+ }
+ },
+ },
+ },
+ };
+};
diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js
index 0cbc46dd90c1c..3a60dfd051e6d 100644
--- a/scripts/jest/preprocessor.js
+++ b/scripts/jest/preprocessor.js
@@ -20,13 +20,14 @@ var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction')
// Use require.resolve to be resilient to file moves, npm updates, etc
var pathToBabel = path.join(require.resolve('babel-core'), '..', 'package.json');
var pathToModuleMap = require.resolve('fbjs/module-map');
-var pathToBabelPluginDev = require.resolve('fbjs-scripts/babel-6/dev-expression');
+var pathToBabelPluginDevWithCode = require.resolve('../error-codes/dev-expression-with-codes');
var pathToBabelPluginModules = require.resolve('fbjs-scripts/babel-6/rewrite-modules');
var pathToBabelrc = path.join(__dirname, '..', '..', '.babelrc');
// TODO: make sure this stays in sync with gulpfile
var babelOptions = {
plugins: [
+ pathToBabelPluginDevWithCode, // this pass has to run before `rewrite-modules`
[babelPluginModules, {
map: Object.assign(
{},
@@ -68,7 +69,7 @@ module.exports = {
pathToBabel,
pathToBabelrc,
pathToModuleMap,
- pathToBabelPluginDev,
+ pathToBabelPluginDevWithCode,
pathToBabelPluginModules,
]),
};
diff --git a/src/renderers/dom/__tests__/ReactDOMProduction-test.js b/src/renderers/dom/__tests__/ReactDOMProduction-test.js
index 718909ed86894..2704f15767631 100644
--- a/src/renderers/dom/__tests__/ReactDOMProduction-test.js
+++ b/src/renderers/dom/__tests__/ReactDOMProduction-test.js
@@ -8,7 +8,6 @@
*
* @emails react-core
*/
-
'use strict';
describe('ReactDOMProduction', function() {
@@ -87,4 +86,20 @@ describe('ReactDOMProduction', function() {
expect(container.childNodes.length).toBe(0);
});
+ it('should throw with an error code in production', function() {
+ expect(function() {
+ var Component = React.createClass({
+ render: function() {
+ return ['this is wrong'];
+ },
+ });
+ var container = document.createElement('div');
+ ReactDOM.render(