diff --git a/.travis.yml b/.travis.yml index 7ed95d643dcc6..8ecb5b86618fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -81,6 +81,7 @@ script: >> src/renderers/dom/shared/ReactDOMFeatureFlags.js ./node_modules/.bin/grunt jest:normal git checkout -- src/renderers/dom/shared/ReactDOMFeatureFlags.js + ./node_modules/.bin/gulp react:extract-errors else ./node_modules/.bin/grunt $TEST_TYPE fi diff --git a/gulpfile.js b/gulpfile.js index 9a80c53b4ee48..cb287ff2aa6fd 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,6 +15,7 @@ var flatten = require('gulp-flatten'); var del = require('del'); var babelPluginModules = require('fbjs-scripts/babel-6/rewrite-modules'); +var extractErrors = require('./scripts/error-codes/gulp-extract-errors'); var paths = { react: { @@ -46,9 +47,13 @@ var moduleMap = Object.assign( } ); +var errorCodeOpts = { + errorMapFilePath: 'scripts/error-codes/codes.json', +}; + var babelOpts = { plugins: [ - [babelPluginModules, { map: moduleMap }], + [babelPluginModules, {map: moduleMap}], ], }; @@ -64,4 +69,10 @@ gulp.task('react:modules', function() { .pipe(gulp.dest(paths.react.lib)); }); +gulp.task('react:extract-errors', function() { + return gulp + .src(paths.react.src) + .pipe(extractErrors(errorCodeOpts)); +}); + gulp.task('default', ['react:modules']); diff --git a/package.json b/package.json index 82561f1482727..c8d41013c8e83 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-plugin-transform-react-jsx-source": "^6.8.0", "babel-preset-react": "^6.5.0", + "babel-traverse": "^6.9.0", + "babylon": "^6.8.0", "browserify": "^13.0.0", "bundle-collapser": "^1.1.1", "coffee-script": "^1.8.0", @@ -51,6 +53,7 @@ "gulp": "^3.9.0", "gulp-babel": "^6.0.0", "gulp-flatten": "^0.2.0", + "gulp-util": "^3.0.7", "gzip-js": "~0.3.2", "jest": "^12.1.1", "loose-envify": "^1.1.0", @@ -95,6 +98,7 @@ "testPathDirs": [ "/eslint-rules", "/mocks", + "/scripts", "/src", "node_modules/fbjs" ], diff --git a/scripts/error-codes/Types.js b/scripts/error-codes/Types.js new file mode 100644 index 0000000000000..5f7fe7f0062c4 --- /dev/null +++ b/scripts/error-codes/Types.js @@ -0,0 +1,13 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +/*:: export type ErrorMap = { [id: string]: string; }; */ diff --git a/scripts/error-codes/__tests__/evalToString-test.js b/scripts/error-codes/__tests__/evalToString-test.js new file mode 100644 index 0000000000000..efc6b4bb6958f --- /dev/null +++ b/scripts/error-codes/__tests__/evalToString-test.js @@ -0,0 +1,36 @@ +/** + * 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 babylon = require('babylon'); + +var parse = (source) => babylon.parse( + `(${source});` +).program.body[0].expression; // quick way to get an exp node + +var parseAndEval = (source) => evalToString(parse(source)); + +describe('evalToString', () => { + it('should support StringLiteral', () => { + expect(parseAndEval(`'foobar'`)).toBe('foobar'); + expect(parseAndEval(`'yowassup'`)).toBe('yowassup'); + }); + + it('should support string concat (`+`)', () => { + expect(parseAndEval(`'foo ' + 'bar'`)).toBe('foo bar'); + }); + + it('should throw when it finds other types', () => { + expect(() => parseAndEval(`'foo ' + true`)).toThrowError(/Unsupported type/); + expect(() => parseAndEval(`'foo ' + 3`)).toThrowError(/Unsupported type/); + expect(() => parseAndEval(`'foo ' + null`)).toThrowError(/Unsupported type/); + expect(() => parseAndEval(`'foo ' + undefined`)).toThrowError(/Unsupported type/); + }); +}); diff --git a/scripts/error-codes/__tests__/invertObject-test.js b/scripts/error-codes/__tests__/invertObject-test.js new file mode 100644 index 0000000000000..1b8c35c6febb2 --- /dev/null +++ b/scripts/error-codes/__tests__/invertObject-test.js @@ -0,0 +1,54 @@ +/** + * 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 invertObject = require('../invertObject'); + +var objectValues = (target) => Object.keys(target).map((key) => target[key]); + +describe('invertObject', () => { + it('should return an empty object for an empty input', () => { + expect(invertObject({})).toEqual({}); + }); + + it('should invert key-values', () => { + expect(invertObject({ + a: '3', + b: '4', + })).toEqual({ + 3: 'a', + 4: 'b', + }); + }); + + it('should take the last value when there\'re duplications in vals', () => { + expect(invertObject({ + a: '3', + b: '4', + c: '3', + })).toEqual({ + 4: 'b', + 3: 'c', + }); + }); + + it('should perserve the original order', () => { + expect(Object.keys(invertObject({ + a: '3', + b: '4', + c: '3', + }))).toEqual(['3', '4']); + + expect(objectValues(invertObject({ + a: '3', + b: '4', + c: '3', + }))).toEqual(['c', 'b']); + }); +}); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json new file mode 100644 index 0000000000000..9d7e85362360f --- /dev/null +++ b/scripts/error-codes/codes.json @@ -0,0 +1,138 @@ +{ + "0": "React.addons.createFragment(...): Encountered an invalid child; DOM elements are not valid children of React components.", + "1": "update(): expected target of %s to be an array; got %s.", + "2": "update(): expected spec of %s to be an array; got %s. Did you forget to wrap your parameter in an array?", + "3": "update(): You provided a key path to update() that did not contain one of %s. Did you forget to include {%s: ...}?", + "4": "Cannot have more than one key in an object with %s", + "5": "update(): %s expects a spec of type 'object'; got %s", + "6": "update(): %s expects a target of type 'object'; got %s", + "7": "Expected %s target to be an array; got %s", + "8": "update(): expected spec of %s to be an array of arrays; got %s. Did you forget to wrap your parameters in an array?", + "9": "update(): expected spec of %s to be a function; got %s.", + "10": "findAllInRenderedTree(...): instance must be a composite component", + "11": "TestUtils.scryRenderedDOMComponentsWithClass expects a className as a second argument.", + "12": "ReactShallowRenderer render(): Invalid component element.%s", + "13": "ReactShallowRenderer render(): Shallow rendering works only with custom components, not primitives (%s). Instead of calling `.render(el)` and inspecting the rendered output, look at `el.props` directly instead.", + "14": "TestUtils.Simulate expects a component instance and not a ReactElement.TestUtils.Simulate will not work if you are using shallow rendering.", + "15": "reactComponentExpect(...): instance must be a composite component", + "16": "Do not override existing functions.", + "17": "All native instances should have a tag.", + "18": "Expected a component class, got %s.%s", + "19": "Expect a native root tag, instead got %s", + "20": "RawText \"%s\" must be wrapped in an explicit component.", + "21": "findNodeHandle(...): Argument is not a component (type: %s, keys: %s)", + "22": "findNodeHandle(...): Unable to find node handle for unmounted component.", + "23": "onlyChild must be passed a children with exactly one child.", + "24": "Mismatched list of contexts in callback queue", + "25": "Trying to release an instance into a pool of a different type.", + "26": "Unexpected node: %s", + "27": "Transaction.perform(...): Cannot initialize a transaction when there is already an outstanding transaction.", + "28": "Transaction.closeAll(): Cannot close transaction when none are open.", + "29": "accumulate(...): Accumulated items must be not be null or undefined.", + "30": "accumulateInto(...): Accumulated items must not be null or undefined.", + "31": "Objects are not valid as a React child (found: %s).%s", + "32": "Unable to find element with ID %s.", + "33": "getNodeFromInstance: Invalid argument.", + "34": "React DOM tree root should always have a node reference.", + "35": "isAncestor: Invalid argument.", + "36": "getParentInstance: Invalid argument.", + "37": "_registerComponent(...): Target container is not a DOM element.", + "38": "parentComponent must be a valid React Component", + "39": "ReactDOM.render(): Invalid component element.%s", + "40": "unmountComponentAtNode(...): Target container is not a DOM element.", + "41": "mountComponentIntoNode(...): Target container is not valid.", + "42": "You're trying to render a component to the document using server rendering but the checksum was invalid. This usually means you rendered a different component type or props on the client from the one on the server, or your render() methods are impure. React cannot handle this case due to cross-browser quirks by rendering at the document root. You should look for environment dependent code in your components and ensure the props are the same client and server side:\n%s", + "43": "You're trying to render a component to the document but you didn't use server rendering. We can't do this without using server rendering due to cross-browser quirks. See ReactDOMServer.renderToString() for server rendering.", + "44": "findDOMNode was called on an unmounted component.", + "45": "Element appears to be neither ReactComponent nor DOMNode (keys: %s)", + "46": "renderToString(): You must pass a valid ReactElement.", + "47": "renderToStaticMarkup(): You must pass a valid ReactElement.", + "48": "injectDOMPropertyConfig(...): You're trying to inject DOM property '%s' which has already been injected. You may be accidentally injecting the same DOM property config twice, or you may be injecting two configs that have conflicting property names.", + "49": "DOMProperty: Properties that have side effects must use property: %s", + "50": "DOMProperty: Value can be one of boolean, overloaded boolean, or numeric value, but not a combination: %s", + "51": "dangerouslyRenderMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use ReactDOMServer.renderToString for server rendering.", + "52": "dangerouslyRenderMarkup(...): Missing markup.", + "53": "Danger: Assigning to an already-occupied result index.", + "54": "Danger: Did not assign to every index of resultList.", + "55": "Danger: Expected markup to render %s nodes, but rendered %s.", + "56": "dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use ReactDOMServer.renderToString() for server rendering.", + "57": "dangerouslyReplaceNodeWithMarkup(...): Missing markup.", + "58": "dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the node. This is because browser quirks make this unreliable and/or slow. If you want to render to the root you must use server rendering. See ReactDOMServer.renderToString().", + "59": "%s is a void element tag and must not have `children` or use `props.dangerouslySetInnerHTML`.%s", + "60": "Can only set one of `children` or `props.dangerouslySetInnerHTML`.", + "61": "`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.", + "62": "The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.%s", + "63": "Must be mounted to trap events", + "64": "trapBubbledEvent(...): Requires node to be rendered.", + "65": "Invalid tag: %s", + "66": "<%s> tried to unmount. Because of cross-browser quirks it is impossible to unmount some top-level components (eg , , and ) reliably and efficiently. To fix this, have a single top-level component that never unmounts render these elements.", + "67": "Missing closing comment for text component %s", + "68": "Expected devtool events to fire for the child before its parent includes it in onSetChildren().", + "69": "Expected onSetDisplayName() to fire for the child before its parent includes it in onSetChildren().", + "70": "Expected onSetChildren() or onSetText() to fire for the child before its parent includes it in onSetChildren().", + "71": "Expected onMountComponent() to fire for the child before its parent includes it in onSetChildren().", + "72": "Expected onSetParent() and onSetChildren() to be consistent (%s has parents %s and %s).", + "73": "ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.", + "74": "ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.", + "75": "ReactClass: You're attempting to use a component class or function as a mixin. Instead, just use a regular object.", + "76": "ReactClass: You're attempting to use a component as a mixin. Instead, just use a regular object.", + "77": "ReactClass: Unexpected spec policy %s for key %s when mixing in component specs.", + "78": "ReactClass: You are attempting to define a reserved property, `%s`, that shouldn't be on the \"statics\" key. Define it as an instance property instead; it will still be accessible on the constructor.", + "79": "ReactClass: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.", + "80": "mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.", + "81": "mergeIntoWithNoDuplicateKeys(): Tried to merge two objects with the same key: `%s`. This conflict may be due to a mixin; in particular, this may be caused by two getInitialState() or getDefaultProps() methods returning objects with clashing keys.", + "82": "%s.getInitialState(): must return an object or null", + "83": "createClass(...): Class specification must implement a `render` method.", + "84": "%s: %s type `%s` is invalid; it must be a function, usually from React.PropTypes.", + "85": "setState(...): takes an object of state variables to update or a function which returns an object of state variables.", + "86": "SimpleEventPlugin: Unhandled event type, `%s`.", + "87": "Cannot provide a checkedLink and a valueLink. If you want to use checkedLink, you probably don't want to use valueLink and vice versa.", + "88": "Cannot provide a valueLink and a value or onChange event. If you want to use value or onChange, you probably don't want to use valueLink.", + "89": "Cannot provide a checkedLink and a checked property or onChange event. If you want to use checked or onChange, you probably don't want to use checkedLink", + "90": "ReactDOMInput: Mixing React and non-React radio inputs with the same `name` is not supported.", + "91": "`dangerouslySetInnerHTML` does not make sense on