From a2982819df7ba8dd467e2e4e2793179a6096920f Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Sat, 2 Jul 2016 00:04:33 -0700 Subject: [PATCH] This backports bfd1531eca38e4e25a9517a00d5be1283bda6696 from react 15.2 (https://github.com/facebook/react/pull/6882); it took some help from 1d0c1b18170768d71105dee55cb8bbd3674f8251 to get the basic gulp setup (0.14 beta v2 code), and took a lot of dependency wrangling. `grunt extract-errors` should run the script now. I'm starting with a fresh codes.json because these will never match up with the official ones unfortunately. Things skipped: - travis integration. I don't know anything about travis so don't know if / how to hook this up for it. - inserting this as part of a full build process. It looks like it's a oneoff in master too fwiw --- Gruntfile.js | 21 +++ gulpfile.js | 40 +++++ package.json | 11 +- scripts/error-codes/Types.js | 13 ++ .../__tests__/evalToString-test.js | 36 ++++ .../__tests__/invertObject-test.js | 54 ++++++ scripts/error-codes/codes.json | 157 ++++++++++++++++++ scripts/error-codes/evalToString.js | 27 +++ scripts/error-codes/gulp-extract-errors.js | 117 +++++++++++++ scripts/error-codes/invertObject.js | 35 ++++ 10 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 gulpfile.js create mode 100644 scripts/error-codes/Types.js create mode 100644 scripts/error-codes/__tests__/evalToString-test.js create mode 100644 scripts/error-codes/__tests__/invertObject-test.js create mode 100644 scripts/error-codes/codes.json create mode 100644 scripts/error-codes/evalToString.js create mode 100644 scripts/error-codes/gulp-extract-errors.js create mode 100644 scripts/error-codes/invertObject.js diff --git a/Gruntfile.js b/Gruntfile.js index ce5f731816bc5..a4347076e13ee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,7 @@ 'use strict'; +var assign = require('object-assign'); +var path = require('path'); var exec = require('child_process').exec; var jsxTask = require('./grunt/tasks/jsx'); var browserifyTask = require('./grunt/tasks/browserify'); @@ -33,6 +35,21 @@ module.exports = function(grunt) { grunt.config.set('compress', require('./grunt/config/compress')); + function spawnGulp(args, opts, done) { + grunt.util.spawn({ + // This could be more flexible (require.resolve & lookup bin in package) + // but if it breaks we'll fix it then. + cmd: path.join('node_modules', '.bin', 'gulp'), + args: args, + opts: assign({stdio: 'inherit'}, opts), + }, function(err, result, code) { + if (err) { + grunt.fail.fatal('Something went wrong running gulp: ', result); + } + done(code === 0); + }); + } + Object.keys(grunt.file.readJSON('package.json').devDependencies) .filter(function(npmTaskName) { return npmTaskName.indexOf('grunt-') === 0; }) .filter(function(npmTaskName) { return npmTaskName != 'grunt-cli'; }) @@ -253,6 +270,10 @@ module.exports = function(grunt) { 'release:msg' ]); + grunt.registerTask('extract-errors', function() { + spawnGulp(['react:extract-errors'], null, this.async()); + }); + // The default task - build - to keep setup easy grunt.registerTask('default', ['build']); }; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000000000..cb8492d865af6 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,40 @@ +/** + * Copyright 2013-2015, 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 gulp = require('gulp'); +var babel = require('gulp-babel'); +var flatten = require('gulp-flatten'); +var del = require('del'); + +var extractErrors = require('./scripts/error-codes/gulp-extract-errors'); + +var paths = { + react: { + src: [ + 'src/**/*.js', + '!src/**/__tests__/**/*.js', + '!test/all.js', + ], + lib: 'build/modules', + }, +}; + +var errorCodeOpts = { + errorMapFilePath: 'scripts/error-codes/codes.json', +}; + +gulp.task('react:extract-errors', function() { + return gulp + .src(paths.react.src) + .pipe(extractErrors(errorCodeOpts)); +}); + +gulp.task('default', ['react:extract-errors']); diff --git a/package.json b/package.json index 9405318ec09af..03c0ffa27461c 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,18 @@ "url": "https://github.com/facebook/react" }, "dependencies": { + "babel-traverse": "^6.10.4", + "babylon": "^6.8.2", "commoner": "^0.10.0", + "del": "^2.2.1", "esprima-fb": "^6001.1.0-dev-harmony-fb", - "jstransform": "^6.3.2" + "gulp": "^3.9.0", + "gulp-babel": "^6.0.0", + "gulp-flatten": "^0.2.0", + "gulp-util": "^3.0.7", + "jstransform": "^6.3.2", + "object-assign": "^4.1.0", + "through2": "^2.0.1" }, "devDependencies": { "benchmark": "~1.0.0", 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..7c13aa873cea9 --- /dev/null +++ b/scripts/error-codes/codes.json @@ -0,0 +1,157 @@ +{ + "0": "update(): expected target of %s to be an array; got %s.", + "1": "update(): expected spec of %s to be an array; got %s. Did you forget to wrap your parameter in an array?", + "2": "update(): You provided a key path to update() that did not contain one of %s. Did you forget to include {%s: ...}?", + "3": "Cannot have more than one key in an object with %s", + "4": "update(): %s expects a spec of type 'object'; got %s", + "5": "update(): %s expects a target of type 'object'; got %s", + "6": "Expected %s target to be an array; got %s", + "7": "update(): expected spec of %s to be an array of arrays; got %s. Did you forget to wrap your parameters in an array?", + "8": "update(): expected spec of %s to be a function; got %s.", + "9": "ReactComponent: injectEnvironment() can only be called once.", + "10": "replaceProps(...): Can only update a mounted component.", + "11": "replaceProps(...): You called `setProps` or `replaceProps` on a component with a parent. This is an anti-pattern since props will get reactively updated when rendered. Instead, change the owner's `render` method to pass the correct value as props to the component where it is created.", + "12": "mountComponent(%s, ...): Can only mount an unmounted component. Make sure to avoid storing components between renders or reusing a single component instance in multiple places.", + "13": "unmountComponent(): Can only unmount a mounted component.", + "14": "receiveComponent(...): Can only update a mounted component.", + "15": "%s: %s type `%s` is invalid; it must be a function, usually from React.PropTypes.", + "16": "ReactCompositeComponentInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.", + "17": "ReactCompositeComponentInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.", + "18": "replaceState(...): Can only update a mounted or mounting component.", + "19": "replaceState(...): Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.", + "20": "replaceState(...): Cannot update while unmounting component. This usually means you called setState() on an unmounted component.", + "21": "ReactCompositeComponent: You're attempting to use a component class as a mixin. Instead, just use a regular object.", + "22": "ReactCompositeComponent: You're attempting to use a component as a mixin. Instead, just use a regular object.", + "23": "ReactCompositeComponent: Unexpected spec policy %s for key %s when mixing in component specs.", + "24": "ReactCompositeComponent: 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.", + "25": "ReactCompositeComponent: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.", + "26": "mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects", + "27": "mergeObjectsWithNoDuplicateKeys(): 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.", + "28": "%s.getInitialState(): must return an object or null", + "29": "setState(...): takes an object of state variables to update.", + "30": "%s.getChildContext(): childContextTypes must be defined in order to use getChildContext().", + "31": "%s.getChildContext(): key \"%s\" is not defined in childContextTypes.", + "32": "forceUpdate(...): Can only force an update on mounted or mounting components.", + "33": "forceUpdate(...): Cannot force an update while unmounting component or within a `render` function.", + "34": "%s.render(): A valid ReactComponent must be returned. You may have returned undefined, an array or some other invalid object.", + "35": "createClass(...): Class specification must implement a `render` method.", + "36": "Trying to return null from a render, but no null placeholder component was injected.", + "37": "getNextDescendantID(%s, %s): Received an invalid React DOM ID.", + "38": "getNextDescendantID(...): React has made an invalid assumption about the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.", + "39": "getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s", + "40": "traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.", + "41": "traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do not have a parent path.", + "42": "traverseParentPath(%s, %s, ...): Detected an infinite loop while traversing the React DOM ID tree. This may be due to malformed IDs: %s", + "43": "This is suppose to accept a element factory", + "44": "There is no registered component for the tag %s", + "45": "addComponentAsRefTo(...): Only a ReactOwner can have refs. This usually means that you're trying to add a ref to a component that doesn't have an owner (that is, was not created inside of another component's `render` method). Try rendering this component inside of a new top-level component which will hold the ref.", + "46": "removeComponentAsRefFrom(...): Only a ReactOwner can have refs. This usually means that you're trying to remove a ref to a component that doesn't have an owner (that is, was not created inside of another component's `render` method). Try rendering this component inside of a new top-level component which will hold the ref.", + "47": "attachRef(%s, ...): Only a component's owner can store a ref to it.", + "48": "%s: You can't call transferPropsTo() on a component that you don't own, %s. This usually means you are calling transferPropsTo() on a component passed in as props or children.", + "49": "ReactUpdates: must inject a reconcile transaction class and batching strategy", + "50": "Expected flush transaction's stored dirty-components length (%s) to match dirty-components array length (%s).", + "51": "enqueueUpdate(...): You called `setProps`, `replaceProps`, `setState`, `replaceState`, or `forceUpdate` with a callback that isn't callable.", + "52": "ReactUpdates.asap: Can't enqueue an asap callback in a context whereupdates are not being batched.", + "53": "ReactUpdates: must provide a reconcile transaction class", + "54": "ReactUpdates: must provide a batching strategy", + "55": "ReactUpdates: must provide a batchedUpdates() function", + "56": "ReactUpdates: must provide an isBatchingUpdates boolean attribute", + "57": "Expected %s listener to be a function, instead got type %s", + "58": "processEventQueue(): Additional events were enqueued while processing an event queue. Support for this has not yet been implemented.", + "59": "EventPluginRegistry: Cannot inject event plugins that do not exist in the plugin ordering, `%s`.", + "60": "EventPluginRegistry: Event plugins must implement an `extractEvents` method, but `%s` does not.", + "61": "EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.", + "62": "EventPluginHub: More than one plugin attempted to publish the same event name, `%s`.", + "63": "EventPluginHub: More than one plugin attempted to publish the same registration name, `%s`.", + "64": "EventPluginRegistry: Cannot inject event plugin ordering more than once. You are likely trying to load more than one copy of React.", + "65": "EventPluginRegistry: Cannot inject two different event plugins using the same name, `%s`.", + "66": "EventPluginUtils.injection.injectMount(...): Injected Mount module is missing getNode.", + "67": "EventPluginUtils: Invalid `event`.", + "68": "executeDirectDispatch(...): Invalid `event`.", + "69": "Mismatched list of contexts in callback queue", + "70": "LegacyImmutableObject: Attempted to set fields on an object that is not an instance of LegacyImmutableObject.", + "71": "OrderedMap: IDs returned by the key extraction function must be unique.", + "72": "OrderedMap: Key must be non-empty, non-null string or number.", + "73": "OrderedMap: `mapRange` and `forEachRange` expect non-negative start and length arguments within the bounds of the instance.", + "74": "OrderedMap: Corrupted instance of OrderedMap detected.", + "75": "OrderedMap.merge(...): Expected an OrderedMap instance.", + "76": "mapKeyRange must be given keys that are present.", + "77": "OrderedMap.mapKeyRange(...): `endKey` must not come before `startIndex`.", + "78": "forEachKeyRange must be given keys that are present.", + "79": "OrderedMap.forEachKeyRange(...): `endKey` must not come before `startIndex`.", + "80": "OrderedMap.nthKeyAfter: The key `%s` does not exist in this instance.", + "81": "OrderedMap.from(...): Expected an OrderedMap instance.", + "82": "OrderedMap.fromArray(...): First argument must be an array.", + "83": "OrderedMap.fromArray(...): Second argument must be a function used to determine the unique key for each entry.", + "84": "Trying to release an instance into a pool of a different type.", + "85": "Transaction.perform(...): Cannot initialize a transaction when there is already an outstanding transaction.", + "86": "Transaction.closeAll(): Cannot close transaction when none are open.", + "87": "accumulate(...): Accumulated items must be not be null or undefined.", + "88": "accumulateInto(...): Accumulated items must not be null or undefined.", + "89": "keyMirror(...): Argument must be an object.", + "90": "onlyChild must be passed a children with exactly one child.", + "91": "traverseAllChildren(...): Encountered an invalid child; DOM elements are not valid children of React components.", + "92": "Invalid analyticsEvent:%s for analyticsID:%s", + "93": "createAnalyticsPlugin(...): The DOM is not supported in the execution environment.", + "94": "createAnalyticsPlugin(...): You must provide a callback.", + "95": "SimpleEventPlugin: Unhandled event type, `%s`.", + "96": "renderToString(): You must pass a valid ReactElement.", + "97": "renderToStaticMarkup(): You must pass a valid ReactElement.", + "98": "getDOMNode(): A component must be mounted to have a DOM node.", + "99": "mountComponentIntoNode(...): Target container is not valid.", + "100": "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.", + "101": "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 renderComponentToString() for server rendering.", + "102": "Can only set one of `children` or `props.dangerouslySetInnerHTML`.", + "103": "The `style` prop expects a mapping from style properties to values, not a string.", + "104": "Invalid tag: %s", + "105": "updatePropertyByID(...): %s", + "106": "ReactMount: Two valid but unequal nodes with the same `%s`: %s", + "107": "ReactMount: Unexpected modification of `%s`", + "108": "_registerComponent(...): Target container is not a DOM element.", + "109": "renderComponent(): Invalid component element.%s", + "110": "Tried to get element with id of \"%s\" but it is not present on the page.", + "111": "ReactMount: Root element ID differed from reactRootID.", + "112": "findComponentRoot(..., %s): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a when using tables, nesting tags like
,

, or , or using non-SVG elements in an parent. Try inspecting the child nodes of the element with React ID `%s`.", + "113": "CSSCore.addClass takes only a single class name. \"%s\" contains multiple classes.", + "114": "CSSCore.removeClass takes only a single class name. \"%s\" contains multiple classes.", + "115": "CSS.hasClass takes only a single class name.", + "116": "createNodesFromMarkup dummy not initialized", + "117": "createNodesFromMarkup(...): Unexpected

, or , or using non-SVG elements in an parent. Try inspecting the child nodes of the element with React ID `%s`.", + "133": "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.", + "134": "DOMProperty: Cannot require using both attribute and property: %s", + "135": "DOMProperty: Properties that have side effects must use property: %s", + "136": "DOMProperty: Value can be one of boolean, overloaded boolean, or numeric value, but not a combination: %s", + "137": "dangerouslyRenderMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use React.renderToString for server rendering.", + "138": "dangerouslyRenderMarkup(...): Missing markup.", + "139": "Danger: Assigning to an already-occupied result index.", + "140": "Danger: Did not assign to every index of resultList.", + "141": "Danger: Expected markup to render %s nodes, but rendered %s.", + "142": "dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use React.renderToString for server rendering.", + "143": "dangerouslyReplaceNodeWithMarkup(...): Missing markup.", + "144": "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 renderComponentToString().", + "145": "Cannot provide a checkedLink and a valueLink. If you want to use checkedLink, you probably don't want to use valueLink and vice versa.", + "146": "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.", + "147": "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", + "148": "Must be mounted to trap events", + "149": "ReactDOMInput: Mixing React and non-React radio inputs with the same `name` is not supported.", + "150": "ReactDOMInput: Unknown radio button ID %s.", + "151": "If you supply `defaultValue` on a , and ) reliably and efficiently. To fix this, have a single top-level component that never unmounts render these elements." +} diff --git a/scripts/error-codes/evalToString.js b/scripts/error-codes/evalToString.js new file mode 100644 index 0000000000000..a8a0b9c6b1df5 --- /dev/null +++ b/scripts/error-codes/evalToString.js @@ -0,0 +1,27 @@ +/** + * 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'; + +function evalToString(ast/* : Object */)/* : string */ { + switch (ast.type) { + case 'StringLiteral': + return ast.value; + case 'BinaryExpression': // `+` + if (ast.operator !== '+') { + throw new Error('Unsupported binary operator ' + ast.operator); + } + return evalToString(ast.left) + evalToString(ast.right); + default: + throw new Error('Unsupported type ' + ast.type); + } +} + +module.exports = evalToString; diff --git a/scripts/error-codes/gulp-extract-errors.js b/scripts/error-codes/gulp-extract-errors.js new file mode 100644 index 0000000000000..cc873db9d4b05 --- /dev/null +++ b/scripts/error-codes/gulp-extract-errors.js @@ -0,0 +1,117 @@ +/** + * 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 babylon = require('babylon'); +var fs = require('fs'); +var gutil = require('gulp-util'); +var path = require('path'); +var through = require('through2'); +var traverse = require('babel-traverse').default; + +var evalToString = require('./evalToString'); +var invertObject = require('./invertObject'); + +var PLUGIN_NAME = 'extract-errors'; + +var babylonOptions = { + sourceType: 'module', + // As a parser, babylon has its own options and we can't directly + // import/require a babel preset. It should be kept **the same** as + // the `babel-plugin-syntax-*` ones specified in + // https://github.com/facebook/fbjs/blob/master/babel-preset/configure.js + plugins: [ + 'classProperties', + 'flow', + 'jsx', + 'trailingFunctionCommas', + 'objectRestSpread', + ], +}; + +module.exports = function(opts) { + if (!opts || !('errorMapFilePath' in opts)) { + throw new gutil.PluginError( + PLUGIN_NAME, + 'Missing options. Ensure you pass an object with `errorMapFilePath`.' + ); + } + + var errorMapFilePath = opts.errorMapFilePath; + var existingErrorMap; + try { + existingErrorMap = require( + path.join(__dirname, path.basename(errorMapFilePath)) + ); + } catch (e) { + existingErrorMap = {}; + } + + var allErrorIDs = Object.keys(existingErrorMap); + var currentID; + + if (allErrorIDs.length === 0) { // Map is empty + currentID = 0; + } else { + currentID = Math.max.apply(null, allErrorIDs) + 1; + } + + // Here we invert the map object in memory for faster error code lookup + existingErrorMap = invertObject(existingErrorMap); + + function transform(file, enc, cb) { + if (file.isNull()) { + cb(null, file); + return; + } + + if (file.isStream()) { + cb(new gutil.PluginError(PLUGIN_NAME, 'Streaming not supported')); + return; + } + + var source = file.contents.toString(); + var ast = babylon.parse(source, babylonOptions); + + traverse(ast, { + CallExpression: { + exit: function(astPath) { + if (astPath.get('callee').isIdentifier({name: 'invariant'})) { + var node = astPath.node; + + // error messages can be concatenated (`+`) at runtime, so here's a + // trivial partial evaluator that interprets the literal value + var errorMsgLiteral = evalToString(node.arguments[1]); + if (existingErrorMap.hasOwnProperty(errorMsgLiteral)) { + return; + } + + existingErrorMap[errorMsgLiteral] = '' + (currentID++); + } + }, + }, + }); + + cb(); + } + + function flush(cb) { + fs.writeFile( + errorMapFilePath, + JSON.stringify(invertObject(existingErrorMap), null, 2) + '\n', + 'utf-8', + function() { + // avoid calling cb with fs.write callback data + cb(); + } + ); + } + + return through.obj(transform, flush); +}; diff --git a/scripts/error-codes/invertObject.js b/scripts/error-codes/invertObject.js new file mode 100644 index 0000000000000..3fb79975a588a --- /dev/null +++ b/scripts/error-codes/invertObject.js @@ -0,0 +1,35 @@ +/** + * 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'; + +/*:: import type { ErrorMap } from './Types' */ + +/** + * turns + * { 'MUCH ERROR': '0', 'SUCH WRONG': '1' } + * into + * { 0: 'MUCH ERROR', 1: 'SUCH WRONG' } + */ +function invertObject(targetObj/* : ErrorMap */)/* : ErrorMap */ { + var result = {}; + var mapKeys = Object.keys(targetObj); + + for (var i = 0; i < mapKeys.length; i++) { + var originalKey = mapKeys[i]; + var originalVal = targetObj[originalKey]; + + result[originalVal] = originalKey; + } + + return result; +} + +module.exports = invertObject;