From cc6d4aac1e3be10e5905bf928bef3ebf7affbf9c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 23 Aug 2015 03:47:58 +0300 Subject: [PATCH 001/161] 2.0.0-alpha --- CHANGELOG.md | 17 +++++++++++++++++ Injection.js | 7 ------- getRootInstancesFromReactMount.js | 7 ------- index.js | 7 +------ isReactClassish.js | 12 +----------- makeExportsHot.js | 6 ------ package.json | 4 ++-- 7 files changed, 21 insertions(+), 39 deletions(-) delete mode 100644 Injection.js delete mode 100644 getRootInstancesFromReactMount.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f834c6a1..15f782c6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ ## Changelog +### 2.0.0-alpha + +**Experimental release that isn't really representative on what will go in 2.0, but uses the new engine.** + +Some ideas of what should be possible with the new engine: + +* There is no requirement to pass `getRootInstances()` anymore, so React Hot Loader doesn't need `react/lib/ReactMount` or walk the tree, which was somewhat fragile and changing between versions +* Static methods and properties are now hot-reloaded +* Instance getters and setters are now hot reloaded +* Static getters and setters are now hot reloaded +* Deleted instance methods are now deleted during hot reloading +* Single method form of [autobind-decorator](https://github.com/andreypopp/autobind-decorator) is now supported + +What might get broken: + +* We no longer overwrite or even touch the original class. Every time makeHot is invoked, it will return a special proxy class. This means a caveat: for example, static methods will only be hot-reloaded if you refer to them as `this.constructor.doSomething()` instead of `FooBar.doSomething()`. This is because React Hot Loader calls `makeHot` right before exporting, so `FooBar` still refers to the original class. Similarly, `this.constructor === App` will be `false` inside `App` unless you call `App = makeHot(App)` manually, which you can't do with React Hot Loader. **I'm not sure how much of a problem this will be, so let me know if it pains you.** In the longer term, we will deprecate React Hot Loader in favor of a Babel plugin which will be able to rewrite class definitions correctly, so it shouldn't be a problem for a long time. If there is demand, we can introduce a mode that rewrites passed classes, too. + ### 1.2.9 * Silently ignore exports that raise an error when accessed (#114) diff --git a/Injection.js b/Injection.js deleted file mode 100644 index 5420af594..000000000 --- a/Injection.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -var RootInstanceProvider = require('./RootInstanceProvider'); - -module.exports = { - RootInstanceProvider: RootInstanceProvider.injection -}; \ No newline at end of file diff --git a/getRootInstancesFromReactMount.js b/getRootInstancesFromReactMount.js deleted file mode 100644 index dee04daa0..000000000 --- a/getRootInstancesFromReactMount.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -function getRootInstancesFromReactMount(ReactMount) { - return ReactMount._instancesByReactRootID || ReactMount._instancesByContainerID || []; -} - -module.exports = getRootInstancesFromReactMount; \ No newline at end of file diff --git a/index.js b/index.js index a509f4498..f3181c754 100644 --- a/index.js +++ b/index.js @@ -28,13 +28,8 @@ module.exports = function (source, map) { 'if (module.hot) {', '(function () {', 'var ReactHotAPI = require(' + JSON.stringify(require.resolve('react-hot-api')) + '),', - 'RootInstanceProvider = require(' + JSON.stringify(require.resolve('./RootInstanceProvider')) + '),', - 'ReactMount = require("react/lib/ReactMount"),', 'React = require("react");', - - 'module.makeHot = module.hot.data ? module.hot.data.makeHot : ReactHotAPI(function () {', - 'return RootInstanceProvider.getRootInstances(ReactMount);', - '}, React);', + 'module.makeHot = module.hot.data ? module.hot.data.makeHot : ReactHotAPI(React);', '})();', '}', '(function () {', diff --git a/isReactClassish.js b/isReactClassish.js index de38ac9a0..ecadb244c 100644 --- a/isReactClassish.js +++ b/isReactClassish.js @@ -29,17 +29,7 @@ function isReactClassish(Class, React) { return false; } - // React 0.13 - if (hasRender(Class) || descendsFromReactComponent(Class, React)) { - return true; - } - - // React 0.12 and earlier - if (Class.type && hasRender(Class.type)) { - return true; - } - - return false; + return hasRender(Class) || descendsFromReactComponent(Class, React); } module.exports = isReactClassish; \ No newline at end of file diff --git a/makeExportsHot.js b/makeExportsHot.js index 778a00f11..3a23c521f 100644 --- a/makeExportsHot.js +++ b/makeExportsHot.js @@ -23,12 +23,6 @@ function makeExportsHot(m, React) { continue; } - if (exportsReactClass && key === 'type') { - // React 0.12 also puts classes under `type` property for compat. - // Skip to avoid updating twice. - continue; - } - var value; try { value = freshExports[key]; diff --git a/package.json b/package.json index 0dd696f11..3aabe398c 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "react-hot-loader", - "version": "1.2.9", + "version": "2.0.0-alpha", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { - "react-hot-api": "^0.4.5", + "react-hot-api": "^0.5.0-alpha", "source-map": "^0.4.4" }, "repository": { From 5c8e2b5b83f18ce87054c2e626ff2edd5b85d22d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 23 Aug 2015 13:44:14 +0300 Subject: [PATCH 002/161] Fix a weird descriptor problem --- makeExportsHot.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/makeExportsHot.js b/makeExportsHot.js index 3a23c521f..4d059f93e 100644 --- a/makeExportsHot.js +++ b/makeExportsHot.js @@ -34,7 +34,12 @@ function makeExportsHot(m, React) { continue; } - if (Object.getOwnPropertyDescriptor(m.exports, key).writable) { + var descriptor = Object.getOwnPropertyDescriptor(m.exports, key); + if (!descriptor) { + continue; + } + + if (descriptor.writable) { m.exports[key] = m.makeHot(value, '__MODULE_EXPORTS_' + key); foundReactClasses = true; } else { From 3bacf501f7524e79815025e5cf9eca8733f4703d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 23 Aug 2015 13:45:06 +0300 Subject: [PATCH 003/161] 2.0.0-alpha-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3aabe398c..9304f30c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "2.0.0-alpha", + "version": "2.0.0-alpha-1", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { From 794beb0510448279968c49096da960e7d6c571e2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 24 Aug 2015 16:53:16 +0300 Subject: [PATCH 004/161] Enforce a newer version of React Hot API --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9304f30c3..cbf2dac5c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { - "react-hot-api": "^0.5.0-alpha", + "react-hot-api": "^0.5.0-alpha-2", "source-map": "^0.4.4" }, "repository": { From ff5430d6dead9a7c17dd848370f7681c6b8f051f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 24 Aug 2015 16:53:24 +0300 Subject: [PATCH 005/161] 2.0.0-alpha-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbf2dac5c..06e3b1cda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "2.0.0-alpha-1", + "version": "2.0.0-alpha-2", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { From 34b2985cc3ebd5aeb3fb17059ca82cf8998eccb6 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Aug 2015 00:57:55 +0300 Subject: [PATCH 006/161] 2.0.0-alpha-3 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 06e3b1cda..357d88b9e 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "react-hot-loader", - "version": "2.0.0-alpha-2", + "version": "2.0.0-alpha-3", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { - "react-hot-api": "^0.5.0-alpha-2", + "react-hot-api": "^0.5.0-alpha-3", "source-map": "^0.4.4" }, "repository": { From 4d5acbf6b329bf1f8df41be2922f1feb042175b9 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 29 Aug 2015 01:34:26 +0300 Subject: [PATCH 007/161] 2.0.0-alpha-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 357d88b9e..dd4f7e677 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "2.0.0-alpha-3", + "version": "2.0.0-alpha-4", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { From 8a92ef0376e33893235638efae5d2826a44a0eda Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 16:51:14 +0100 Subject: [PATCH 008/161] Rewrite with a different approach --- index.js | 90 -------------------------------------------- isReactClassish.js | 35 ----------------- isReactElementish.js | 12 ------ makeExportsHot.js | 53 -------------------------- package.json | 3 +- patch.js | 47 +++++++++++++++++++++++ webpack.js | 65 ++++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 191 deletions(-) delete mode 100644 index.js delete mode 100644 isReactClassish.js delete mode 100644 isReactElementish.js delete mode 100644 makeExportsHot.js create mode 100644 patch.js create mode 100644 webpack.js diff --git a/index.js b/index.js deleted file mode 100644 index db602f217..000000000 --- a/index.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -var path = require('path'), - SourceNode = require('source-map').SourceNode, - SourceMapConsumer = require('source-map').SourceMapConsumer, - makeIdentitySourceMap = require('./makeIdentitySourceMap'); - -module.exports = function (source, map) { - if (this.cacheable) { - this.cacheable(); - } - - var resourcePath = this.resourcePath; - if (/[\\/]webpack[\\/]buildin[\\/]module\.js|[\\/]react-hot-loader[\\/]|[\\/]react[\\/]lib[\\/]/.test(resourcePath)) { - return this.callback(null, source, map); - } - - var acceptUpdates = this.query !== '?manual', - filename = path.basename(resourcePath), - separator = '\n\n', - prependText, - appendText, - node, - result; - - prependText = [ - '/* REACT HOT LOADER */', - 'if (module.hot) {', - '(function () {', - 'var ReactHotAPI = require(' + JSON.stringify(require.resolve('react-hot-api')) + '),', - 'React = require("react");', - 'module.makeHot = module.hot.data ? module.hot.data.makeHot : ReactHotAPI(React);', - '})();', - '}', - 'try {', - '(function () {', - ].join(' '); - - appendText = [ - '/* REACT HOT LOADER */', - '}).call(this);', - '} finally {', - 'if (module.hot) {', - '(function () {', - 'var foundReactClasses = module.hot.data && module.hot.data.foundReactClasses || false;', - 'if (module.exports && module.makeHot) {', - 'var makeExportsHot = require(' + JSON.stringify(require.resolve('./makeExportsHot')) + ');', - 'if (makeExportsHot(module, require("react"))) {', - 'foundReactClasses = true;', - '}', - 'var shouldAcceptModule = ' + JSON.stringify(acceptUpdates) + ' && foundReactClasses;', - 'if (shouldAcceptModule) {', - 'module.hot.accept(function (err) {', - 'if (err) {', - 'console.error("Cannot apply hot update to " + ' + JSON.stringify(filename) + ' + ": " + err.message);', - '}', - '});', - '}', - '}', - 'module.hot.dispose(function (data) {', - 'data.makeHot = module.makeHot;', - 'data.foundReactClasses = foundReactClasses;', - '});', - '})();', - '}', - '}' - ].join(' '); - - if (this.sourceMap === false) { - return this.callback(null, [ - prependText, - source, - appendText - ].join(separator)); - } - - if (!map) { - map = makeIdentitySourceMap(source, this.resourcePath); - } - - node = new SourceNode(null, null, null, [ - new SourceNode(null, null, this.resourcePath, prependText), - SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), - new SourceNode(null, null, this.resourcePath, appendText) - ]).join(separator); - - result = node.toStringWithSourceMap(); - - this.callback(null, result.code, result.map.toString()); -}; diff --git a/isReactClassish.js b/isReactClassish.js deleted file mode 100644 index ecadb244c..000000000 --- a/isReactClassish.js +++ /dev/null @@ -1,35 +0,0 @@ -function hasRender(Class) { - var prototype = Class.prototype; - if (!prototype) { - return false; - } - - return typeof prototype.render === 'function'; -} - -function descendsFromReactComponent(Class, React) { - if (!React.Component) { - return false; - } - - var Base = Object.getPrototypeOf(Class); - while (Base) { - if (Base === React.Component) { - return true; - } - - Base = Object.getPrototypeOf(Base); - } - - return false; -} - -function isReactClassish(Class, React) { - if (typeof Class !== 'function') { - return false; - } - - return hasRender(Class) || descendsFromReactComponent(Class, React); -} - -module.exports = isReactClassish; \ No newline at end of file diff --git a/isReactElementish.js b/isReactElementish.js deleted file mode 100644 index 909e31c9d..000000000 --- a/isReactElementish.js +++ /dev/null @@ -1,12 +0,0 @@ -var isReactClassish = require('./isReactClassish'); - -function isReactElementish(obj, React) { - if (!obj) { - return false; - } - - return Object.prototype.toString.call(obj.props) === '[object Object]' && - isReactClassish(obj.type, React); -} - -module.exports = isReactElementish; \ No newline at end of file diff --git a/makeExportsHot.js b/makeExportsHot.js deleted file mode 100644 index 4d059f93e..000000000 --- a/makeExportsHot.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -var isReactClassish = require('./isReactClassish'), - isReactElementish = require('./isReactElementish'); - -function makeExportsHot(m, React) { - if (isReactElementish(m.exports, React)) { - // React elements are never valid React classes - return false; - } - - var freshExports = m.exports, - exportsReactClass = isReactClassish(m.exports, React), - foundReactClasses = false; - - if (exportsReactClass) { - m.exports = m.makeHot(m.exports, '__MODULE_EXPORTS'); - foundReactClasses = true; - } - - for (var key in m.exports) { - if (!Object.prototype.hasOwnProperty.call(freshExports, key)) { - continue; - } - - var value; - try { - value = freshExports[key]; - } catch (err) { - continue; - } - - if (!isReactClassish(value, React)) { - continue; - } - - var descriptor = Object.getOwnPropertyDescriptor(m.exports, key); - if (!descriptor) { - continue; - } - - if (descriptor.writable) { - m.exports[key] = m.makeHot(value, '__MODULE_EXPORTS_' + key); - foundReactClasses = true; - } else { - console.warn("Can't make class " + key + " hot reloadable due to being read-only. To fix this you can try two solutions. First, you can exclude files or directories (for example, /node_modules/) using 'exclude' option in loader configuration. Second, if you are using Babel, you can enable loose mode for `es6.modules` using the 'loose' option. See: http://babeljs.io/docs/advanced/loose/ and http://babeljs.io/docs/usage/options/"); - } - } - - return foundReactClasses; -} - -module.exports = makeExportsHot; diff --git a/package.json b/package.json index dd4f7e677..6e5c4fb57 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { - "react-hot-api": "^0.5.0-alpha-3", + "react-deep-force-update": "^2.0.1", + "react-proxy": "^3.0.0-alpha.0", "source-map": "^0.4.4" }, "repository": { diff --git a/patch.js b/patch.js new file mode 100644 index 000000000..074f6bb5a --- /dev/null +++ b/patch.js @@ -0,0 +1,47 @@ +var createProxy = require('react-proxy').default; +var proxies = {}; +function resolveType(type) { + if (!type) { + return type; + } + var source = type.__source; + if (!source || !source.fileName || !source.exportName) { + return type; + } + + var fairlyUniqueID = source.fileName + '#' + source.exportName; + if (!proxies[fairlyUniqueID]) { + proxies[fairlyUniqueID] = createProxy(type); + } else { + proxies[fairlyUniqueID].update(type); + } + return proxies[fairlyUniqueID].get(); +} + +function patchReact() { + var React = require('react'); + if (React.createElement.isPatchedByReactHotLoader) { + throw new Error('Cannot patch React twice.'); + } + + var createElement = React.createElement; + function patchedCreateElement(type) { + return createElement.apply(this, [ + resolveType(type) + ].concat( + Array.prototype.slice.call(arguments, 1) + )); + } + patchedCreateElement.isPatchedByReactHotLoader = true; + React.createElement = patchedCreateElement; +} + +function forceUpdateTree(inst) { + var deepForceUpdate = require('react-deep-force-update'); + if (inst) { + deepForceUpdate(inst); + } +} + +patchReact() +module.exports.forceUpdateTree = forceUpdateTree \ No newline at end of file diff --git a/webpack.js b/webpack.js new file mode 100644 index 000000000..65fcc3e55 --- /dev/null +++ b/webpack.js @@ -0,0 +1,65 @@ +'use strict'; + +var path = require('path'), + SourceNode = require('source-map').SourceNode, + SourceMapConsumer = require('source-map').SourceMapConsumer, + makeIdentitySourceMap = require('./makeIdentitySourceMap'); + +module.exports = function (source, map) { + if (this.cacheable) { + this.cacheable(); + } + + var filename = path.basename(this.resourcePath); + var separator = '\n\n'; + var appendText; + var node; + var result; + + appendText = [ + '(function () {', + 'for (var key in module.exports) {', + 'if (!Object.prototype.hasOwnProperty.call(module.exports, key)) {', + 'continue;', + '}', + 'var exported = module.exports[key];', + 'try {', + 'if (typeof exported !== "function") {', + 'continue;', + '}', + 'if (Object.prototype.hasOwnProperty.call(exported, "__source")) {', + 'continue;', + '}', + 'Object.defineProperty(exported, "__source", {', + 'enumerable: false,', + 'configurable: true,', + 'value: {', + 'fileName: ' + JSON.stringify(filename) + ',', + 'exportName: key', + '}', + '})', + '} catch (err) { }', + '}', + '})();' + ].join(' '); + + if (this.sourceMap === false) { + return this.callback(null, [ + source, + appendText + ].join(separator)); + } + + if (!map) { + map = makeIdentitySourceMap(source, this.resourcePath); + } + + node = new SourceNode(null, null, null, [ + SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), + new SourceNode(null, null, this.resourcePath, appendText) + ]).join(separator); + + result = node.toStringWithSourceMap(); + + this.callback(null, result.code, result.map.toString()); +}; From 985426bbecaffff36ee6ed995876c937daf0c115 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 16:51:26 +0100 Subject: [PATCH 009/161] 3.0.0-alpha.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e5c4fb57..b4e16e24e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "2.0.0-alpha-4", + "version": "3.0.0-alpha.0", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { From 99054087d33f3d65d118d38fa8dca7e94b9516c9 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 19:38:53 +0100 Subject: [PATCH 010/161] Add AppContainer --- index.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + patch.js | 13 +++---------- 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 000000000..c6f4ee430 --- /dev/null +++ b/index.js @@ -0,0 +1,49 @@ +var React = require('react'); +var deepForceUpdate = require('react-deep-force-update'); + +var AppContainer = React.createClass({ + getDefaultProps: function() { + return { + errorReporter: require('redbox-react') + }; + }, + + getInitialState: function() { + return { + error: null + }; + }, + + componentWillReceiveProps: function(nextProps) { + if (nextProps.component !== this.props.component) { + this.setState({ + error: null + }); + } + }, + + componentDidUpdate: function(prevProps) { + if (prevProps.component !== this.props.component) { + deepForceUpdate(this); + } + }, + + unstable_handleError: function(error) { + this.setState({ + error: error + }); + }, + + render: function() { + if (this.state.error) { + return React.createElement( + this.props.errorReporter, + { error: this.state.error } + ); + } + + return React.createElement(this.props.component); + } +}); + +module.exports.AppContainer = AppContainer; diff --git a/package.json b/package.json index b4e16e24e..a86fe38f4 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "react-deep-force-update": "^2.0.1", "react-proxy": "^3.0.0-alpha.0", + "redbox-react": "^1.2.3", "source-map": "^0.4.4" }, "repository": { diff --git a/patch.js b/patch.js index 074f6bb5a..1991ded21 100644 --- a/patch.js +++ b/patch.js @@ -1,3 +1,5 @@ +var React = require('react'); + var createProxy = require('react-proxy').default; var proxies = {}; function resolveType(type) { @@ -19,7 +21,6 @@ function resolveType(type) { } function patchReact() { - var React = require('react'); if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); } @@ -36,12 +37,4 @@ function patchReact() { React.createElement = patchedCreateElement; } -function forceUpdateTree(inst) { - var deepForceUpdate = require('react-deep-force-update'); - if (inst) { - deepForceUpdate(inst); - } -} - -patchReact() -module.exports.forceUpdateTree = forceUpdateTree \ No newline at end of file +patchReact(); From eed72e86678ae6ac8541e6830f2fbefb770a28ef Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 19:39:08 +0100 Subject: [PATCH 011/161] 3.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a86fe38f4..5956ec2ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.0", + "version": "3.0.0-alpha.1", "description": "Tweak React components in real time.", "main": "index.js", "dependencies": { From f1234e5cee621b62f83c6d65aebc7f9d48ce2fb0 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 20:43:43 +0100 Subject: [PATCH 012/161] Kill old module --- RootInstanceProvider.js | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 RootInstanceProvider.js diff --git a/RootInstanceProvider.js b/RootInstanceProvider.js deleted file mode 100644 index 5ad8f1130..000000000 --- a/RootInstanceProvider.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -var getRootInstancesFromReactMount = require('./getRootInstancesFromReactMount'); - -var injectedProvider = null, - didWarn = false; - -function warnOnce() { - if (!didWarn) { - console.warn( - 'It appears that React Hot Loader isn\'t configured correctly. ' + - 'If you\'re using NPM, make sure your dependencies don\'t drag duplicate React distributions into their node_modules and that require("react") corresponds to the React instance you render your app with.', - 'If you\'re using a precompiled version of React, see https://github.com/gaearon/react-hot-loader/tree/master/docs#usage-with-external-react for integration instructions.' - ); - } - - didWarn = true; -} - -var RootInstanceProvider = { - injection: { - injectProvider: function (provider) { - injectedProvider = provider; - } - }, - - getRootInstances: function (ReactMount) { - if (injectedProvider) { - return injectedProvider.getRootInstances(); - } - - var instances = ReactMount && getRootInstancesFromReactMount(ReactMount) || []; - if (!Object.keys(instances).length) { - warnOnce(); - } - - return instances; - } -}; - -module.exports = RootInstanceProvider; \ No newline at end of file From ae896b013591d2db47688bb88ab107c93fc8ccfd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 21:19:35 +0100 Subject: [PATCH 013/161] Refactor and move files --- .jshintrc | 6 -- webpack.js | 65 ------------------- webpack/index.js | 48 ++++++++++++++ .../makeIdentitySourceMap.js | 0 webpack/tagCommonJSExports.js | 34 ++++++++++ 5 files changed, 82 insertions(+), 71 deletions(-) delete mode 100644 .jshintrc delete mode 100644 webpack.js create mode 100644 webpack/index.js rename makeIdentitySourceMap.js => webpack/makeIdentitySourceMap.js (100%) create mode 100644 webpack/tagCommonJSExports.js diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 4ebfa5be6..000000000 --- a/.jshintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "node": true, - "globals": { - "window": true - } -} \ No newline at end of file diff --git a/webpack.js b/webpack.js deleted file mode 100644 index 65fcc3e55..000000000 --- a/webpack.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -var path = require('path'), - SourceNode = require('source-map').SourceNode, - SourceMapConsumer = require('source-map').SourceMapConsumer, - makeIdentitySourceMap = require('./makeIdentitySourceMap'); - -module.exports = function (source, map) { - if (this.cacheable) { - this.cacheable(); - } - - var filename = path.basename(this.resourcePath); - var separator = '\n\n'; - var appendText; - var node; - var result; - - appendText = [ - '(function () {', - 'for (var key in module.exports) {', - 'if (!Object.prototype.hasOwnProperty.call(module.exports, key)) {', - 'continue;', - '}', - 'var exported = module.exports[key];', - 'try {', - 'if (typeof exported !== "function") {', - 'continue;', - '}', - 'if (Object.prototype.hasOwnProperty.call(exported, "__source")) {', - 'continue;', - '}', - 'Object.defineProperty(exported, "__source", {', - 'enumerable: false,', - 'configurable: true,', - 'value: {', - 'fileName: ' + JSON.stringify(filename) + ',', - 'exportName: key', - '}', - '})', - '} catch (err) { }', - '}', - '})();' - ].join(' '); - - if (this.sourceMap === false) { - return this.callback(null, [ - source, - appendText - ].join(separator)); - } - - if (!map) { - map = makeIdentitySourceMap(source, this.resourcePath); - } - - node = new SourceNode(null, null, null, [ - SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), - new SourceNode(null, null, this.resourcePath, appendText) - ]).join(separator); - - result = node.toStringWithSourceMap(); - - this.callback(null, result.code, result.map.toString()); -}; diff --git a/webpack/index.js b/webpack/index.js new file mode 100644 index 000000000..7ccdb4ddf --- /dev/null +++ b/webpack/index.js @@ -0,0 +1,48 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var SourceNode = require('source-map').SourceNode; +var SourceMapConsumer = require('source-map').SourceMapConsumer; +var makeIdentitySourceMap = require('./makeIdentitySourceMap'); + +var tagCommonJSExportsSource; + +function transform(source, map) { + if (this.cacheable) { + this.cacheable(); + } + + if (!tagCommonJSExportsSource) { + tagCommonJSExportsSource = fs.readFileSync( + path.join(__dirname, 'tagCommonJSExports.js'), + 'utf8' + ).split(/\n\s*/).join(' '); + } + + var separator = '\n\n'; + var appendText = tagCommonJSExportsSource.replace( + '__FILENAME__', + JSON.stringify(path.basename(this.resourcePath)) + ); + + if (this.sourceMap === false) { + return this.callback(null, [ + source, + appendText + ].join(separator)); + } + + if (!map) { + map = makeIdentitySourceMap(source, this.resourcePath); + } + var node = new SourceNode(null, null, null, [ + SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), + new SourceNode(null, null, this.resourcePath, appendText) + ]).join(separator); + + var result = node.toStringWithSourceMap(); + this.callback(null, result.code, result.map.toString()); +}; + +module.exports = transform; diff --git a/makeIdentitySourceMap.js b/webpack/makeIdentitySourceMap.js similarity index 100% rename from makeIdentitySourceMap.js rename to webpack/makeIdentitySourceMap.js diff --git a/webpack/tagCommonJSExports.js b/webpack/tagCommonJSExports.js new file mode 100644 index 000000000..50e691ecc --- /dev/null +++ b/webpack/tagCommonJSExports.js @@ -0,0 +1,34 @@ +(function () { + /* react-hot-loader/webpack */ + var hasOwn = Object.prototype.hasOwnProperty; + + function tag(fn, exportName) { + if (typeof fn !== 'function') { + return; + } + if (hasOwn.call(fn, '__source')) { + return; + } + try { + Object.defineProperty(fn, '__source', { + enumerable: false, + configurable: true, + value: { + fileName: __FILENAME__, + exportName: exportName + } + }); + } catch (err) { } + } + + if (typeof module.exports === 'function') { + tag(module.exports, '*'); + return; + } + + for (var key in module.exports) { + if (hasOwn.call(module.exports, key)) { + tag(module.exports[key], key) + } + } +})(); \ No newline at end of file From af0dd198871d1b0def26a6cb5ae1c74ba33ded95 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 21:38:38 +0100 Subject: [PATCH 014/161] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b873c0885..aa3971bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### 3.0.0-alpha + +Big changes both to internals and usage. No docs yet but you can look at https://github.com/gaearon/react-hot-boilerplate/pull/61 for an example. + ### 2.0.0-alpha **Experimental release that isn't really representative on what will go in 2.0, but uses the new engine.** From cce9055ed3113e0bec677d0aaf9e3c6300e9b0a3 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 22:12:37 +0100 Subject: [PATCH 015/161] Add build process with Babel --- .babelrc | 3 ++ .gitignore | 1 + example/README.md | 2 - index.js | 50 +------------------ package.json | 18 ++++++- patch.js | 40 --------------- src/AppContainer.js | 46 +++++++++++++++++ src/index.js | 2 + src/patch.js | 32 ++++++++++++ {webpack => src/webpack}/index.js | 19 ++++--- .../webpack}/makeIdentitySourceMap.js | 6 +-- .../webpack}/tagCommonJSExports.js | 4 +- webpack.js | 1 + 13 files changed, 117 insertions(+), 107 deletions(-) create mode 100644 .babelrc delete mode 100644 example/README.md delete mode 100644 patch.js create mode 100644 src/AppContainer.js create mode 100644 src/index.js create mode 100644 src/patch.js rename {webpack => src/webpack}/index.js (65%) rename {webpack => src/webpack}/makeIdentitySourceMap.js (71%) rename {webpack => src/webpack}/tagCommonJSExports.js (87%) create mode 100644 webpack.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..facd18092 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react"] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd4f2b066..7721b4b7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules +lib .DS_Store diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 281de3915..000000000 --- a/example/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Here for an example? -Check out **[starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)** made by community! diff --git a/index.js b/index.js index c6f4ee430..144ec7fa8 100644 --- a/index.js +++ b/index.js @@ -1,49 +1 @@ -var React = require('react'); -var deepForceUpdate = require('react-deep-force-update'); - -var AppContainer = React.createClass({ - getDefaultProps: function() { - return { - errorReporter: require('redbox-react') - }; - }, - - getInitialState: function() { - return { - error: null - }; - }, - - componentWillReceiveProps: function(nextProps) { - if (nextProps.component !== this.props.component) { - this.setState({ - error: null - }); - } - }, - - componentDidUpdate: function(prevProps) { - if (prevProps.component !== this.props.component) { - deepForceUpdate(this); - } - }, - - unstable_handleError: function(error) { - this.setState({ - error: error - }); - }, - - render: function() { - if (this.state.error) { - return React.createElement( - this.props.errorReporter, - { error: this.state.error } - ); - } - - return React.createElement(this.props.component); - } -}); - -module.exports.AppContainer = AppContainer; +module.exports = require('./lib/index'); \ No newline at end of file diff --git a/package.json b/package.json index 5956ec2ea..fd52c5dea 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,15 @@ "version": "3.0.0-alpha.1", "description": "Tweak React components in real time.", "main": "index.js", + "files": [ + "lib", + "webpack.js" + ], + "scripts": { + "clean": "rimraf lib", + "build": "babel src --out-dir lib", + "prepublish": "npm run clean && npm run build" + }, "dependencies": { "react-deep-force-update": "^2.0.1", "react-proxy": "^3.0.0-alpha.0", @@ -30,5 +39,12 @@ "bugs": { "url": "https://github.com/gaearon/react-hot-loader/issues" }, - "homepage": "https://github.com/gaearon/react-hot-loader" + "homepage": "https://github.com/gaearon/react-hot-loader", + "devDependencies": { + "babel-cli": "^6.7.5", + "babel-core": "^6.7.6", + "babel-preset-es2015": "^6.6.0", + "babel-preset-react": "^6.5.0", + "rimraf": "^2.5.2" + } } diff --git a/patch.js b/patch.js deleted file mode 100644 index 1991ded21..000000000 --- a/patch.js +++ /dev/null @@ -1,40 +0,0 @@ -var React = require('react'); - -var createProxy = require('react-proxy').default; -var proxies = {}; -function resolveType(type) { - if (!type) { - return type; - } - var source = type.__source; - if (!source || !source.fileName || !source.exportName) { - return type; - } - - var fairlyUniqueID = source.fileName + '#' + source.exportName; - if (!proxies[fairlyUniqueID]) { - proxies[fairlyUniqueID] = createProxy(type); - } else { - proxies[fairlyUniqueID].update(type); - } - return proxies[fairlyUniqueID].get(); -} - -function patchReact() { - if (React.createElement.isPatchedByReactHotLoader) { - throw new Error('Cannot patch React twice.'); - } - - var createElement = React.createElement; - function patchedCreateElement(type) { - return createElement.apply(this, [ - resolveType(type) - ].concat( - Array.prototype.slice.call(arguments, 1) - )); - } - patchedCreateElement.isPatchedByReactHotLoader = true; - React.createElement = patchedCreateElement; -} - -patchReact(); diff --git a/src/AppContainer.js b/src/AppContainer.js new file mode 100644 index 000000000..b436d9492 --- /dev/null +++ b/src/AppContainer.js @@ -0,0 +1,46 @@ +const React = require('react'); +const deepForceUpdate = require('react-deep-force-update'); +const Redbox = require('redbox-react'); +const { Component } = React; + +class AppContainer extends Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.component !== this.props.component) { + this.setState({ + error: null + }); + } + } + + componentDidUpdate(prevProps) { + if (prevProps.component !== this.props.component) { + deepForceUpdate(this); + } + } + + unstable_handleError(error) { + this.setState({ + error: error + }); + } + + render() { + const { error } = this.state; + if (error) { + return ; + } else { + return ; + } + } +} + +AppContainer.defaultProps = { + errorReporter: Redbox +}; + +module.exports = AppContainer; diff --git a/src/index.js b/src/index.js new file mode 100644 index 000000000..06c9f455a --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +require('./patch'); +module.exports.AppContainer = require('./AppContainer'); diff --git a/src/patch.js b/src/patch.js new file mode 100644 index 000000000..6b691f8d7 --- /dev/null +++ b/src/patch.js @@ -0,0 +1,32 @@ +const React = require('react'); +const createProxy = require('react-proxy').default; + +let proxies = {}; +function resolveType(type) { + if (!type) { + return type; + } + const source = type.__source; + if (!source || !source.fileName || !source.exportName) { + return type; + } + + const fairlyUniqueID = source.fileName + '#' + source.exportName; + if (!proxies[fairlyUniqueID]) { + proxies[fairlyUniqueID] = createProxy(type); + } else { + proxies[fairlyUniqueID].update(type); + } + return proxies[fairlyUniqueID].get(); +} + +if (React.createElement.isPatchedByReactHotLoader) { + throw new Error('Cannot patch React twice.'); +} + +const createElement = React.createElement; +function patchedCreateElement(type, ...args) { + return createElement(resolveType(type), ...args); +} +patchedCreateElement.isPatchedByReactHotLoader = true; +React.createElement = patchedCreateElement; diff --git a/webpack/index.js b/src/webpack/index.js similarity index 65% rename from webpack/index.js rename to src/webpack/index.js index 7ccdb4ddf..0f0f861e2 100644 --- a/webpack/index.js +++ b/src/webpack/index.js @@ -1,12 +1,11 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var SourceNode = require('source-map').SourceNode; -var SourceMapConsumer = require('source-map').SourceMapConsumer; -var makeIdentitySourceMap = require('./makeIdentitySourceMap'); +const fs = require('fs'); +const path = require('path'); +const { SourceNode, SourceMapConsumer } = require('source-map'); +const makeIdentitySourceMap = require('./makeIdentitySourceMap'); -var tagCommonJSExportsSource; +let tagCommonJSExportsSource = null; function transform(source, map) { if (this.cacheable) { @@ -20,8 +19,8 @@ function transform(source, map) { ).split(/\n\s*/).join(' '); } - var separator = '\n\n'; - var appendText = tagCommonJSExportsSource.replace( + const separator = '\n\n'; + const appendText = tagCommonJSExportsSource.replace( '__FILENAME__', JSON.stringify(path.basename(this.resourcePath)) ); @@ -36,12 +35,12 @@ function transform(source, map) { if (!map) { map = makeIdentitySourceMap(source, this.resourcePath); } - var node = new SourceNode(null, null, null, [ + const node = new SourceNode(null, null, null, [ SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), new SourceNode(null, null, this.resourcePath, appendText) ]).join(separator); - var result = node.toStringWithSourceMap(); + const result = node.toStringWithSourceMap(); this.callback(null, result.code, result.map.toString()); }; diff --git a/webpack/makeIdentitySourceMap.js b/src/webpack/makeIdentitySourceMap.js similarity index 71% rename from webpack/makeIdentitySourceMap.js rename to src/webpack/makeIdentitySourceMap.js index defd0321d..f6db242bf 100644 --- a/webpack/makeIdentitySourceMap.js +++ b/src/webpack/makeIdentitySourceMap.js @@ -1,12 +1,12 @@ 'use strict'; -var SourceMapGenerator = require('source-map').SourceMapGenerator; +const { SourceMapGenerator } = require('source-map'); function makeIdentitySourceMap(content, resourcePath) { - var map = new SourceMapGenerator(); + const map = new SourceMapGenerator(); map.setSourceContent(resourcePath, content); - content.split('\n').map(function (line, index) { + content.split('\n').map((line, index) => { map.addMapping({ source: resourcePath, original: { diff --git a/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js similarity index 87% rename from webpack/tagCommonJSExports.js rename to src/webpack/tagCommonJSExports.js index 50e691ecc..04dc5fcc6 100644 --- a/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,6 +1,6 @@ (function () { /* react-hot-loader/webpack */ - var hasOwn = Object.prototype.hasOwnProperty; + const hasOwn = Object.prototype.hasOwnProperty; function tag(fn, exportName) { if (typeof fn !== 'function') { @@ -26,7 +26,7 @@ return; } - for (var key in module.exports) { + for (let key in module.exports) { if (hasOwn.call(module.exports, key)) { tag(module.exports[key], key) } diff --git a/webpack.js b/webpack.js new file mode 100644 index 000000000..ca7a6f582 --- /dev/null +++ b/webpack.js @@ -0,0 +1 @@ +module.exports = require('./lib/webpack'); \ No newline at end of file From 030dcb2515e3ec85452ec5b721f2480096bbaa1c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 22:51:52 +0100 Subject: [PATCH 016/161] Bring back react-hot-loader/patch and add feature testing for it --- patch.js | 1 + src/AppContainer.dev.js | 66 ++++++++++++++++++++++++++++++++++++++++ src/AppContainer.js | 51 +++---------------------------- src/AppContainer.prod.js | 10 ++++++ src/index.js | 4 +-- src/patch.dev.js | 32 +++++++++++++++++++ src/patch.js | 37 +++------------------- src/patch.prod.js | 1 + 8 files changed, 122 insertions(+), 80 deletions(-) create mode 100644 patch.js create mode 100644 src/AppContainer.dev.js create mode 100644 src/AppContainer.prod.js create mode 100644 src/patch.dev.js create mode 100644 src/patch.prod.js diff --git a/patch.js b/patch.js new file mode 100644 index 000000000..576d66edc --- /dev/null +++ b/patch.js @@ -0,0 +1 @@ +module.exports = require('./lib/patch'); \ No newline at end of file diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js new file mode 100644 index 000000000..e604f0be4 --- /dev/null +++ b/src/AppContainer.dev.js @@ -0,0 +1,66 @@ +const React = require('react'); +const deepForceUpdate = require('react-deep-force-update'); +const Redbox = require('redbox-react'); +const { Component } = React; + +function ensureCreateElementIsPatched() { + const A = () => {}; + A.__source = { fileName: 'fake', exportName: 'fake' } + const B = () => {}; + B.__source = { fileName: 'fake', exportName: 'fake' } + + if (.type !== .type) { + console.error( + 'React Hot Loader: It appears that you did not run ' + + '"react-hot-loader/patch". Make sure that this module ' + + 'runs before any other code. For example, if you use Webpack, ' + + 'you can add "react-hot-loader/patch" as the very first item to the ' + + '"entry" array in its config. Alternatively, you can add ' + + 'require("react-hot-loader/patch") as the very first line ' + + 'in the application code, before any other imports.' + ); + } +} +ensureCreateElementIsPatched(); + +class AppContainer extends Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + + componentWillReceiveProps(nextProps) { + if (nextProps.component !== this.props.component) { + this.setState({ + error: null + }); + } + } + + componentDidUpdate(prevProps) { + if (prevProps.component !== this.props.component) { + deepForceUpdate(this); + } + } + + unstable_handleError(error) { + this.setState({ + error: error + }); + } + + render() { + const { error } = this.state; + if (error) { + return ; + } else { + return ; + } + } +} + +AppContainer.defaultProps = { + errorReporter: Redbox +}; + +module.exports = AppContainer; diff --git a/src/AppContainer.js b/src/AppContainer.js index b436d9492..319bde127 100644 --- a/src/AppContainer.js +++ b/src/AppContainer.js @@ -1,46 +1,5 @@ -const React = require('react'); -const deepForceUpdate = require('react-deep-force-update'); -const Redbox = require('redbox-react'); -const { Component } = React; - -class AppContainer extends Component { - constructor(props) { - super(props); - this.state = { error: null }; - } - - componentWillReceiveProps(nextProps) { - if (nextProps.component !== this.props.component) { - this.setState({ - error: null - }); - } - } - - componentDidUpdate(prevProps) { - if (prevProps.component !== this.props.component) { - deepForceUpdate(this); - } - } - - unstable_handleError(error) { - this.setState({ - error: error - }); - } - - render() { - const { error } = this.state; - if (error) { - return ; - } else { - return ; - } - } -} - -AppContainer.defaultProps = { - errorReporter: Redbox -}; - -module.exports = AppContainer; +if (process.env.NODE_ENV === 'production') { + module.exports = require('./AppContainer.prod'); +} else { + module.exports = require('./AppContainer.dev'); +} \ No newline at end of file diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js new file mode 100644 index 000000000..a8949656d --- /dev/null +++ b/src/AppContainer.prod.js @@ -0,0 +1,10 @@ +const React = require('react'); +const { Component } = React; + +class AppContainer extends Component { + render() { + return ; + } +} + +module.exports = AppContainer; diff --git a/src/index.js b/src/index.js index 06c9f455a..a79a47592 100644 --- a/src/index.js +++ b/src/index.js @@ -1,2 +1,2 @@ -require('./patch'); -module.exports.AppContainer = require('./AppContainer'); +const AppContainer = require('./AppContainer'); +module.exports.AppContainer = AppContainer; diff --git a/src/patch.dev.js b/src/patch.dev.js new file mode 100644 index 000000000..6b691f8d7 --- /dev/null +++ b/src/patch.dev.js @@ -0,0 +1,32 @@ +const React = require('react'); +const createProxy = require('react-proxy').default; + +let proxies = {}; +function resolveType(type) { + if (!type) { + return type; + } + const source = type.__source; + if (!source || !source.fileName || !source.exportName) { + return type; + } + + const fairlyUniqueID = source.fileName + '#' + source.exportName; + if (!proxies[fairlyUniqueID]) { + proxies[fairlyUniqueID] = createProxy(type); + } else { + proxies[fairlyUniqueID].update(type); + } + return proxies[fairlyUniqueID].get(); +} + +if (React.createElement.isPatchedByReactHotLoader) { + throw new Error('Cannot patch React twice.'); +} + +const createElement = React.createElement; +function patchedCreateElement(type, ...args) { + return createElement(resolveType(type), ...args); +} +patchedCreateElement.isPatchedByReactHotLoader = true; +React.createElement = patchedCreateElement; diff --git a/src/patch.js b/src/patch.js index 6b691f8d7..f04b3ded1 100644 --- a/src/patch.js +++ b/src/patch.js @@ -1,32 +1,5 @@ -const React = require('react'); -const createProxy = require('react-proxy').default; - -let proxies = {}; -function resolveType(type) { - if (!type) { - return type; - } - const source = type.__source; - if (!source || !source.fileName || !source.exportName) { - return type; - } - - const fairlyUniqueID = source.fileName + '#' + source.exportName; - if (!proxies[fairlyUniqueID]) { - proxies[fairlyUniqueID] = createProxy(type); - } else { - proxies[fairlyUniqueID].update(type); - } - return proxies[fairlyUniqueID].get(); -} - -if (React.createElement.isPatchedByReactHotLoader) { - throw new Error('Cannot patch React twice.'); -} - -const createElement = React.createElement; -function patchedCreateElement(type, ...args) { - return createElement(resolveType(type), ...args); -} -patchedCreateElement.isPatchedByReactHotLoader = true; -React.createElement = patchedCreateElement; +if (process.env.NODE_ENV === 'production') { + module.exports = require('./patch.prod'); +} else { + module.exports = require('./patch.dev'); +} \ No newline at end of file diff --git a/src/patch.prod.js b/src/patch.prod.js new file mode 100644 index 000000000..ce13cc68c --- /dev/null +++ b/src/patch.prod.js @@ -0,0 +1 @@ +/* noop */ \ No newline at end of file From b8317712e29eea88ec31f13853e50eac731e978b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 17 Apr 2016 23:37:19 +0100 Subject: [PATCH 017/161] Add scaffold for the Babel plugin --- babel.js | 1 + package.json | 5 +++++ src/babel/index.js | 9 +++++++++ test/babel/fixtures/class/.babelrc | 5 +++++ test/babel/fixtures/class/actual.js | 3 +++ test/babel/fixtures/class/expected.js | 3 +++ test/babel/index.js | 29 +++++++++++++++++++++++++++ test/index.js | 1 + 8 files changed, 56 insertions(+) create mode 100644 babel.js create mode 100644 src/babel/index.js create mode 100644 test/babel/fixtures/class/.babelrc create mode 100644 test/babel/fixtures/class/actual.js create mode 100644 test/babel/fixtures/class/expected.js create mode 100644 test/babel/index.js create mode 100644 test/index.js diff --git a/babel.js b/babel.js new file mode 100644 index 000000000..93c319a08 --- /dev/null +++ b/babel.js @@ -0,0 +1 @@ +module.exports = require('./lib/babel'); \ No newline at end of file diff --git a/package.json b/package.json index fd52c5dea..632805181 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,14 @@ "main": "index.js", "files": [ "lib", + "babel.js", "webpack.js" ], "scripts": { "clean": "rimraf lib", "build": "babel src --out-dir lib", + "test": "mocha --compilers js:babel-core/register ./test", + "test:watch": "npm run test -- --watch", "prepublish": "npm run clean && npm run build" }, "dependencies": { @@ -45,6 +48,8 @@ "babel-core": "^6.7.6", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", + "expect": "^1.16.0", + "mocha": "^2.4.5", "rimraf": "^2.5.2" } } diff --git a/src/babel/index.js b/src/babel/index.js new file mode 100644 index 000000000..3e8cdf796 --- /dev/null +++ b/src/babel/index.js @@ -0,0 +1,9 @@ +export default function({ types: t, template }) { + function hasParentFunction(path) { + return !!path.findParent(parentPath => parentPath.isFunction()); + } + + return { + visitor: { } + }; +} diff --git a/test/babel/fixtures/class/.babelrc b/test/babel/fixtures/class/.babelrc new file mode 100644 index 000000000..828611846 --- /dev/null +++ b/test/babel/fixtures/class/.babelrc @@ -0,0 +1,5 @@ +{ + "plugins": [ + ["../../../../src/babel"] + ] +} diff --git a/test/babel/fixtures/class/actual.js b/test/babel/fixtures/class/actual.js new file mode 100644 index 000000000..996d84bf5 --- /dev/null +++ b/test/babel/fixtures/class/actual.js @@ -0,0 +1,3 @@ +class Foo extends React.Component { + render() {} +} diff --git a/test/babel/fixtures/class/expected.js b/test/babel/fixtures/class/expected.js new file mode 100644 index 000000000..996d84bf5 --- /dev/null +++ b/test/babel/fixtures/class/expected.js @@ -0,0 +1,3 @@ +class Foo extends React.Component { + render() {} +} diff --git a/test/babel/index.js b/test/babel/index.js new file mode 100644 index 000000000..5572f8d03 --- /dev/null +++ b/test/babel/index.js @@ -0,0 +1,29 @@ +import path from 'path'; +import fs from 'fs'; +import expect from 'expect'; +import { transformFileSync } from 'babel-core'; + +function trim(str) { + return str.replace(/^\s+|\s+$/, ''); +} + +describe('tags potential React components', () => { + const fixturesDir = path.join(__dirname, 'fixtures'); + fs.readdirSync(fixturesDir).map((caseName) => { + it(caseName.split('-').join(' '), () => { + const fixtureDir = path.join(fixturesDir, caseName); + let actualPath = path.join(fixtureDir, 'actual.js'); + const actual = transformFileSync(actualPath).code; + + if (path.sep === '\\') { + // Specific case of windows, transformFileSync return code with '/' + actualPath = actualPath.replace(/\\/g, '/'); + } + + const expected = fs.readFileSync( + path.join(fixtureDir, 'expected.js') + ).toString().replace(/%FIXTURE_PATH%/g, actualPath); + expect(trim(actual)).toEqual(trim(expected)); + }); + }); +}); diff --git a/test/index.js b/test/index.js new file mode 100644 index 000000000..17ba30ada --- /dev/null +++ b/test/index.js @@ -0,0 +1 @@ +import './babel'; From 5c696047db941f24600a6f234eed231e2f8f9b48 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 05:51:55 +0100 Subject: [PATCH 018/161] Add Babel plugin --- package.json | 1 + src/AppContainer.dev.js | 4 +- src/babel/index.js | 105 +++++++++++++++++- src/patch.dev.js | 7 +- src/webpack/tagCommonJSExports.js | 14 +-- .../fixtures/{class => bindings}/.babelrc | 0 test/babel/fixtures/bindings/actual.js | 19 ++++ test/babel/fixtures/bindings/expected.js | 52 +++++++++ test/babel/fixtures/class/actual.js | 3 - test/babel/fixtures/class/expected.js | 3 - test/babel/fixtures/counter/.babelrc | 6 + test/babel/fixtures/counter/actual.js | 2 + test/babel/fixtures/counter/expected.js | 47 ++++++++ test/babel/index.js | 22 ++-- 14 files changed, 253 insertions(+), 32 deletions(-) rename test/babel/fixtures/{class => bindings}/.babelrc (100%) create mode 100644 test/babel/fixtures/bindings/actual.js create mode 100644 test/babel/fixtures/bindings/expected.js delete mode 100644 test/babel/fixtures/class/actual.js delete mode 100644 test/babel/fixtures/class/expected.js create mode 100644 test/babel/fixtures/counter/.babelrc create mode 100644 test/babel/fixtures/counter/actual.js create mode 100644 test/babel/fixtures/counter/expected.js diff --git a/package.json b/package.json index 632805181..c6d57a7e8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "prepublish": "npm run clean && npm run build" }, "dependencies": { + "babel-template": "^6.7.0", "react-deep-force-update": "^2.0.1", "react-proxy": "^3.0.0-alpha.0", "redbox-react": "^1.2.3", diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index e604f0be4..3db706f97 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -5,9 +5,9 @@ const { Component } = React; function ensureCreateElementIsPatched() { const A = () => {}; - A.__source = { fileName: 'fake', exportName: 'fake' } + A.__source = { fileName: 'fake', localName: 'fake' } const B = () => {}; - B.__source = { fileName: 'fake', exportName: 'fake' } + B.__source = { fileName: 'fake', localName: 'fake' } if (.type !== .type) { console.error( diff --git a/src/babel/index.js b/src/babel/index.js index 3e8cdf796..99d8ef456 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -1,9 +1,108 @@ +import template from 'babel-template'; + +const buildRegistration = template(` + tagSource(ID, NAME); +`); +const buildTagger = template(` +(function () { + function tagSource(fn, localName) { + if (typeof fn !== "function") { + return; + } + + if (fn.hasOwnProperty("__source")) { + return; + } + + try { + Object.defineProperty(fn, "__source", { + enumerable: false, + configurable: true, + value: { + fileName: FILENAME, + localName: localName + } + }); + } catch (err) {} + } + REGISTRATIONS +})(); +`); + export default function({ types: t, template }) { - function hasParentFunction(path) { - return !!path.findParent(parentPath => parentPath.isFunction()); + function shouldRegisterBinding(binding) { + let { type, node } = binding.path; + switch (type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'VariableDeclaration': + return true; + case 'VariableDeclarator': + const { init } = node; + if (t.isCallExpression(init) && init.callee.name === 'require') { + return false; + } + return true; + default: + return false; + } } + const DEFAULT_EXPORT = Symbol(); return { - visitor: { } + visitor: { + ExportDefaultDeclaration(path) { + if (path.node.declaration.id) { + return; + } + + const id = path.scope.generateUidIdentifier('default'); + const expression = t.isExpression(path.node.declaration) ? + path.node.declaration : + t.toExpression(path.node.declaration); + + path.insertBefore( + t.variableDeclaration('const', [ + t.variableDeclarator(id, expression) + ]) + ) + path.node.declaration = id; + path.parent[DEFAULT_EXPORT] = id; + }, + + Program: { + enter({ node }) { + node[DEFAULT_EXPORT] = null; + }, + + exit({ node, scope }, { file }) { + let registrations = []; + + for (let id in scope.bindings) { + const binding = scope.bindings[id]; + if (shouldRegisterBinding(binding)) { + registrations.push(buildRegistration({ + ID: binding.identifier, + NAME: t.stringLiteral(id) + })); + } + } + + if (node[DEFAULT_EXPORT]) { + registrations.push(buildRegistration({ + ID: node[DEFAULT_EXPORT], + NAME: t.stringLiteral('default') + })); + } + + node.body.push( + buildTagger({ + FILENAME: t.stringLiteral(file.opts.filename), + REGISTRATIONS: registrations + }) + ) + } + } + } }; } diff --git a/src/patch.dev.js b/src/patch.dev.js index 6b691f8d7..7105260c3 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -6,12 +6,15 @@ function resolveType(type) { if (!type) { return type; } + if (!Object.hasOwnProperty.call(type, '__source')) { + return type; + } const source = type.__source; - if (!source || !source.fileName || !source.exportName) { + if (!source || !source.fileName || !source.localName) { return type; } - const fairlyUniqueID = source.fileName + '#' + source.exportName; + const fairlyUniqueID = source.fileName + '#' + source.localName; if (!proxies[fairlyUniqueID]) { proxies[fairlyUniqueID] = createProxy(type); } else { diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index 04dc5fcc6..b7445d70e 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,12 +1,10 @@ (function () { /* react-hot-loader/webpack */ - const hasOwn = Object.prototype.hasOwnProperty; - - function tag(fn, exportName) { + function tagSource(fn, localName) { if (typeof fn !== 'function') { return; } - if (hasOwn.call(fn, '__source')) { + if (fn.hasOwnProperty('__source')) { return; } try { @@ -15,20 +13,20 @@ configurable: true, value: { fileName: __FILENAME__, - exportName: exportName + localName: localName } }); } catch (err) { } } if (typeof module.exports === 'function') { - tag(module.exports, '*'); + tagSource(module.exports, 'module.exports'); return; } for (let key in module.exports) { - if (hasOwn.call(module.exports, key)) { - tag(module.exports[key], key) + if (Object.prototype.hasOwnProperty.call(module.exports, key)) { + tagSource(module.exports[key], `module.exports.${key}`); } } })(); \ No newline at end of file diff --git a/test/babel/fixtures/class/.babelrc b/test/babel/fixtures/bindings/.babelrc similarity index 100% rename from test/babel/fixtures/class/.babelrc rename to test/babel/fixtures/bindings/.babelrc diff --git a/test/babel/fixtures/bindings/actual.js b/test/babel/fixtures/bindings/actual.js new file mode 100644 index 000000000..83cb321ed --- /dev/null +++ b/test/babel/fixtures/bindings/actual.js @@ -0,0 +1,19 @@ +import P, { Q } from "left-pad"; +const A = 42; +function B() { + function R() {} + class S {} + const T = 42; +} +export class C { + U() { + function V() { + class W {} + } + } +} +const D = class X {}; +let E = D; +var Y = require("left-pad"); +var { Z } = require("left-pad"); +export default React.createClass({}); \ No newline at end of file diff --git a/test/babel/fixtures/bindings/expected.js b/test/babel/fixtures/bindings/expected.js new file mode 100644 index 000000000..5a706fa0f --- /dev/null +++ b/test/babel/fixtures/bindings/expected.js @@ -0,0 +1,52 @@ +import P, { Q } from "left-pad"; +const A = 42; +function B() { + function R() {} + class S {} + const T = 42; +} +export class C { + U() { + function V() { + class W {} + } + } +} +const D = class X {}; +let E = D; +var Y = require("left-pad"); +var { Z } = require("left-pad"); + +const _default = React.createClass({}); + +export default _default; + +(function () { + function tagSource(fn, localName) { + if (typeof fn !== "function") { + return; + } + + if (fn.hasOwnProperty("__source")) { + return; + } + + try { + Object.defineProperty(fn, "__source", { + enumerable: false, + configurable: true, + value: { + fileName: __FILENAME__, + localName: localName + } + }); + } catch (err) {} + } + + tagSource(A, "A"); + tagSource(B, "B"); + tagSource(C, "C"); + tagSource(D, "D"); + tagSource(E, "E"); + tagSource(_default, "default"); +})(); diff --git a/test/babel/fixtures/class/actual.js b/test/babel/fixtures/class/actual.js deleted file mode 100644 index 996d84bf5..000000000 --- a/test/babel/fixtures/class/actual.js +++ /dev/null @@ -1,3 +0,0 @@ -class Foo extends React.Component { - render() {} -} diff --git a/test/babel/fixtures/class/expected.js b/test/babel/fixtures/class/expected.js deleted file mode 100644 index 996d84bf5..000000000 --- a/test/babel/fixtures/class/expected.js +++ /dev/null @@ -1,3 +0,0 @@ -class Foo extends React.Component { - render() {} -} diff --git a/test/babel/fixtures/counter/.babelrc b/test/babel/fixtures/counter/.babelrc new file mode 100644 index 000000000..cf052fd63 --- /dev/null +++ b/test/babel/fixtures/counter/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": ["es2015", "react"], + "plugins": [ + ["../../../../src/babel"] + ] +} diff --git a/test/babel/fixtures/counter/actual.js b/test/babel/fixtures/counter/actual.js new file mode 100644 index 000000000..c53c914a7 --- /dev/null +++ b/test/babel/fixtures/counter/actual.js @@ -0,0 +1,2 @@ +class Counter { } +export default () =>
\ No newline at end of file diff --git a/test/babel/fixtures/counter/expected.js b/test/babel/fixtures/counter/expected.js new file mode 100644 index 000000000..9d5a59b23 --- /dev/null +++ b/test/babel/fixtures/counter/expected.js @@ -0,0 +1,47 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Counter = function Counter() { + _classCallCheck(this, Counter); +}; + +var _default = function _default() { + return React.createElement( + "div", + null, + React.createElement(Counter, null) + ); +}; + +exports.default = _default; + +(function () { + function tagSource(fn, localName) { + if (typeof fn !== "function") { + return; + } + + if (fn.hasOwnProperty("__source")) { + return; + } + + try { + Object.defineProperty(fn, "__source", { + enumerable: false, + configurable: true, + value: { + fileName: __FILENAME__, + localName: localName + } + }); + } catch (err) {} + } + + tagSource(Counter, "Counter"); + tagSource(_default, "default"); +})(); \ No newline at end of file diff --git a/test/babel/index.js b/test/babel/index.js index 5572f8d03..cf80faf03 100644 --- a/test/babel/index.js +++ b/test/babel/index.js @@ -9,20 +9,20 @@ function trim(str) { describe('tags potential React components', () => { const fixturesDir = path.join(__dirname, 'fixtures'); - fs.readdirSync(fixturesDir).map((caseName) => { - it(caseName.split('-').join(' '), () => { - const fixtureDir = path.join(fixturesDir, caseName); - let actualPath = path.join(fixtureDir, 'actual.js'); + fs.readdirSync(fixturesDir).map(fixtureName => { + const fixtureDir = path.join(fixturesDir, fixtureName); + if (!fs.statSync(fixtureDir).isDirectory()) { + return; + } + it(fixtureName.split('-').join(' '), () => { + const actualPath = path.join(fixtureDir, 'actual.js'); const actual = transformFileSync(actualPath).code; - - if (path.sep === '\\') { - // Specific case of windows, transformFileSync return code with '/' - actualPath = actualPath.replace(/\\/g, '/'); - } - + const templatePath = path.sep === '\\' ? + actualPath.replace(/\\/g, '/') : + actualPath; const expected = fs.readFileSync( path.join(fixtureDir, 'expected.js') - ).toString().replace(/%FIXTURE_PATH%/g, actualPath); + ).toString().replace('__FILENAME__', JSON.stringify(templatePath)); expect(trim(actual)).toEqual(trim(expected)); }); }); From a26dbc98cd167bbcfcca0b749b779dbfe61d1ae5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 05:52:06 +0100 Subject: [PATCH 019/161] 3.0.0-alpha.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6d57a7e8..50f1f4b6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.1", + "version": "3.0.0-alpha.2", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From bb8db7b593ebaf66ab62dd542eeb03939ad89454 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 05:53:30 +0100 Subject: [PATCH 020/161] Add missing file --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 50f1f4b6e..36cf59791 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "files": [ "lib", "babel.js", + "patch.js", "webpack.js" ], "scripts": { From 273a42453db366a715a39b0273ba46cf3dbc8e14 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 05:53:46 +0100 Subject: [PATCH 021/161] 3.0.0-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36cf59791..3df475546 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.2", + "version": "3.0.0-alpha.3", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 47042fbb8529c826fe90940d2f093b3a97f444a5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 11:35:54 +0100 Subject: [PATCH 022/161] Add a few friendly errors --- src/AppContainer.dev.js | 39 +++++++++++++++++++++------------------ src/index.js | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 3db706f97..3454538cf 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -3,25 +3,14 @@ const deepForceUpdate = require('react-deep-force-update'); const Redbox = require('redbox-react'); const { Component } = React; -function ensureCreateElementIsPatched() { - const A = () => {}; - A.__source = { fileName: 'fake', localName: 'fake' } - const B = () => {}; - B.__source = { fileName: 'fake', localName: 'fake' } - - if (
.type !== .type) { - console.error( - 'React Hot Loader: It appears that you did not run ' + - '"react-hot-loader/patch". Make sure that this module ' + - 'runs before any other code. For example, if you use Webpack, ' + - 'you can add "react-hot-loader/patch" as the very first item to the ' + - '"entry" array in its config. Alternatively, you can add ' + - 'require("react-hot-loader/patch") as the very first line ' + - 'in the application code, before any other imports.' - ); - } +let wasCreateElementPatched = false; +const A = () => {}; +A.__source = { fileName: 'fake', localName: 'fake' } +const B = () => {}; +B.__source = { fileName: 'fake', localName: 'fake' } +if (.type === .type) { + wasCreateElementPatched = true; } -ensureCreateElementIsPatched(); class AppContainer extends Component { constructor(props) { @@ -29,6 +18,20 @@ class AppContainer extends Component { this.state = { error: null }; } + componentDidMount() { + if (!wasCreateElementPatched) { + console.error( + 'React Hot Loader: It appears that "react-hot-loader/patch" ' + + 'did not run immediately before the app started. Make sure that it ' + + 'runs before any other code. For example, if you use Webpack, ' + + 'you can add "react-hot-loader/patch" as the very first item to the ' + + '"entry" array in its config. Alternatively, you can add ' + + 'require("react-hot-loader/patch") as the very first line ' + + 'in the application code, before any other imports.' + ); + } + } + componentWillReceiveProps(nextProps) { if (nextProps.component !== this.props.component) { this.setState({ diff --git a/src/index.js b/src/index.js index a79a47592..7e7dcbe32 100644 --- a/src/index.js +++ b/src/index.js @@ -1,2 +1,24 @@ const AppContainer = require('./AppContainer'); + +module.exports = function warnAboutIncorrectUsage() { + if (this && this.callback) { + throw new Error( + 'React Hot Loader is now more than a Webpack loader. ' + + 'Make sure that in your Webpack "loaders" configuration ' + + 'you use "react-hot-loader/webpack" rather than just ' + + '"react-hot-loader". Alternatively, if you already use Babel, ' + + 'you may remove it from "loaders" completely, and instead add ' + + '"react-hot-loader/babel" to "plugins" in your .babelrc.' + ); + } else { + throw new Error( + 'React Hot Loader does not have a default export. ' + + 'If you use the import statement, make sure to include ' + + 'curly braces: import { AppContainer } from "react-hot-loader". ' + + 'If you use CommonJS, make sure to read the named export: ' + + 'require("react-hot-loader").AppContainer.' + ); + } +} + module.exports.AppContainer = AppContainer; From b05646e8ba0b973c31566a975d699eb59b8d44b7 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 11:36:09 +0100 Subject: [PATCH 023/161] 3.0.0-alpha.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3df475546..1eb696917 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.3", + "version": "3.0.0-alpha.4", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From a1ac86cf102026eaffccc0c000f3d4a92c360c82 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 12:22:33 +0100 Subject: [PATCH 024/161] Better error messages --- src/babel/index.js | 15 ++++++++++++++- src/index.js | 25 ++++++++++++++++++------- src/webpack/index.js | 12 ++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index 99d8ef456..2313c1f31 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -29,7 +29,20 @@ const buildTagger = template(` })(); `); -export default function({ types: t, template }) { +module.exports = function(args) { + if (this && this.callback) { + throw new Error( + 'React Hot Loader: You are erroneously trying to use a Babel plugin ' + + 'as a Webpack loader. Replace "react-hot-loader/babel" with ' + + '"react-hot-loader/webpack" in the "loaders" section of your Webpack ' + + 'configuration. Alternatively, if you use Babel, we recommend that you ' + + 'remove "react-hot-loader/babel" from the "loaders" section ' + + 'altogether, and instead add "react-hot-loader/babel" to the "plugins" ' + + 'section of your .babelrc file.' + ); + } + const { types: t } = args; + function shouldRegisterBinding(binding) { let { type, node } = binding.path; switch (type) { diff --git a/src/index.js b/src/index.js index 7e7dcbe32..52d8753cd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,30 @@ const AppContainer = require('./AppContainer'); -module.exports = function warnAboutIncorrectUsage() { +module.exports = function warnAboutIncorrectUsage(arg) { if (this && this.callback) { throw new Error( 'React Hot Loader is now more than a Webpack loader. ' + - 'Make sure that in your Webpack "loaders" configuration ' + - 'you use "react-hot-loader/webpack" rather than just ' + - '"react-hot-loader". Alternatively, if you already use Babel, ' + - 'you may remove it from "loaders" completely, and instead add ' + - '"react-hot-loader/babel" to "plugins" in your .babelrc.' + 'Replace "react-hot-loader" or "react-hot" with "react-hot-loader/webpack" ' + + 'in the "loaders" section of your Webpack configuration. ' + + 'Alternatively, if you use Babel, we recommend that you remove ' + + '"react-hot-loader" from the "loaders" section altogether, and ' + + 'instead add "react-hot-loader/babel" to the "plugins" section of ' + + 'your .babelrc file.' + ); + } else if (arg && arg.types && arg.types.IfStatement) { + throw new Error( + 'React Hot Loader is more than a Babel plugin. ' + + 'Replace "react-hot-loader" with "react-hot-loader/babel" ' + + 'in the "plugins" section of your .babelrc file. ' + + 'Alternatively, if you’d rather not use Babel for some reason, ' + + 'you may remove "react-hot-loader" from the "plugins" section ' + + 'altogether, and instead add "react-hot-loader/webpack" to the ' + + '"loaders" section of your Webpack configuration.' ); } else { throw new Error( 'React Hot Loader does not have a default export. ' + - 'If you use the import statement, make sure to include ' + + 'If you use the import statement, make sure to include the ' + 'curly braces: import { AppContainer } from "react-hot-loader". ' + 'If you use CommonJS, make sure to read the named export: ' + 'require("react-hot-loader").AppContainer.' diff --git a/src/webpack/index.js b/src/webpack/index.js index 0f0f861e2..8865bbe57 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -8,6 +8,18 @@ const makeIdentitySourceMap = require('./makeIdentitySourceMap'); let tagCommonJSExportsSource = null; function transform(source, map) { + if (source && source.types && source.types.IfStatement) { + throw new Error( + 'React Hot Loader: You are erroneously trying to use a Webpack loader ' + + 'as a Babel plugin. Replace "react-hot-loader/webpack" with ' + + '"react-hot-loader/babel" in the "plugins" section of your .babelrc file. ' + + 'Alternatively, if you’d rather not use Babel for some reason, ' + + 'you may remove "react-hot-loader/webpack" from the "plugins" section in ' + + 'your .babelrc file altogether, and instead add "react-hot-loader/webpack" ' + + 'to the "loaders" section in your Webpack configuration.' + ); + } + if (this.cacheable) { this.cacheable(); } From 16b26f6d89e684b3c31318282f7ba6143c703022 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 12:22:51 +0100 Subject: [PATCH 025/161] 3.0.0-alpha.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1eb696917..9a7e98ab0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From ea3288b349258c253d76eb4b992f413c2a62e3e9 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 12:31:54 +0100 Subject: [PATCH 026/161] Disable in production --- src/babel/index.js | 4 +++ src/webpack/tagCommonJSExports.js | 48 ++++++++++++++++--------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index 2313c1f31..cb0f2fe9a 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -43,6 +43,10 @@ module.exports = function(args) { } const { types: t } = args; + if (process.env.NODE_ENV === 'production') { + return { visitor: {} }; + } + function shouldRegisterBinding(binding) { let { type, node } = binding.path; switch (type) { diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index b7445d70e..8367b4e56 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,32 +1,34 @@ (function () { /* react-hot-loader/webpack */ - function tagSource(fn, localName) { - if (typeof fn !== 'function') { - return; + if (process.env.NODE_ENV !== 'production') { + function tagSource(fn, localName) { + if (typeof fn !== 'function') { + return; + } + if (fn.hasOwnProperty('__source')) { + return; + } + try { + Object.defineProperty(fn, '__source', { + enumerable: false, + configurable: true, + value: { + fileName: __FILENAME__, + localName: localName + } + }); + } catch (err) { } } - if (fn.hasOwnProperty('__source')) { + + if (typeof module.exports === 'function') { + tagSource(module.exports, 'module.exports'); return; } - try { - Object.defineProperty(fn, '__source', { - enumerable: false, - configurable: true, - value: { - fileName: __FILENAME__, - localName: localName - } - }); - } catch (err) { } - } - - if (typeof module.exports === 'function') { - tagSource(module.exports, 'module.exports'); - return; - } - for (let key in module.exports) { - if (Object.prototype.hasOwnProperty.call(module.exports, key)) { - tagSource(module.exports[key], `module.exports.${key}`); + for (let key in module.exports) { + if (Object.prototype.hasOwnProperty.call(module.exports, key)) { + tagSource(module.exports[key], `module.exports.${key}`); + } } } })(); \ No newline at end of file From 2509a5e110e3918b08a7beb8ab6fca277dd5d216 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 12:32:10 +0100 Subject: [PATCH 027/161] 3.0.0-alpha.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a7e98ab0..4205c3e68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From f32de87cbab895e92fb5240ca279c12ea0a2f700 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 13:01:47 +0100 Subject: [PATCH 028/161] Tweak errors again --- src/babel/index.js | 10 +++++----- src/index.js | 26 ++++++++++++++------------ src/webpack/index.js | 6 +++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index cb0f2fe9a..e3d45faf0 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -33,12 +33,12 @@ module.exports = function(args) { if (this && this.callback) { throw new Error( 'React Hot Loader: You are erroneously trying to use a Babel plugin ' + - 'as a Webpack loader. Replace "react-hot-loader/babel" with ' + - '"react-hot-loader/webpack" in the "loaders" section of your Webpack ' + - 'configuration. Alternatively, if you use Babel, we recommend that you ' + + 'as a Webpack loader. We recommend that you use Babel, ' + 'remove "react-hot-loader/babel" from the "loaders" section ' + - 'altogether, and instead add "react-hot-loader/babel" to the "plugins" ' + - 'section of your .babelrc file.' + 'of your Webpack configuration, and instead add ' + + '"react-hot-loader/babel" to the "plugins" section of your .babelrc file. ' + + 'If you prefer not to use Babel, replace "react-hot-loader/babel" with ' + + '"react-hot-loader/webpack" in the "loaders" section of your Webpack configuration. ' ); } const { types: t } = args; diff --git a/src/index.js b/src/index.js index 52d8753cd..cf3913a07 100644 --- a/src/index.js +++ b/src/index.js @@ -3,23 +3,25 @@ const AppContainer = require('./AppContainer'); module.exports = function warnAboutIncorrectUsage(arg) { if (this && this.callback) { throw new Error( - 'React Hot Loader is now more than a Webpack loader. ' + - 'Replace "react-hot-loader" or "react-hot" with "react-hot-loader/webpack" ' + - 'in the "loaders" section of your Webpack configuration. ' + - 'Alternatively, if you use Babel, we recommend that you remove ' + - '"react-hot-loader" from the "loaders" section altogether, and ' + - 'instead add "react-hot-loader/babel" to the "plugins" section of ' + - 'your .babelrc file.' + 'React Hot Loader: The Webpack loader is now exported separately. ' + + 'If you use Babel, we recommend that you remove "react-hot-loader" ' + + 'from the "loaders" section of your Webpack configuration altogether, ' + + 'and instead add "react-hot-loader/babel" to the "plugins" section ' + + 'of your .babelrc file. ' + + 'If you prefer not to use Babel, replace "react-hot-loader" or ' + + '"react-hot" with "react-hot-loader/webpack" in the "loaders" section ' + + 'of your Webpack configuration.' ); } else if (arg && arg.types && arg.types.IfStatement) { throw new Error( - 'React Hot Loader is more than a Babel plugin. ' + + 'React Hot Loader: The Babel plugin is exported separately. ' + 'Replace "react-hot-loader" with "react-hot-loader/babel" ' + 'in the "plugins" section of your .babelrc file. ' + - 'Alternatively, if you’d rather not use Babel for some reason, ' + - 'you may remove "react-hot-loader" from the "plugins" section ' + - 'altogether, and instead add "react-hot-loader/webpack" to the ' + - '"loaders" section of your Webpack configuration.' + 'While we recommend the above, if you prefer not to use Babel, ' + + 'you may remove "react-hot-loader" from the "plugins" section of ' + + 'your .babelrc file altogether, and instead add ' + + '"react-hot-loader/webpack" to the "loaders" section of your Webpack ' + + 'configuration.' ); } else { throw new Error( diff --git a/src/webpack/index.js b/src/webpack/index.js index 8865bbe57..64c82a882 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -13,10 +13,10 @@ function transform(source, map) { 'React Hot Loader: You are erroneously trying to use a Webpack loader ' + 'as a Babel plugin. Replace "react-hot-loader/webpack" with ' + '"react-hot-loader/babel" in the "plugins" section of your .babelrc file. ' + - 'Alternatively, if you’d rather not use Babel for some reason, ' + - 'you may remove "react-hot-loader/webpack" from the "plugins" section in ' + + 'While we recommend the above, if you prefer not to use Babel, ' + + 'you may remove "react-hot-loader/webpack" from the "plugins" section of ' + 'your .babelrc file altogether, and instead add "react-hot-loader/webpack" ' + - 'to the "loaders" section in your Webpack configuration.' + 'to the "loaders" section of your Webpack configuration.' ); } From 29085b3aa4373f715e36302a5e2bd0b3a64d7e0e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 13:02:07 +0100 Subject: [PATCH 029/161] 3.0.0-alpha.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4205c3e68..48f04e591 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.6", + "version": "3.0.0-alpha.7", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 520dc193accf7be2989ac3b244bd933fe218c9ac Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 13:11:15 +0100 Subject: [PATCH 030/161] I heard you like props --- src/AppContainer.dev.js | 2 +- src/AppContainer.prod.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 3454538cf..39064ae1e 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -57,7 +57,7 @@ class AppContainer extends Component { if (error) { return ; } else { - return ; + return ; } } } diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js index a8949656d..ae628782a 100644 --- a/src/AppContainer.prod.js +++ b/src/AppContainer.prod.js @@ -3,7 +3,7 @@ const { Component } = React; class AppContainer extends Component { render() { - return ; + return ; } } From 13c1a5a4c5e3063e420761c1b1bd05cd07f7734d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 13:11:25 +0100 Subject: [PATCH 031/161] 3.0.0-alpha.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48f04e591..7bb6e9e09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.7", + "version": "3.0.0-alpha.8", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 68b9f02ae8cfdae4ca32a29b7b6e97fdae81362e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 16:15:20 +0100 Subject: [PATCH 032/161] Style tweak --- test/babel/fixtures/bindings/.babelrc | 4 +--- test/babel/fixtures/counter/.babelrc | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/babel/fixtures/bindings/.babelrc b/test/babel/fixtures/bindings/.babelrc index 828611846..ae9707bdc 100644 --- a/test/babel/fixtures/bindings/.babelrc +++ b/test/babel/fixtures/bindings/.babelrc @@ -1,5 +1,3 @@ { - "plugins": [ - ["../../../../src/babel"] - ] + "plugins": ["../../../../src/babel"] } diff --git a/test/babel/fixtures/counter/.babelrc b/test/babel/fixtures/counter/.babelrc index cf052fd63..c29348a52 100644 --- a/test/babel/fixtures/counter/.babelrc +++ b/test/babel/fixtures/counter/.babelrc @@ -1,6 +1,4 @@ { "presets": ["es2015", "react"], - "plugins": [ - ["../../../../src/babel"] - ] + "plugins": ["../../../../src/babel"] } From 8bdeaa0748f31a199bb47481646b1fdbc2cba106 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 16:59:55 +0100 Subject: [PATCH 033/161] Work around https://phabricator.babeljs.io/T7298 Fixes #246 --- src/babel/index.js | 31 +++++++++--------- test/babel/fixtures/issue-246/.babelrc | 4 +++ test/babel/fixtures/issue-246/actual.js | 3 ++ test/babel/fixtures/issue-246/expected.js | 38 +++++++++++++++++++++++ 4 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 test/babel/fixtures/issue-246/.babelrc create mode 100644 test/babel/fixtures/issue-246/actual.js create mode 100644 test/babel/fixtures/issue-246/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index e3d45faf0..76e0418d1 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -65,7 +65,7 @@ module.exports = function(args) { } } - const DEFAULT_EXPORT = Symbol(); + const REGISTRATIONS = Symbol(); return { visitor: { ExportDefaultDeclaration(path) { @@ -84,34 +84,31 @@ module.exports = function(args) { ]) ) path.node.declaration = id; - path.parent[DEFAULT_EXPORT] = id; + path.parent[REGISTRATIONS].push( + buildRegistration({ + ID: id, + NAME: t.stringLiteral('default') + }) + ); }, Program: { - enter({ node }) { - node[DEFAULT_EXPORT] = null; - }, - - exit({ node, scope }, { file }) { - let registrations = []; - + enter({ node, scope }) { + node[REGISTRATIONS] = []; for (let id in scope.bindings) { const binding = scope.bindings[id]; if (shouldRegisterBinding(binding)) { - registrations.push(buildRegistration({ + node[REGISTRATIONS].push(buildRegistration({ ID: binding.identifier, NAME: t.stringLiteral(id) })); } } + }, - if (node[DEFAULT_EXPORT]) { - registrations.push(buildRegistration({ - ID: node[DEFAULT_EXPORT], - NAME: t.stringLiteral('default') - })); - } - + exit({ node, scope }, { file }) { + let registrations = node[REGISTRATIONS]; + node[REGISTRATIONS] = null; node.body.push( buildTagger({ FILENAME: t.stringLiteral(file.opts.filename), diff --git a/test/babel/fixtures/issue-246/.babelrc b/test/babel/fixtures/issue-246/.babelrc new file mode 100644 index 000000000..d2550c6d6 --- /dev/null +++ b/test/babel/fixtures/issue-246/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015"], + "plugins": ["../../../../src/babel"] +} \ No newline at end of file diff --git a/test/babel/fixtures/issue-246/actual.js b/test/babel/fixtures/issue-246/actual.js new file mode 100644 index 000000000..39cfd1dac --- /dev/null +++ b/test/babel/fixtures/issue-246/actual.js @@ -0,0 +1,3 @@ +export function spread(...args) { + return args.push(1) +} \ No newline at end of file diff --git a/test/babel/fixtures/issue-246/expected.js b/test/babel/fixtures/issue-246/expected.js new file mode 100644 index 000000000..dcd5d93f0 --- /dev/null +++ b/test/babel/fixtures/issue-246/expected.js @@ -0,0 +1,38 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.spread = spread; +function spread() { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + return args.push(1); +} + +(function () { + function tagSource(fn, localName) { + if (typeof fn !== "function") { + return; + } + + if (fn.hasOwnProperty("__source")) { + return; + } + + try { + Object.defineProperty(fn, "__source", { + enumerable: false, + configurable: true, + value: { + fileName: __FILENAME__, + localName: localName + } + }); + } catch (err) {} + } + + tagSource(spread, "spread"); +})(); \ No newline at end of file From e6e74a3a2cb33c656128027f412c8727722ba822 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 17:00:19 +0100 Subject: [PATCH 034/161] 3.0.0-alpha.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7bb6e9e09..b1d497bcd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.8", + "version": "3.0.0-alpha.9", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 4acd85cb1d682dec1bda4053954b208539340a80 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 17:35:00 +0100 Subject: [PATCH 035/161] Warn and bail out when __source appears out of the blue Fixes #241 --- src/patch.dev.js | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index 7105260c3..c34ea8890 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -2,25 +2,50 @@ const React = require('react'); const createProxy = require('react-proxy').default; let proxies = {}; +let warnedAboutTypes = {}; function resolveType(type) { - if (!type) { + if (!type || typeof type === 'string') { return type; } - if (!Object.hasOwnProperty.call(type, '__source')) { + + if ( + !Object.hasOwnProperty.call(type, '__source') || + !type.__source || + !type.__source.fileName || + !type.__source.localName + ) { + try { + Object.defineProperty(type, '__noSourceFound', { + configurable: true, + enumerable: false, + value: true + }); + } catch (err) {} return type; } + const source = type.__source; - if (!source || !source.fileName || !source.localName) { + const id = source.fileName + '#' + source.localName; + + if (type.hasOwnProperty('__noSourceFound')) { + if (!warnedAboutTypes[id]) { + warnedAboutTypes[id] = true; + console.error( + `React Hot Loader: ${source.localName} from ${source.fileName} will not ` + + `hot reload correctly because it contains an imperative call like ` + + `ReactDOM.render() in the same file. Split ${source.localName} into a ` + + `separate file for hot reloading to work.` + ); + } return type; } - const fairlyUniqueID = source.fileName + '#' + source.localName; - if (!proxies[fairlyUniqueID]) { - proxies[fairlyUniqueID] = createProxy(type); + if (!proxies[id]) { + proxies[id] = createProxy(type); } else { - proxies[fairlyUniqueID].update(type); + proxies[id].update(type); } - return proxies[fairlyUniqueID].get(); + return proxies[id].get(); } if (React.createElement.isPatchedByReactHotLoader) { From 3c1238f4e21be9d5ed1750658143d1504920057b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 17:35:27 +0100 Subject: [PATCH 036/161] 3.0.0-alpha.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1d497bcd..100549a11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.9", + "version": "3.0.0-alpha.10", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 48b2382dfd6997d65b295994c7ad0ef714fddb5a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 18:40:56 +0100 Subject: [PATCH 037/161] Don't update proxy to the same type twice Fixes #248 --- src/patch.dev.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index c34ea8890..69931a84c 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,6 +1,16 @@ const React = require('react'); const createProxy = require('react-proxy').default; +function setFlag(obj, key) { + try { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + value: true + }); + } catch (err) {} +} + let proxies = {}; let warnedAboutTypes = {}; function resolveType(type) { @@ -14,13 +24,7 @@ function resolveType(type) { !type.__source.fileName || !type.__source.localName ) { - try { - Object.defineProperty(type, '__noSourceFound', { - configurable: true, - enumerable: false, - value: true - }); - } catch (err) {} + setFlag(type, '__noSourceFound'); return type; } @@ -42,9 +46,11 @@ function resolveType(type) { if (!proxies[id]) { proxies[id] = createProxy(type); - } else { + } else if (!type.hasOwnProperty('__hasBeenUsedForProxy')) { proxies[id].update(type); } + setFlag(type, '__hasBeenUsedForProxy'); + return proxies[id].get(); } From ce34ca075b43ca9fc4ddee2fabec995424b69c2d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 18:41:19 +0100 Subject: [PATCH 038/161] 3.0.0-alpha.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 100549a11..bd8d79d3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.10", + "version": "3.0.0-alpha.11", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 07cf73745bbfa8562768bf33abb7bdaf791c40dd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 18 Apr 2016 23:58:32 +0100 Subject: [PATCH 039/161] Add comments --- src/AppContainer.dev.js | 12 ++++++++++++ src/babel/index.js | 19 ++++++++++++++++++- src/patch.dev.js | 15 +++++++++++++++ src/webpack/index.js | 3 +++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 39064ae1e..b91eaf90a 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -3,6 +3,9 @@ const deepForceUpdate = require('react-deep-force-update'); const Redbox = require('redbox-react'); const { Component } = React; +// Feature check for the createElement() patch. +// If createElement() was patched, types with +// the same __source will resolve to the same type. let wasCreateElementPatched = false; const A = () => {}; A.__source = { fileName: 'fake', localName: 'fake' } @@ -33,6 +36,8 @@ class AppContainer extends Component { } componentWillReceiveProps(nextProps) { + // Hot reload is happening. + // Retry rendering! if (nextProps.component !== this.props.component) { this.setState({ error: null @@ -41,11 +46,18 @@ class AppContainer extends Component { } componentDidUpdate(prevProps) { + // Hot reload has finished. + // Force-update the whole tree, including + // components that refuse to update. if (prevProps.component !== this.props.component) { deepForceUpdate(this); } } + // This hook is going to become official in React 15.x. + // In 15.0, it only catches errors on initial mount. + // Later it will work for updates as well: + // https://github.com/facebook/react/pull/6020 unstable_handleError(error) { this.setState({ error: error diff --git a/src/babel/index.js b/src/babel/index.js index 76e0418d1..d0dc54896 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -30,6 +30,7 @@ const buildTagger = template(` `); module.exports = function(args) { + // This is a Babel plugin, but the user put it in the Webpack config. if (this && this.callback) { throw new Error( 'React Hot Loader: You are erroneously trying to use a Babel plugin ' + @@ -43,10 +44,14 @@ module.exports = function(args) { } const { types: t } = args; + // No-op in production. if (process.env.NODE_ENV === 'production') { return { visitor: {} }; } + // Gather top-level variables, functions, and classes. + // Try our best to avoid variables from require(). + // Ideally we only want to find components defined by the user. function shouldRegisterBinding(binding) { let { type, node } = binding.path; switch (type) { @@ -69,21 +74,27 @@ module.exports = function(args) { return { visitor: { ExportDefaultDeclaration(path) { + // Default exports with names are going + // to be in scope anyway so no need to bother. if (path.node.declaration.id) { return; } + // Move export default right hand side to a variable + // so we can later refer to it and tag it with __source. const id = path.scope.generateUidIdentifier('default'); const expression = t.isExpression(path.node.declaration) ? path.node.declaration : t.toExpression(path.node.declaration); - path.insertBefore( t.variableDeclaration('const', [ t.variableDeclarator(id, expression) ]) ) path.node.declaration = id; + + // It won't appear in scope.bindings + // so we'll manually remember it exists. path.parent[REGISTRATIONS].push( buildRegistration({ ID: id, @@ -95,6 +106,9 @@ module.exports = function(args) { Program: { enter({ node, scope }) { node[REGISTRATIONS] = []; + + // Everything in the top level scope, when reasonable, + // is going to get tagged with __source. for (let id in scope.bindings) { const binding = scope.bindings[id]; if (shouldRegisterBinding(binding)) { @@ -109,6 +123,9 @@ module.exports = function(args) { exit({ node, scope }, { file }) { let registrations = node[REGISTRATIONS]; node[REGISTRATIONS] = null; + + // Inject the generated tagging code at the very end + // so that it is as minimally intrusive as possible. node.body.push( buildTagger({ FILENAME: t.stringLiteral(file.opts.filename), diff --git a/src/patch.dev.js b/src/patch.dev.js index 69931a84c..2e54471ef 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -14,10 +14,12 @@ function setFlag(obj, key) { let proxies = {}; let warnedAboutTypes = {}; function resolveType(type) { + // We only care about composite components if (!type || typeof type === 'string') { return type; } + // If the type is not tagged, return it as is. if ( !Object.hasOwnProperty.call(type, '__source') || !type.__source || @@ -28,10 +30,16 @@ function resolveType(type) { return type; } + // Uniquely identifiy the component in code across reloads. const source = type.__source; const id = source.fileName + '#' + source.localName; if (type.hasOwnProperty('__noSourceFound')) { + // This component didn't have a source last time, but now it has? + // This means createElement() was called during module definition. + // Bail out, or the component will be unmounted unexpectedly this time, + // as we'll return proxy but we returned the original class the last time. + // https://github.com/gaearon/react-hot-loader/issues/241 if (!warnedAboutTypes[id]) { warnedAboutTypes[id] = true; console.error( @@ -44,13 +52,20 @@ function resolveType(type) { return type; } + // We use React Proxy to generate classes that behave almost + // the same way as the original classes but are updatable with + // new versions without destroying original instances. if (!proxies[id]) { proxies[id] = createProxy(type); } else if (!type.hasOwnProperty('__hasBeenUsedForProxy')) { proxies[id].update(type); } + // Don't update proxy with the same class. + // This makes sure stale old classes never revive. + // https://github.com/gaearon/react-hot-loader/issues/248 setFlag(type, '__hasBeenUsedForProxy'); + // Give proxy class to React instead of the real class. return proxies[id].get(); } diff --git a/src/webpack/index.js b/src/webpack/index.js index 64c82a882..8fd197d73 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -8,6 +8,7 @@ const makeIdentitySourceMap = require('./makeIdentitySourceMap'); let tagCommonJSExportsSource = null; function transform(source, map) { + // This is a Webpack loader, but the user put it in the Babel config. if (source && source.types && source.types.IfStatement) { throw new Error( 'React Hot Loader: You are erroneously trying to use a Webpack loader ' + @@ -24,6 +25,7 @@ function transform(source, map) { this.cacheable(); } + // Read the helper once. if (!tagCommonJSExportsSource) { tagCommonJSExportsSource = fs.readFileSync( path.join(__dirname, 'tagCommonJSExports.js'), @@ -31,6 +33,7 @@ function transform(source, map) { ).split(/\n\s*/).join(' '); } + // Parameterize the helper with the current filename. const separator = '\n\n'; const appendText = tagCommonJSExportsSource.replace( '__FILENAME__', From 005c32ddbf9929980a315d30691ed6c8f7a8a8b6 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 19 Apr 2016 00:15:03 +0100 Subject: [PATCH 040/161] Add a hack for React Router --- src/patch.dev.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index 2e54471ef..fad19f69b 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -73,9 +73,35 @@ if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); } +// This is lame but let's focus on shipping. +// https://github.com/gaearon/react-hot-loader/issues/249 +function isReactRouterish(type) { + return type && ( + type.displayName === 'Route' || + type.name === 'Route' // In case Ryan and Michael embrace ES6 classes + ); +} + const createElement = React.createElement; -function patchedCreateElement(type, ...args) { - return createElement(resolveType(type), ...args); +function patchedCreateElement(type, props, ...args) { + // Ideally we want to teach React Router to receive children. + // We're not in a perfect world, and a dirty workaround works for now. + // https://github.com/reactjs/react-router/issues/2182 + if ( + isReactRouterish(type) && + props && + typeof props.component === 'function' + ) { + // Side effect 😱 + // Force proxies to update since React Router ignores new props. + resolveType(props.component); + } + + // Trick React into rendering a proxy so that + // its state is preserved when the class changes. + // This will update the proxy if it's for a known type. + const resolvedType = resolveType(type); + return createElement(resolvedType, props, ...args); } patchedCreateElement.isPatchedByReactHotLoader = true; React.createElement = patchedCreateElement; From 43de47ec570c097636a095e24ead89dc81b92282 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 19 Apr 2016 00:15:14 +0100 Subject: [PATCH 041/161] 3.0.0-alpha.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd8d79d3f..884beff24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.11", + "version": "3.0.0-alpha.12", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From deb8794951649f8b951a2bffb3e7fa824ba1ecc2 Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Tue, 19 Apr 2016 08:23:51 -0400 Subject: [PATCH 042/161] Closes #244. --- src/AppContainer.dev.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index b91eaf90a..082d50088 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -38,7 +38,7 @@ class AppContainer extends Component { componentWillReceiveProps(nextProps) { // Hot reload is happening. // Retry rendering! - if (nextProps.component !== this.props.component) { + if (nextProps.children !== this.props.children) { this.setState({ error: null }); @@ -49,7 +49,7 @@ class AppContainer extends Component { // Hot reload has finished. // Force-update the whole tree, including // components that refuse to update. - if (prevProps.component !== this.props.component) { + if (prevProps.children !== this.props.children) { deepForceUpdate(this); } } @@ -69,7 +69,7 @@ class AppContainer extends Component { if (error) { return ; } else { - return ; + return React.cloneElement(this.props.children); } } } From 003548ef5ecf0c4f57eb0aa1f673830e663473bc Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Tue, 19 Apr 2016 14:50:51 -0400 Subject: [PATCH 043/161] Cleaning up stuff per Dan's comments. --- src/AppContainer.dev.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 082d50088..dcd600f22 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -49,7 +49,7 @@ class AppContainer extends Component { // Hot reload has finished. // Force-update the whole tree, including // components that refuse to update. - if (prevProps.children !== this.props.children) { + if (prevProps.children.type !== this.props.children.type) { deepForceUpdate(this); } } @@ -69,11 +69,23 @@ class AppContainer extends Component { if (error) { return ; } else { - return React.cloneElement(this.props.children); + return React.Children.only(this.props.children); } } } +AppContainer.propTypes = { + children: function (props, propName, componentName, location, propFullName) { + if (typeof props[propName].type !== 'function') { + return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a React Component!`); + } + + if (React.Children.count(props[propName]) > 1) { + return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a single React Component!`); + } + } +}; + AppContainer.defaultProps = { errorReporter: Redbox }; From feb65490fd10af8a81416f63962cbb185d0e2822 Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Tue, 19 Apr 2016 23:42:32 -0400 Subject: [PATCH 044/161] Updated message and check type in componentWillReceiveProps. --- src/AppContainer.dev.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index dcd600f22..0496ed1d0 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -38,7 +38,7 @@ class AppContainer extends Component { componentWillReceiveProps(nextProps) { // Hot reload is happening. // Retry rendering! - if (nextProps.children !== this.props.children) { + if (nextProps.children.type !== this.props.children.type) { this.setState({ error: null }); @@ -77,7 +77,7 @@ class AppContainer extends Component { AppContainer.propTypes = { children: function (props, propName, componentName, location, propFullName) { if (typeof props[propName].type !== 'function') { - return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a React Component!`); + return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a single React element with your app’s root component, e.g. .`); } if (React.Children.count(props[propName]) > 1) { From ed61d7bdeb3184a1ec1572917ec941a06f211bdc Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Wed, 20 Apr 2016 11:42:44 -0400 Subject: [PATCH 045/161] Update AppContainer.prod --- src/AppContainer.prod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js index ae628782a..ae9a37739 100644 --- a/src/AppContainer.prod.js +++ b/src/AppContainer.prod.js @@ -3,7 +3,7 @@ const { Component } = React; class AppContainer extends Component { render() { - return ; + return React.Children.only(this.props.children); } } From 8ad91723a9de1971c3fcc5119e919b316dd6e976 Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Wed, 20 Apr 2016 12:54:20 -0400 Subject: [PATCH 046/161] Updated PropType warning. --- src/AppContainer.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 0496ed1d0..0ffdfd456 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -81,7 +81,7 @@ AppContainer.propTypes = { } if (React.Children.count(props[propName]) > 1) { - return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a single React Component!`); + return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a single React element with your app’s root component, e.g. .`); } } }; From 531f8a59d4d312a369ee15b62ab355749c1ea788 Mon Sep 17 00:00:00 2001 From: Filip Zywicki Date: Thu, 21 Apr 2016 15:16:44 +0200 Subject: [PATCH 047/161] Fix hack for React Router to handle also IndexRoute --- src/patch.dev.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index fad19f69b..76bd32e4e 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -78,7 +78,9 @@ if (React.createElement.isPatchedByReactHotLoader) { function isReactRouterish(type) { return type && ( type.displayName === 'Route' || - type.name === 'Route' // In case Ryan and Michael embrace ES6 classes + type.name === 'Route' || // In case Ryan and Michael embrace ES6 classes + type.displayName === 'IndexRoute' || + type.name === 'IndexRoute' ); } From 4e2c4ac4e29fe3465beee20277d30ccffa3e57db Mon Sep 17 00:00:00 2001 From: Harry Hedger Date: Fri, 22 Apr 2016 00:06:16 -0400 Subject: [PATCH 048/161] Removed starter kits from docs/README.md --- docs/README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/docs/README.md b/docs/README.md index 93c35db3a..23b7fdb39 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,25 +1,5 @@ ### Starter Kits - * [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate) (Bare minimum) -* [react-starter](https://github.com/webpack/react-starter) (react-router, includes production configs) -* [isomorphic-hot-loader](https://github.com/irvinebroque/isomorphic-hot-loader) (react-router, isomorphic) -* [isomorphic-react-template](https://github.com/gpbl/isomorphic-react-template/) (react-router, isomorphic) -* [coffee-react-quickstart](https://github.com/KyleAMathews/coffee-react-quickstart) (react-router, CoffeeScript, Gulp) -* [react-static-boilerplate](https://github.com/koistya/react-static-boilerplate) (static site generator; React, PostCSS, Webpack, BrowserSync) -* [boilerplate-webpack-react](https://github.com/tcoopman/boilerplate-webpack-react) (react-router, isomorphic) -* [este](http://github.com/steida/este) (react-router, isomorphic, Flux, Babel) -* [react-isomorphic-starterkit](https://github.com/RickWong/react-isomorphic-starterkit) (react-router, react-async, isomorphic) -* [yarsk](https://github.com/bradleyboy/yarsk) (Babel, Karma + Mocha, automated publishing to GitHub pages) -* [react-web](https://github.com/darul75/web-react) (Babel, react-router, Alt flux) -* [esnext-quickstart](https://github.com/nkbt/esnext-quickstart) (compile-time ESLint, ES6, Babel, Karma + Jasmine + Coverage) -* [react-webpack-boilerplate](https://github.com/srn/react-webpack-boilerplate) (One-click Heroku deployable, Node.js server) -* [flask-react-boilerplate](https://github.com/alexkuz/flask-react-boilerplate) (One-click Heroku deployable, Flask server + PostgreSQL, Babel, Flummox) -* [react-kickstart](https://github.com/vesparny/react-kickstart) (react-router, mocha + chai + istanbul) -* [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) (isomorphic, redux, client and server async data fetching, babel, react-router) -* [go-starter-kit](https://github.com/olebedev/go-starter-kit) (hot reloadable golang/react/flummox/css-module isomorphic starter kit) -* [react-fullstack-skeleton](https://github.com/fortruce/react-fullstack-skeleton) (react w/ backend api server) - -Don't be shy, add your own. ### Migrating to 1.0 From 84a1f5a82b4a676fd825196e9dedb68022233d9b Mon Sep 17 00:00:00 2001 From: Harry Hedger Date: Fri, 22 Apr 2016 00:10:42 -0400 Subject: [PATCH 049/161] Removed old documentation. --- docs/README.md | 82 -------------------------------------------------- 1 file changed, 82 deletions(-) diff --git a/docs/README.md b/docs/README.md index 23b7fdb39..77f71aba6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,88 +1,6 @@ ### Starter Kits * [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate) (Bare minimum) -### Migrating to 1.0 - -React Hot Loader has reached 1.0, and it's a breaking change. When React Hot Loader just started, it used a regex to find `createClass` calls and replace them with its own implementation. This turned out to be a bad idea for a number of reasons: - -* Doesn't work when components are created through wrappers (e.g. [OmniscientJS](http://omniscientjs.github.io)); -* Doesn't work when author calls React differently; -* Causes false positives in React source code comments and elsewhere; -* Most importantly, won't work with ES6 classes that will be future of React. - -Here's how we're solving these problems in 1.0: - -#### Only `module.exports` and its own properties are hot by default - -With 1.0, we no longer parse your sources. Instead, we only now make `module.exports` and its [own properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) hot by default, and only if their prototype declares `render` method or descends from `React.Component`. **If you've been splitting each component in a separate file, that means no change for you here!** This allows us to support exotic wrappers. - -If you use inheritance in React 0.13, base classes will only be opted into hot reloading if they descend from `React.Component` or define `render` method. Otherwise you need to explicitly call `module.makeHot` as described below. - -#### You can make hot anything else via opt-in `module.makeHot` API - -But what if you *want* to have several hot-reloadable components in one file? Or what if you want to export a function creating components, or an object with several components as properties? For that, 1.0 **adds first public API to hot loader: `module.makeHot`**. This method will be present on `module` object if hot loader is enabled, and allows you to make any component hot: - -```js -var Something = React.createClass({ - ... -}; - -if (module.makeHot) { // Won't be true in production - Something = module.makeHot(Something); -} -``` - -Explicit API can also be used inside functions: - -```js -function generateClass(param) { - var Class = return React.createClass({ - ... - }; - - if (module.makeHot) { - Class = module.makeHot(Class, param); - } - - return Class; -} - -``` - -Note the second parameter: `makeHot` needs some way to distinguish components of same type inside on module. By default, it uses `displayName` of given component class, but in case of dynamically generated classes (or if you're not using JSX), you have to provide it yourself. - -### Manual mode (experimental) - -You can now use `react-hot?manual` instead of `react-hot` in Webpack config to turn on manual mode. In manual mode, “accepting” hot updates is up to you; modules won't accept themselves automatically. This can be used, for example, to put reloading logic on very top of the application and [hot-reload routes as well as components](https://github.com/rackt/react-router/pull/606#issuecomment-66936975). It will also work better when you have a lot of modules that export component-generating functions because updates will propagate to the top. (Don't worry if you don't understand this; it's just something experimental you might want to try to integrate hot reloading deeper into your app.) - -### Usage with external React - -If you're using external standalone React bundle instead of NPM package, Hot Loader will fail because it relies on `react/lib/ReactMount` which is not exposed in precompiled React. It needs `ReactMount` to keep track of mounted React component instances on the page. However, you can supply your own root instance provider: - -```js -// Your app's index.js - -var React = require('react'), - router = require('./router'); - -var rootInstance = null; - -router.run(function (Handler, state) { - rootInstance = React.render(, document.body); -}); - -if (module.hot) { - require('react-hot-loader/Injection').RootInstanceProvider.injectProvider({ - getRootInstances: function () { - // Help React Hot Loader figure out the root component instances on the page: - return [rootInstance]; - } - }); -} -``` - -You'll only need this if you [use a precompiled version of React](https://github.com/gaearon/react-hot-loader/issues/53). If you use React NPM package, this is not necessary. You should generally use React NPM package unless you have good reason not to. - ### Source Maps If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. From 5d551eb0e5c3330f0ae5ef74db22c5da7260d5fb Mon Sep 17 00:00:00 2001 From: Harry Hedger Date: Fri, 22 Apr 2016 01:20:21 -0400 Subject: [PATCH 050/161] Migration guide. Announcement on main README --- README.md | 20 ++++++++++----- docs/README.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6be5ccede..750c89a3e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ # React Hot Loader [![npm package](https://img.shields.io/npm/v/react-hot-loader.svg?style=flat-square)](https://www.npmjs.org/package/react-hot-loader) +**React Hot Loader 3 has arrived.** + +It fixes some long-standing issues with both React Hot Loader and React Transform. **It is intended as a replacement for both.** + +Some nice things about it: + +* Editing functional components preserves state +* Works great with higher order components +* Requires little configuration +* Automatically disabled in production +* Works with or without Babel (you can remove `react-hot-loader/babel` from `.babelrc` and instead add `react-hot-loader/webpack` to `loaders`) + This is a **stable for daily use in development** implementation of [React live code editing](http://www.youtube.com/watch?v=pw4fKkyPPg8). * Get inspired by a **[demo video](https://vimeo.com/100010922)** and **[try the live demo](http://gaearon.github.io/react-hot-loader/)**. -* Read **[the integration walkthrough](http://gaearon.github.io/react-hot-loader/getstarted/).** +* Read **[the Getting Started guide](http://gaearon.github.io/react-hot-loader/getstarted/).** * Use **[one of the starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)** for your next React project. @@ -27,14 +39,10 @@ To use React Hot Loader in an existing project, you need to * enable Hot Module Replacement, which is a Webpack feature; * configure Webpack to use React Hot Loader for JS or JSX files. -These steps are covered by **[the walkthrough](http://gaearon.github.io/react-hot-loader/getstarted/)**. +These steps are covered by **[the Getting Started guide](http://gaearon.github.io/react-hot-loader/getstarted/)**. If you'd rather stay with **Browserify**, check out **[LiveReactload](https://github.com/milankinen/livereactload)** by Matti Lankinen. -## Flux - -**[Redux](https://github.com/gaearon/redux)** is a Flux implementation that supports hot reloading of everything out of the box. Read **[The Evolution of Flux Frameworks](https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31)** for some context around its creation. - ## React Native You can use React Hot Loader to tweak a React Native application. Check out **[react-native-webpack-server](https://github.com/mjohnston/react-native-webpack-server)** by Michael Johnston. diff --git a/docs/README.md b/docs/README.md index 77f71aba6..a3abe338a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,68 @@ -### Starter Kits +### Starter Kit * [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate) (Bare minimum) +### Migration to 3.0 +- If you're using Babel and ES6, remove the `react-hot` loader from any loaders in your Webpack config, and add `react-hot-loader/babel` to the `plugins` section of your `.babelrc`: + +```js +{ + "presets": ["es2015-loose", "stage-0", "react"], + "plugins": ["react-hot-loader/babel"] +} +``` + +- If you're *not* using Babel, or you're using Babel without ES6, replace the `react-hot` loader in your Webpack config with `react-hot-loader/webpack`: + +```js +{ + test: /\.js$/, + loaders: ['react-hot', 'babel'], + include: path.join(__dirname, '..', '..', 'src') +} + +// becomes +{ + test: /\.js$/, + loaders: ['react-hot-loader/webpack', 'babel'], + include: path.join(__dirname, '..', '..', 'src') +} +``` + +- 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any code runs before `react-hot-loader/patch` has, so put it in the first position. + +- `` - AppContainer is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. + +- React Hot Loader 3 does not hide the hot module replacement API, so the following needs to be added below wherever you call `ReactDOM.render` in your app: + +```js +import React from 'react' +import ReactDOM from 'react-dom' +import { AppContainer } from 'react-hot-loader' +import App from './containers/App' + +ReactDOM.render( + + + , + document.getElementById('root') +); + +// Hot Module Replacement API +if (module.hot) { + module.hot.accept('./containers/App', () => { + render( + + + + />, + document.getElementById('root') + ); + }); +} +``` + +You can also check out [this commit for the migration of a TodoMVC app from 1.0 to 3.0.](https://github.com/gaearon/redux-devtools/commit/64f58b7010a1b2a71ad16716eb37ac1031f93915) + ### Source Maps If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. @@ -8,7 +70,3 @@ If you use `devtool: 'source-map'` (or its equivalent), source maps will be emit Source maps slow down your project. Use `devtool: 'eval'` for best build performance. Hot reloading code is just one line in the beginning and one line in the end of each module so you might not need source maps at all. - -### React Hot API - -If you're authoring a build tool, you might be interested to hear that React Hot Loader brains have been extracted into runtime-agnostic [React Hot API](https://github.com/gaearon/react-hot-api). React Hot Loader just binds that API to Webpack runtime, but you can implement yours too. From eec65321fbd63a3cb3201a33fb5413d51d7fa365 Mon Sep 17 00:00:00 2001 From: Harry Hedger Date: Fri, 22 Apr 2016 01:31:40 -0400 Subject: [PATCH 051/161] Cleaned up the top of README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 750c89a3e..98adab5dd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# React Hot Loader [![npm package](https://img.shields.io/npm/v/react-hot-loader.svg?style=flat-square)](https://www.npmjs.org/package/react-hot-loader) +# React Hot Loader 3 [![npm package](https://img.shields.io/npm/v/react-hot-loader.svg?style=flat-square)](https://www.npmjs.org/package/react-hot-loader) -**React Hot Loader 3 has arrived.** +### React Hot Loader 3 has arrived! -It fixes some long-standing issues with both React Hot Loader and React Transform. **It is intended as a replacement for both.** +It fixes some long-standing issues with both React Hot Loader and React Transform. + +**It is intended as a replacement for both.** Some nice things about it: @@ -12,6 +14,10 @@ Some nice things about it: * Automatically disabled in production * Works with or without Babel (you can remove `react-hot-loader/babel` from `.babelrc` and instead add `react-hot-loader/webpack` to `loaders`) +Check out [the Migration to 3.0 guide](https://github.com/gaearon/react-hot-loader/tree/master/docs#migration-to-30) to learn how to migrate your app to 3.0. + +### Learn + This is a **stable for daily use in development** implementation of [React live code editing](http://www.youtube.com/watch?v=pw4fKkyPPg8). * Get inspired by a **[demo video](https://vimeo.com/100010922)** and **[try the live demo](http://gaearon.github.io/react-hot-loader/)**. From 9c97785645894cb2d274e52a3404edbb2231570a Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Thu, 21 Apr 2016 17:40:58 +0200 Subject: [PATCH 052/161] Modify react router hack to operate on `Router` instead of `Route` elements --- src/patch.dev.js | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index 76bd32e4e..6b8d8cf63 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -77,26 +77,39 @@ if (React.createElement.isPatchedByReactHotLoader) { // https://github.com/gaearon/react-hot-loader/issues/249 function isReactRouterish(type) { return type && ( - type.displayName === 'Route' || - type.name === 'Route' || // In case Ryan and Michael embrace ES6 classes - type.displayName === 'IndexRoute' || - type.name === 'IndexRoute' + type.displayName === 'Router' || + type.name === 'Router' // In case Ryan and Michael embrace ES6 classes ); } +function forceUpdateComponentsOfRouteAndChildRoutes(route) { + // TODO: check whether it is possible to also handle the `getComponent` case here + if (route.component && typeof route.component === 'function') { + // Side effect 😱 + // Force proxies to update since React Router ignores new props. + resolveType(route.component); + } + + // Child routes will be in `children` when defining routes using react + // elements and in `routes` or `childRoutes` when using plain routes + const childRoutes = [] + .concat(route.routes, route.childRoutes, route.children) + .filter(Boolean); + + for (const childRoute of childRoutes) { + // When using `Route` element routes the relevant objects will be under props + // and when using plain routes directly on the route + forceUpdateComponentsOfRouteAndChildRoutes(childRoute.props || childRoute); + } +} + const createElement = React.createElement; function patchedCreateElement(type, props, ...args) { // Ideally we want to teach React Router to receive children. // We're not in a perfect world, and a dirty workaround works for now. // https://github.com/reactjs/react-router/issues/2182 - if ( - isReactRouterish(type) && - props && - typeof props.component === 'function' - ) { - // Side effect 😱 - // Force proxies to update since React Router ignores new props. - resolveType(props.component); + if (isReactRouterish(type) && props) { + forceUpdateComponentsOfRouteAndChildRoutes(props); } // Trick React into rendering a proxy so that From 9abd7f44b3893d580c523ea788ef1492d687f373 Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Fri, 22 Apr 2016 23:55:27 +0200 Subject: [PATCH 053/161] Extend react router hack to handle routes with multiple components --- src/patch.dev.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index 6b8d8cf63..9f0e3fad8 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -84,12 +84,24 @@ function isReactRouterish(type) { function forceUpdateComponentsOfRouteAndChildRoutes(route) { // TODO: check whether it is possible to also handle the `getComponent` case here - if (route.component && typeof route.component === 'function') { + if (route.component && typeof(route.component) === 'function') { // Side effect 😱 // Force proxies to update since React Router ignores new props. resolveType(route.component); } + if (route.components) { + for (const key in route.components) { + if (!route.components.hasOwnProperty(key)) continue; + + const component = route.components[key]; + + if (typeof(component) === 'function') { + resolveType(component); + } + } + } + // Child routes will be in `children` when defining routes using react // elements and in `routes` or `childRoutes` when using plain routes const childRoutes = [] From e4b609e6d866daba440b23d017b07f4847477c1a Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Sun, 24 Apr 2016 22:46:34 +0200 Subject: [PATCH 054/161] Split resolveType and react router hack into separate files --- src/hackRouter.js | 51 ++++++++++++++++++++ src/patch.dev.js | 117 ++------------------------------------------- src/resolveType.js | 72 ++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 114 deletions(-) create mode 100644 src/hackRouter.js create mode 100644 src/resolveType.js diff --git a/src/hackRouter.js b/src/hackRouter.js new file mode 100644 index 000000000..db6fbe85d --- /dev/null +++ b/src/hackRouter.js @@ -0,0 +1,51 @@ +const resolveType = require('./resolveType').default; + +// This is lame but let's focus on shipping. +// https://github.com/gaearon/react-hot-loader/issues/249 +function isReactRouterish(type) { + return type && ( + type.displayName === 'Router' || + type.name === 'Router' // In case Ryan and Michael embrace ES6 classes + ); +} + +function forceUpdateComponentsOfRouteAndChildRoutes(route) { + // TODO: check whether it is possible to also handle the `getComponent` case here + if (route.component && typeof(route.component) === 'function') { + // Side effect 😱 + // Force proxies to update since React Router ignores new props. + resolveType(route.component); + } + + if (route.components) { + for (const key in route.components) { + if (!route.components.hasOwnProperty(key)) continue; + + const component = route.components[key]; + + if (typeof(component) === 'function') { + resolveType(component); + } + } + } + + // Child routes will be in `children` when defining routes using react + // elements and in `routes` or `childRoutes` when using plain routes + const childRoutes = [] + .concat(route.routes, route.childRoutes, route.children) + .filter(Boolean); + + for (const childRoute of childRoutes) { + // When using `Route` element routes the relevant objects will be under props + // and when using plain routes directly on the route + forceUpdateComponentsOfRouteAndChildRoutes(childRoute.props || childRoute); + } +} + +export default function hackRouter(type, props) { + if (isReactRouterish(type) && props) { + forceUpdateComponentsOfRouteAndChildRoutes(props); + } +} + +module.exports.default = hackRouter; diff --git a/src/patch.dev.js b/src/patch.dev.js index 9f0e3fad8..183bac822 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,128 +1,17 @@ const React = require('react'); -const createProxy = require('react-proxy').default; - -function setFlag(obj, key) { - try { - Object.defineProperty(obj, key, { - configurable: true, - enumerable: false, - value: true - }); - } catch (err) {} -} - -let proxies = {}; -let warnedAboutTypes = {}; -function resolveType(type) { - // We only care about composite components - if (!type || typeof type === 'string') { - return type; - } - - // If the type is not tagged, return it as is. - if ( - !Object.hasOwnProperty.call(type, '__source') || - !type.__source || - !type.__source.fileName || - !type.__source.localName - ) { - setFlag(type, '__noSourceFound'); - return type; - } - - // Uniquely identifiy the component in code across reloads. - const source = type.__source; - const id = source.fileName + '#' + source.localName; - - if (type.hasOwnProperty('__noSourceFound')) { - // This component didn't have a source last time, but now it has? - // This means createElement() was called during module definition. - // Bail out, or the component will be unmounted unexpectedly this time, - // as we'll return proxy but we returned the original class the last time. - // https://github.com/gaearon/react-hot-loader/issues/241 - if (!warnedAboutTypes[id]) { - warnedAboutTypes[id] = true; - console.error( - `React Hot Loader: ${source.localName} from ${source.fileName} will not ` + - `hot reload correctly because it contains an imperative call like ` + - `ReactDOM.render() in the same file. Split ${source.localName} into a ` + - `separate file for hot reloading to work.` - ); - } - return type; - } - - // We use React Proxy to generate classes that behave almost - // the same way as the original classes but are updatable with - // new versions without destroying original instances. - if (!proxies[id]) { - proxies[id] = createProxy(type); - } else if (!type.hasOwnProperty('__hasBeenUsedForProxy')) { - proxies[id].update(type); - } - // Don't update proxy with the same class. - // This makes sure stale old classes never revive. - // https://github.com/gaearon/react-hot-loader/issues/248 - setFlag(type, '__hasBeenUsedForProxy'); - - // Give proxy class to React instead of the real class. - return proxies[id].get(); -} +const resolveType = require('./resolveType').default; +const hackRouter = require('./hackRouter').default; if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); } -// This is lame but let's focus on shipping. -// https://github.com/gaearon/react-hot-loader/issues/249 -function isReactRouterish(type) { - return type && ( - type.displayName === 'Router' || - type.name === 'Router' // In case Ryan and Michael embrace ES6 classes - ); -} - -function forceUpdateComponentsOfRouteAndChildRoutes(route) { - // TODO: check whether it is possible to also handle the `getComponent` case here - if (route.component && typeof(route.component) === 'function') { - // Side effect 😱 - // Force proxies to update since React Router ignores new props. - resolveType(route.component); - } - - if (route.components) { - for (const key in route.components) { - if (!route.components.hasOwnProperty(key)) continue; - - const component = route.components[key]; - - if (typeof(component) === 'function') { - resolveType(component); - } - } - } - - // Child routes will be in `children` when defining routes using react - // elements and in `routes` or `childRoutes` when using plain routes - const childRoutes = [] - .concat(route.routes, route.childRoutes, route.children) - .filter(Boolean); - - for (const childRoute of childRoutes) { - // When using `Route` element routes the relevant objects will be under props - // and when using plain routes directly on the route - forceUpdateComponentsOfRouteAndChildRoutes(childRoute.props || childRoute); - } -} - const createElement = React.createElement; function patchedCreateElement(type, props, ...args) { // Ideally we want to teach React Router to receive children. // We're not in a perfect world, and a dirty workaround works for now. // https://github.com/reactjs/react-router/issues/2182 - if (isReactRouterish(type) && props) { - forceUpdateComponentsOfRouteAndChildRoutes(props); - } + hackRouter(type, props); // Trick React into rendering a proxy so that // its state is preserved when the class changes. diff --git a/src/resolveType.js b/src/resolveType.js new file mode 100644 index 000000000..6fb30424c --- /dev/null +++ b/src/resolveType.js @@ -0,0 +1,72 @@ +const createProxy = require('react-proxy').default; + +let proxies = {}; +let warnedAboutTypes = {}; + +function setFlag(obj, key) { + try { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + value: true + }); + } catch (err) {} +} + +function resolveType(type) { + // We only care about composite components + if (!type || typeof type === 'string') { + return type; + } + + // If the type is not tagged, return it as is. + if ( + !Object.hasOwnProperty.call(type, '__source') || + !type.__source || + !type.__source.fileName || + !type.__source.localName + ) { + setFlag(type, '__noSourceFound'); + return type; + } + + // Uniquely identifiy the component in code across reloads. + const source = type.__source; + const id = source.fileName + '#' + source.localName; + + if (type.hasOwnProperty('__noSourceFound')) { + // This component didn't have a source last time, but now it has? + // This means createElement() was called during module definition. + // Bail out, or the component will be unmounted unexpectedly this time, + // as we'll return proxy but we returned the original class the last time. + // https://github.com/gaearon/react-hot-loader/issues/241 + if (!warnedAboutTypes[id]) { + warnedAboutTypes[id] = true; + console.error( + `React Hot Loader: ${source.localName} from ${source.fileName} will not ` + + `hot reload correctly because it contains an imperative call like ` + + `ReactDOM.render() in the same file. Split ${source.localName} into a ` + + `separate file for hot reloading to work.` + ); + } + return type; + } + + // We use React Proxy to generate classes that behave almost + // the same way as the original classes but are updatable with + // new versions without destroying original instances. + if (!proxies[id]) { + proxies[id] = createProxy(type); + } else if (!type.hasOwnProperty('__hasBeenUsedForProxy')) { + proxies[id].update(type); + } + // Don't update proxy with the same class. + // This makes sure stale old classes never revive. + // https://github.com/gaearon/react-hot-loader/issues/248 + setFlag(type, '__hasBeenUsedForProxy'); + + // Give proxy class to React instead of the real class. + return proxies[id].get(); +} + +module.exports.default = resolveType; From 317b5c44c4666a7e1dcbafa14b119f8ad366af73 Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Mon, 25 Apr 2016 00:06:55 +0200 Subject: [PATCH 055/161] Refactor router hack to be more testable --- src/hackRouter.js | 14 ++++++-------- src/patch.dev.js | 6 +++--- src/resolveType.js | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/hackRouter.js b/src/hackRouter.js index db6fbe85d..a117f540a 100644 --- a/src/hackRouter.js +++ b/src/hackRouter.js @@ -1,5 +1,3 @@ -const resolveType = require('./resolveType').default; - // This is lame but let's focus on shipping. // https://github.com/gaearon/react-hot-loader/issues/249 function isReactRouterish(type) { @@ -9,12 +7,12 @@ function isReactRouterish(type) { ); } -function forceUpdateComponentsOfRouteAndChildRoutes(route) { +function forceUpdateComponentsOfRouteAndChildRoutes(route, forceUpdateFunction) { // TODO: check whether it is possible to also handle the `getComponent` case here if (route.component && typeof(route.component) === 'function') { // Side effect 😱 // Force proxies to update since React Router ignores new props. - resolveType(route.component); + forceUpdateFunction(route.component); } if (route.components) { @@ -24,7 +22,7 @@ function forceUpdateComponentsOfRouteAndChildRoutes(route) { const component = route.components[key]; if (typeof(component) === 'function') { - resolveType(component); + forceUpdateFunction(component); } } } @@ -38,13 +36,13 @@ function forceUpdateComponentsOfRouteAndChildRoutes(route) { for (const childRoute of childRoutes) { // When using `Route` element routes the relevant objects will be under props // and when using plain routes directly on the route - forceUpdateComponentsOfRouteAndChildRoutes(childRoute.props || childRoute); + forceUpdateComponentsOfRouteAndChildRoutes(childRoute.props || childRoute, forceUpdateFunction); } } -export default function hackRouter(type, props) { +export default function hackRouter(type, props, forceUpdateFunction) { if (isReactRouterish(type) && props) { - forceUpdateComponentsOfRouteAndChildRoutes(props); + forceUpdateComponentsOfRouteAndChildRoutes(props, forceUpdateFunction); } } diff --git a/src/patch.dev.js b/src/patch.dev.js index 183bac822..8ba5fbbce 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,6 +1,6 @@ const React = require('react'); -const resolveType = require('./resolveType').default; -const hackRouter = require('./hackRouter').default; +const resolveType = require('./resolveType'); +const hackRouter = require('./hackRouter'); if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); @@ -11,7 +11,7 @@ function patchedCreateElement(type, props, ...args) { // Ideally we want to teach React Router to receive children. // We're not in a perfect world, and a dirty workaround works for now. // https://github.com/reactjs/react-router/issues/2182 - hackRouter(type, props); + hackRouter(type, props, resolveType); // Trick React into rendering a proxy so that // its state is preserved when the class changes. diff --git a/src/resolveType.js b/src/resolveType.js index 6fb30424c..b9925cc83 100644 --- a/src/resolveType.js +++ b/src/resolveType.js @@ -69,4 +69,4 @@ function resolveType(type) { return proxies[id].get(); } -module.exports.default = resolveType; +module.exports = resolveType; From cdd8c6d598ae75bf657a6da5b12e10712f3056b8 Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Mon, 25 Apr 2016 01:24:44 +0200 Subject: [PATCH 056/161] Add tests for react router hack --- test/hackRouter/index.js | 126 +++++++++++++++++++++++++++++++++++++++ test/index.js | 1 + 2 files changed, 127 insertions(+) create mode 100644 test/hackRouter/index.js diff --git a/test/hackRouter/index.js b/test/hackRouter/index.js new file mode 100644 index 000000000..49924bd59 --- /dev/null +++ b/test/hackRouter/index.js @@ -0,0 +1,126 @@ +import hackRouter from '../../src/hackRouter'; +import expect, { createSpy } from 'expect'; + +const forceUpdateSpy = createSpy(); + +const Router = { + displayName: 'Router', +}; + +describe('hackRouter', () => { + beforeEach(() => { + forceUpdateSpy.reset(); + }); + + describe('when routes are passed in as children', () => { + /* Equivalent to: + + + + */ + + const props = { + children: [ + { props: { component: function a() {} } }, + { props: { component: function b() {} } }, + ], + }; + + it('calls force update on each route\'s component', () => { + hackRouter(Router, props, forceUpdateSpy); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[0].props.component); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[1].props.component); + }); + + describe('and the top component is not a router', () => { + it('does not call force update on any component', () => { + const NotRouter = { displayName: 'DefinitelyNotARouter' }; + hackRouter(NotRouter, props, forceUpdateSpy); + expect(forceUpdateSpy).toNotHaveBeenCalled(); + }); + }); + }); + + describe('when routes are passed in as children of children', () => { + /* Equivalent to: + + + + + */ + + const props = { + children: [ + { + props: { + component: function a() {}, + children: [ + { props: { component: function b() {} } }, + ], + }, + }, + ], + }; + + it('calls force update on each route\'s component', () => { + hackRouter(Router, props, forceUpdateSpy); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[0].props.component); + expect(forceUpdateSpy).toHaveBeenCalledWith( + props.children[0].props.children[0].props.component + ); + }); + }); + + describe('when routes are passed in using the `routes` prop', () => { + const props = { + routes: [ + { component: function a() {} }, + { component: function b() {} }, + ], + }; + + it('calls force update on each route\'s components', () => { + hackRouter(Router, props, forceUpdateSpy); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[1].component); + }); + }); + + describe('when routes are passed in using the `routes` and `childRoutes` props', () => { + const props = { + routes: [ + { + component: function a() {}, + childRoutes: [ + { component: function b() {} }, + ], + }, + ], + }; + + it('calls force update on each route\'s components', () => { + hackRouter(Router, props, forceUpdateSpy); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].childRoutes[0].component); + }); + }); + + describe('when there are multiple components on one route', () => { + const props = { + routes: [ + { + components: { + a: function a() {}, + b: function b() {}, + }, + }, + ], + }; + + it('calls force update on each component', () => { + hackRouter(Router, props, forceUpdateSpy); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.a); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.b); + }); + }); +}); diff --git a/test/index.js b/test/index.js index 17ba30ada..697e90e46 100644 --- a/test/index.js +++ b/test/index.js @@ -1 +1,2 @@ import './babel'; +import './hackRouter'; From 4a96dbe334fdbe564ffb9a3f21fbb59009c151e0 Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Mon, 25 Apr 2016 09:45:21 -0400 Subject: [PATCH 057/161] Add old API for backwards compatibility. --- src/AppContainer.dev.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 0ffdfd456..cecc2d5df 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -38,6 +38,12 @@ class AppContainer extends Component { componentWillReceiveProps(nextProps) { // Hot reload is happening. // Retry rendering! + if (nextProps.component !== this.props.component) { + this.setState({ + error: null + }); + } + if (nextProps.children.type !== this.props.children.type) { this.setState({ error: null @@ -49,6 +55,10 @@ class AppContainer extends Component { // Hot reload has finished. // Force-update the whole tree, including // components that refuse to update. + if (prevProps.component !== this.props.component) { + deepForceUpdate(this); + } + if (prevProps.children.type !== this.props.children.type) { deepForceUpdate(this); } @@ -68,7 +78,12 @@ class AppContainer extends Component { const { error } = this.state; if (error) { return ; - } else { + } + + if (this.props.component) { + return ; + } + else { return React.Children.only(this.props.children); } } From f2a21abde6bb6a479dbee5d8006ab332a829eeae Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Mon, 25 Apr 2016 01:25:56 +0200 Subject: [PATCH 058/161] Refactor react router hack more --- src/hackRouter.js | 79 +++++++++++++++++++++------------------- test/hackRouter/index.js | 22 +++++------ 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/hackRouter.js b/src/hackRouter.js index a117f540a..209b26e74 100644 --- a/src/hackRouter.js +++ b/src/hackRouter.js @@ -1,5 +1,16 @@ -// This is lame but let's focus on shipping. -// https://github.com/gaearon/react-hot-loader/issues/249 + +function ensureArray(maybeArray) { + if (!maybeArray) { + return []; + } else { + if (Array.isArray(maybeArray)) { + return maybeArray; + } else { + return [maybeArray]; + } + } +} + function isReactRouterish(type) { return type && ( type.displayName === 'Router' || @@ -7,43 +18,35 @@ function isReactRouterish(type) { ); } -function forceUpdateComponentsOfRouteAndChildRoutes(route, forceUpdateFunction) { - // TODO: check whether it is possible to also handle the `getComponent` case here - if (route.component && typeof(route.component) === 'function') { - // Side effect 😱 - // Force proxies to update since React Router ignores new props. - forceUpdateFunction(route.component); - } - - if (route.components) { - for (const key in route.components) { - if (!route.components.hasOwnProperty(key)) continue; - - const component = route.components[key]; - - if (typeof(component) === 'function') { - forceUpdateFunction(component); - } - } - } - - // Child routes will be in `children` when defining routes using react - // elements and in `routes` or `childRoutes` when using plain routes - const childRoutes = [] - .concat(route.routes, route.childRoutes, route.children) - .filter(Boolean); - - for (const childRoute of childRoutes) { - // When using `Route` element routes the relevant objects will be under props - // and when using plain routes directly on the route - forceUpdateComponentsOfRouteAndChildRoutes(childRoute.props || childRoute, forceUpdateFunction); - } +function extractComponents(routes) { + return routes + .map(route => { + // The route properties are at different locations depending on if plain routes + // or regular element routes are used + const component = route.component || (route.props && route.props.component); + const componentsMap = route.components || (route.props && route.props.components) || {}; + const childRoutes = route.childRoutes || (route.props && ensureArray(route.props.children)); + + const components = Object.keys(componentsMap).map(key => componentsMap[key]); + const childRouteComponents = childRoutes && extractComponents(childRoutes); + + return [].concat(component, components, childRouteComponents); + }) + .reduce((reduceTarget, components) => reduceTarget.concat(components), []) // flatten + .filter(c => typeof(c) === 'function'); } -export default function hackRouter(type, props, forceUpdateFunction) { - if (isReactRouterish(type) && props) { - forceUpdateComponentsOfRouteAndChildRoutes(props, forceUpdateFunction); - } +function fixupReactRouter(props, forceUpdateFunction) { + // Ideally we want to teach React Router to receive children. + // We're not in a perfect world, and a dirty workaround works for now. + // https://github.com/reactjs/react-router/issues/2182 + const routes = props.routes || ensureArray(props.children); + const components = extractComponents(routes); + components.forEach((component) => { + // Side effect 😱 + // Force proxies to update since React Router ignores new props. + forceUpdateFunction(component); + }); } -module.exports.default = hackRouter; +module.exports = hackRouter; diff --git a/test/hackRouter/index.js b/test/hackRouter/index.js index 49924bd59..1a23693d6 100644 --- a/test/hackRouter/index.js +++ b/test/hackRouter/index.js @@ -50,24 +50,20 @@ describe('hackRouter', () => { */ const props = { - children: [ - { - props: { - component: function a() {}, - children: [ - { props: { component: function b() {} } }, - ], + children: { + props: { + component: function a() {}, + children: { + props: { component: function b() {} }, }, }, - ], + } }; it('calls force update on each route\'s component', () => { - hackRouter(Router, props, forceUpdateSpy); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[0].props.component); - expect(forceUpdateSpy).toHaveBeenCalledWith( - props.children[0].props.children[0].props.component - ); + fixupReactRouter(props, forceUpdateSpy); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.children.props.component); + expect(forceUpdateSpy).toHaveBeenCalledWith(props.children.props.children.props.component); }); }); From 48744703cab7a25860b02e8c9ea92d1650480a39 Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Mon, 25 Apr 2016 01:52:35 +0200 Subject: [PATCH 059/161] Rename hackRouter -> fixupReactRouter --- src/{hackRouter.js => fixupReactRouter.js} | 2 +- src/patch.dev.js | 4 ++-- test/{hackRouter => fixupReactRouter}/index.js | 14 +++++++------- test/index.js | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename src/{hackRouter.js => fixupReactRouter.js} (98%) rename test/{hackRouter => fixupReactRouter}/index.js (89%) diff --git a/src/hackRouter.js b/src/fixupReactRouter.js similarity index 98% rename from src/hackRouter.js rename to src/fixupReactRouter.js index 209b26e74..97c6a4a7d 100644 --- a/src/hackRouter.js +++ b/src/fixupReactRouter.js @@ -49,4 +49,4 @@ function fixupReactRouter(props, forceUpdateFunction) { }); } -module.exports = hackRouter; +module.exports = fixupReactRouter; diff --git a/src/patch.dev.js b/src/patch.dev.js index 8ba5fbbce..59a4b22af 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,6 +1,6 @@ const React = require('react'); const resolveType = require('./resolveType'); -const hackRouter = require('./hackRouter'); +const fixupReactRouter = require('./fixupReactRouter'); if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); @@ -11,7 +11,7 @@ function patchedCreateElement(type, props, ...args) { // Ideally we want to teach React Router to receive children. // We're not in a perfect world, and a dirty workaround works for now. // https://github.com/reactjs/react-router/issues/2182 - hackRouter(type, props, resolveType); + fixupReactRouter(type, props, resolveType); // Trick React into rendering a proxy so that // its state is preserved when the class changes. diff --git a/test/hackRouter/index.js b/test/fixupReactRouter/index.js similarity index 89% rename from test/hackRouter/index.js rename to test/fixupReactRouter/index.js index 1a23693d6..b1698bde9 100644 --- a/test/hackRouter/index.js +++ b/test/fixupReactRouter/index.js @@ -1,4 +1,4 @@ -import hackRouter from '../../src/hackRouter'; +import fixupReactRouter from '../../src/fixupReactRouter'; import expect, { createSpy } from 'expect'; const forceUpdateSpy = createSpy(); @@ -7,7 +7,7 @@ const Router = { displayName: 'Router', }; -describe('hackRouter', () => { +describe('fixupReactRouter', () => { beforeEach(() => { forceUpdateSpy.reset(); }); @@ -27,7 +27,7 @@ describe('hackRouter', () => { }; it('calls force update on each route\'s component', () => { - hackRouter(Router, props, forceUpdateSpy); + fixupReactRouter(Router, props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[0].props.component); expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[1].props.component); }); @@ -35,7 +35,7 @@ describe('hackRouter', () => { describe('and the top component is not a router', () => { it('does not call force update on any component', () => { const NotRouter = { displayName: 'DefinitelyNotARouter' }; - hackRouter(NotRouter, props, forceUpdateSpy); + fixupReactRouter(NotRouter, props, forceUpdateSpy); expect(forceUpdateSpy).toNotHaveBeenCalled(); }); }); @@ -76,7 +76,7 @@ describe('hackRouter', () => { }; it('calls force update on each route\'s components', () => { - hackRouter(Router, props, forceUpdateSpy); + fixupReactRouter(Router, props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[1].component); }); @@ -95,7 +95,7 @@ describe('hackRouter', () => { }; it('calls force update on each route\'s components', () => { - hackRouter(Router, props, forceUpdateSpy); + fixupReactRouter(Router, props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].childRoutes[0].component); }); @@ -114,7 +114,7 @@ describe('hackRouter', () => { }; it('calls force update on each component', () => { - hackRouter(Router, props, forceUpdateSpy); + fixupReactRouter(Router, props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.a); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.b); }); diff --git a/test/index.js b/test/index.js index 697e90e46..dd764961d 100644 --- a/test/index.js +++ b/test/index.js @@ -1,2 +1,2 @@ import './babel'; -import './hackRouter'; +import './fixupReactRouter'; From ceb05d4490b1362057f26b1f55cbc6dac8556ead Mon Sep 17 00:00:00 2001 From: Alexander Christiansson Date: Mon, 25 Apr 2016 02:18:36 +0200 Subject: [PATCH 060/161] Expose isReactRouterish and use it directly in patch --- src/fixupReactRouter.js | 2 +- src/patch.dev.js | 11 +++++----- test/fixupReactRouter/index.js | 37 +++++++++++++++++++--------------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/fixupReactRouter.js b/src/fixupReactRouter.js index 97c6a4a7d..31b4784b6 100644 --- a/src/fixupReactRouter.js +++ b/src/fixupReactRouter.js @@ -49,4 +49,4 @@ function fixupReactRouter(props, forceUpdateFunction) { }); } -module.exports = fixupReactRouter; +module.exports = { isReactRouterish, fixupReactRouter }; diff --git a/src/patch.dev.js b/src/patch.dev.js index 59a4b22af..d4f9b99d0 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,6 +1,6 @@ const React = require('react'); const resolveType = require('./resolveType'); -const fixupReactRouter = require('./fixupReactRouter'); +const { isReactRouterish, fixupReactRouter } = require('./fixupReactRouter'); if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); @@ -8,10 +8,11 @@ if (React.createElement.isPatchedByReactHotLoader) { const createElement = React.createElement; function patchedCreateElement(type, props, ...args) { - // Ideally we want to teach React Router to receive children. - // We're not in a perfect world, and a dirty workaround works for now. - // https://github.com/reactjs/react-router/issues/2182 - fixupReactRouter(type, props, resolveType); + // This is lame but let's focus on shipping. + // https://github.com/gaearon/react-hot-loader/issues/249 + if (isReactRouterish(type)) { + fixupReactRouter(props, resolveType); + } // Trick React into rendering a proxy so that // its state is preserved when the class changes. diff --git a/test/fixupReactRouter/index.js b/test/fixupReactRouter/index.js index b1698bde9..1a0638351 100644 --- a/test/fixupReactRouter/index.js +++ b/test/fixupReactRouter/index.js @@ -1,11 +1,24 @@ -import fixupReactRouter from '../../src/fixupReactRouter'; +import { isReactRouterish, fixupReactRouter } from '../../src/fixupReactRouter'; import expect, { createSpy } from 'expect'; const forceUpdateSpy = createSpy(); -const Router = { - displayName: 'Router', -}; + +describe('isReactRouterish', () => { + describe('when given a router', () => { + it('is true when given a router', () => { + const Router = { displayName: 'Router' }; + expect(isReactRouterish(Router)).toBe(true); + }); + }); + + describe('when not given a router', () => { + it('returns false', () => { + const NotRouter = { displayName: 'DefinitelyNotARouter' }; + expect(isReactRouterish(NotRouter)).toBe(false); + }); + }); +}); describe('fixupReactRouter', () => { beforeEach(() => { @@ -27,18 +40,10 @@ describe('fixupReactRouter', () => { }; it('calls force update on each route\'s component', () => { - fixupReactRouter(Router, props, forceUpdateSpy); + fixupReactRouter(props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[0].props.component); expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[1].props.component); }); - - describe('and the top component is not a router', () => { - it('does not call force update on any component', () => { - const NotRouter = { displayName: 'DefinitelyNotARouter' }; - fixupReactRouter(NotRouter, props, forceUpdateSpy); - expect(forceUpdateSpy).toNotHaveBeenCalled(); - }); - }); }); describe('when routes are passed in as children of children', () => { @@ -76,7 +81,7 @@ describe('fixupReactRouter', () => { }; it('calls force update on each route\'s components', () => { - fixupReactRouter(Router, props, forceUpdateSpy); + fixupReactRouter(props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[1].component); }); @@ -95,7 +100,7 @@ describe('fixupReactRouter', () => { }; it('calls force update on each route\'s components', () => { - fixupReactRouter(Router, props, forceUpdateSpy); + fixupReactRouter(props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].childRoutes[0].component); }); @@ -114,7 +119,7 @@ describe('fixupReactRouter', () => { }; it('calls force update on each component', () => { - fixupReactRouter(Router, props, forceUpdateSpy); + fixupReactRouter(props, forceUpdateSpy); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.a); expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.b); }); From ee7751818a2e3f19d89dff11755b554a35be8d4a Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Mon, 25 Apr 2016 13:51:29 -0400 Subject: [PATCH 061/161] Update compat for AppContainer.prod.js. - Fix style --- src/AppContainer.dev.js | 3 +-- src/AppContainer.prod.js | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index cecc2d5df..fd8d7f1cd 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -82,8 +82,7 @@ class AppContainer extends Component { if (this.props.component) { return ; - } - else { + } else { return React.Children.only(this.props.children); } } diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js index ae9a37739..b32788b7d 100644 --- a/src/AppContainer.prod.js +++ b/src/AppContainer.prod.js @@ -3,6 +3,10 @@ const { Component } = React; class AppContainer extends Component { render() { + if (this.props.component) { + return ; + } + return React.Children.only(this.props.children); } } From b29375cb09c2866523f0c5743f4f26ad18377a8a Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Mon, 25 Apr 2016 21:04:44 +0200 Subject: [PATCH 062/161] docs: early draft --- docs/README.md | 226 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 151 insertions(+), 75 deletions(-) diff --git a/docs/README.md b/docs/README.md index 93c35db3a..a1cd9fbfa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,116 +1,192 @@ +# React Hot Loader v3 + +**_Draft docs_** + +## Intro + +React Hot Loader lets us modify our React components and see the changes in realtime. No more waiting for your entire project to rebuild every time you save a file; your webpage stays loaded and the modified component updates instantly. + +XXX do we even need an intro here? What will be here vs main README? e.g. +* animated gif (should definitely be main README) +* link to reacteurope video + +**Differences from Past Approaches and Issues Solved** + +First we had React Hot Loader, which was superseded by React Transform. Each time we solved earlier issues but were faced with new ones. RHLv3 solves all these issues with a more sustainable approach, and is intended as a replacement for both: + +* Preserves state in functional components +* Little configuration required +* Disabled in production +* Works with or without Babel +* Everything you need in a single repo + +For more information on the evolution of approaches taken, see [](https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf)https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf. + +## Boilerplate Example + +What follows is a 3-step guide to integrating React Hot Loader into your current project. Alternatively, you can also clone the boilerplate, for a quick start on a fresh app with everything working out-of-the-box. + +[](https://github.com/gaearon/react-hot-boilerplate/)https://github.com/gaearon/react-hot-boilerplate/ + ### Starter Kits -* [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate) (Bare minimum) -* [react-starter](https://github.com/webpack/react-starter) (react-router, includes production configs) -* [isomorphic-hot-loader](https://github.com/irvinebroque/isomorphic-hot-loader) (react-router, isomorphic) -* [isomorphic-react-template](https://github.com/gpbl/isomorphic-react-template/) (react-router, isomorphic) -* [coffee-react-quickstart](https://github.com/KyleAMathews/coffee-react-quickstart) (react-router, CoffeeScript, Gulp) -* [react-static-boilerplate](https://github.com/koistya/react-static-boilerplate) (static site generator; React, PostCSS, Webpack, BrowserSync) -* [boilerplate-webpack-react](https://github.com/tcoopman/boilerplate-webpack-react) (react-router, isomorphic) -* [este](http://github.com/steida/este) (react-router, isomorphic, Flux, Babel) -* [react-isomorphic-starterkit](https://github.com/RickWong/react-isomorphic-starterkit) (react-router, react-async, isomorphic) -* [yarsk](https://github.com/bradleyboy/yarsk) (Babel, Karma + Mocha, automated publishing to GitHub pages) -* [react-web](https://github.com/darul75/web-react) (Babel, react-router, Alt flux) -* [esnext-quickstart](https://github.com/nkbt/esnext-quickstart) (compile-time ESLint, ES6, Babel, Karma + Jasmine + Coverage) -* [react-webpack-boilerplate](https://github.com/srn/react-webpack-boilerplate) (One-click Heroku deployable, Node.js server) -* [flask-react-boilerplate](https://github.com/alexkuz/flask-react-boilerplate) (One-click Heroku deployable, Flask server + PostgreSQL, Babel, Flummox) -* [react-kickstart](https://github.com/vesparny/react-kickstart) (react-router, mocha + chai + istanbul) -* [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) (isomorphic, redux, client and server async data fetching, babel, react-router) -* [go-starter-kit](https://github.com/olebedev/go-starter-kit) (hot reloadable golang/react/flummox/css-module isomorphic starter kit) -* [react-fullstack-skeleton](https://github.com/fortruce/react-fullstack-skeleton) (react w/ backend api server) +**XXX: from old doc, but we only want ones that are up to date with RHLv3** + +## Integrating into your own App + +### Step 1/3: Enabling Hot Module Replacement (HMR) + +HMR allows us to replace modules in-place without restarting the server, here's how you can enable it: -Don't be shy, add your own. +**Webpack** -### Migrating to 1.0 +* Create a development Webpack config separate from production one +* Add HotModuleReplacementPlugin to development Webpack config +* If you only render on the client, consider using WebpackDevServer + * Easier to set up + * Enable hot: true and add its entry points +* If you use server rendering, consider using Express server + webpack-dev-middleware +* More work but also more control +* Show how to add webpack-dev-middleware and its entry point -React Hot Loader has reached 1.0, and it's a breaking change. When React Hot Loader just started, it used a regex to find `createClass` calls and replace them with its own implementation. This turned out to be a bad idea for a number of reasons: +**XXX cleanup, details** -* Doesn't work when components are created through wrappers (e.g. [OmniscientJS](http://omniscientjs.github.io)); -* Doesn't work when author calls React differently; -* Causes false positives in React source code comments and elsewhere; -* Most importantly, won't work with ES6 classes that will be future of React. +**Browserify** -Here's how we're solving these problems in 1.0: +If you have this setup working, please consider submitting instructions as a PR. -#### Only `module.exports` and its own properties are hot by default +**Meteor** -With 1.0, we no longer parse your sources. Instead, we only now make `module.exports` and its [own properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) hot by default, and only if their prototype declares `render` method or descends from `React.Component`. **If you've been splitting each component in a separate file, that means no change for you here!** This allows us to support exotic wrappers. +* If you're using [webpack:webpack](https://atmospherejs.com/webpack/webpack) you can follow the webpack instructions or ask for help in [this](https://forums.meteor.com/t/use-webpack-with-meteor-simply-by-adding-packages-meteor-webpack-1-0-is-out/18819) forum post. -If you use inheritance in React 0.13, base classes will only be opted into hot reloading if they descend from `React.Component` or define `render` method. Otherwise you need to explicitly call `module.makeHot` as described below. +* Otherwise, for HMR in "native" Meteor, type: `meteor remove ecmascript && meteor add gadicc:ecmascript-hot` or see the [README](https://github.com/gadicc/meteor-react-hotloader#readme) for more details. There are also some Meteor-specific RHLv3 install instructions [here](https://github.com/gadicc/meteor-react-hotloader/blob/master/docs/React_Hotloading.md). -#### You can make hot anything else via opt-in `module.makeHot` API +### Step 2/3: Using HMR to replace the root component -But what if you *want* to have several hot-reloadable components in one file? Or what if you want to export a function creating components, or an object with several components as properties? For that, 1.0 **adds first public API to hot loader: `module.makeHot`**. This method will be present on `module` object if hot loader is enabled, and allows you to make any component hot: +When the HMR runtime receives an updated module, it first checks to see if the module knows how to update itself, and then ascends the import/require chain looking for a parent module that can accept the update. We want our root component to be able to accept an update from any child component. + +If your client entry point looks like this: ```js -var Something = React.createClass({ - ... -}; +import React from 'react'; +import { render } from 'react-dom'; +import RootContainer from './containers/rootContainer.js'; -if (module.makeHot) { // Won't be true in production - Something = module.makeHot(Something); -} +render(, document.elementById('react-root'); ``` - -Explicit API can also be used inside functions: +you would add the following code to accept changes to RootContainer _or any of it's descendants_. ```js -function generateClass(param) { - var Class = return React.createClass({ - ... - }; + if (module.hot) { + module.hot.accept('./containers/rootContainer.js', function() { + var NextRootContainer = require('./containers/rootContainer.js'; + render(, document.elementById('react-root'); + } + } +``` +Note, with no further steps, this enough to hotload changes to React components, but state will not be preserved. If you externalize all your state in a state store like Redux, this might be enough. - if (module.makeHot) { - Class = module.makeHot(Class, param); - } +### Step 3/3: Adding React Hot Loader to preserve state - return Class; -} +The final step adds adds `react-hot-loader` to our project to preserve _component state_ across hot loads. -``` +1. Install the package: -Note the second parameter: `makeHot` needs some way to distinguish components of same type inside on module. By default, it uses `displayName` of given component class, but in case of dynamically generated classes (or if you're not using JSX), you have to provide it yourself. + ```sh + npm install --save-dev react-hot-loader + ``` +1. Add the package to your config. -### Manual mode (experimental) + a. If you use Babel, modify your `.babelrc` to ensure it includes at least: -You can now use `react-hot?manual` instead of `react-hot` in Webpack config to turn on manual mode. In manual mode, “accepting” hot updates is up to you; modules won't accept themselves automatically. This can be used, for example, to put reloading logic on very top of the application and [hot-reload routes as well as components](https://github.com/rackt/react-router/pull/606#issuecomment-66936975). It will also work better when you have a lot of modules that export component-generating functions because updates will propagate to the top. (Don't worry if you don't understand this; it's just something experimental you might want to try to integrate hot reloading deeper into your app.) + ```js + { + "plugins": [ "react-hot-loader/babel" ] + } + ``` + b. Alternatively, in Webpack, add `react-hot-loader/webpack` to your loaders -### Usage with external React + ```js + XXX + ``` -If you're using external standalone React bundle instead of NPM package, Hot Loader will fail because it relies on `react/lib/ReactMount` which is not exposed in precompiled React. It needs `ReactMount` to keep track of mounted React component instances on the page. However, you can supply your own root instance provider: +1. Add following line to the top of your main entry point: + ```js + import 'react-hot-loader/patch'; + ``` -```js -// Your app's index.js +1. Wrap your `` inside of an ``: + + ```js + import { AppContainer } from 'react-hot-loader'; + render(, + document.getElementById('react-root'); + ``` + **XXX pending [gaearon/react hot loader#244](https://github.com/gaearon/react-hot-loader/issues/244)** -var React = require('react'), - router = require('./router'); + You should do this for both instances, e.g. your original mount and your mount code inside of the `module.hot.accept()` function. `` must wrap only a single, React component. -var rootInstance = null; +That's it! -router.run(function (Handler, state) { - rootInstance = React.render(, document.body); -}); +## Putting it all together + +If you followed all the steps your app's main entry point should now look something like this: + +```js +import 'react-hot-loader/patch'; +import React from 'react'; +import { render } from 'react-dom'; +import { AppContainer } from 'react-hot-loader'; +import RootContainer from './containers/rootContainer.js'; + +render(, + document.getElementById('react-root'); if (module.hot) { - require('react-hot-loader/Injection').RootInstanceProvider.injectProvider({ - getRootInstances: function () { - // Help React Hot Loader figure out the root component instances on the page: - return [rootInstance]; - } - }); + module.hot.accept('./containers/rootContainer.js', function() { + var NextRootContainer = require('./containers/rootContainer.js'; + render(, + document.getElementById('react-root'); + } } ``` -You'll only need this if you [use a precompiled version of React](https://github.com/gaearon/react-hot-loader/issues/53). If you use React NPM package, this is not necessary. You should generally use React NPM package unless you have good reason not to. +### Checking that everything is working properly -### Source Maps +**XXX** -If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. +## Troubleshooting -Source maps slow down your project. Use `devtool: 'eval'` for best build performance. +**XXX could/should be in another file** -Hot reloading code is just one line in the beginning and one line in the end of each module so you might not need source maps at all. +## Tips and Tricks + +**XXX could/should be in another file** + +**How to get an error in your console too:** + +The redbox errors are great to clearly see rendering issues, and avoiding an uncaught error from breaking your app. But there are some advantages to a thrown error in the console too, like filename resolution via sourcemaps, and click-to-open. To get the best of both worlds, modify your app entry point as follows: + +```js +import Redbox from 'redbox-react'; + +const consoleErrorReporter = ({error}) => { + // We throw in a different context, so the app still doesn't break! + setTimeout(() => { throw error; }); + return ; +}; +consoleErrorReporter.propTypes = { + error: React.PropTypes.error +}; + +render( + + + , + document.getElementById('react-root') +); +``` -### React Hot API +## Where to ask for Help -If you're authoring a build tool, you might be interested to hear that React Hot Loader brains have been extracted into runtime-agnostic [React Hot API](https://github.com/gaearon/react-hot-api). React Hot Loader just binds that API to Webpack runtime, but you can implement yours too. +**XXX** \ No newline at end of file From 27bd7e055d2ebd1546a4927214182351bfe9ca90 Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Mon, 25 Apr 2016 21:56:45 +0200 Subject: [PATCH 063/161] fix broken render statements --- docs/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index a1cd9fbfa..b1414dfe1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,7 +72,7 @@ import React from 'react'; import { render } from 'react-dom'; import RootContainer from './containers/rootContainer.js'; -render(, document.elementById('react-root'); +render(, document.elementById('react-root')); ``` you would add the following code to accept changes to RootContainer _or any of it's descendants_. @@ -80,7 +80,7 @@ you would add the following code to accept changes to RootContainer _or any of i if (module.hot) { module.hot.accept('./containers/rootContainer.js', function() { var NextRootContainer = require('./containers/rootContainer.js'; - render(, document.elementById('react-root'); + render(, document.elementById('react-root')); } } ``` @@ -120,7 +120,7 @@ The final step adds adds `react-hot-loader` to our project to preserve _componen ```js import { AppContainer } from 'react-hot-loader'; render(, - document.getElementById('react-root'); + document.getElementById('react-root')); ``` **XXX pending [gaearon/react hot loader#244](https://github.com/gaearon/react-hot-loader/issues/244)** @@ -146,7 +146,7 @@ if (module.hot) { module.hot.accept('./containers/rootContainer.js', function() { var NextRootContainer = require('./containers/rootContainer.js'; render(, - document.getElementById('react-root'); + document.getElementById('react-root')); } } ``` From 8ab36b5026d792ef2c4e433da32a533a131d5342 Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Tue, 26 Apr 2016 06:13:33 +0200 Subject: [PATCH 064/161] fix closing parenthesis on require statements --- docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index b1414dfe1..0d3f7fe10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -79,7 +79,7 @@ you would add the following code to accept changes to RootContainer _or any of i ```js if (module.hot) { module.hot.accept('./containers/rootContainer.js', function() { - var NextRootContainer = require('./containers/rootContainer.js'; + var NextRootContainer = require('./containers/rootContainer.js'); render(, document.elementById('react-root')); } } @@ -144,7 +144,7 @@ render(, if (module.hot) { module.hot.accept('./containers/rootContainer.js', function() { - var NextRootContainer = require('./containers/rootContainer.js'; + var NextRootContainer = require('./containers/rootContainer.js'); render(, document.getElementById('react-root')); } From d0634a45025c14b9d18ada50672e103e103c4f26 Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Tue, 26 Apr 2016 07:36:03 +0200 Subject: [PATCH 065/161] require().default for ES6 modules --- docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 0d3f7fe10..5048258d1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -79,7 +79,7 @@ you would add the following code to accept changes to RootContainer _or any of i ```js if (module.hot) { module.hot.accept('./containers/rootContainer.js', function() { - var NextRootContainer = require('./containers/rootContainer.js'); + var NextRootContainer = require('./containers/rootContainer.js').default; render(, document.elementById('react-root')); } } @@ -144,7 +144,7 @@ render(, if (module.hot) { module.hot.accept('./containers/rootContainer.js', function() { - var NextRootContainer = require('./containers/rootContainer.js'); + var NextRootContainer = require('./containers/rootContainer.js').default; render(, document.getElementById('react-root')); } From e043b10a6d98dfe5c65e57363d4593585f09c614 Mon Sep 17 00:00:00 2001 From: David Tsai Date: Tue, 26 Apr 2016 00:03:52 -0700 Subject: [PATCH 066/161] Adding additional clarification to RHL3 docs along with JSX highlighting in code blocks --- docs/README.md | 87 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/docs/README.md b/docs/README.md index 5048258d1..7187b866e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,7 +20,8 @@ First we had React Hot Loader, which was superseded by React Transform. Each ti * Works with or without Babel * Everything you need in a single repo -For more information on the evolution of approaches taken, see [](https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf)https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf. +For more information on the evolution of approaches taken +To learn more about the evolution of prior approaches, read ["Hot Reloading in React"](https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf) by [Dan Abramov](https://medium.com/@dan_abramov) ## Boilerplate Example @@ -107,7 +108,21 @@ The final step adds adds `react-hot-loader` to our project to preserve _componen b. Alternatively, in Webpack, add `react-hot-loader/webpack` to your loaders ```js - XXX + // webpackConfig.js + + // TODO: Would love some help showing the shape of the webpack config without + // overwhelming users either - just want it to be familiar enough. I suppose we could + // also declare a variable and assign the require statement to it? (Just an idea) + devtool: ..., + entry: [...], + module: { + loaders: [{ + test: /\.js$/, + loaders: ['react-hot-loader/webpack', 'babel'], + include: path.join(__dirname, 'src') + }] + } + ``` 1. Add following line to the top of your main entry point: @@ -119,8 +134,13 @@ The final step adds adds `react-hot-loader` to our project to preserve _componen ```js import { AppContainer } from 'react-hot-loader'; - render(, - document.getElementById('react-root')); + import RootContainer from './containers/rootContainer.js'; + + render(( + + + + ), document.getElementById('react-root')); ``` **XXX pending [gaearon/react hot loader#244](https://github.com/gaearon/react-hot-loader/issues/244)** @@ -128,26 +148,47 @@ The final step adds adds `react-hot-loader` to our project to preserve _componen That's it! -## Putting it all together +## Putting it all together -If you followed all the steps your app's main entry point should now look something like this: +If you've gotten this far - you're almost done! But before showing you what your app's +main entry point might look like, let's clarify a few things. -```js +`AppContainer` +> `AppContainer` is a component provided by *this* library (`react-hot-loader`), it serves to +wrap your entire app in order to provide hot reloading goodness! + +`RootContainer` +> On the other hand, `RootContainer` represents any application's top-level component, prior +to implementing the `AppContainer` mentioned above. Keep in mind that this can be substituted +for an existing wrapper/parent component. + +Your application's main entry point might look like the code presented below. Notice that +we are targeting and subsequently rendering into a particular DOM element's id (conveniently named `react-root`). + +```javascript import 'react-hot-loader/patch'; import React from 'react'; import { render } from 'react-dom'; -import { AppContainer } from 'react-hot-loader'; +// See notes above re: AppContainer and RootContainer +import { AppContainer } from 'react-hot-loader' import RootContainer from './containers/rootContainer.js'; -render(, - document.getElementById('react-root'); +render(( + + + +), document.getElementById('react-root')); if (module.hot) { - module.hot.accept('./containers/rootContainer.js', function() { - var NextRootContainer = require('./containers/rootContainer.js').default; - render(, - document.getElementById('react-root')); - } + module.hot.accept('./containers/rootContainer.js', () => { + const NextRootContainer = require('./containers/rootContainer.js'); + + render(( + + + + ), document.getElementById('react-root')); + }) } ``` @@ -165,9 +206,11 @@ if (module.hot) { **How to get an error in your console too:** -The redbox errors are great to clearly see rendering issues, and avoiding an uncaught error from breaking your app. But there are some advantages to a thrown error in the console too, like filename resolution via sourcemaps, and click-to-open. To get the best of both worlds, modify your app entry point as follows: +The `Redbox` errors are great to clearly see rendering issues, and avoiding an uncaught error from breaking your app. +But there are some advantages to a thrown error in the console too, like filename resolution via sourcemaps, and click-to-open. +To get the best of both worlds, modify your app entry point as follows: -```js +```jsx import Redbox from 'redbox-react'; const consoleErrorReporter = ({error}) => { @@ -175,18 +218,18 @@ const consoleErrorReporter = ({error}) => { setTimeout(() => { throw error; }); return ; }; + consoleErrorReporter.propTypes = { error: React.PropTypes.error }; -render( +render(( - , - document.getElementById('react-root') -); + +), document.getElementById('react-root')); ``` ## Where to ask for Help -**XXX** \ No newline at end of file +**XXX** From 77dd1db4da4cdce679485936767125218300393d Mon Sep 17 00:00:00 2001 From: David Tsai Date: Tue, 26 Apr 2016 00:12:13 -0700 Subject: [PATCH 067/161] Updating syntax in example(s) to be consistent ES6 --- docs/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/README.md b/docs/README.md index 7187b866e..6aaf27764 100644 --- a/docs/README.md +++ b/docs/README.md @@ -68,7 +68,7 @@ When the HMR runtime receives an updated module, it first checks to see if the m If your client entry point looks like this: -```js +```jsx import React from 'react'; import { render } from 'react-dom'; import RootContainer from './containers/rootContainer.js'; @@ -77,10 +77,10 @@ render(, document.elementById('react-root')); ``` you would add the following code to accept changes to RootContainer _or any of it's descendants_. -```js +```jsx if (module.hot) { - module.hot.accept('./containers/rootContainer.js', function() { - var NextRootContainer = require('./containers/rootContainer.js').default; + module.hot.accept('./containers/rootContainer.js', () => { + const NextRootContainer = require('./containers/rootContainer.js').default; render(, document.elementById('react-root')); } } @@ -94,7 +94,7 @@ The final step adds adds `react-hot-loader` to our project to preserve _componen 1. Install the package: ```sh - npm install --save-dev react-hot-loader + $ npm install --save-dev react-hot-loader ``` 1. Add the package to your config. @@ -132,7 +132,7 @@ The final step adds adds `react-hot-loader` to our project to preserve _componen 1. Wrap your `` inside of an ``: - ```js + ```jsx import { AppContainer } from 'react-hot-loader'; import RootContainer from './containers/rootContainer.js'; @@ -165,7 +165,7 @@ for an existing wrapper/parent component. Your application's main entry point might look like the code presented below. Notice that we are targeting and subsequently rendering into a particular DOM element's id (conveniently named `react-root`). -```javascript +```jsx import 'react-hot-loader/patch'; import React from 'react'; import { render } from 'react-dom'; From 1a7f493d07c20b5c8547b0f4cc8db7fb187412b5 Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Tue, 26 Apr 2016 15:14:07 +0200 Subject: [PATCH 068/161] restore original README so we can cleanly merge in hedgerh's changes --- docs/README.md | 279 +++++++++++++++---------------------------------- 1 file changed, 82 insertions(+), 197 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6aaf27764..3801826ae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,235 +1,120 @@ -# React Hot Loader v3 - -**_Draft docs_** - -## Intro - -React Hot Loader lets us modify our React components and see the changes in realtime. No more waiting for your entire project to rebuild every time you save a file; your webpage stays loaded and the modified component updates instantly. - -XXX do we even need an intro here? What will be here vs main README? e.g. -* animated gif (should definitely be main README) -* link to reacteurope video - -**Differences from Past Approaches and Issues Solved** - -First we had React Hot Loader, which was superseded by React Transform. Each time we solved earlier issues but were faced with new ones. RHLv3 solves all these issues with a more sustainable approach, and is intended as a replacement for both: +### Starter Kits -* Preserves state in functional components -* Little configuration required -* Disabled in production -* Works with or without Babel -* Everything you need in a single repo +* [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate) (Bare minimum) +* [react-starter](https://github.com/webpack/react-starter) (react-router, includes production configs) +* [react-tape](https://github.com/fc-io/react-tape) (Babel, blue-tape, css-loader, html-webpack-plugin, production config) +* [isomorphic-hot-loader](https://github.com/irvinebroque/isomorphic-hot-loader) (react-router, isomorphic) +* [isomorphic-react-template](https://github.com/gpbl/isomorphic-react-template/) (react-router, isomorphic) +* [coffee-react-quickstart](https://github.com/KyleAMathews/coffee-react-quickstart) (react-router, CoffeeScript, Gulp) +* [react-static-boilerplate](https://github.com/koistya/react-static-boilerplate) (static site generator; React, PostCSS, Webpack, BrowserSync) +* [boilerplate-webpack-react](https://github.com/tcoopman/boilerplate-webpack-react) (react-router, isomorphic) +* [este](http://github.com/steida/este) (react-router, isomorphic, Flux, Babel) +* [react-isomorphic-starterkit](https://github.com/RickWong/react-isomorphic-starterkit) (react-router, react-async, isomorphic) +* [yarsk](https://github.com/bradleyboy/yarsk) (Babel, Karma + Mocha, automated publishing to GitHub pages) +* [react-web](https://github.com/darul75/web-react) (Babel, react-router, Alt flux) +* [esnext-quickstart](https://github.com/nkbt/esnext-quickstart) (compile-time ESLint, ES6, Babel, Karma + Jasmine + Coverage) +* [react-webpack-boilerplate](https://github.com/srn/react-webpack-boilerplate) (One-click Heroku deployable, Node.js server) +* [flask-react-boilerplate](https://github.com/alexkuz/flask-react-boilerplate) (One-click Heroku deployable, Flask server + PostgreSQL, Babel, Flummox) +* [react-kickstart](https://github.com/vesparny/react-kickstart) (react-router, mocha + chai + istanbul) +* [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) (isomorphic, redux, client and server async data fetching, babel, react-router) +* [go-starter-kit](https://github.com/olebedev/go-starter-kit) (hot reloadable golang/react/flummox/css-module isomorphic starter kit) +* [react-fullstack-skeleton](https://github.com/fortruce/react-fullstack-skeleton) (react w/ backend api server) +* [react-cordova-boilerplate](https://github.com/unimonkiez/react-cordova-boilerplate) (react with react-router, redux, hot loader and server-rendering for cordova) +* [react-hot-boilerplate-ts](https://github.com/wmaurer/react-hot-boilerplate-ts) (hot reloadable typescript starter kit) +* [Megatome](https://github.com/Levelmoney/generator-megatome) (Yeoman generator w/ dynamic switchable rendering on Node/browser, react-router, babel, isomophic, an easy config building environments, bring-your-own-data-model and docker) -For more information on the evolution of approaches taken -To learn more about the evolution of prior approaches, read ["Hot Reloading in React"](https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf) by [Dan Abramov](https://medium.com/@dan_abramov) +Don't be shy, add your own. -## Boilerplate Example +### Migrating to 1.0 -What follows is a 3-step guide to integrating React Hot Loader into your current project. Alternatively, you can also clone the boilerplate, for a quick start on a fresh app with everything working out-of-the-box. +React Hot Loader has reached 1.0, and it's a breaking change. When React Hot Loader just started, it used a regex to find `createClass` calls and replace them with its own implementation. This turned out to be a bad idea for a number of reasons: -[](https://github.com/gaearon/react-hot-boilerplate/)https://github.com/gaearon/react-hot-boilerplate/ +* Doesn't work when components are created through wrappers (e.g. [OmniscientJS](http://omniscientjs.github.io)); +* Doesn't work when author calls React differently; +* Causes false positives in React source code comments and elsewhere; +* Most importantly, won't work with ES6 classes that will be future of React. -### Starter Kits +Here's how we're solving these problems in 1.0: -**XXX: from old doc, but we only want ones that are up to date with RHLv3** +#### Only `module.exports` and its own properties are hot by default -## Integrating into your own App +With 1.0, we no longer parse your sources. Instead, we only now make `module.exports` and its [own properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) hot by default, and only if their prototype declares `render` method or descends from `React.Component`. **If you've been splitting each component in a separate file, that means no change for you here!** This allows us to support exotic wrappers. -### Step 1/3: Enabling Hot Module Replacement (HMR) +If you use inheritance in React 0.13, base classes will only be opted into hot reloading if they descend from `React.Component` or define `render` method. Otherwise you need to explicitly call `module.makeHot` as described below. -HMR allows us to replace modules in-place without restarting the server, here's how you can enable it: +#### You can make hot anything else via opt-in `module.makeHot` API -**Webpack** +But what if you *want* to have several hot-reloadable components in one file? Or what if you want to export a function creating components, or an object with several components as properties? For that, 1.0 **adds first public API to hot loader: `module.makeHot`**. This method will be present on `module` object if hot loader is enabled, and allows you to make any component hot: -* Create a development Webpack config separate from production one -* Add HotModuleReplacementPlugin to development Webpack config -* If you only render on the client, consider using WebpackDevServer - * Easier to set up - * Enable hot: true and add its entry points -* If you use server rendering, consider using Express server + webpack-dev-middleware -* More work but also more control -* Show how to add webpack-dev-middleware and its entry point +```js +var Something = React.createClass({ + ... +}; -**XXX cleanup, details** +if (module.makeHot) { // Won't be true in production + Something = module.makeHot(Something); +} +``` -**Browserify** +Explicit API can also be used inside functions: -If you have this setup working, please consider submitting instructions as a PR. +```js +function generateClass(param) { + var Class = return React.createClass({ + ... + }; -**Meteor** + if (module.makeHot) { + Class = module.makeHot(Class, param); + } -* If you're using [webpack:webpack](https://atmospherejs.com/webpack/webpack) you can follow the webpack instructions or ask for help in [this](https://forums.meteor.com/t/use-webpack-with-meteor-simply-by-adding-packages-meteor-webpack-1-0-is-out/18819) forum post. + return Class; +} -* Otherwise, for HMR in "native" Meteor, type: `meteor remove ecmascript && meteor add gadicc:ecmascript-hot` or see the [README](https://github.com/gadicc/meteor-react-hotloader#readme) for more details. There are also some Meteor-specific RHLv3 install instructions [here](https://github.com/gadicc/meteor-react-hotloader/blob/master/docs/React_Hotloading.md). +``` -### Step 2/3: Using HMR to replace the root component +Note the second parameter: `makeHot` needs some way to distinguish components of same type inside on module. By default, it uses `displayName` of given component class, but in case of dynamically generated classes (or if you're not using JSX), you have to provide it yourself. -When the HMR runtime receives an updated module, it first checks to see if the module knows how to update itself, and then ascends the import/require chain looking for a parent module that can accept the update. We want our root component to be able to accept an update from any child component. +### Manual mode (experimental) -If your client entry point looks like this: +You can now use `react-hot?manual` instead of `react-hot` in Webpack config to turn on manual mode. In manual mode, “accepting” hot updates is up to you; modules won't accept themselves automatically. This can be used, for example, to put reloading logic on very top of the application and [hot-reload routes as well as components](https://github.com/rackt/react-router/pull/606#issuecomment-66936975). It will also work better when you have a lot of modules that export component-generating functions because updates will propagate to the top. (Don't worry if you don't understand this; it's just something experimental you might want to try to integrate hot reloading deeper into your app.) -```jsx -import React from 'react'; -import { render } from 'react-dom'; -import RootContainer from './containers/rootContainer.js'; +### Usage with external React -render(, document.elementById('react-root')); -``` -you would add the following code to accept changes to RootContainer _or any of it's descendants_. - -```jsx - if (module.hot) { - module.hot.accept('./containers/rootContainer.js', () => { - const NextRootContainer = require('./containers/rootContainer.js').default; - render(, document.elementById('react-root')); - } - } -``` -Note, with no further steps, this enough to hotload changes to React components, but state will not be preserved. If you externalize all your state in a state store like Redux, this might be enough. +If you're using external standalone React bundle instead of NPM package, Hot Loader will fail because it relies on `react/lib/ReactMount` which is not exposed in precompiled React. It needs `ReactMount` to keep track of mounted React component instances on the page. However, you can supply your own root instance provider: -### Step 3/3: Adding React Hot Loader to preserve state +```js +// Your app's index.js -The final step adds adds `react-hot-loader` to our project to preserve _component state_ across hot loads. +var React = require('react'), + router = require('./router'); -1. Install the package: +var rootInstance = null; - ```sh - $ npm install --save-dev react-hot-loader - ``` -1. Add the package to your config. - - a. If you use Babel, modify your `.babelrc` to ensure it includes at least: - - ```js - { - "plugins": [ "react-hot-loader/babel" ] - } - ``` - b. Alternatively, in Webpack, add `react-hot-loader/webpack` to your loaders - - ```js - // webpackConfig.js - - // TODO: Would love some help showing the shape of the webpack config without - // overwhelming users either - just want it to be familiar enough. I suppose we could - // also declare a variable and assign the require statement to it? (Just an idea) - devtool: ..., - entry: [...], - module: { - loaders: [{ - test: /\.js$/, - loaders: ['react-hot-loader/webpack', 'babel'], - include: path.join(__dirname, 'src') - }] - } - - ``` - -1. Add following line to the top of your main entry point: - ```js - import 'react-hot-loader/patch'; - ``` - -1. Wrap your `` inside of an ``: - - ```jsx - import { AppContainer } from 'react-hot-loader'; - import RootContainer from './containers/rootContainer.js'; - - render(( - - - - ), document.getElementById('react-root')); - ``` - **XXX pending [gaearon/react hot loader#244](https://github.com/gaearon/react-hot-loader/issues/244)** - - You should do this for both instances, e.g. your original mount and your mount code inside of the `module.hot.accept()` function. `` must wrap only a single, React component. - -That's it! - -## Putting it all together - -If you've gotten this far - you're almost done! But before showing you what your app's -main entry point might look like, let's clarify a few things. - -`AppContainer` -> `AppContainer` is a component provided by *this* library (`react-hot-loader`), it serves to -wrap your entire app in order to provide hot reloading goodness! - -`RootContainer` -> On the other hand, `RootContainer` represents any application's top-level component, prior -to implementing the `AppContainer` mentioned above. Keep in mind that this can be substituted -for an existing wrapper/parent component. - -Your application's main entry point might look like the code presented below. Notice that -we are targeting and subsequently rendering into a particular DOM element's id (conveniently named `react-root`). - -```jsx -import 'react-hot-loader/patch'; -import React from 'react'; -import { render } from 'react-dom'; -// See notes above re: AppContainer and RootContainer -import { AppContainer } from 'react-hot-loader' -import RootContainer from './containers/rootContainer.js'; - -render(( - - - -), document.getElementById('react-root')); +router.run(function (Handler, state) { + rootInstance = React.render(, document.body); +}); if (module.hot) { - module.hot.accept('./containers/rootContainer.js', () => { - const NextRootContainer = require('./containers/rootContainer.js'); - - render(( - - - - ), document.getElementById('react-root')); - }) + require('react-hot-loader/Injection').RootInstanceProvider.injectProvider({ + getRootInstances: function () { + // Help React Hot Loader figure out the root component instances on the page: + return [rootInstance]; + } + }); } ``` -### Checking that everything is working properly - -**XXX** - -## Troubleshooting - -**XXX could/should be in another file** +You'll only need this if you [use a precompiled version of React](https://github.com/gaearon/react-hot-loader/issues/53). If you use React NPM package, this is not necessary. You should generally use React NPM package unless you have good reason not to. -## Tips and Tricks +### Source Maps -**XXX could/should be in another file** +If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. -**How to get an error in your console too:** +Source maps slow down your project. Use `devtool: 'eval'` for best build performance. -The `Redbox` errors are great to clearly see rendering issues, and avoiding an uncaught error from breaking your app. -But there are some advantages to a thrown error in the console too, like filename resolution via sourcemaps, and click-to-open. -To get the best of both worlds, modify your app entry point as follows: - -```jsx -import Redbox from 'redbox-react'; - -const consoleErrorReporter = ({error}) => { - // We throw in a different context, so the app still doesn't break! - setTimeout(() => { throw error; }); - return ; -}; - -consoleErrorReporter.propTypes = { - error: React.PropTypes.error -}; - -render(( - - - -), document.getElementById('react-root')); -``` +Hot reloading code is just one line in the beginning and one line in the end of each module so you might not need source maps at all. -## Where to ask for Help +### React Hot API -**XXX** +If you're authoring a build tool, you might be interested to hear that React Hot Loader brains have been extracted into runtime-agnostic [React Hot API](https://github.com/gaearon/react-hot-api). React Hot Loader just binds that API to Webpack runtime, but you can implement yours too. From ee4f18d3cfb13f663acdef38a36f0a9dffd353e6 Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Tue, 26 Apr 2016 15:28:44 +0200 Subject: [PATCH 069/161] Add Tips & Tricks doc --- docs/TipsAndTricks.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/TipsAndTricks.md diff --git a/docs/TipsAndTricks.md b/docs/TipsAndTricks.md new file mode 100644 index 000000000..a47e9eff7 --- /dev/null +++ b/docs/TipsAndTricks.md @@ -0,0 +1,24 @@ +# Tips and Tricks + +**How to get an error in your console too:** + +The `Redbox` errors are great to clearly see rendering issues, and avoiding an uncaught error from breaking your app. But there are some advantages to a thrown error in the console too, like filename resolution via sourcemaps, and click-to-open. To get the best of both worlds, modify your app entry point as follows: + +```jsx +import Redbox from 'redbox-react'; + +const consoleErrorReporter = ({error}) => { + console.error(error); + return ; +}; + +consoleErrorReporter.propTypes = { + error: React.PropTypes.error +}; + +render(( + + + +), document.getElementById('react-root')); +``` \ No newline at end of file From aae66bfbd503c00bd2acea2565ea15c8214646de Mon Sep 17 00:00:00 2001 From: Jon Brito Date: Wed, 27 Apr 2016 11:23:13 -0400 Subject: [PATCH 070/161] Add backwards compat in prod. - Add a new method to check child type. - Style. --- src/AppContainer.dev.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index fd8d7f1cd..6b750a916 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -38,13 +38,7 @@ class AppContainer extends Component { componentWillReceiveProps(nextProps) { // Hot reload is happening. // Retry rendering! - if (nextProps.component !== this.props.component) { - this.setState({ - error: null - }); - } - - if (nextProps.children.type !== this.props.children.type) { + if (this.hasChildTypeChanged(nextProps)) { this.setState({ error: null }); @@ -55,11 +49,7 @@ class AppContainer extends Component { // Hot reload has finished. // Force-update the whole tree, including // components that refuse to update. - if (prevProps.component !== this.props.component) { - deepForceUpdate(this); - } - - if (prevProps.children.type !== this.props.children.type) { + if (this.hasChildTypeChanged(prevProps)) { deepForceUpdate(this); } } @@ -74,6 +64,19 @@ class AppContainer extends Component { }); } + hasChildTypeChanged(props) { + // Backwards compat + if (props.component !== this.props.component) { + return true; + } + + if (props.children.type !== this.props.children.type) { + return true; + } + + return false; + } + render() { const { error } = this.state; if (error) { From 80e7b66fff2f90844f7f2b001c9a7f5423860e4f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Apr 2016 16:52:18 +0100 Subject: [PATCH 071/161] Refactor and add more tests --- package.json | 1 + src/AppContainer.dev.js | 2 + src/AppContainer.js | 4 +- src/AppContainer.prod.js | 2 + src/fixupReactRouter.js | 55 +++-- src/index.js | 2 + src/patch.dev.js | 25 ++- src/patch.js | 4 +- src/patch.prod.js | 2 +- test/fixupReactRouter/index.js | 380 +++++++++++++++++++++++++-------- 10 files changed, 351 insertions(+), 126 deletions(-) diff --git a/package.json b/package.json index 884beff24..0fdf71f00 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "babel-preset-react": "^6.5.0", "expect": "^1.16.0", "mocha": "^2.4.5", + "react": "^15.0.1", "rimraf": "^2.5.2" } } diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index b91eaf90a..ddf08e48f 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -1,3 +1,5 @@ +'use strict'; + const React = require('react'); const deepForceUpdate = require('react-deep-force-update'); const Redbox = require('redbox-react'); diff --git a/src/AppContainer.js b/src/AppContainer.js index 319bde127..52e4ae279 100644 --- a/src/AppContainer.js +++ b/src/AppContainer.js @@ -1,5 +1,7 @@ +'use strict'; + if (process.env.NODE_ENV === 'production') { module.exports = require('./AppContainer.prod'); } else { module.exports = require('./AppContainer.dev'); -} \ No newline at end of file +} diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js index ae628782a..7de0f8210 100644 --- a/src/AppContainer.prod.js +++ b/src/AppContainer.prod.js @@ -1,3 +1,5 @@ +'use strict'; + const React = require('react'); const { Component } = React; diff --git a/src/fixupReactRouter.js b/src/fixupReactRouter.js index 31b4784b6..950166583 100644 --- a/src/fixupReactRouter.js +++ b/src/fixupReactRouter.js @@ -1,3 +1,6 @@ +'use strict'; + +import { Children, isValidElement } from 'react'; function ensureArray(maybeArray) { if (!maybeArray) { @@ -19,34 +22,40 @@ function isReactRouterish(type) { } function extractComponents(routes) { - return routes + return ensureArray(routes) + .filter(Boolean) .map(route => { - // The route properties are at different locations depending on if plain routes - // or regular element routes are used - const component = route.component || (route.props && route.props.component); - const componentsMap = route.components || (route.props && route.props.components) || {}; - const childRoutes = route.childRoutes || (route.props && ensureArray(route.props.children)); + const isElement = isValidElement(route); + const component = isElement ? route.props.component : route.component; + const namedComponentsByKey = isElement ? route.props.components : route.components; - const components = Object.keys(componentsMap).map(key => componentsMap[key]); - const childRouteComponents = childRoutes && extractComponents(childRoutes); + const indexRoute = route.indexRoute; + const childRoutes = isElement ? + Children.toArray(route.props.children) : + ensureArray(route.childRoutes); + const namedComponents = Object.keys(namedComponentsByKey || {}).map(key => + namedComponentsByKey[key] + ); - return [].concat(component, components, childRouteComponents); + const indexRouteComponents = indexRoute && extractComponents(indexRoute); + const childRouteComponents = childRoutes && extractComponents(childRoutes); + return [].concat( + component, + namedComponents, + indexRouteComponents, + childRouteComponents + ); }) - .reduce((reduceTarget, components) => reduceTarget.concat(components), []) // flatten - .filter(c => typeof(c) === 'function'); + .reduce((flattened, candidates) => flattened.concat(candidates), []) + .filter(c => typeof c === 'function'); } -function fixupReactRouter(props, forceUpdateFunction) { - // Ideally we want to teach React Router to receive children. - // We're not in a perfect world, and a dirty workaround works for now. - // https://github.com/reactjs/react-router/issues/2182 - const routes = props.routes || ensureArray(props.children); - const components = extractComponents(routes); - components.forEach((component) => { - // Side effect 😱 - // Force proxies to update since React Router ignores new props. - forceUpdateFunction(component); - }); +function extractRouteHandlerComponents(props) { + const routes = props.routes || Children.toArray(props.children); + return extractComponents(routes); } -module.exports = { isReactRouterish, fixupReactRouter }; +module.exports = { + isReactRouterish, + extractRouteHandlerComponents +}; diff --git a/src/index.js b/src/index.js index cf3913a07..560d947ac 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +'use strict'; + const AppContainer = require('./AppContainer'); module.exports = function warnAboutIncorrectUsage(arg) { diff --git a/src/patch.dev.js b/src/patch.dev.js index d4f9b99d0..5756144d0 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,6 +1,8 @@ +'use strict'; + const React = require('react'); const resolveType = require('./resolveType'); -const { isReactRouterish, fixupReactRouter } = require('./fixupReactRouter'); +const { isReactRouterish, extractRouteHandlerComponents } = require('./fixupReactRouter'); if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); @@ -8,17 +10,26 @@ if (React.createElement.isPatchedByReactHotLoader) { const createElement = React.createElement; function patchedCreateElement(type, props, ...args) { + // Trick React into rendering a proxy so that + // its state is preserved when the class changes. + // This will update the proxy if it's for a known type. + const resolvedType = resolveType(type); + const element = createElement(resolvedType, props, ...args); + // This is lame but let's focus on shipping. // https://github.com/gaearon/react-hot-loader/issues/249 if (isReactRouterish(type)) { - fixupReactRouter(props, resolveType); + // Ideally we want to teach React Router to receive children. + // We're not in a perfect world, and a dirty workaround works for now. + // https://github.com/reactjs/react-router/issues/2182 + const resolvedProps = element.props; + const routeHandlers = extractRouteHandlerComponents(resolvedProps, resolveType); + // Side effect 😱 + // Force proxies to update since React Router ignores new props. + routeHandlers.forEach(resolveType); } - // Trick React into rendering a proxy so that - // its state is preserved when the class changes. - // This will update the proxy if it's for a known type. - const resolvedType = resolveType(type); - return createElement(resolvedType, props, ...args); + return element; } patchedCreateElement.isPatchedByReactHotLoader = true; React.createElement = patchedCreateElement; diff --git a/src/patch.js b/src/patch.js index f04b3ded1..74b298fe3 100644 --- a/src/patch.js +++ b/src/patch.js @@ -1,5 +1,7 @@ +'use strict'; + if (process.env.NODE_ENV === 'production') { module.exports = require('./patch.prod'); } else { module.exports = require('./patch.dev'); -} \ No newline at end of file +} diff --git a/src/patch.prod.js b/src/patch.prod.js index ce13cc68c..94195180d 100644 --- a/src/patch.prod.js +++ b/src/patch.prod.js @@ -1 +1 @@ -/* noop */ \ No newline at end of file +/* noop */ diff --git a/test/fixupReactRouter/index.js b/test/fixupReactRouter/index.js index 1a0638351..a1b876c67 100644 --- a/test/fixupReactRouter/index.js +++ b/test/fixupReactRouter/index.js @@ -1,127 +1,321 @@ -import { isReactRouterish, fixupReactRouter } from '../../src/fixupReactRouter'; +import React from 'react'; +import { isReactRouterish, extractRouteHandlerComponents } from '../../src/fixupReactRouter'; import expect, { createSpy } from 'expect'; -const forceUpdateSpy = createSpy(); +const spy = createSpy(); +function Router() {} +function Route() {}; +function IndexRoute() {}; +function Redirect() {}; +function NotRouter() {}; describe('isReactRouterish', () => { describe('when given a router', () => { it('is true when given a router', () => { - const Router = { displayName: 'Router' }; expect(isReactRouterish(Router)).toBe(true); }); }); describe('when not given a router', () => { it('returns false', () => { - const NotRouter = { displayName: 'DefinitelyNotARouter' }; expect(isReactRouterish(NotRouter)).toBe(false); }); }); }); -describe('fixupReactRouter', () => { - beforeEach(() => { - forceUpdateSpy.reset(); +describe('extractRouteHandlerComponents', () => { + it('handles something that is not a React Router', () => { + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([]); }); - describe('when routes are passed in as children', () => { - /* Equivalent to: - - - - */ - - const props = { - children: [ - { props: { component: function a() {} } }, - { props: { component: function b() {} } }, - ], - }; - - it('calls force update on each route\'s component', () => { - fixupReactRouter(props, forceUpdateSpy); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[0].props.component); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.children[1].props.component); + describe('when routes are passed as React elements in "children" prop', () => { + it('handles no routes', () => { + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([]); }); - }); - describe('when routes are passed in as children of children', () => { - /* Equivalent to: - - - - - */ - - const props = { - children: { - props: { - component: function a() {}, - children: { - props: { component: function b() {} }, - }, - }, - } - }; - - it('calls force update on each route\'s component', () => { - fixupReactRouter(props, forceUpdateSpy); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.children.props.component); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.children.props.children.props.component); + it('handles a single route', () => { + function a() {} + const element = ( + + + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a]); + }); + + it('handles a single note with multiple components', () => { + function a() {} + function b() {} + function c() {} + const element = ( + + + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b, c]); + }); + + it('handles multiple routes', () => { + function a() {} + function b() {} + const element = ( + + + + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b]); }); - }); - describe('when routes are passed in using the `routes` prop', () => { - const props = { - routes: [ - { component: function a() {} }, - { component: function b() {} }, - ], - }; + it('handles deep routes of different types', () => { + function a() {} + function b() {} + function c() {} + function d() {} + function e() {} + function f() {} + function g() {} + function h() {} + const element = ( + + + + + + + + + + + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b, c, d, e, f, g, h]); + }); - it('calls force update on each route\'s components', () => { - fixupReactRouter(props, forceUpdateSpy); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[1].component); + it('ignores broken routes', () => { + function a() {} + function b() {} + const element = ( + + + + + + + + + {null} + + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b]); }); }); - describe('when routes are passed in using the `routes` and `childRoutes` props', () => { - const props = { - routes: [ - { - component: function a() {}, - childRoutes: [ - { component: function b() {} }, - ], - }, - ], - }; - - it('calls force update on each route\'s components', () => { - fixupReactRouter(props, forceUpdateSpy); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].component); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].childRoutes[0].component); + describe('when routes are passed as React elements in "routes" prop', () => { + it('handles a single route', () => { + function a() {} + const element = ( + + } /> + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a]); + }); + + it('handles a single note with multiple components', () => { + function a() {} + function b() {} + function c() {} + const element = ( + + } /> + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b, c]); + }); + + it('handles multiple routes', () => { + function a() {} + function b() {} + const element = ( + , + + ]} /> + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b]); + }); + + it('handles deep routes of different types', () => { + function a() {} + function b() {} + function c() {} + function d() {} + function e() {} + function f() {} + function g() {} + function h() {} + const element = ( + + + + + + + , + , + + ]} /> + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b, c, d, e, f, g, h]); + }); + + it('ignores broken routes', () => { + function a() {} + function b() {} + const element = ( + + + + + + + + {null} + + } /> + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b]); }); }); - describe('when there are multiple components on one route', () => { - const props = { - routes: [ - { - components: { - a: function a() {}, - b: function b() {}, - }, - }, - ], - }; - - it('calls force update on each component', () => { - fixupReactRouter(props, forceUpdateSpy); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.a); - expect(forceUpdateSpy).toHaveBeenCalledWith(props.routes[0].components.b); + describe('when routes are passed as plain objects in "routes" prop', () => { + it('handles a single route', () => { + function a() {} + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a]); + }); + + it('handles a single note with multiple components', () => { + function a() {} + function b() {} + function c() {} + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b, c]); + }); + + it('handles multiple routes', () => { + function a() {} + function b() {} + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b]); + }); + + it('handles deep routes of different types', () => { + function a() {} + function b() {} + function c() {} + function d() {} + function e() {} + function f() {} + function g() {} + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b, c, d, e, f, g]); + }); + + it('ignores broken routes', () => { + function a() {} + function b() {} + const element = ( + + ); + expect( + extractRouteHandlerComponents(element.props) + ).toEqual([a, b]); }); }); }); From b7cca0f9dee26ef9a43c6bf4837d7819bc6612c2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 28 Apr 2016 17:16:34 +0100 Subject: [PATCH 072/161] 3.0.0-alpha.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0fdf71f00..d698490ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.12", + "version": "3.0.0-alpha.13", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From a53f3a74f6b416627e5227f65d0711e8d3393a31 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 30 Apr 2016 17:19:02 +0100 Subject: [PATCH 073/161] Fix backward compat for children prop --- src/AppContainer.dev.js | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 4470d406c..8420a6213 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -72,11 +72,9 @@ class AppContainer extends Component { return true; } - if (props.children.type !== this.props.children.type) { - return true; - } - - return false; + return props.children && + this.props.children && + props.children.type !== this.props.children.type; } render() { @@ -94,13 +92,25 @@ class AppContainer extends Component { } AppContainer.propTypes = { - children: function (props, propName, componentName, location, propFullName) { - if (typeof props[propName].type !== 'function') { - return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a single React element with your app’s root component, e.g. .`); + component: function (props) { + if (props.component) { + return new Error( + `Passing "component" prop to is deprecated. ` + + `Replace with .` + ); } - - if (React.Children.count(props[propName]) > 1) { - return new Error(`Invalid prop ${propFullName} supplied to ${componentName}. Expected a single React element with your app’s root component, e.g. .`); + }, + props: function (props) { + if (props.props) { + return new Error( + `Passing "props" prop to is deprecated. ` + + `Replace with .` + ); + } + }, + children: function (props) { + if (React.Children.count(props.children) !== 1) { + return new Error(`Invalid prop "children" supplied to AppContainer. Expected a single React element with your app’s root component, e.g. .`); } } }; From b11621b2b4e9f5ee0e48fd828f838c6a49cb9a5c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 30 Apr 2016 17:49:50 +0100 Subject: [PATCH 074/161] Ensure we emit semis before and after generated code Fixes #263 --- src/babel/index.js | 9 +++++---- src/webpack/index.js | 7 ++++++- src/webpack/tagCommonJSExports.js | 2 +- test/babel/fixtures/bindings/expected.js | 3 +++ test/babel/fixtures/counter/expected.js | 5 ++++- test/babel/fixtures/issue-246/expected.js | 5 ++++- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index d0dc54896..5b43ef14e 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -1,8 +1,7 @@ import template from 'babel-template'; -const buildRegistration = template(` - tagSource(ID, NAME); -`); +const buildRegistration = template('tagSource(ID, NAME);'); +const buildSemi = template(';'); const buildTagger = template(` (function () { function tagSource(fn, localName) { @@ -126,12 +125,14 @@ module.exports = function(args) { // Inject the generated tagging code at the very end // so that it is as minimally intrusive as possible. + node.body.push(buildSemi()); node.body.push( buildTagger({ FILENAME: t.stringLiteral(file.opts.filename), REGISTRATIONS: registrations }) - ) + ); + node.body.push(buildSemi()); } } } diff --git a/src/webpack/index.js b/src/webpack/index.js index 8fd197d73..8f4aef379 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -30,7 +30,12 @@ function transform(source, map) { tagCommonJSExportsSource = fs.readFileSync( path.join(__dirname, 'tagCommonJSExports.js'), 'utf8' - ).split(/\n\s*/).join(' '); + ) + // Babel inserts these. + // Ideally we'd opt out for one file but this is simpler. + .replace(/['"]use strict['"];/, '') + .split(/\n\s*/) + .join(' '); } // Parameterize the helper with the current filename. diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index 8367b4e56..aa6af029c 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,4 +1,4 @@ -(function () { +;(function () { /* react-hot-loader/webpack */ if (process.env.NODE_ENV !== 'production') { function tagSource(fn, localName) { diff --git a/test/babel/fixtures/bindings/expected.js b/test/babel/fixtures/bindings/expected.js index 5a706fa0f..9d4065aa8 100644 --- a/test/babel/fixtures/bindings/expected.js +++ b/test/babel/fixtures/bindings/expected.js @@ -20,6 +20,7 @@ var { Z } = require("left-pad"); const _default = React.createClass({}); export default _default; +; (function () { function tagSource(fn, localName) { @@ -50,3 +51,5 @@ export default _default; tagSource(E, "E"); tagSource(_default, "default"); })(); + +; diff --git a/test/babel/fixtures/counter/expected.js b/test/babel/fixtures/counter/expected.js index 9d5a59b23..9b0ee5812 100644 --- a/test/babel/fixtures/counter/expected.js +++ b/test/babel/fixtures/counter/expected.js @@ -19,6 +19,7 @@ var _default = function _default() { }; exports.default = _default; +; (function () { function tagSource(fn, localName) { @@ -44,4 +45,6 @@ exports.default = _default; tagSource(Counter, "Counter"); tagSource(_default, "default"); -})(); \ No newline at end of file +})(); + +; diff --git a/test/babel/fixtures/issue-246/expected.js b/test/babel/fixtures/issue-246/expected.js index dcd5d93f0..49eb296e3 100644 --- a/test/babel/fixtures/issue-246/expected.js +++ b/test/babel/fixtures/issue-246/expected.js @@ -11,6 +11,7 @@ function spread() { return args.push(1); } +; (function () { function tagSource(fn, localName) { @@ -35,4 +36,6 @@ function spread() { } tagSource(spread, "spread"); -})(); \ No newline at end of file +})(); + +; From 146f8d5ac68714e252e42fdf2afc01180ebcc030 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 30 Apr 2016 17:55:05 +0100 Subject: [PATCH 075/161] 3.0.0-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d698490ae..0e459640e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-alpha.13", + "version": "3.0.0-beta.0", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From dcf836ff14cb86ed648779beff27c5ba73039fc6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sat, 30 Apr 2016 22:42:45 +0100 Subject: [PATCH 076/161] added AppContainer test --- package.json | 4 + test/AppContainer/AppContainer.dev.js | 160 ++++++++++++++++++++++++++ test/AppContainer/index.js | 1 + test/AppContainer/setup.js | 21 ++++ test/index.js | 1 + 5 files changed, 187 insertions(+) create mode 100644 test/AppContainer/AppContainer.dev.js create mode 100644 test/AppContainer/index.js create mode 100644 test/AppContainer/setup.js diff --git a/package.json b/package.json index 0e459640e..03e14ea47 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,13 @@ "babel-core": "^6.7.6", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", + "enzyme": "^2.2.0", "expect": "^1.16.0", + "jsdom": "^8.4.1", "mocha": "^2.4.5", "react": "^15.0.1", + "react-addons-test-utils": "^15.0.2", + "react-dom": "^15.0.2", "rimraf": "^2.5.2" } } diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js new file mode 100644 index 000000000..64a1f4a54 --- /dev/null +++ b/test/AppContainer/AppContainer.dev.js @@ -0,0 +1,160 @@ +import './setup' +import React, {Component} from 'react' +import expect, {createSpy} from 'expect' +import {mount} from 'enzyme' + +import AppContainer from '../../src/AppContainer.dev' + +const tag = (comp, name) => comp.__source = { fileName: name, localName: name } + +describe('', () => { + describe('when passed children', () => { + it('renders it', () => { + const spy = createSpy() + class App extends Component { + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) + + it('doesnt force update the tree when receiving the same', () => { + const spy = createSpy() + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + wrapper.setProps({children: }) + expect(spy.calls.length).toBe(1) + }) + + it('force updates the tree when receiving different', () => { + const spy = createSpy() + + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
ho
+ } + } + tag(App, 'App') + wrapper.setProps({children: }) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + }) + + describe('when passed component prop', () => { + it('renders it', () => { + const spy = createSpy() + class App extends Component { + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) + + it('doesnt force update the tree when receiving the same', () => { + const spy = createSpy() + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + wrapper.setProps({component: App}) + expect(spy.calls.length).toBe(1) + }) + + it('force updates the tree when receiving different', () => { + const spy = createSpy() + + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
ho
+ } + } + tag(App, 'App') + wrapper.setProps({component: App}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + }) +}) diff --git a/test/AppContainer/index.js b/test/AppContainer/index.js new file mode 100644 index 000000000..920ccbf6a --- /dev/null +++ b/test/AppContainer/index.js @@ -0,0 +1 @@ +import './AppContainer.dev' diff --git a/test/AppContainer/setup.js b/test/AppContainer/setup.js new file mode 100644 index 000000000..cb827ae1b --- /dev/null +++ b/test/AppContainer/setup.js @@ -0,0 +1,21 @@ +// the if statement is to prevent patching again when using mocha watch mode +if (!require('react').createElement.isPatchedByReactHotLoader) + require('../../src/patch.dev') + +// copied from https://github.com/lelandrichardson/enzyme-example-mocha/blob/master/test/.setup.js +var jsdom = require('jsdom').jsdom; + +var exposedProperties = ['window', 'navigator', 'document']; + +global.document = jsdom(''); +global.window = document.defaultView; +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: 'node.js' +}; diff --git a/test/index.js b/test/index.js index dd764961d..924d7fd8b 100644 --- a/test/index.js +++ b/test/index.js @@ -1,2 +1,3 @@ import './babel'; import './fixupReactRouter'; +import './AppContainer'; From 2a1e384d54e1919117f70f75dd20ad2490b1d9f5 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sun, 1 May 2016 17:13:03 +0100 Subject: [PATCH 077/161] fixed force update for AppContainer children --- src/AppContainer.dev.js | 21 ++++----------------- test/AppContainer/AppContainer.dev.js | 8 ++++---- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 8420a6213..49a591715 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -40,18 +40,16 @@ class AppContainer extends Component { componentWillReceiveProps(nextProps) { // Hot reload is happening. // Retry rendering! - if (this.hasChildTypeChanged(nextProps)) { - this.setState({ - error: null - }); - } + this.setState({ + error: null + }); } componentDidUpdate(prevProps) { // Hot reload has finished. // Force-update the whole tree, including // components that refuse to update. - if (this.hasChildTypeChanged(prevProps)) { + if (prevProps !== this.props) { deepForceUpdate(this); } } @@ -66,17 +64,6 @@ class AppContainer extends Component { }); } - hasChildTypeChanged(props) { - // Backwards compat - if (props.component !== this.props.component) { - return true; - } - - return props.children && - this.props.children && - props.children.type !== this.props.children.type; - } - render() { const { error } = this.state; if (error) { diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 64a1f4a54..1b172e9bd 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -25,7 +25,7 @@ describe('', () => { expect(spy.calls.length).toBe(1) }) - it('doesnt force update the tree when receiving the same', () => { + it('force updates the tree when receiving the same', () => { const spy = createSpy() class App extends Component { shouldComponentUpdate() { @@ -42,7 +42,7 @@ describe('', () => { const wrapper = mount() expect(spy.calls.length).toBe(1) wrapper.setProps({children: }) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2) }) it('force updates the tree when receiving different', () => { @@ -100,7 +100,7 @@ describe('', () => { expect(spy.calls.length).toBe(1) }) - it('doesnt force update the tree when receiving the same', () => { + it('force updates the tree when receiving the same', () => { const spy = createSpy() class App extends Component { shouldComponentUpdate() { @@ -117,7 +117,7 @@ describe('', () => { const wrapper = mount() expect(spy.calls.length).toBe(1) wrapper.setProps({component: App}) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2) }) it('force updates the tree when receiving different', () => { From aa80800d301f79aa44ec31d949166354490ae47e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sun, 1 May 2016 18:54:13 +0100 Subject: [PATCH 078/161] More tests for AppContainer (#273) * added test for keeping state on hot reload with class and sfc root component * tests for HOC-wrapped root --- package.json | 1 + src/AppContainer.dev.js | 8 +- test/AppContainer/AppContainer.dev.js | 323 ++++++++++++++++++-------- 3 files changed, 228 insertions(+), 104 deletions(-) diff --git a/package.json b/package.json index 03e14ea47..4aeb5dc11 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react": "^15.0.1", "react-addons-test-utils": "^15.0.2", "react-dom": "^15.0.2", + "recompose": "^0.17.0", "rimraf": "^2.5.2" } } diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 49a591715..8ab2800a4 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -43,15 +43,9 @@ class AppContainer extends Component { this.setState({ error: null }); - } - - componentDidUpdate(prevProps) { - // Hot reload has finished. // Force-update the whole tree, including // components that refuse to update. - if (prevProps !== this.props) { - deepForceUpdate(this); - } + deepForceUpdate(this); } // This hook is going to become official in React 15.x. diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 1b172e9bd..e6f7bafb4 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -9,152 +9,281 @@ const tag = (comp, name) => comp.__source = { fileName: name, localName: name } describe('', () => { describe('when passed children', () => { - it('renders it', () => { - const spy = createSpy() - class App extends Component { - render() { - spy() - return
hey
+ describe('with class root', () => { + it('renders it', () => { + const spy = createSpy() + class App extends Component { + render() { + spy() + return
hey
+ } } - } - tag(App, 'App') + tag(App, 'App') - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - it('force updates the tree when receiving the same', () => { - const spy = createSpy() - class App extends Component { - shouldComponentUpdate() { - return false - } + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - render() { - spy() - return
hey
- } - } - tag(App, 'App') + class App extends Component { + shouldComponentUpdate() { + return false + } - const wrapper = mount() - expect(spy.calls.length).toBe(1) - wrapper.setProps({children: }) - expect(spy.calls.length).toBe(2) - }) + render() { + spy() + return
hey
+ } + } + tag(App, 'App') - it('force updates the tree when receiving different', () => { - const spy = createSpy() + const wrapper = mount() + expect(spy.calls.length).toBe(1) - class App extends Component { - shouldComponentUpdate() { - return false - } + { + class App extends Component { + shouldComponentUpdate() { + return false + } - render() { - spy() - return
hey
+ render() { + spy() + return
ho
+ } + } + tag(App, 'App') + wrapper.setProps({children: }) } - } - tag(App, 'App') - const wrapper = mount() - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { + it('hot-reloads without losing state', () => { class App extends Component { + componentWillMount() { + this.state = 'old' + } + shouldComponentUpdate() { return false } render() { - spy() - return
ho
+ return
old render + {this.state} state
} } tag(App, 'App') - wrapper.setProps({children: }) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') + + { + class App extends Component { + componentWillMount() { + this.state = 'new' + } + + shouldComponentUpdate() { + return false + } + + render() { + return
new render + {this.state} state
+ } + } + tag(App, 'App') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state') + }) }) - }) - describe('when passed component prop', () => { - it('renders it', () => { - const spy = createSpy() - class App extends Component { - render() { + describe('with SFC root', () => { + it('renders it', () => { + const spy = createSpy() + const App = () => { spy() return
hey
} - } - tag(App, 'App') + tag(App, 'App') - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - it('force updates the tree when receiving the same', () => { - const spy = createSpy() - class App extends Component { - shouldComponentUpdate() { - return false - } + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - render() { + const App = () => { spy() return
hey
} - } - tag(App, 'App') + tag(App, 'App') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { + const App = () => { + spy() + return
ho
+ } + tag(App, 'App') + wrapper.setProps({children: }) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const wrapper = mount() - expect(spy.calls.length).toBe(1) - wrapper.setProps({component: App}) - expect(spy.calls.length).toBe(2) + it('hot-reloads without losing state', () => { + class App extends Component { + componentWillMount() { + this.state = 'old' + } + + shouldComponentUpdate() { return false } + + render() { + return
old render + {this.state} state
+ } + } + tag(App, 'App') + + const Root = () => + tag(Root, 'Root') + + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') + + { + class App extends Component { + componentWillMount() { + this.state = 'new' + } + + shouldComponentUpdate() { return false } + + render() { + return
new render + {this.state} state
+ } + } + tag(App, 'App') + + const Root = () => + tag(Root, 'Root') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state') + }) }) - it('force updates the tree when receiving different', () => { - const spy = createSpy() + describe('with HOC-wrapped root', () => { + const mapProps = require('recompose').mapProps + it('renders it', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
+ } + } + tag(App, 'App') + + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + tag(Enhanced, 'Enhanced') + + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(wrapper.find('App').prop('n')).toBe(15) + expect(spy.calls.length).toBe(1) + }) - class App extends Component { - shouldComponentUpdate() { - return false + it('force updates the tree on receiving new children', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
+ } } + tag(App, 'App') - render() { - spy() - return
hey
+ const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + tag(Enhanced, 'Enhanced') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { + class App extends React.Component { + render() { + spy() + return
ho
+ } + } + tag(App, 'App') + + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + tag(Enhanced, 'Enhanced') + wrapper.setProps({children: }) } - } - tag(App, 'App') - const wrapper = mount() - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { + it('hot-reloads without losing state', () => { class App extends Component { - shouldComponentUpdate() { - return false + componentWillMount() { + this.state = 'old' } + shouldComponentUpdate() { return false } + render() { - spy() - return
ho
+ return
old render + {this.state} state + {this.props.n}
} } tag(App, 'App') - wrapper.setProps({component: App}) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + tag(Enhanced, 'Enhanced') + + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state + 15') + + { + class App extends Component { + componentWillMount() { + this.state = 'new' + } + + shouldComponentUpdate() { return false } + + render() { + return
new render + {this.state} state + {this.props.n}
+ } + } + tag(App, 'App') + + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + tag(Enhanced, 'Enhanced') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state + 20') + }) }) }) }) From 6522b76117547d443bdaa29d8963a5a0140c5202 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 1 May 2016 23:19:31 +0100 Subject: [PATCH 079/161] Bump React dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4aeb5dc11..911974a76 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "expect": "^1.16.0", "jsdom": "^8.4.1", "mocha": "^2.4.5", - "react": "^15.0.1", + "react": "^15.0.2", "react-addons-test-utils": "^15.0.2", "react-dom": "^15.0.2", "recompose": "^0.17.0", From 55de8a02e3e410347c0513c4dd7a042f48bd9565 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 1 May 2016 14:38:49 +0100 Subject: [PATCH 080/161] (WIP) Get rid of _source --- src/babel/index.js | 16 ++---------- src/patch.dev.js | 20 ++------------- src/resolveType.js | 62 ++++++++++------------------------------------ 3 files changed, 17 insertions(+), 81 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index 5b43ef14e..903f3391c 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -9,20 +9,8 @@ const buildTagger = template(` return; } - if (fn.hasOwnProperty("__source")) { - return; - } - - try { - Object.defineProperty(fn, "__source", { - enumerable: false, - configurable: true, - value: { - fileName: FILENAME, - localName: localName - } - }); - } catch (err) {} + var id = FILENAME + '#' + localName; + __REACT_HOT_LOADER__.set(id, fn); } REGISTRATIONS })(); diff --git a/src/patch.dev.js b/src/patch.dev.js index 5756144d0..f42c1cf5a 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -2,34 +2,18 @@ const React = require('react'); const resolveType = require('./resolveType'); -const { isReactRouterish, extractRouteHandlerComponents } = require('./fixupReactRouter'); if (React.createElement.isPatchedByReactHotLoader) { throw new Error('Cannot patch React twice.'); } const createElement = React.createElement; -function patchedCreateElement(type, props, ...args) { +function patchedCreateElement(type, ...args) { // Trick React into rendering a proxy so that // its state is preserved when the class changes. // This will update the proxy if it's for a known type. const resolvedType = resolveType(type); - const element = createElement(resolvedType, props, ...args); - - // This is lame but let's focus on shipping. - // https://github.com/gaearon/react-hot-loader/issues/249 - if (isReactRouterish(type)) { - // Ideally we want to teach React Router to receive children. - // We're not in a perfect world, and a dirty workaround works for now. - // https://github.com/reactjs/react-router/issues/2182 - const resolvedProps = element.props; - const routeHandlers = extractRouteHandlerComponents(resolvedProps, resolveType); - // Side effect 😱 - // Force proxies to update since React Router ignores new props. - routeHandlers.forEach(resolveType); - } - - return element; + return createElement(resolvedType, ...args); } patchedCreateElement.isPatchedByReactHotLoader = true; React.createElement = patchedCreateElement; diff --git a/src/resolveType.js b/src/resolveType.js index b9925cc83..0049cb8a2 100644 --- a/src/resolveType.js +++ b/src/resolveType.js @@ -1,17 +1,13 @@ const createProxy = require('react-proxy').default; -let proxies = {}; -let warnedAboutTypes = {}; +let proxiesByID = {}; +let idsByType = new WeakMap(); -function setFlag(obj, key) { - try { - Object.defineProperty(obj, key, { - configurable: true, - enumerable: false, - value: true - }); - } catch (err) {} -} +window.__REACT_HOT_LOADER__ = { + set(id, component) { + idsByType.set(component, id); + } +}; function resolveType(type) { // We only care about composite components @@ -19,54 +15,22 @@ function resolveType(type) { return type; } - // If the type is not tagged, return it as is. - if ( - !Object.hasOwnProperty.call(type, '__source') || - !type.__source || - !type.__source.fileName || - !type.__source.localName - ) { - setFlag(type, '__noSourceFound'); - return type; - } - - // Uniquely identifiy the component in code across reloads. - const source = type.__source; - const id = source.fileName + '#' + source.localName; - - if (type.hasOwnProperty('__noSourceFound')) { - // This component didn't have a source last time, but now it has? - // This means createElement() was called during module definition. - // Bail out, or the component will be unmounted unexpectedly this time, - // as we'll return proxy but we returned the original class the last time. - // https://github.com/gaearon/react-hot-loader/issues/241 - if (!warnedAboutTypes[id]) { - warnedAboutTypes[id] = true; - console.error( - `React Hot Loader: ${source.localName} from ${source.fileName} will not ` + - `hot reload correctly because it contains an imperative call like ` + - `ReactDOM.render() in the same file. Split ${source.localName} into a ` + - `separate file for hot reloading to work.` - ); - } + var id = idsByType.get(type); + if (!id) { return type; } // We use React Proxy to generate classes that behave almost // the same way as the original classes but are updatable with // new versions without destroying original instances. - if (!proxies[id]) { - proxies[id] = createProxy(type); + if (!proxiesByID[id]) { + proxiesByID[id] = createProxy(type); } else if (!type.hasOwnProperty('__hasBeenUsedForProxy')) { - proxies[id].update(type); + proxiesByID[id].update(type); } - // Don't update proxy with the same class. - // This makes sure stale old classes never revive. - // https://github.com/gaearon/react-hot-loader/issues/248 - setFlag(type, '__hasBeenUsedForProxy'); // Give proxy class to React instead of the real class. - return proxies[id].get(); + return proxiesByID[id].get(); } module.exports = resolveType; From fa2b7453e3ba4607b0e307abe7491112d7262e8d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 1 May 2016 23:37:04 +0100 Subject: [PATCH 081/161] Add global dep and fix Babel tests --- package.json | 1 + src/babel/index.js | 9 +++++++-- src/resolveType.js | 3 ++- src/webpack/tagCommonJSExports.js | 21 ++++++++------------- test/babel/fixtures/bindings/expected.js | 21 ++++++++------------- test/babel/fixtures/counter/expected.js | 21 ++++++++------------- test/babel/fixtures/issue-246/expected.js | 21 ++++++++------------- 7 files changed, 42 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 911974a76..24fa7657b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "babel-template": "^6.7.0", + "global": "^4.3.0", "react-deep-force-update": "^2.0.1", "react-proxy": "^3.0.0-alpha.0", "redbox-react": "^1.2.3", diff --git a/src/babel/index.js b/src/babel/index.js index 903f3391c..af7c05c6f 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -4,13 +4,18 @@ const buildRegistration = template('tagSource(ID, NAME);'); const buildSemi = template(';'); const buildTagger = template(` (function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + var fileName = FILENAME; function tagSource(fn, localName) { if (typeof fn !== "function") { return; } - var id = FILENAME + '#' + localName; - __REACT_HOT_LOADER__.set(id, fn); + var id = fileName + '#' + localName; + __REACT_HOT_LOADER__.register(id, fn); } REGISTRATIONS })(); diff --git a/src/resolveType.js b/src/resolveType.js index 0049cb8a2..a59541e8e 100644 --- a/src/resolveType.js +++ b/src/resolveType.js @@ -1,9 +1,10 @@ const createProxy = require('react-proxy').default; +const global = require('global'); let proxiesByID = {}; let idsByType = new WeakMap(); -window.__REACT_HOT_LOADER__ = { +global.__REACT_HOT_LOADER__ = { set(id, component) { idsByType.set(component, id); } diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index aa6af029c..dd7e72d23 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,23 +1,18 @@ ;(function () { /* react-hot-loader/webpack */ if (process.env.NODE_ENV !== 'production') { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + const fileName = __FILENAME__; function tagSource(fn, localName) { if (typeof fn !== 'function') { return; } - if (fn.hasOwnProperty('__source')) { - return; - } - try { - Object.defineProperty(fn, '__source', { - enumerable: false, - configurable: true, - value: { - fileName: __FILENAME__, - localName: localName - } - }); - } catch (err) { } + + const id = fileName + '#' + localName; + __REACT_HOT_LOADER__.register(id, fn); } if (typeof module.exports === 'function') { diff --git a/test/babel/fixtures/bindings/expected.js b/test/babel/fixtures/bindings/expected.js index 9d4065aa8..08359c749 100644 --- a/test/babel/fixtures/bindings/expected.js +++ b/test/babel/fixtures/bindings/expected.js @@ -23,25 +23,20 @@ export default _default; ; (function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + var fileName = __FILENAME__; + function tagSource(fn, localName) { if (typeof fn !== "function") { return; } - if (fn.hasOwnProperty("__source")) { - return; - } + var id = fileName + '#' + localName; - try { - Object.defineProperty(fn, "__source", { - enumerable: false, - configurable: true, - value: { - fileName: __FILENAME__, - localName: localName - } - }); - } catch (err) {} + __REACT_HOT_LOADER__.register(id, fn); } tagSource(A, "A"); diff --git a/test/babel/fixtures/counter/expected.js b/test/babel/fixtures/counter/expected.js index 9b0ee5812..2bc08b486 100644 --- a/test/babel/fixtures/counter/expected.js +++ b/test/babel/fixtures/counter/expected.js @@ -22,25 +22,20 @@ exports.default = _default; ; (function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + var fileName = __FILENAME__; + function tagSource(fn, localName) { if (typeof fn !== "function") { return; } - if (fn.hasOwnProperty("__source")) { - return; - } + var id = fileName + '#' + localName; - try { - Object.defineProperty(fn, "__source", { - enumerable: false, - configurable: true, - value: { - fileName: __FILENAME__, - localName: localName - } - }); - } catch (err) {} + __REACT_HOT_LOADER__.register(id, fn); } tagSource(Counter, "Counter"); diff --git a/test/babel/fixtures/issue-246/expected.js b/test/babel/fixtures/issue-246/expected.js index 49eb296e3..7a1c33e78 100644 --- a/test/babel/fixtures/issue-246/expected.js +++ b/test/babel/fixtures/issue-246/expected.js @@ -14,25 +14,20 @@ function spread() { ; (function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + var fileName = __FILENAME__; + function tagSource(fn, localName) { if (typeof fn !== "function") { return; } - if (fn.hasOwnProperty("__source")) { - return; - } + var id = fileName + '#' + localName; - try { - Object.defineProperty(fn, "__source", { - enumerable: false, - configurable: true, - value: { - fileName: __FILENAME__, - localName: localName - } - }); - } catch (err) {} + __REACT_HOT_LOADER__.register(id, fn); } tagSource(spread, "spread"); From 015c21eb9c55e7de171dbe11ed90eb1ebe799612 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 1 May 2016 23:37:50 +0100 Subject: [PATCH 082/161] Remove React Router workaround as unnecessary --- src/fixupReactRouter.js | 61 ------- test/fixupReactRouter/index.js | 321 --------------------------------- 2 files changed, 382 deletions(-) delete mode 100644 src/fixupReactRouter.js delete mode 100644 test/fixupReactRouter/index.js diff --git a/src/fixupReactRouter.js b/src/fixupReactRouter.js deleted file mode 100644 index 950166583..000000000 --- a/src/fixupReactRouter.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -import { Children, isValidElement } from 'react'; - -function ensureArray(maybeArray) { - if (!maybeArray) { - return []; - } else { - if (Array.isArray(maybeArray)) { - return maybeArray; - } else { - return [maybeArray]; - } - } -} - -function isReactRouterish(type) { - return type && ( - type.displayName === 'Router' || - type.name === 'Router' // In case Ryan and Michael embrace ES6 classes - ); -} - -function extractComponents(routes) { - return ensureArray(routes) - .filter(Boolean) - .map(route => { - const isElement = isValidElement(route); - const component = isElement ? route.props.component : route.component; - const namedComponentsByKey = isElement ? route.props.components : route.components; - - const indexRoute = route.indexRoute; - const childRoutes = isElement ? - Children.toArray(route.props.children) : - ensureArray(route.childRoutes); - const namedComponents = Object.keys(namedComponentsByKey || {}).map(key => - namedComponentsByKey[key] - ); - - const indexRouteComponents = indexRoute && extractComponents(indexRoute); - const childRouteComponents = childRoutes && extractComponents(childRoutes); - return [].concat( - component, - namedComponents, - indexRouteComponents, - childRouteComponents - ); - }) - .reduce((flattened, candidates) => flattened.concat(candidates), []) - .filter(c => typeof c === 'function'); -} - -function extractRouteHandlerComponents(props) { - const routes = props.routes || Children.toArray(props.children); - return extractComponents(routes); -} - -module.exports = { - isReactRouterish, - extractRouteHandlerComponents -}; diff --git a/test/fixupReactRouter/index.js b/test/fixupReactRouter/index.js deleted file mode 100644 index a1b876c67..000000000 --- a/test/fixupReactRouter/index.js +++ /dev/null @@ -1,321 +0,0 @@ -import React from 'react'; -import { isReactRouterish, extractRouteHandlerComponents } from '../../src/fixupReactRouter'; -import expect, { createSpy } from 'expect'; - -const spy = createSpy(); - -function Router() {} -function Route() {}; -function IndexRoute() {}; -function Redirect() {}; -function NotRouter() {}; - -describe('isReactRouterish', () => { - describe('when given a router', () => { - it('is true when given a router', () => { - expect(isReactRouterish(Router)).toBe(true); - }); - }); - - describe('when not given a router', () => { - it('returns false', () => { - expect(isReactRouterish(NotRouter)).toBe(false); - }); - }); -}); - -describe('extractRouteHandlerComponents', () => { - it('handles something that is not a React Router', () => { - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([]); - }); - - describe('when routes are passed as React elements in "children" prop', () => { - it('handles no routes', () => { - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([]); - }); - - it('handles a single route', () => { - function a() {} - const element = ( - - - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a]); - }); - - it('handles a single note with multiple components', () => { - function a() {} - function b() {} - function c() {} - const element = ( - - - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b, c]); - }); - - it('handles multiple routes', () => { - function a() {} - function b() {} - const element = ( - - - - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b]); - }); - - it('handles deep routes of different types', () => { - function a() {} - function b() {} - function c() {} - function d() {} - function e() {} - function f() {} - function g() {} - function h() {} - const element = ( - - - - - - - - - - - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b, c, d, e, f, g, h]); - }); - - it('ignores broken routes', () => { - function a() {} - function b() {} - const element = ( - - - - - - - - - {null} - - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b]); - }); - }); - - describe('when routes are passed as React elements in "routes" prop', () => { - it('handles a single route', () => { - function a() {} - const element = ( - - } /> - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a]); - }); - - it('handles a single note with multiple components', () => { - function a() {} - function b() {} - function c() {} - const element = ( - - } /> - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b, c]); - }); - - it('handles multiple routes', () => { - function a() {} - function b() {} - const element = ( - , - - ]} /> - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b]); - }); - - it('handles deep routes of different types', () => { - function a() {} - function b() {} - function c() {} - function d() {} - function e() {} - function f() {} - function g() {} - function h() {} - const element = ( - - - - - - - , - , - - ]} /> - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b, c, d, e, f, g, h]); - }); - - it('ignores broken routes', () => { - function a() {} - function b() {} - const element = ( - - - - - - - - {null} - - } /> - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b]); - }); - }); - - describe('when routes are passed as plain objects in "routes" prop', () => { - it('handles a single route', () => { - function a() {} - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a]); - }); - - it('handles a single note with multiple components', () => { - function a() {} - function b() {} - function c() {} - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b, c]); - }); - - it('handles multiple routes', () => { - function a() {} - function b() {} - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b]); - }); - - it('handles deep routes of different types', () => { - function a() {} - function b() {} - function c() {} - function d() {} - function e() {} - function f() {} - function g() {} - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b, c, d, e, f, g]); - }); - - it('ignores broken routes', () => { - function a() {} - function b() {} - const element = ( - - ); - expect( - extractRouteHandlerComponents(element.props) - ).toEqual([a, b]); - }); - }); -}); From 7178f26593e43b1ba21586fc4c63f57cb50e8c38 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 1 May 2016 23:47:19 +0100 Subject: [PATCH 083/161] Get tests to pass --- src/AppContainer.dev.js | 13 +------------ src/patch.dev.js | 10 +++++----- src/resolveType.js | 4 ++-- test/AppContainer/AppContainer.dev.js | 4 +++- test/AppContainer/setup.js | 8 +------- test/index.js | 1 - 6 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 8ab2800a4..e644effcd 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -5,17 +5,6 @@ const deepForceUpdate = require('react-deep-force-update'); const Redbox = require('redbox-react'); const { Component } = React; -// Feature check for the createElement() patch. -// If createElement() was patched, types with -// the same __source will resolve to the same type. -let wasCreateElementPatched = false; -const A = () => {}; -A.__source = { fileName: 'fake', localName: 'fake' } -const B = () => {}; -B.__source = { fileName: 'fake', localName: 'fake' } -if (
.type === .type) { - wasCreateElementPatched = true; -} class AppContainer extends Component { constructor(props) { @@ -24,7 +13,7 @@ class AppContainer extends Component { } componentDidMount() { - if (!wasCreateElementPatched) { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { console.error( 'React Hot Loader: It appears that "react-hot-loader/patch" ' + 'did not run immediately before the app started. Make sure that it ' + diff --git a/src/patch.dev.js b/src/patch.dev.js index f42c1cf5a..276829a88 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -3,10 +3,6 @@ const React = require('react'); const resolveType = require('./resolveType'); -if (React.createElement.isPatchedByReactHotLoader) { - throw new Error('Cannot patch React twice.'); -} - const createElement = React.createElement; function patchedCreateElement(type, ...args) { // Trick React into rendering a proxy so that @@ -16,4 +12,8 @@ function patchedCreateElement(type, ...args) { return createElement(resolvedType, ...args); } patchedCreateElement.isPatchedByReactHotLoader = true; -React.createElement = patchedCreateElement; + +if (!React.createElement.isPatchedByReactHotLoader) { + React.createElement = patchedCreateElement; +} + diff --git a/src/resolveType.js b/src/resolveType.js index a59541e8e..f400c89c9 100644 --- a/src/resolveType.js +++ b/src/resolveType.js @@ -5,7 +5,7 @@ let proxiesByID = {}; let idsByType = new WeakMap(); global.__REACT_HOT_LOADER__ = { - set(id, component) { + register(id, component) { idsByType.set(component, id); } }; @@ -26,7 +26,7 @@ function resolveType(type) { // new versions without destroying original instances. if (!proxiesByID[id]) { proxiesByID[id] = createProxy(type); - } else if (!type.hasOwnProperty('__hasBeenUsedForProxy')) { + } else { proxiesByID[id].update(type); } diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index e6f7bafb4..eaeff3af6 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -5,7 +5,9 @@ import {mount} from 'enzyme' import AppContainer from '../../src/AppContainer.dev' -const tag = (comp, name) => comp.__source = { fileName: name, localName: name } +const tag = (comp, name) => { + global.__REACT_HOT_LOADER__.register(name, comp); +}; describe('', () => { describe('when passed children', () => { diff --git a/test/AppContainer/setup.js b/test/AppContainer/setup.js index cb827ae1b..d923573d7 100644 --- a/test/AppContainer/setup.js +++ b/test/AppContainer/setup.js @@ -1,12 +1,7 @@ -// the if statement is to prevent patching again when using mocha watch mode -if (!require('react').createElement.isPatchedByReactHotLoader) - require('../../src/patch.dev') +require('../../src/patch.dev'); -// copied from https://github.com/lelandrichardson/enzyme-example-mocha/blob/master/test/.setup.js var jsdom = require('jsdom').jsdom; - var exposedProperties = ['window', 'navigator', 'document']; - global.document = jsdom(''); global.window = document.defaultView; Object.keys(document.defaultView).forEach((property) => { @@ -15,7 +10,6 @@ Object.keys(document.defaultView).forEach((property) => { global[property] = document.defaultView[property]; } }); - global.navigator = { userAgent: 'node.js' }; diff --git a/test/index.js b/test/index.js index 924d7fd8b..1b98ec6fc 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,2 @@ import './babel'; -import './fixupReactRouter'; import './AppContainer'; From 544411823f5f5c9ae725d2cb215d19773919c17e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 00:18:41 +0100 Subject: [PATCH 084/161] Add rudimentary test for resolveType --- src/AppContainer.dev.js | 1 - test/index.js | 1 + test/resolveType/index.js | 12 ++++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/resolveType/index.js diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index e644effcd..ca6b7bfc7 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -5,7 +5,6 @@ const deepForceUpdate = require('react-deep-force-update'); const Redbox = require('redbox-react'); const { Component } = React; - class AppContainer extends Component { constructor(props) { super(props); diff --git a/test/index.js b/test/index.js index 1b98ec6fc..2f8b1e0e8 100644 --- a/test/index.js +++ b/test/index.js @@ -1,2 +1,3 @@ import './babel'; import './AppContainer'; +import './resolveType'; \ No newline at end of file diff --git a/test/resolveType/index.js b/test/resolveType/index.js new file mode 100644 index 000000000..c82aa9b5b --- /dev/null +++ b/test/resolveType/index.js @@ -0,0 +1,12 @@ +import '../../src/patch.dev'; +import resolveType from '../../src/resolveType'; +import expect from 'expect'; + +describe('resolveType', () => { + it('is identity for unrecognized types', () => { + expect(resolveType()).toBe(undefined); + expect(resolveType(42)).toBe(42); + expect(resolveType('div')).toBe('div'); + expect(resolveType(expect)).toBe(expect); + }); +}); \ No newline at end of file From 1804da88f97281e4d9bc12cf763b8d99f027c01c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 01:23:04 +0100 Subject: [PATCH 085/161] Test resolveType() more thoroughly --- src/resolveType.js | 4 ++ test/AppContainer/AppContainer.dev.js | 53 ++++++++++++++------------- test/resolveType/index.js | 50 ++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/resolveType.js b/src/resolveType.js index f400c89c9..f918690b1 100644 --- a/src/resolveType.js +++ b/src/resolveType.js @@ -7,6 +7,10 @@ let idsByType = new WeakMap(); global.__REACT_HOT_LOADER__ = { register(id, component) { idsByType.set(component, id); + }, + reset() { + proxiesByID = {}; + idsByType = new WeakMap(); } }; diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index eaeff3af6..57691bb0f 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -4,12 +4,13 @@ import expect, {createSpy} from 'expect' import {mount} from 'enzyme' import AppContainer from '../../src/AppContainer.dev' - -const tag = (comp, name) => { - global.__REACT_HOT_LOADER__.register(name, comp); -}; +const RHL = global.__REACT_HOT_LOADER__ describe('', () => { + beforeEach(() => { + RHL.reset() + }); + describe('when passed children', () => { describe('with class root', () => { it('renders it', () => { @@ -20,7 +21,7 @@ describe('', () => { return
hey
} } - tag(App, 'App') + RHL.register('App', App) const wrapper = mount() expect(wrapper.find('App').length).toBe(1) @@ -41,7 +42,7 @@ describe('', () => { return
hey
} } - tag(App, 'App') + RHL.register('App', App) const wrapper = mount() expect(spy.calls.length).toBe(1) @@ -57,7 +58,7 @@ describe('', () => { return
ho
} } - tag(App, 'App') + RHL.register('App', App) wrapper.setProps({children: }) } @@ -79,7 +80,7 @@ describe('', () => { return
old render + {this.state} state
} } - tag(App, 'App') + RHL.register('App', App) const wrapper = mount() expect(wrapper.text()).toBe('old render + old state') @@ -98,7 +99,7 @@ describe('', () => { return
new render + {this.state} state
} } - tag(App, 'App') + RHL.register('App', App) wrapper.setProps({children: }) } @@ -113,7 +114,7 @@ describe('', () => { spy() return
hey
} - tag(App, 'App') + RHL.register('App', App) const wrapper = mount() expect(wrapper.find('App').length).toBe(1) @@ -128,7 +129,7 @@ describe('', () => { spy() return
hey
} - tag(App, 'App') + RHL.register('App', App) const wrapper = mount() expect(spy.calls.length).toBe(1) @@ -138,7 +139,7 @@ describe('', () => { spy() return
ho
} - tag(App, 'App') + RHL.register('App', App) wrapper.setProps({children: }) } @@ -158,10 +159,10 @@ describe('', () => { return
old render + {this.state} state
} } - tag(App, 'App') + RHL.register('App', App) const Root = () => - tag(Root, 'Root') + RHL.register('Root', Root) const wrapper = mount() expect(wrapper.text()).toBe('old render + old state') @@ -178,10 +179,10 @@ describe('', () => { return
new render + {this.state} state
} } - tag(App, 'App') + RHL.register('App', App) const Root = () => - tag(Root, 'Root') + RHL.register('Root', Root) wrapper.setProps({children: }) } @@ -199,10 +200,10 @@ describe('', () => { return
hey
} } - tag(App, 'App') + RHL.register('App', App) const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - tag(Enhanced, 'Enhanced') + RHL.register('Enhanced', Enhanced) const wrapper = mount() expect(wrapper.find('App').length).toBe(1) @@ -219,10 +220,10 @@ describe('', () => { return
hey
} } - tag(App, 'App') + RHL.register('App', App) const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - tag(Enhanced, 'Enhanced') + RHL.register('Enhanced', Enhanced) const wrapper = mount() expect(spy.calls.length).toBe(1) @@ -234,10 +235,10 @@ describe('', () => { return
ho
} } - tag(App, 'App') + RHL.register('App', App) const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - tag(Enhanced, 'Enhanced') + RHL.register('Enhanced', Enhanced) wrapper.setProps({children: }) } @@ -257,10 +258,10 @@ describe('', () => { return
old render + {this.state} state + {this.props.n}
} } - tag(App, 'App') + RHL.register('App', App) const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - tag(Enhanced, 'Enhanced') + RHL.register('Enhanced', Enhanced) const wrapper = mount() expect(wrapper.text()).toBe('old render + old state + 15') @@ -277,10 +278,10 @@ describe('', () => { return
new render + {this.state} state + {this.props.n}
} } - tag(App, 'App') + RHL.register('App', App) const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - tag(Enhanced, 'Enhanced') + RHL.register('Enhanced', Enhanced) wrapper.setProps({children: }) } diff --git a/test/resolveType/index.js b/test/resolveType/index.js index c82aa9b5b..27bcf30f6 100644 --- a/test/resolveType/index.js +++ b/test/resolveType/index.js @@ -2,11 +2,59 @@ import '../../src/patch.dev'; import resolveType from '../../src/resolveType'; import expect from 'expect'; +const RHL = global.__REACT_HOT_LOADER__; +function a1() {} +function a2() {} +function a3() {} +function b1() {} +function b2() {} + describe('resolveType', () => { + beforeEach(() => { + RHL.reset(); + }); + it('is identity for unrecognized types', () => { expect(resolveType()).toBe(undefined); expect(resolveType(42)).toBe(42); expect(resolveType('div')).toBe('div'); - expect(resolveType(expect)).toBe(expect); + expect(resolveType(a1)).toBe(a1); + }); + + it('resolves registered types by their last ID', () => { + RHL.register('a', a1); + expect(resolveType(a1)).toNotBe(a1); + const a = resolveType(a1); + expect(a).toBeA('function'); + expect(resolveType(a)).toBe(a); + + RHL.register('a', a2); + expect(resolveType(a1)).toBe(a); + expect(resolveType(a2)).toBe(a); + expect(resolveType(a)).toBe(a); + + RHL.register('a', a3); + expect(resolveType(a1)).toBe(a); + expect(resolveType(a2)).toBe(a); + expect(resolveType(a3)).toBe(a); + expect(resolveType(a)).toBe(a); + + RHL.register('b', b1); + const b = resolveType(b1); + expect(resolveType(a1)).toBe(a); + expect(resolveType(a2)).toBe(a); + expect(resolveType(a3)).toBe(a); + expect(resolveType(a)).toBe(a); + expect(resolveType(b1)).toBe(b); + expect(resolveType(b)).toBe(b); + + RHL.register('b', b2); + expect(resolveType(a1)).toBe(a); + expect(resolveType(a2)).toBe(a); + expect(resolveType(a3)).toBe(a); + expect(resolveType(a)).toBe(a); + expect(resolveType(b1)).toBe(b); + expect(resolveType(b2)).toBe(b); + expect(resolveType(b)).toBe(b); }); }); \ No newline at end of file From 0371b62e4da215db7ea132bc6401ed59542c2c4d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 01:41:42 +0100 Subject: [PATCH 086/161] Consolidate logic and add more tests --- src/patch.dev.js | 41 ++++++++++++++- src/resolveType.js | 41 --------------- test/index.js | 2 +- test/patch/index.js | 1 + test/patch/patch.dev.js | 102 ++++++++++++++++++++++++++++++++++++++ test/resolveType/index.js | 60 ---------------------- 6 files changed, 143 insertions(+), 104 deletions(-) delete mode 100644 src/resolveType.js create mode 100644 test/patch/index.js create mode 100644 test/patch/patch.dev.js delete mode 100644 test/resolveType/index.js diff --git a/src/patch.dev.js b/src/patch.dev.js index 276829a88..ab31c7609 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -1,7 +1,45 @@ 'use strict'; const React = require('react'); -const resolveType = require('./resolveType'); +const createProxy = require('react-proxy').default; +const global = require('global'); + +let proxiesByID = {}; +let idsByType = new WeakMap(); + +global.__REACT_HOT_LOADER__ = { + register(id, component) { + idsByType.set(component, id); + }, + reset() { + proxiesByID = {}; + idsByType = new WeakMap(); + } +}; + +function resolveType(type) { + // We only care about composite components + if (!type || typeof type === 'string') { + return type; + } + + var id = idsByType.get(type); + if (!id) { + return type; + } + + // We use React Proxy to generate classes that behave almost + // the same way as the original classes but are updatable with + // new versions without destroying original instances. + if (!proxiesByID[id]) { + proxiesByID[id] = createProxy(type); + } else { + proxiesByID[id].update(type); + } + + // Give proxy class to React instead of the real class. + return proxiesByID[id].get(); +} const createElement = React.createElement; function patchedCreateElement(type, ...args) { @@ -16,4 +54,3 @@ patchedCreateElement.isPatchedByReactHotLoader = true; if (!React.createElement.isPatchedByReactHotLoader) { React.createElement = patchedCreateElement; } - diff --git a/src/resolveType.js b/src/resolveType.js deleted file mode 100644 index f918690b1..000000000 --- a/src/resolveType.js +++ /dev/null @@ -1,41 +0,0 @@ -const createProxy = require('react-proxy').default; -const global = require('global'); - -let proxiesByID = {}; -let idsByType = new WeakMap(); - -global.__REACT_HOT_LOADER__ = { - register(id, component) { - idsByType.set(component, id); - }, - reset() { - proxiesByID = {}; - idsByType = new WeakMap(); - } -}; - -function resolveType(type) { - // We only care about composite components - if (!type || typeof type === 'string') { - return type; - } - - var id = idsByType.get(type); - if (!id) { - return type; - } - - // We use React Proxy to generate classes that behave almost - // the same way as the original classes but are updatable with - // new versions without destroying original instances. - if (!proxiesByID[id]) { - proxiesByID[id] = createProxy(type); - } else { - proxiesByID[id].update(type); - } - - // Give proxy class to React instead of the real class. - return proxiesByID[id].get(); -} - -module.exports = resolveType; diff --git a/test/index.js b/test/index.js index 2f8b1e0e8..87c34185a 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,3 @@ import './babel'; import './AppContainer'; -import './resolveType'; \ No newline at end of file +import './patch'; \ No newline at end of file diff --git a/test/patch/index.js b/test/patch/index.js new file mode 100644 index 000000000..d27bfd2e7 --- /dev/null +++ b/test/patch/index.js @@ -0,0 +1 @@ +import './patch.dev'; diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js new file mode 100644 index 000000000..f3c75f9a4 --- /dev/null +++ b/test/patch/patch.dev.js @@ -0,0 +1,102 @@ +import '../../src/patch.dev'; +import expect from 'expect'; +import React from 'react'; + +const RHL = global.__REACT_HOT_LOADER__; +function A1() {} +function A2() {} +function A3() {} +function B1() {} +function B2() {} + +describe('patch', () => { + beforeEach(() => { + RHL.reset(); + }); + + it('is identity for unrecognized types', () => { + expect(
.type).toBe('div'); + expect(.type).toBe(A1); + }); + + it('resolves registered types by their last ID', () => { + RHL.register('a', A1); + expect(.type).toNotBe(A1); + const A = .type; + expect(A).toBeA('function'); + expect(.type).toBe(A); + + RHL.register('a', A2); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + + RHL.register('a', A3); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + + RHL.register('b', B1); + const B = .type; + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(B); + expect(.type).toBe(B); + + RHL.register('b', B2); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(B); + expect(.type).toBe(B); + expect(.type).toBe(B); + }); + + it('passes props through', () => { + expect(
.props).toEqual({ + x: 42, + y: 'lol' + }); + expect(.props).toEqual({ + x: 42, + y: 'lol' + }); + + RHL.register('b', B1); + expect(.props).toEqual({ + x: 42, + y: 'lol' + }); + RHL.register('b', B2); + expect(.props).toEqual({ + x: 42, + y: 'lol' + }); + }); + + it('passes children through', () => { + expect(
{'Hi'}{'Bye'}
.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + expect({'Hi'}{'Bye'}.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + + RHL.register('b', B1); + expect({'Hi'}{'Bye'}.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + RHL.register('b', B2); + expect({'Hi'}{'Bye'}.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + }); +}); \ No newline at end of file diff --git a/test/resolveType/index.js b/test/resolveType/index.js deleted file mode 100644 index 27bcf30f6..000000000 --- a/test/resolveType/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import '../../src/patch.dev'; -import resolveType from '../../src/resolveType'; -import expect from 'expect'; - -const RHL = global.__REACT_HOT_LOADER__; -function a1() {} -function a2() {} -function a3() {} -function b1() {} -function b2() {} - -describe('resolveType', () => { - beforeEach(() => { - RHL.reset(); - }); - - it('is identity for unrecognized types', () => { - expect(resolveType()).toBe(undefined); - expect(resolveType(42)).toBe(42); - expect(resolveType('div')).toBe('div'); - expect(resolveType(a1)).toBe(a1); - }); - - it('resolves registered types by their last ID', () => { - RHL.register('a', a1); - expect(resolveType(a1)).toNotBe(a1); - const a = resolveType(a1); - expect(a).toBeA('function'); - expect(resolveType(a)).toBe(a); - - RHL.register('a', a2); - expect(resolveType(a1)).toBe(a); - expect(resolveType(a2)).toBe(a); - expect(resolveType(a)).toBe(a); - - RHL.register('a', a3); - expect(resolveType(a1)).toBe(a); - expect(resolveType(a2)).toBe(a); - expect(resolveType(a3)).toBe(a); - expect(resolveType(a)).toBe(a); - - RHL.register('b', b1); - const b = resolveType(b1); - expect(resolveType(a1)).toBe(a); - expect(resolveType(a2)).toBe(a); - expect(resolveType(a3)).toBe(a); - expect(resolveType(a)).toBe(a); - expect(resolveType(b1)).toBe(b); - expect(resolveType(b)).toBe(b); - - RHL.register('b', b2); - expect(resolveType(a1)).toBe(a); - expect(resolveType(a2)).toBe(a); - expect(resolveType(a3)).toBe(a); - expect(resolveType(a)).toBe(a); - expect(resolveType(b1)).toBe(b); - expect(resolveType(b2)).toBe(b); - expect(resolveType(b)).toBe(b); - }); -}); \ No newline at end of file From b5fff25ca5b44cf6a763a3027d0c767499628c4c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 02:12:28 +0100 Subject: [PATCH 087/161] Tweak registration API --- src/babel/index.js | 13 ++----- src/patch.dev.js | 12 +++++- src/webpack/tagCommonJSExports.js | 25 ++++++------ test/AppContainer/AppContainer.dev.js | 44 +++++++++++----------- test/babel/fixtures/bindings/expected.js | 21 ++++------- test/babel/fixtures/counter/expected.js | 15 +------- test/babel/fixtures/issue-246/expected.js | 14 +------ test/babel/fixtures/name-clash/.babelrc | 3 ++ test/babel/fixtures/name-clash/actual.js | 2 + test/babel/fixtures/name-clash/expected.js | 16 ++++++++ test/patch/patch.dev.js | 20 +++++----- 11 files changed, 89 insertions(+), 96 deletions(-) create mode 100644 test/babel/fixtures/name-clash/.babelrc create mode 100644 test/babel/fixtures/name-clash/actual.js create mode 100644 test/babel/fixtures/name-clash/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index af7c05c6f..83f861f30 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -1,6 +1,8 @@ import template from 'babel-template'; -const buildRegistration = template('tagSource(ID, NAME);'); +const buildRegistration = template( + '__REACT_HOT_LOADER__.register(ID, NAME, FILENAME);' +); const buildSemi = template(';'); const buildTagger = template(` (function () { @@ -8,15 +10,6 @@ const buildTagger = template(` return; } - var fileName = FILENAME; - function tagSource(fn, localName) { - if (typeof fn !== "function") { - return; - } - - var id = fileName + '#' + localName; - __REACT_HOT_LOADER__.register(id, fn); - } REGISTRATIONS })(); `); diff --git a/src/patch.dev.js b/src/patch.dev.js index ab31c7609..dfeb85184 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -8,7 +8,17 @@ let proxiesByID = {}; let idsByType = new WeakMap(); global.__REACT_HOT_LOADER__ = { - register(id, component) { + register(component, uniqueLocalName, fileName) { + if (typeof component !== 'function') { + return; + } + if (!uniqueLocalName || !fileName) { + return; + } + if (typeof uniqueLocalName !== 'string' || typeof fileName !== 'string') { + return; + } + var id = fileName + '#' + uniqueLocalName; idsByType.set(component, id); }, reset() { diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index dd7e72d23..bb82e658c 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -5,25 +5,24 @@ return; } - const fileName = __FILENAME__; - function tagSource(fn, localName) { - if (typeof fn !== 'function') { - return; - } - - const id = fileName + '#' + localName; - __REACT_HOT_LOADER__.register(id, fn); - } - if (typeof module.exports === 'function') { - tagSource(module.exports, 'module.exports'); + __REACT_HOT_LOADER__.register(module.exports, 'module.exports', __FILENAME__); return; } for (let key in module.exports) { - if (Object.prototype.hasOwnProperty.call(module.exports, key)) { - tagSource(module.exports[key], `module.exports.${key}`); + if (!Object.prototype.hasOwnProperty.call(module.exports, key)) { + continue; } + + let namedExport; + try { + namedExport = module.exports[key]; + } catch (err) { + continue; + } + + __REACT_HOT_LOADER__.register(namedExport, key, __FILENAME__); } } })(); \ No newline at end of file diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 57691bb0f..2fcf11c31 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -21,7 +21,7 @@ describe('', () => { return
hey
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const wrapper = mount() expect(wrapper.find('App').length).toBe(1) @@ -42,7 +42,7 @@ describe('', () => { return
hey
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const wrapper = mount() expect(spy.calls.length).toBe(1) @@ -58,7 +58,7 @@ describe('', () => { return
ho
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') wrapper.setProps({children: }) } @@ -80,7 +80,7 @@ describe('', () => { return
old render + {this.state} state
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const wrapper = mount() expect(wrapper.text()).toBe('old render + old state') @@ -99,7 +99,7 @@ describe('', () => { return
new render + {this.state} state
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') wrapper.setProps({children: }) } @@ -114,7 +114,7 @@ describe('', () => { spy() return
hey
} - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const wrapper = mount() expect(wrapper.find('App').length).toBe(1) @@ -129,7 +129,7 @@ describe('', () => { spy() return
hey
} - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const wrapper = mount() expect(spy.calls.length).toBe(1) @@ -139,7 +139,7 @@ describe('', () => { spy() return
ho
} - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') wrapper.setProps({children: }) } @@ -159,10 +159,10 @@ describe('', () => { return
old render + {this.state} state
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Root = () => - RHL.register('Root', Root) + RHL.register(Root, 'Root', 'test.js') const wrapper = mount() expect(wrapper.text()).toBe('old render + old state') @@ -179,10 +179,10 @@ describe('', () => { return
new render + {this.state} state
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Root = () => - RHL.register('Root', Root) + RHL.register(Root, 'Root', 'test.js') wrapper.setProps({children: }) } @@ -200,10 +200,10 @@ describe('', () => { return
hey
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register('Enhanced', Enhanced) + RHL.register(Enhanced, 'Enhanced', 'test.js') const wrapper = mount() expect(wrapper.find('App').length).toBe(1) @@ -220,10 +220,10 @@ describe('', () => { return
hey
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register('Enhanced', Enhanced) + RHL.register(Enhanced, 'Enhanced', 'test.js') const wrapper = mount() expect(spy.calls.length).toBe(1) @@ -235,10 +235,10 @@ describe('', () => { return
ho
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register('Enhanced', Enhanced) + RHL.register(Enhanced, 'Enhanced', 'test.js') wrapper.setProps({children: }) } @@ -258,10 +258,10 @@ describe('', () => { return
old render + {this.state} state + {this.props.n}
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register('Enhanced', Enhanced) + RHL.register(Enhanced, 'Enhanced', 'test.js') const wrapper = mount() expect(wrapper.text()).toBe('old render + old state + 15') @@ -278,10 +278,10 @@ describe('', () => { return
new render + {this.state} state + {this.props.n}
} } - RHL.register('App', App) + RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register('Enhanced', Enhanced) + RHL.register(Enhanced, 'Enhanced', 'test.js') wrapper.setProps({children: }) } diff --git a/test/babel/fixtures/bindings/expected.js b/test/babel/fixtures/bindings/expected.js index 08359c749..083fb1072 100644 --- a/test/babel/fixtures/bindings/expected.js +++ b/test/babel/fixtures/bindings/expected.js @@ -27,24 +27,17 @@ export default _default; return; } - var fileName = __FILENAME__; + __REACT_HOT_LOADER__.register(A, "A", FILENAME); - function tagSource(fn, localName) { - if (typeof fn !== "function") { - return; - } + __REACT_HOT_LOADER__.register(B, "B", FILENAME); - var id = fileName + '#' + localName; + __REACT_HOT_LOADER__.register(C, "C", FILENAME); - __REACT_HOT_LOADER__.register(id, fn); - } + __REACT_HOT_LOADER__.register(D, "D", FILENAME); + + __REACT_HOT_LOADER__.register(E, "E", FILENAME); - tagSource(A, "A"); - tagSource(B, "B"); - tagSource(C, "C"); - tagSource(D, "D"); - tagSource(E, "E"); - tagSource(_default, "default"); + __REACT_HOT_LOADER__.register(_default, "default", FILENAME); })(); ; diff --git a/test/babel/fixtures/counter/expected.js b/test/babel/fixtures/counter/expected.js index 2bc08b486..cc27bf67e 100644 --- a/test/babel/fixtures/counter/expected.js +++ b/test/babel/fixtures/counter/expected.js @@ -26,20 +26,9 @@ exports.default = _default; return; } - var fileName = __FILENAME__; + __REACT_HOT_LOADER__.register(Counter, "Counter", FILENAME); - function tagSource(fn, localName) { - if (typeof fn !== "function") { - return; - } - - var id = fileName + '#' + localName; - - __REACT_HOT_LOADER__.register(id, fn); - } - - tagSource(Counter, "Counter"); - tagSource(_default, "default"); + __REACT_HOT_LOADER__.register(_default, "default", FILENAME); })(); ; diff --git a/test/babel/fixtures/issue-246/expected.js b/test/babel/fixtures/issue-246/expected.js index 7a1c33e78..b909b5e9b 100644 --- a/test/babel/fixtures/issue-246/expected.js +++ b/test/babel/fixtures/issue-246/expected.js @@ -18,19 +18,7 @@ function spread() { return; } - var fileName = __FILENAME__; - - function tagSource(fn, localName) { - if (typeof fn !== "function") { - return; - } - - var id = fileName + '#' + localName; - - __REACT_HOT_LOADER__.register(id, fn); - } - - tagSource(spread, "spread"); + __REACT_HOT_LOADER__.register(spread, "spread", FILENAME); })(); ; diff --git a/test/babel/fixtures/name-clash/.babelrc b/test/babel/fixtures/name-clash/.babelrc new file mode 100644 index 000000000..ae9707bdc --- /dev/null +++ b/test/babel/fixtures/name-clash/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["../../../../src/babel"] +} diff --git a/test/babel/fixtures/name-clash/actual.js b/test/babel/fixtures/name-clash/actual.js new file mode 100644 index 000000000..709c8853a --- /dev/null +++ b/test/babel/fixtures/name-clash/actual.js @@ -0,0 +1,2 @@ +const _default = 10; +export default 42; \ No newline at end of file diff --git a/test/babel/fixtures/name-clash/expected.js b/test/babel/fixtures/name-clash/expected.js new file mode 100644 index 000000000..d9bc46eae --- /dev/null +++ b/test/babel/fixtures/name-clash/expected.js @@ -0,0 +1,16 @@ +const _default = 10; +const _default2 = 42; +export default _default2; +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(_default, "_default", FILENAME); + + __REACT_HOT_LOADER__.register(_default2, "default", FILENAME); +})(); + +; diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index f3c75f9a4..b944ca858 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -1,5 +1,5 @@ import '../../src/patch.dev'; -import expect from 'expect'; +import expect, { spyOn } from 'expect'; import React from 'react'; const RHL = global.__REACT_HOT_LOADER__; @@ -20,24 +20,24 @@ describe('patch', () => { }); it('resolves registered types by their last ID', () => { - RHL.register('a', A1); + RHL.register(A1, 'a', 'test.js'); expect(.type).toNotBe(A1); const A = .type; expect(A).toBeA('function'); expect(
.type).toBe(A); - RHL.register('a', A2); + RHL.register(A2, 'a', 'test.js'); expect(.type).toBe(A); expect(.type).toBe(A); expect(.type).toBe(A); - RHL.register('a', A3); + RHL.register(A3, 'a', 'test.js'); expect(.type).toBe(A); expect(.type).toBe(A); expect(.type).toBe(A); expect(.type).toBe(A); - RHL.register('b', B1); + RHL.register(B1, 'b', 'test.js'); const B = .type; expect(.type).toBe(A); expect(.type).toBe(A); @@ -46,7 +46,7 @@ describe('patch', () => { expect(.type).toBe(B); expect(.type).toBe(B); - RHL.register('b', B2); + RHL.register(B2, 'b', 'test.js'); expect(.type).toBe(A); expect(.type).toBe(A); expect(.type).toBe(A); @@ -66,12 +66,12 @@ describe('patch', () => { y: 'lol' }); - RHL.register('b', B1); + RHL.register(B1, 'b', 'test.js'); expect(.props).toEqual({ x: 42, y: 'lol' }); - RHL.register('b', B2); + RHL.register(B2, 'b', 'test.js'); expect(.props).toEqual({ x: 42, y: 'lol' @@ -88,12 +88,12 @@ describe('patch', () => { 'Bye' ]); - RHL.register('b', B1); + RHL.register(B1, 'b', 'test.js'); expect({'Hi'}{'Bye'}.props.children).toEqual([ 'Hi', 'Bye' ]); - RHL.register('b', B2); + RHL.register(B2, 'b', 'test.js'); expect({'Hi'}{'Bye'}.props.children).toEqual([ 'Hi', 'Bye' From 3b9bd93fe4cb6f573aa205c43504220535af7f9c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 03:07:18 +0100 Subject: [PATCH 088/161] Fix Babel plugin --- src/babel/index.js | 13 +++++++------ test/babel/fixtures/bindings/expected.js | 12 ++++++------ test/babel/fixtures/counter/expected.js | 4 ++-- test/babel/fixtures/issue-246/expected.js | 2 +- test/babel/fixtures/name-clash/expected.js | 4 ++-- test/babel/index.js | 2 +- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index 83f861f30..c20c3d525 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -58,7 +58,7 @@ module.exports = function(args) { const REGISTRATIONS = Symbol(); return { visitor: { - ExportDefaultDeclaration(path) { + ExportDefaultDeclaration(path, { file }) { // Default exports with names are going // to be in scope anyway so no need to bother. if (path.node.declaration.id) { @@ -83,13 +83,14 @@ module.exports = function(args) { path.parent[REGISTRATIONS].push( buildRegistration({ ID: id, - NAME: t.stringLiteral('default') + NAME: t.stringLiteral('default'), + FILENAME: t.stringLiteral(file.opts.filename) }) ); }, Program: { - enter({ node, scope }) { + enter({ node, scope }, { file }) { node[REGISTRATIONS] = []; // Everything in the top level scope, when reasonable, @@ -99,13 +100,14 @@ module.exports = function(args) { if (shouldRegisterBinding(binding)) { node[REGISTRATIONS].push(buildRegistration({ ID: binding.identifier, - NAME: t.stringLiteral(id) + NAME: t.stringLiteral(id), + FILENAME: t.stringLiteral(file.opts.filename) })); } } }, - exit({ node, scope }, { file }) { + exit({ node, scope }) { let registrations = node[REGISTRATIONS]; node[REGISTRATIONS] = null; @@ -114,7 +116,6 @@ module.exports = function(args) { node.body.push(buildSemi()); node.body.push( buildTagger({ - FILENAME: t.stringLiteral(file.opts.filename), REGISTRATIONS: registrations }) ); diff --git a/test/babel/fixtures/bindings/expected.js b/test/babel/fixtures/bindings/expected.js index 083fb1072..6e8494e7f 100644 --- a/test/babel/fixtures/bindings/expected.js +++ b/test/babel/fixtures/bindings/expected.js @@ -27,17 +27,17 @@ export default _default; return; } - __REACT_HOT_LOADER__.register(A, "A", FILENAME); + __REACT_HOT_LOADER__.register(A, "A", __FILENAME__); - __REACT_HOT_LOADER__.register(B, "B", FILENAME); + __REACT_HOT_LOADER__.register(B, "B", __FILENAME__); - __REACT_HOT_LOADER__.register(C, "C", FILENAME); + __REACT_HOT_LOADER__.register(C, "C", __FILENAME__); - __REACT_HOT_LOADER__.register(D, "D", FILENAME); + __REACT_HOT_LOADER__.register(D, "D", __FILENAME__); - __REACT_HOT_LOADER__.register(E, "E", FILENAME); + __REACT_HOT_LOADER__.register(E, "E", __FILENAME__); - __REACT_HOT_LOADER__.register(_default, "default", FILENAME); + __REACT_HOT_LOADER__.register(_default, "default", __FILENAME__); })(); ; diff --git a/test/babel/fixtures/counter/expected.js b/test/babel/fixtures/counter/expected.js index cc27bf67e..5658a7158 100644 --- a/test/babel/fixtures/counter/expected.js +++ b/test/babel/fixtures/counter/expected.js @@ -26,9 +26,9 @@ exports.default = _default; return; } - __REACT_HOT_LOADER__.register(Counter, "Counter", FILENAME); + __REACT_HOT_LOADER__.register(Counter, "Counter", __FILENAME__); - __REACT_HOT_LOADER__.register(_default, "default", FILENAME); + __REACT_HOT_LOADER__.register(_default, "default", __FILENAME__); })(); ; diff --git a/test/babel/fixtures/issue-246/expected.js b/test/babel/fixtures/issue-246/expected.js index b909b5e9b..95de82c72 100644 --- a/test/babel/fixtures/issue-246/expected.js +++ b/test/babel/fixtures/issue-246/expected.js @@ -18,7 +18,7 @@ function spread() { return; } - __REACT_HOT_LOADER__.register(spread, "spread", FILENAME); + __REACT_HOT_LOADER__.register(spread, "spread", __FILENAME__); })(); ; diff --git a/test/babel/fixtures/name-clash/expected.js b/test/babel/fixtures/name-clash/expected.js index d9bc46eae..ebb8b38a0 100644 --- a/test/babel/fixtures/name-clash/expected.js +++ b/test/babel/fixtures/name-clash/expected.js @@ -8,9 +8,9 @@ export default _default2; return; } - __REACT_HOT_LOADER__.register(_default, "_default", FILENAME); + __REACT_HOT_LOADER__.register(_default, "_default", __FILENAME__); - __REACT_HOT_LOADER__.register(_default2, "default", FILENAME); + __REACT_HOT_LOADER__.register(_default2, "default", __FILENAME__); })(); ; diff --git a/test/babel/index.js b/test/babel/index.js index cf80faf03..acfc26a9b 100644 --- a/test/babel/index.js +++ b/test/babel/index.js @@ -22,7 +22,7 @@ describe('tags potential React components', () => { actualPath; const expected = fs.readFileSync( path.join(fixtureDir, 'expected.js') - ).toString().replace('__FILENAME__', JSON.stringify(templatePath)); + ).toString().replace(/__FILENAME__/g, JSON.stringify(templatePath)); expect(trim(actual)).toEqual(trim(expected)); }); }); From bb833402264f403dc68d00ee5f3d860b554248f2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 03:07:34 +0100 Subject: [PATCH 089/161] Handle late registrations --- src/patch.dev.js | 38 ++++++++++++++++++++++++++------- test/patch/patch.dev.js | 47 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index dfeb85184..7edcfbb96 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -4,12 +4,14 @@ const React = require('react'); const createProxy = require('react-proxy').default; const global = require('global'); -let proxiesByID = {}; -let idsByType = new WeakMap(); +let proxiesByID; +let didWarnAboutID; +let hasCreatedElementsByType; +let idsByType; -global.__REACT_HOT_LOADER__ = { - register(component, uniqueLocalName, fileName) { - if (typeof component !== 'function') { +const hooks = { + register(type, uniqueLocalName, fileName) { + if (typeof type !== 'function') { return; } if (!uniqueLocalName || !fileName) { @@ -18,21 +20,41 @@ global.__REACT_HOT_LOADER__ = { if (typeof uniqueLocalName !== 'string' || typeof fileName !== 'string') { return; } - var id = fileName + '#' + uniqueLocalName; - idsByType.set(component, id); + const id = fileName + '#' + uniqueLocalName; + if (!idsByType.has(type) && hasCreatedElementsByType.has(type)) { + if (!didWarnAboutID[id]) { + didWarnAboutID[id] = true; + const baseName = fileName.replace(/^.*[\\\/]/, ''); + console.error( + `React Hot Loader: ${uniqueLocalName} in ${fileName} will not hot reload ` + + `correctly because ${baseName} uses <${uniqueLocalName} /> during ` + + `module definition. For hot reloading to work, move ${uniqueLocalName} ` + + `into a separate file and import it from ${baseName}.` + ); + } + return; + } + idsByType.set(type, id); }, reset() { proxiesByID = {}; + didWarnAboutID = {}; + hasCreatedElementsByType = new WeakMap(); idsByType = new WeakMap(); } }; +hooks.reset(); +global.__REACT_HOT_LOADER__ = hooks; + function resolveType(type) { // We only care about composite components - if (!type || typeof type === 'string') { + if (typeof type !== 'function') { return type; } + hasCreatedElementsByType.set(type, true); + var id = idsByType.get(type); if (!id) { return type; diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index b944ca858..bd62bcc10 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -19,10 +19,39 @@ describe('patch', () => { expect(.type).toBe(A1); }); + it('ignores late registrations', () => { + function Kanye() {} + function Kanye2() {} + + React.createElement(Kanye); + + // By this time we have to assume the user might have rendered it. + // It's unsafe to resolve it to a different type afterwards, + // or we may cause an existing component to unmount unpredictably. + // https://github.com/gaearon/react-hot-loader/issues/241 + + const spy = spyOn(console, 'error'); + RHL.register(Kanye, 'Yeezy', '/wow/test.js'); + expect(console.error.calls.length).toBe(1); + expect(console.error.calls[0].arguments[0]).toBe( + 'React Hot Loader: Yeezy in /wow/test.js will not hot reload ' + + 'correctly because test.js uses during ' + + 'module definition. For hot reloading to work, move Yeezy ' + + 'into a separate file and import it from test.js.' + ); + expect(.type).toBe(Kanye); + expect(.type).toBe(Kanye2); + + RHL.register(Kanye2, 'Yeezy', '/wow/test.js'); + expect(console.error.calls.length).toBe(1); + expect(.type).toBe(Kanye); + expect(.type).toBe(Kanye2); + }); + it('resolves registered types by their last ID', () => { RHL.register(A1, 'a', 'test.js'); - expect(.type).toNotBe(A1); const A = .type; + expect(A).toNotBe(A1); expect(A).toBeA('function'); expect(.type).toBe(A); @@ -56,6 +85,22 @@ describe('patch', () => { expect(.type).toBe(B); }); + it('works with reexported types', () => { + RHL.register(A1, 'a', 'test.js'); + RHL.register(A1, 'x', 'test2.js'); + + const A = .type; + expect(A.type).toNotBe(A1); + expect(A).toBeA('function'); + expect(.type).toBe(A); + + RHL.register(A2, 'a', 'test.js'); + RHL.register(A2, 'x', 'test2.js'); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + }); + it('passes props through', () => { expect(
.props).toEqual({ x: 42, From 7610fd6fc6296f848c880224736c32f30a5dc019 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 03:10:18 +0100 Subject: [PATCH 090/161] Fix Webpack loader --- src/webpack/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/index.js b/src/webpack/index.js index 8f4aef379..ea371f17a 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -41,7 +41,7 @@ function transform(source, map) { // Parameterize the helper with the current filename. const separator = '\n\n'; const appendText = tagCommonJSExportsSource.replace( - '__FILENAME__', + /__FILENAME__/g, JSON.stringify(path.basename(this.resourcePath)) ); From e41e11701754c5026b36d286495bd0ff1bc27777 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 04:10:48 +0100 Subject: [PATCH 091/161] Add support for cached elements Fixes #249 --- src/patch.dev.js | 25 ++++--- test/AppContainer/AppContainer.dev.js | 100 ++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index 7edcfbb96..d04367638 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -34,7 +34,18 @@ const hooks = { } return; } + + // Remember the ID. idsByType.set(type, id); + + // We use React Proxy to generate classes that behave almost + // the same way as the original classes but are updatable with + // new versions without destroying original instances. + if (!proxiesByID[id]) { + proxiesByID[id] = createProxy(type); + } else { + proxiesByID[id].update(type); + } }, reset() { proxiesByID = {}; @@ -55,22 +66,18 @@ function resolveType(type) { hasCreatedElementsByType.set(type, true); + // When available, give proxy class to React instead of the real class. var id = idsByType.get(type); if (!id) { return type; } - // We use React Proxy to generate classes that behave almost - // the same way as the original classes but are updatable with - // new versions without destroying original instances. - if (!proxiesByID[id]) { - proxiesByID[id] = createProxy(type); - } else { - proxiesByID[id].update(type); + var proxy = proxiesByID[id]; + if (!proxy) { + return type; } - // Give proxy class to React instead of the real class. - return proxiesByID[id].get(); + return proxy.get(); } const createElement = React.createElement; diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 2fcf11c31..a71a117d4 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -66,6 +66,44 @@ describe('', () => { expect(wrapper.contains(
ho
)).toBe(true) }) + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
hey
+ } + } + RHL.register(App, 'App', 'test.js') + + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
ho
+ } + } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + it('hot-reloads without losing state', () => { class App extends Component { componentWillMount() { @@ -147,6 +185,32 @@ describe('', () => { expect(wrapper.contains(
ho
)).toBe(true) }) + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + + const App = () => { + spy() + return
hey
+ } + RHL.register(App, 'App', 'test.js') + + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { + const App = () => { + spy() + return
ho
+ } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + it('hot-reloads without losing state', () => { class App extends Component { componentWillMount() { @@ -246,6 +310,42 @@ describe('', () => { expect(wrapper.contains(
ho
)).toBe(true) }) + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
+ } + } + RHL.register(App, 'App', 'test.js') + + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') + + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { + class App extends React.Component { + render() { + spy() + return
ho
+ } + } + RHL.register(App, 'App', 'test.js') + + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') + wrapper.setProps({children: element}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + it('hot-reloads without losing state', () => { class App extends Component { componentWillMount() { From f3c5d1771c4b1de7ad75ca95a9266e0d09480341 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 04:17:47 +0100 Subject: [PATCH 092/161] Fix test watching --- src/patch.dev.js | 5 +++-- test/patch/patch.dev.js | 34 +++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index d04367638..1b47e80fa 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -47,6 +47,7 @@ const hooks = { proxiesByID[id].update(type); } }, + reset() { proxiesByID = {}; didWarnAboutID = {}; @@ -56,7 +57,6 @@ const hooks = { }; hooks.reset(); -global.__REACT_HOT_LOADER__ = hooks; function resolveType(type) { // We only care about composite components @@ -90,6 +90,7 @@ function patchedCreateElement(type, ...args) { } patchedCreateElement.isPatchedByReactHotLoader = true; -if (!React.createElement.isPatchedByReactHotLoader) { +if (typeof global.__REACT_HOT_LOADER__ === 'undefined') { React.createElement = patchedCreateElement; + global.__REACT_HOT_LOADER__ = hooks; } diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index bd62bcc10..a90f981a5 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -31,21 +31,25 @@ describe('patch', () => { // https://github.com/gaearon/react-hot-loader/issues/241 const spy = spyOn(console, 'error'); - RHL.register(Kanye, 'Yeezy', '/wow/test.js'); - expect(console.error.calls.length).toBe(1); - expect(console.error.calls[0].arguments[0]).toBe( - 'React Hot Loader: Yeezy in /wow/test.js will not hot reload ' + - 'correctly because test.js uses during ' + - 'module definition. For hot reloading to work, move Yeezy ' + - 'into a separate file and import it from test.js.' - ); - expect(.type).toBe(Kanye); - expect(.type).toBe(Kanye2); - - RHL.register(Kanye2, 'Yeezy', '/wow/test.js'); - expect(console.error.calls.length).toBe(1); - expect(.type).toBe(Kanye); - expect(.type).toBe(Kanye2); + try { + RHL.register(Kanye, 'Yeezy', '/wow/test.js'); + expect(console.error.calls.length).toBe(1); + expect(console.error.calls[0].arguments[0]).toBe( + 'React Hot Loader: Yeezy in /wow/test.js will not hot reload ' + + 'correctly because test.js uses during ' + + 'module definition. For hot reloading to work, move Yeezy ' + + 'into a separate file and import it from test.js.' + ); + expect(.type).toBe(Kanye); + expect(.type).toBe(Kanye2); + + RHL.register(Kanye2, 'Yeezy', '/wow/test.js'); + expect(console.error.calls.length).toBe(1); + expect(.type).toBe(Kanye); + expect(.type).toBe(Kanye2); + } finally { + spy.restore(); + } }); it('resolves registered types by their last ID', () => { From 730aaddf7f05af45e2def000755c3d3fb9220cb4 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 04:36:43 +0100 Subject: [PATCH 093/161] Make WeakMap optional --- src/patch.dev.js | 68 +++- test/AppContainer/AppContainer.dev.js | 479 +++++++++++++------------- test/patch/patch.dev.js | 261 +++++++------- 3 files changed, 439 insertions(+), 369 deletions(-) diff --git a/src/patch.dev.js b/src/patch.dev.js index 1b47e80fa..7b4bf7b32 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -4,6 +4,66 @@ const React = require('react'); const createProxy = require('react-proxy').default; const global = require('global'); +class ComponentMap { + constructor(useWeakMap) { + if (useWeakMap) { + this.wm = new WeakMap(); + } else { + this.slots = {}; + } + } + + getSlot(type) { + const key = type.displayName || type.name || 'Unknown'; + if (!this.slots[key]) { + this.slots[key] = []; + } + return this.slots[key]; + } + + get(type) { + if (this.wm) { + return this.wm.get(type); + } else { + const slot = this.getSlot(type); + for (let i = 0; i < slot.length; i++) { + if (slot[i].key === type) { + return slot[i].value; + } + } + } + } + + set(type, value) { + if (this.wm) { + this.wm.set(type, value); + } else { + const slot = this.getSlot(type); + for (let i = 0; i < slot.length; i++) { + if (slot[i].key === type) { + slot[i].value = value; + return; + } + } + slot.push({ key: type, value }); + } + } + + has(type) { + if (this.wm) { + return this.wm.has(type); + } else { + const slot = this.getSlot(type); + for (let i = 0; i < slot.length; i++) { + if (slot[i].key === type) { + return true; + } + } + return false; + } + } +} + let proxiesByID; let didWarnAboutID; let hasCreatedElementsByType; @@ -48,15 +108,15 @@ const hooks = { } }, - reset() { + reset(useWeakMap) { proxiesByID = {}; didWarnAboutID = {}; - hasCreatedElementsByType = new WeakMap(); - idsByType = new WeakMap(); + hasCreatedElementsByType = new ComponentMap(useWeakMap); + idsByType = new ComponentMap(useWeakMap); } }; -hooks.reset(); +hooks.reset(typeof WeakMap === 'function'); function resolveType(type) { // We only care about composite components diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index a71a117d4..c8aabb3b5 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -6,48 +6,33 @@ import {mount} from 'enzyme' import AppContainer from '../../src/AppContainer.dev' const RHL = global.__REACT_HOT_LOADER__ -describe('', () => { - beforeEach(() => { - RHL.reset() - }); - - describe('when passed children', () => { - describe('with class root', () => { - it('renders it', () => { - const spy = createSpy() - class App extends Component { - render() { - spy() - return
hey
- } - } - RHL.register(App, 'App', 'test.js') - - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) - - it('force updates the tree on receiving new children', () => { - const spy = createSpy() +function runAllTests(useWeakMap) { + describe('', () => { + beforeEach(() => { + RHL.reset(useWeakMap) + }) - class App extends Component { - shouldComponentUpdate() { - return false + describe('when passed children', () => { + describe('with class root', () => { + it('renders it', () => { + const spy = createSpy() + class App extends Component { + render() { + spy() + return
hey
+ } } + RHL.register(App, 'App', 'test.js') - render() { - spy() - return
hey
- } - } - RHL.register(App, 'App', 'test.js') + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - const wrapper = mount() - expect(spy.calls.length).toBe(1) + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - { class App extends Component { shouldComponentUpdate() { return false @@ -55,37 +40,36 @@ describe('', () => { render() { spy() - return
ho
+ return
hey
} } RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const wrapper = mount() + expect(spy.calls.length).toBe(1) - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + { + class App extends Component { + shouldComponentUpdate() { + return false + } - class App extends Component { - shouldComponentUpdate() { - return false + render() { + spy() + return
ho
+ } + } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) } - render() { - spy() - return
hey
- } - } - RHL.register(App, 'App', 'test.js') + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() - { class App extends Component { shouldComponentUpdate() { return false @@ -93,40 +77,38 @@ describe('', () => { render() { spy() - return
ho
+ return
hey
} } RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) - it('hot-reloads without losing state', () => { - class App extends Component { - componentWillMount() { - this.state = 'old' - } - - shouldComponentUpdate() { - return false - } + { + class App extends Component { + shouldComponentUpdate() { + return false + } - render() { - return
old render + {this.state} state
+ render() { + spy() + return
ho
+ } + } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) } - } - RHL.register(App, 'App', 'test.js') - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { + it('hot-reloads without losing state', () => { class App extends Component { componentWillMount() { - this.state = 'new' + this.state = 'old' } shouldComponentUpdate() { @@ -134,259 +116,282 @@ describe('', () => { } render() { - return
new render + {this.state} state
+ return
old render + {this.state} state
} } RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) - } - expect(wrapper.text()).toBe('new render + old state') - }) - }) + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') - describe('with SFC root', () => { - it('renders it', () => { - const spy = createSpy() - const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') - - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + { + class App extends Component { + componentWillMount() { + this.state = 'new' + } - it('force updates the tree on receiving new children', () => { - const spy = createSpy() + shouldComponentUpdate() { + return false + } - const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') + render() { + return
new render + {this.state} state
+ } + } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } - const wrapper = mount() - expect(spy.calls.length).toBe(1) + expect(wrapper.text()).toBe('new render + old state') + }) + }) - { + describe('with SFC root', () => { + it('renders it', () => { + const spy = createSpy() const App = () => { spy() - return
ho
+ return
hey
} RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) - - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() - - const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - { const App = () => { spy() - return
ho
+ return
hey
} RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const wrapper = mount() + expect(spy.calls.length).toBe(1) - it('hot-reloads without losing state', () => { - class App extends Component { - componentWillMount() { - this.state = 'old' + { + const App = () => { + spy() + return
ho
+ } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) } - shouldComponentUpdate() { return false } + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() - render() { - return
old render + {this.state} state
+ const App = () => { + spy() + return
hey
} - } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js') - const Root = () => - RHL.register(Root, 'Root', 'test.js') + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + { + const App = () => { + spy() + return
ho
+ } + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } - { + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('hot-reloads without losing state', () => { class App extends Component { componentWillMount() { - this.state = 'new' + this.state = 'old' } shouldComponentUpdate() { return false } render() { - return
new render + {this.state} state
+ return
old render + {this.state} state
} } RHL.register(App, 'App', 'test.js') const Root = () => RHL.register(Root, 'Root', 'test.js') - wrapper.setProps({children: }) - } - expect(wrapper.text()).toBe('new render + old state') - }) - }) + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') - describe('with HOC-wrapped root', () => { - const mapProps = require('recompose').mapProps - it('renders it', () => { - const spy = createSpy() - class App extends React.Component { - render() { - spy() - return
hey
- } - } - RHL.register(App, 'App', 'test.js') + { + class App extends Component { + componentWillMount() { + this.state = 'new' + } + + shouldComponentUpdate() { return false } + + render() { + return
new render + {this.state} state
+ } + } + RHL.register(App, 'App', 'test.js') - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + const Root = () => + RHL.register(Root, 'Root', 'test.js') + wrapper.setProps({children: }) + } - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(wrapper.find('App').prop('n')).toBe(15) - expect(spy.calls.length).toBe(1) + expect(wrapper.text()).toBe('new render + old state') + }) }) - it('force updates the tree on receiving new children', () => { - const spy = createSpy() - class App extends React.Component { - render() { - spy() - return
hey
+ describe('with HOC-wrapped root', () => { + const mapProps = require('recompose').mapProps + it('renders it', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
+ } } - } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js') - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') - const wrapper = mount() - expect(spy.calls.length).toBe(1) + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(wrapper.find('App').prop('n')).toBe(15) + expect(spy.calls.length).toBe(1) + }) - { + it('force updates the tree on receiving new children', () => { + const spy = createSpy() class App extends React.Component { render() { spy() - return
ho
+ return
hey
} } RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: }) - } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const wrapper = mount() + expect(spy.calls.length).toBe(1) + { + class App extends React.Component { + render() { + spy() + return
ho
+ } + } + RHL.register(App, 'App', 'test.js') - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() - class App extends React.Component { - render() { - spy() - return
hey
+ const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') + wrapper.setProps({children: }) } - } - RHL.register(App, 'App', 'test.js') - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) - { + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() class App extends React.Component { render() { spy() - return
ho
+ return
hey
} } RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: element}) - } - - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) - it('hot-reloads without losing state', () => { - class App extends Component { - componentWillMount() { - this.state = 'old' - } + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) - shouldComponentUpdate() { return false } + { + class App extends React.Component { + render() { + spy() + return
ho
+ } + } + RHL.register(App, 'App', 'test.js') - render() { - return
old render + {this.state} state + {this.props.n}
+ const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') + wrapper.setProps({children: element}) } - } - RHL.register(App, 'App', 'test.js') - - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state + 15') + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { + it('hot-reloads without losing state', () => { class App extends Component { componentWillMount() { - this.state = 'new' + this.state = 'old' } shouldComponentUpdate() { return false } render() { - return
new render + {this.state} state + {this.props.n}
+ return
old render + {this.state} state + {this.props.n}
} } RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: }) - } - expect(wrapper.text()).toBe('new render + old state + 20') + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state + 15') + + { + class App extends Component { + componentWillMount() { + this.state = 'new' + } + + shouldComponentUpdate() { return false } + + render() { + return
new render + {this.state} state + {this.props.n}
+ } + } + RHL.register(App, 'App', 'test.js') + + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state + 20') + }) }) }) }) -}) +} + +runAllTests(true) +runAllTests(false) diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index a90f981a5..8232f5a9a 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -9,143 +9,148 @@ function A3() {} function B1() {} function B2() {} -describe('patch', () => { - beforeEach(() => { - RHL.reset(); - }); - - it('is identity for unrecognized types', () => { - expect(
.type).toBe('div'); - expect(.type).toBe(A1); - }); +function runAllTests(useWeakMap) { + describe('patch', () => { + beforeEach(() => { + RHL.reset(useWeakMap); + }); - it('ignores late registrations', () => { - function Kanye() {} - function Kanye2() {} - - React.createElement(Kanye); - - // By this time we have to assume the user might have rendered it. - // It's unsafe to resolve it to a different type afterwards, - // or we may cause an existing component to unmount unpredictably. - // https://github.com/gaearon/react-hot-loader/issues/241 - - const spy = spyOn(console, 'error'); - try { - RHL.register(Kanye, 'Yeezy', '/wow/test.js'); - expect(console.error.calls.length).toBe(1); - expect(console.error.calls[0].arguments[0]).toBe( - 'React Hot Loader: Yeezy in /wow/test.js will not hot reload ' + - 'correctly because test.js uses during ' + - 'module definition. For hot reloading to work, move Yeezy ' + - 'into a separate file and import it from test.js.' - ); - expect(.type).toBe(Kanye); - expect(.type).toBe(Kanye2); - - RHL.register(Kanye2, 'Yeezy', '/wow/test.js'); - expect(console.error.calls.length).toBe(1); - expect(.type).toBe(Kanye); - expect(.type).toBe(Kanye2); - } finally { - spy.restore(); - } - }); + it('is identity for unrecognized types', () => { + expect(
.type).toBe('div'); + expect(.type).toBe(A1); + }); - it('resolves registered types by their last ID', () => { - RHL.register(A1, 'a', 'test.js'); - const A = .type; - expect(A).toNotBe(A1); - expect(A).toBeA('function'); - expect(.type).toBe(A); - - RHL.register(A2, 'a', 'test.js'); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - - RHL.register(A3, 'a', 'test.js'); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - - RHL.register(B1, 'b', 'test.js'); - const B = .type; - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(B); - expect(.type).toBe(B); - - RHL.register(B2, 'b', 'test.js'); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(B); - expect(.type).toBe(B); - expect(.type).toBe(B); - }); + it('ignores late registrations', () => { + function Kanye() {} + function Kanye2() {} + + React.createElement(Kanye); + + // By this time we have to assume the user might have rendered it. + // It's unsafe to resolve it to a different type afterwards, + // or we may cause an existing component to unmount unpredictably. + // https://github.com/gaearon/react-hot-loader/issues/241 + + const spy = spyOn(console, 'error'); + try { + RHL.register(Kanye, 'Yeezy', '/wow/test.js'); + expect(console.error.calls.length).toBe(1); + expect(console.error.calls[0].arguments[0]).toBe( + 'React Hot Loader: Yeezy in /wow/test.js will not hot reload ' + + 'correctly because test.js uses during ' + + 'module definition. For hot reloading to work, move Yeezy ' + + 'into a separate file and import it from test.js.' + ); + expect(.type).toBe(Kanye); + expect(.type).toBe(Kanye2); + + RHL.register(Kanye2, 'Yeezy', '/wow/test.js'); + expect(console.error.calls.length).toBe(1); + expect(.type).toBe(Kanye); + expect(.type).toBe(Kanye2); + } finally { + spy.restore(); + } + }); - it('works with reexported types', () => { - RHL.register(A1, 'a', 'test.js'); - RHL.register(A1, 'x', 'test2.js'); + it('resolves registered types by their last ID', () => { + RHL.register(A1, 'a', 'test.js'); + const A = .type; + expect(A).toNotBe(A1); + expect(A).toBeA('function'); + expect(.type).toBe(A); + + RHL.register(A2, 'a', 'test.js'); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + + RHL.register(A3, 'a', 'test.js'); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + + RHL.register(B1, 'b', 'test.js'); + const B = .type; + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(B); + expect(.type).toBe(B); + + RHL.register(B2, 'b', 'test.js'); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(B); + expect(.type).toBe(B); + expect(.type).toBe(B); + }); - const A = .type; - expect(A.type).toNotBe(A1); - expect(A).toBeA('function'); - expect(.type).toBe(A); + it('works with reexported types', () => { + RHL.register(A1, 'a', 'test.js'); + RHL.register(A1, 'x', 'test2.js'); - RHL.register(A2, 'a', 'test.js'); - RHL.register(A2, 'x', 'test2.js'); - expect(.type).toBe(A); - expect(.type).toBe(A); - expect(.type).toBe(A); - }); + const A = .type; + expect(A.type).toNotBe(A1); + expect(A).toBeA('function'); + expect(.type).toBe(A); - it('passes props through', () => { - expect(
.props).toEqual({ - x: 42, - y: 'lol' - }); - expect(.props).toEqual({ - x: 42, - y: 'lol' + RHL.register(A2, 'a', 'test.js'); + RHL.register(A2, 'x', 'test2.js'); + expect(.type).toBe(A); + expect(.type).toBe(A); + expect(.type).toBe(A); }); - RHL.register(B1, 'b', 'test.js'); - expect(.props).toEqual({ - x: 42, - y: 'lol' + it('passes props through', () => { + expect(
.props).toEqual({ + x: 42, + y: 'lol' + }); + expect(.props).toEqual({ + x: 42, + y: 'lol' + }); + + RHL.register(B1, 'b', 'test.js'); + expect(.props).toEqual({ + x: 42, + y: 'lol' + }); + RHL.register(B2, 'b', 'test.js'); + expect(.props).toEqual({ + x: 42, + y: 'lol' + }); }); - RHL.register(B2, 'b', 'test.js'); - expect(.props).toEqual({ - x: 42, - y: 'lol' + + it('passes children through', () => { + expect(
{'Hi'}{'Bye'}
.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + expect({'Hi'}{'Bye'}.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + + RHL.register(B1, 'b', 'test.js'); + expect({'Hi'}{'Bye'}.props.children).toEqual([ + 'Hi', + 'Bye' + ]); + RHL.register(B2, 'b', 'test.js'); + expect({'Hi'}{'Bye'}.props.children).toEqual([ + 'Hi', + 'Bye' + ]); }); }); +} - it('passes children through', () => { - expect(
{'Hi'}{'Bye'}
.props.children).toEqual([ - 'Hi', - 'Bye' - ]); - expect({'Hi'}{'Bye'}.props.children).toEqual([ - 'Hi', - 'Bye' - ]); - - RHL.register(B1, 'b', 'test.js'); - expect({'Hi'}{'Bye'}.props.children).toEqual([ - 'Hi', - 'Bye' - ]); - RHL.register(B2, 'b', 'test.js'); - expect({'Hi'}{'Bye'}.props.children).toEqual([ - 'Hi', - 'Bye' - ]); - }); -}); \ No newline at end of file +runAllTests(true); +runAllTests(false); From 6b5d7cf7776a6f4d2c15e62736b53ec9829a76e2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 2 May 2016 04:38:57 +0100 Subject: [PATCH 094/161] 3.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 24fa7657b..389a7a182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-beta.0", + "version": "3.0.0-beta.1", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 3943b5dd3c5d751bc02499e27cf141aa7a06b83d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 20:22:56 +0100 Subject: [PATCH 095/161] added travis.yml --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..56a566c75 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "5" + - "6" From f9d88c9c2e99865b7ae16c7e4e20bcdfa9238ebc Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 19:43:33 +0100 Subject: [PATCH 096/161] added test case for createClass and createFactory --- test/AppContainer/AppContainer.dev.js | 274 +++++++++++++++++++++++++- 1 file changed, 273 insertions(+), 1 deletion(-) diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index c8aabb3b5..4b544a264 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -7,7 +7,7 @@ import AppContainer from '../../src/AppContainer.dev' const RHL = global.__REACT_HOT_LOADER__ function runAllTests(useWeakMap) { - describe('', () => { + describe(` [useWeakMap == ${useWeakMap}] `, () => { beforeEach(() => { RHL.reset(useWeakMap) }) @@ -146,6 +146,278 @@ function runAllTests(useWeakMap) { }) }) + describe('with createClass root', () => { + it('renders it', () => { + const spy = createSpy() + const App = React.createClass({ + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) + + it('force updates the tree on receiving new children', () => { + const spy = createSpy() + + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
ho
+ } + }) + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
ho
+ } + }) + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('hot-reloads without losing state', () => { + const App = React.createClass({ + componentWillMount() { + this.state = 'old' + }, + + shouldComponentUpdate() { + return false + }, + + render() { + return
old render + {this.state} state
+ } + }) + RHL.register(App, 'App', 'test.js') + + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') + + { + const App = React.createClass({ + componentWillMount() { + this.state = 'new' + }, + + shouldComponentUpdate() { + return false + }, + + render() { + return
new render + {this.state} state
+ } + }) + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state') + }) + }) + + describe('with createFactory root', () => { + it('renders it', () => { + const spy = createSpy() + const App = React.createClass({ + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + + const wrapper = mount({AppF()}) + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) + + it('force updates the tree on receiving new children', () => { + const spy = createSpy() + + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + + const wrapper = mount({AppF()}) + expect(spy.calls.length).toBe(1) + + { + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
ho
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + wrapper.setProps({children: AppF()}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + + const element = AppF() + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
ho
+ } + }) + RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('hot-reloads without losing state', () => { + const App = React.createClass({ + componentWillMount() { + this.state = 'old' + }, + + shouldComponentUpdate() { + return false + }, + + render() { + return
old render + {this.state} state
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + + const wrapper = mount({AppF()}) + expect(wrapper.text()).toBe('old render + old state') + + { + const App = React.createClass({ + componentWillMount() { + this.state = 'new' + }, + + shouldComponentUpdate() { + return false + }, + + render() { + return
new render + {this.state} state
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + wrapper.setProps({children: AppF()}) + } + + expect(wrapper.text()).toBe('new render + old state') + }) + }) + describe('with SFC root', () => { it('renders it', () => { const spy = createSpy() From b20f36d1fe37a16336568c5d2f611ad86d68ec42 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 21:04:20 +0100 Subject: [PATCH 097/161] added tests for rendering cached never-rendered elements --- test/AppContainer/AppContainer.dev.js | 945 +++++++++++++++----------- 1 file changed, 557 insertions(+), 388 deletions(-) diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 4b544a264..5189795fe 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -1,75 +1,128 @@ -import './setup' -import React, {Component} from 'react' -import expect, {createSpy} from 'expect' -import {mount} from 'enzyme' +import './setup'; +import React, {Component} from 'react'; +import expect, {createSpy} from 'expect'; +import {mount} from 'enzyme'; -import AppContainer from '../../src/AppContainer.dev' -const RHL = global.__REACT_HOT_LOADER__ +import AppContainer from '../../src/AppContainer.dev'; +const RHL = global.__REACT_HOT_LOADER__; function runAllTests(useWeakMap) { - describe(` [useWeakMap == ${useWeakMap}] `, () => { + describe(` [useWeakMap == ${useWeakMap}]`, () => { beforeEach(() => { - RHL.reset(useWeakMap) - }) + RHL.reset(useWeakMap); + }); + + describe('with class root', () => { + it('renders children', () => { + const spy = createSpy(); + class App extends Component { + render() { + spy(); + return
hey
; + } + } + RHL.register(App, 'App', 'test.js'); + + const wrapper = mount(); + expect(wrapper.find('App').length).toBe(1); + expect(wrapper.contains(
hey
)).toBe(true); + expect(spy.calls.length).toBe(1); + }); + + it('force updates the tree on receiving new children', () => { + const spy = createSpy(); - describe('when passed children', () => { - describe('with class root', () => { - it('renders it', () => { - const spy = createSpy() + class App extends Component { + shouldComponentUpdate() { + return false + } + + render() { + spy() + return
hey
+ } + } + RHL.register(App, 'App', 'test.js'); + + const wrapper = mount(); + expect(spy.calls.length).toBe(1); + + { class App extends Component { + shouldComponentUpdate() { + return false + } + render() { spy() - return
hey
+ return
ho
} } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({children: }); + } - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy(); - it('force updates the tree on receiving new children', () => { - const spy = createSpy() + class App extends Component { + shouldComponentUpdate() { + return false; + } + + render() { + spy(); + return
hey
; + } + } + RHL.register(App, 'App', 'test.js'); + const element = ; + const wrapper = mount({element}); + expect(spy.calls.length).toBe(1); + + { class App extends Component { shouldComponentUpdate() { - return false + return false; } render() { - spy() - return
hey
+ spy(); + return
ho
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({children: element}); + } - const wrapper = mount() - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); - { - class App extends Component { - shouldComponentUpdate() { - return false - } + it('renders latest children on receiving cached never-rendered children', () => { + const spy = createSpy(); - render() { - spy() - return
ho
- } - } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) + class App extends Component { + shouldComponentUpdate() { + return false; } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + render() { + spy() + return
hey
+ } + } + RHL.register(App, 'App', 'test.js') - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const element = + let wrapper + { class App extends Component { shouldComponentUpdate() { return false @@ -77,38 +130,40 @@ function runAllTests(useWeakMap) { render() { spy() - return
hey
+ return
ho
} } RHL.register(App, 'App', 'test.js') + wrapper = mount({element}) + } - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(1) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { - class App extends Component { - shouldComponentUpdate() { - return false - } + it('hot-reloads children without losing state', () => { + class App extends Component { + componentWillMount() { + this.state = 'old' + } - render() { - spy() - return
ho
- } - } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + shouldComponentUpdate() { + return false } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + render() { + return
old render + {this.state} state
+ } + } + RHL.register(App, 'App', 'test.js') - it('hot-reloads without losing state', () => { + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') + + { class App extends Component { componentWillMount() { - this.state = 'old' + this.state = 'new' } shouldComponentUpdate() { @@ -116,56 +171,91 @@ function runAllTests(useWeakMap) { } render() { - return
old render + {this.state} state
+ return
new render + {this.state} state
} } RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state') + }) + }) - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + describe('with createClass root', () => { + it('renders children', () => { + const spy = createSpy() + const App = React.createClass({ + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') - { - class App extends Component { - componentWillMount() { - this.state = 'new' - } + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - shouldComponentUpdate() { - return false - } + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - render() { - return
new render + {this.state} state
- } - } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) - } + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, - expect(wrapper.text()).toBe('new render + old state') + render() { + spy() + return
hey
+ } }) - }) + RHL.register(App, 'App', 'test.js') - describe('with createClass root', () => { - it('renders it', () => { - const spy = createSpy() + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + render() { spy() - return
hey
+ return
ho
} }) RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
hey
+ } }) + RHL.register(App, 'App', 'test.js') - it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + { const App = React.createClass({ shouldComponentUpdate() { return false @@ -173,36 +263,36 @@ function runAllTests(useWeakMap) { render() { spy() - return
hey
+ return
ho
} }) RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const wrapper = mount() - expect(spy.calls.length).toBe(1) + it('renders latest children on receiving cached never-rendered children', () => { + const spy = createSpy() - { - const App = React.createClass({ - shouldComponentUpdate() { - return false - }, + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, - render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) + render() { + spy() + return
hey
} - - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) }) + RHL.register(App, 'App', 'test.js') - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const element = + let wrapper + { const App = React.createClass({ shouldComponentUpdate() { return false @@ -210,38 +300,40 @@ function runAllTests(useWeakMap) { render() { spy() - return
hey
+ return
ho
} }) RHL.register(App, 'App', 'test.js') + wrapper = mount({element}) + } + + expect(spy.calls.length).toBe(1) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + it('hot-reloads children without losing state', () => { + const App = React.createClass({ + componentWillMount() { + this.state = 'old' + }, - { - const App = React.createClass({ - shouldComponentUpdate() { - return false - }, + shouldComponentUpdate() { + return false + }, - render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + render() { + return
old render + {this.state} state
} - - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) }) + RHL.register(App, 'App', 'test.js') + + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') - it('hot-reloads without losing state', () => { + { const App = React.createClass({ componentWillMount() { - this.state = 'old' + this.state = 'new' }, shouldComponentUpdate() { @@ -249,57 +341,95 @@ function runAllTests(useWeakMap) { }, render() { - return
old render + {this.state} state
+ return
new render + {this.state} state
} }) RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } + + expect(wrapper.text()).toBe('new render + old state') + }) + }) - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + describe('with createFactory root', () => { + it('renders children', () => { + const spy = createSpy() + const App = React.createClass({ + render() { + spy() + return
hey
+ } + }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) - { - const App = React.createClass({ - componentWillMount() { - this.state = 'new' - }, + const wrapper = mount({AppF()}) + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - shouldComponentUpdate() { - return false - }, + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - render() { - return
new render + {this.state} state
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) - } + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, - expect(wrapper.text()).toBe('new render + old state') + render() { + spy() + return
hey
+ } }) - }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + + const wrapper = mount({AppF()}) + expect(spy.calls.length).toBe(1) - describe('with createFactory root', () => { - it('renders it', () => { - const spy = createSpy() + { const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + render() { spy() - return
hey
+ return
ho
} }) RHL.register(App, 'App', 'test.js') const AppF = React.createFactory(App) + wrapper.setProps({children: AppF()}) + } + + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const wrapper = mount({AppF()}) - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, + + render() { + spy() + return
hey
+ } }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) - it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const element = AppF() + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + { const App = React.createClass({ shouldComponentUpdate() { return false @@ -307,38 +437,37 @@ function runAllTests(useWeakMap) { render() { spy() - return
hey
+ return
ho
} }) RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + wrapper.setProps({children: element}) + } - const wrapper = mount({AppF()}) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { - const App = React.createClass({ - shouldComponentUpdate() { - return false - }, + it('renders latest children on receiving cached never-rendered children', () => { + const spy = createSpy() - render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) - wrapper.setProps({children: AppF()}) - } + const App = React.createClass({ + shouldComponentUpdate() { + return false + }, - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) + render() { + spy() + return
hey
+ } }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const element = AppF() + let wrapper + { const App = React.createClass({ shouldComponentUpdate() { return false @@ -346,39 +475,41 @@ function runAllTests(useWeakMap) { render() { spy() - return
hey
+ return
ho
} }) RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + wrapper = mount({element}) + } + + expect(spy.calls.length).toBe(1) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const element = AppF() - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + it('hot-reloads children without losing state', () => { + const App = React.createClass({ + componentWillMount() { + this.state = 'old' + }, - { - const App = React.createClass({ - shouldComponentUpdate() { - return false - }, + shouldComponentUpdate() { + return false + }, - render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + render() { + return
old render + {this.state} state
} - - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) }) + RHL.register(App, 'App', 'test.js') + const AppF = React.createFactory(App) + + const wrapper = mount({AppF()}) + expect(wrapper.text()).toBe('old render + old state') - it('hot-reloads without losing state', () => { + { const App = React.createClass({ componentWillMount() { - this.state = 'old' + this.state = 'new' }, shouldComponentUpdate() { @@ -386,284 +517,322 @@ function runAllTests(useWeakMap) { }, render() { - return
old render + {this.state} state
+ return
new render + {this.state} state
} }) RHL.register(App, 'App', 'test.js') const AppF = React.createFactory(App) + wrapper.setProps({children: AppF()}) + } - const wrapper = mount({AppF()}) - expect(wrapper.text()).toBe('old render + old state') + expect(wrapper.text()).toBe('new render + old state') + }) + }) - { - const App = React.createClass({ - componentWillMount() { - this.state = 'new' - }, + describe('with SFC root', () => { + it('renders children', () => { + const spy = createSpy() + const App = () => { + spy() + return
hey
+ } + RHL.register(App, 'App', 'test.js') + + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(spy.calls.length).toBe(1) + }) - shouldComponentUpdate() { - return false - }, + it('force updates the tree on receiving new children', () => { + const spy = createSpy() - render() { - return
new render + {this.state} state
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) - wrapper.setProps({children: AppF()}) - } + const App = () => { + spy() + return
hey
+ } + RHL.register(App, 'App', 'test.js') - expect(wrapper.text()).toBe('new render + old state') - }) - }) + const wrapper = mount() + expect(spy.calls.length).toBe(1) - describe('with SFC root', () => { - it('renders it', () => { - const spy = createSpy() + { const App = () => { spy() - return
hey
+ return
ho
} RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: }) + } - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - it('force updates the tree on receiving new children', () => { - const spy = createSpy() + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + const App = () => { + spy() + return
hey
+ } + RHL.register(App, 'App', 'test.js') + + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { const App = () => { spy() - return
hey
+ return
ho
} RHL.register(App, 'App', 'test.js') + wrapper.setProps({children: element}) + } - const wrapper = mount() - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { - const App = () => { - spy() - return
ho
- } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) - } + it('renders latest children on receiving cached never-rendered children', () => { + const spy = createSpy() - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const App = () => { + spy() + return
hey
+ } + RHL.register(App, 'App', 'test.js') - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const element = + let wrapper + { const App = () => { spy() - return
hey
+ return
ho
} RHL.register(App, 'App', 'test.js') + wrapper = mount({element}) + } - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(1) + expect(wrapper.contains(
ho
)).toBe(true) + }) - { - const App = () => { - spy() - return
ho
- } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + it('hot-reloads children without losing state', () => { + class App extends Component { + componentWillMount() { + this.state = 'old' } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + shouldComponentUpdate() { return false } - it('hot-reloads without losing state', () => { + render() { + return
old render + {this.state} state
+ } + } + RHL.register(App, 'App', 'test.js') + + const Root = () => + RHL.register(Root, 'Root', 'test.js') + + const wrapper = mount() + expect(wrapper.text()).toBe('old render + old state') + + { class App extends Component { componentWillMount() { - this.state = 'old' + this.state = 'new' } shouldComponentUpdate() { return false } render() { - return
old render + {this.state} state
+ return
new render + {this.state} state
} } RHL.register(App, 'App', 'test.js') const Root = () => RHL.register(Root, 'Root', 'test.js') + wrapper.setProps({children: }) + } - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + expect(wrapper.text()).toBe('new render + old state') + }) + }) - { - class App extends Component { - componentWillMount() { - this.state = 'new' - } + describe('with HOC-wrapped root', () => { + const mapProps = require('recompose').mapProps + it('renders children', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
+ } + } + RHL.register(App, 'App', 'test.js') - shouldComponentUpdate() { return false } + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') - render() { - return
new render + {this.state} state
- } - } - RHL.register(App, 'App', 'test.js') + const wrapper = mount() + expect(wrapper.find('App').length).toBe(1) + expect(wrapper.contains(
hey
)).toBe(true) + expect(wrapper.find('App').prop('n')).toBe(15) + expect(spy.calls.length).toBe(1) + }) - const Root = () => - RHL.register(Root, 'Root', 'test.js') - wrapper.setProps({children: }) + it('force updates the tree on receiving new children', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
} + } + RHL.register(App, 'App', 'test.js') - expect(wrapper.text()).toBe('new render + old state') - }) - }) + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') - describe('with HOC-wrapped root', () => { - const mapProps = require('recompose').mapProps - it('renders it', () => { - const spy = createSpy() + const wrapper = mount() + expect(spy.calls.length).toBe(1) + + { class App extends React.Component { render() { spy() - return
hey
+ return
ho
} } RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) RHL.register(Enhanced, 'Enhanced', 'test.js') + wrapper.setProps({children: }) + } - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(wrapper.find('App').prop('n')).toBe(15) - expect(spy.calls.length).toBe(1) - }) + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) + + + it('force updates the tree on receiving cached children', () => { + const spy = createSpy() + class App extends React.Component { + render() { + spy() + return
hey
+ } + } + RHL.register(App, 'App', 'test.js') - it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) + RHL.register(Enhanced, 'Enhanced', 'test.js') + + const element = + const wrapper = mount({element}) + expect(spy.calls.length).toBe(1) + + { class App extends React.Component { render() { spy() - return
hey
+ return
ho
} } RHL.register(App, 'App', 'test.js') const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) RHL.register(Enhanced, 'Enhanced', 'test.js') + wrapper.setProps({children: element}) + } - const wrapper = mount() - expect(spy.calls.length).toBe(1) - - { - class App extends React.Component { - render() { - spy() - return
ho
- } - } - RHL.register(App, 'App', 'test.js') + expect(spy.calls.length).toBe(2) + expect(wrapper.contains(
ho
)).toBe(true) + }) - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: }) + it('renders latest children on receiving cached never-rendered children', () => { + const spy = createSpy(); + class App extends React.Component { + render() { + spy(); + return
hey
; } + } + RHL.register(App, 'App', 'test.js'); - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); + const element = ; + let wrapper; - it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + { class App extends React.Component { render() { - spy() - return
hey
+ spy(); + return
ho
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); + wrapper = mount({element}); + } - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + expect(spy.calls.length).toBe(1); + expect(wrapper.contains(
ho
)).toBe(true); + }); - { - class App extends React.Component { - render() { - spy() - return
ho
- } - } - RHL.register(App, 'App', 'test.js') + it('hot-reloads children without losing state', () => { + class App extends Component { + componentWillMount() { + this.state = 'old'; + } + + shouldComponentUpdate() { return false; } - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: element}) + render() { + return
old render + {this.state} state + {this.props.n}
; } + } + RHL.register(App, 'App', 'test.js'); - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); - it('hot-reloads without losing state', () => { + const wrapper = mount(); + expect(wrapper.text()).toBe('old render + old state + 15'); + + { class App extends Component { componentWillMount() { - this.state = 'old' + this.state = 'new'; } - shouldComponentUpdate() { return false } + shouldComponentUpdate() { return false; } render() { - return
old render + {this.state} state + {this.props.n}
+ return
new render + {this.state} state + {this.props.n}
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state + 15') - - { - class App extends Component { - componentWillMount() { - this.state = 'new' - } - - shouldComponentUpdate() { return false } - - render() { - return
new render + {this.state} state + {this.props.n}
- } - } - RHL.register(App, 'App', 'test.js') - - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: }) - } + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); + wrapper.setProps({children: }); + } - expect(wrapper.text()).toBe('new render + old state + 20') - }) - }) - }) - }) -} + expect(wrapper.text()).toBe('new render + old state + 20'); + }); + }); + }); +}; -runAllTests(true) -runAllTests(false) +runAllTests(true); +runAllTests(false); From f60df139ac43224b0bf3ea1d8ad353125c8fd1ad Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 21:34:37 +0100 Subject: [PATCH 098/161] added eslint --- .eslintignore | 1 + .eslintrc | 12 ++++++++++++ package.json | 6 ++++++ test/.eslintrc | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 test/.eslintrc diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..573e3fd21 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +test/babel/fixtures diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..a090590b2 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,12 @@ +{ + "extends": "airbnb", + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + }, + }, + "rules": { + "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }], + "jsx-quotes": ["error", "prefer-single"] + } +} diff --git a/package.json b/package.json index 389a7a182..116420e65 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build": "babel src --out-dir lib", "test": "mocha --compilers js:babel-core/register ./test", "test:watch": "npm run test -- --watch", + "lint": "eslint .", "prepublish": "npm run clean && npm run build" }, "dependencies": { @@ -52,6 +53,11 @@ "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "enzyme": "^2.2.0", + "eslint": "^2.9.0", + "eslint-config-airbnb": "^8.0.0", + "eslint-plugin-import": "^1.6.1", + "eslint-plugin-jsx-a11y": "^1.0.4", + "eslint-plugin-react": "^5.0.1", "expect": "^1.16.0", "jsdom": "^8.4.1", "mocha": "^2.4.5", diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 000000000..5746db428 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,19 @@ +{ + "extends": "../.eslintrc", + "env": { + "mocha": true + }, + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + }, + }, + rules: { + "react/prefer-stateless-function": 0, + "react/no-multi-comp": 0, + "react/prefer-es6-class": 0, + "react/prop-types": 0, + "no-shadow": 0, + "new-cap": 0, + }, +} From a268ad60e11e59c8163d90734bbea30701d68bc4 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 22:25:28 +0100 Subject: [PATCH 099/161] fixed most of the eslint errors --- .eslintignore | 1 + babel.js | 2 +- index.js | 2 +- patch.js | 2 +- src/AppContainer.dev.js | 42 +++++++++++++++----------- src/AppContainer.js | 2 ++ src/AppContainer.prod.js | 2 ++ src/babel/index.js | 44 ++++++++++++++-------------- src/index.js | 2 +- src/patch.dev.js | 36 ++++++++++++----------- src/patch.js | 2 ++ src/webpack/index.js | 6 ++-- src/webpack/makeIdentitySourceMap.js | 8 ++--- src/webpack/tagCommonJSExports.js | 4 +-- test/AppContainer/index.js | 2 +- test/AppContainer/setup.js | 6 ++-- test/index.js | 2 +- test/patch/patch.dev.js | 18 +++++++----- webpack.js | 2 +- 19 files changed, 102 insertions(+), 83 deletions(-) diff --git a/.eslintignore b/.eslintignore index 573e3fd21..827bbdd31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ test/babel/fixtures +lib diff --git a/babel.js b/babel.js index 93c319a08..600871d52 100644 --- a/babel.js +++ b/babel.js @@ -1 +1 @@ -module.exports = require('./lib/babel'); \ No newline at end of file +module.exports = require('./lib/babel'); diff --git a/index.js b/index.js index 144ec7fa8..b58e228f6 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./lib/index'); \ No newline at end of file +module.exports = require('./lib/index'); diff --git a/patch.js b/patch.js index 576d66edc..019ee4a44 100644 --- a/patch.js +++ b/patch.js @@ -1 +1 @@ -module.exports = require('./lib/patch'); \ No newline at end of file +module.exports = require('./lib/patch'); diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index ca6b7bfc7..f659a77d1 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -13,7 +13,7 @@ class AppContainer extends Component { componentDidMount() { if (typeof __REACT_HOT_LOADER__ === 'undefined') { - console.error( + console.error( // eslint-disable-line no-console 'React Hot Loader: It appears that "react-hot-loader/patch" ' + 'did not run immediately before the app started. Make sure that it ' + 'runs before any other code. For example, if you use Webpack, ' + @@ -25,11 +25,11 @@ class AppContainer extends Component { } } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps() { // Hot reload is happening. // Retry rendering! this.setState({ - error: null + error: null, }); // Force-update the whole tree, including // components that refuse to update. @@ -40,9 +40,9 @@ class AppContainer extends Component { // In 15.0, it only catches errors on initial mount. // Later it will work for updates as well: // https://github.com/facebook/react/pull/6020 - unstable_handleError(error) { + unstable_handleError(error) { // eslint-disable-line camelcase this.setState({ - error: error + error, }); } @@ -54,38 +54,46 @@ class AppContainer extends Component { if (this.props.component) { return ; - } else { - return React.Children.only(this.props.children); } + + return React.Children.only(this.props.children); } } AppContainer.propTypes = { - component: function (props) { + component(props) { if (props.component) { return new Error( - `Passing "component" prop to is deprecated. ` + - `Replace with .` + 'Passing "component" prop to is deprecated. ' + + 'Replace with .' ); } + + return undefined; }, - props: function (props) { + props(props) { if (props.props) { return new Error( - `Passing "props" prop to is deprecated. ` + - `Replace with .` + 'Passing "props" prop to is deprecated. ' + + 'Replace ' + + 'with .' ); } + + return undefined; }, - children: function (props) { + children(props) { if (React.Children.count(props.children) !== 1) { - return new Error(`Invalid prop "children" supplied to AppContainer. Expected a single React element with your app’s root component, e.g. .`); + return new Error('Invalid prop "children" supplied to AppContainer.' + + 'Expected a single React element with your app’s root component, e.g. .'); } - } + + return undefined; + }, }; AppContainer.defaultProps = { - errorReporter: Redbox + errorReporter: Redbox, }; module.exports = AppContainer; diff --git a/src/AppContainer.js b/src/AppContainer.js index 52e4ae279..940f98071 100644 --- a/src/AppContainer.js +++ b/src/AppContainer.js @@ -1,3 +1,5 @@ +/* eslint-disable global-require */ + 'use strict'; if (process.env.NODE_ENV === 'production') { diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js index 6107a1c40..569ba0954 100644 --- a/src/AppContainer.prod.js +++ b/src/AppContainer.prod.js @@ -1,3 +1,5 @@ +/* eslint-disable react/prop-types */ + 'use strict'; const React = require('react'); diff --git a/src/babel/index.js b/src/babel/index.js index c20c3d525..bcaed0e82 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,7 +14,7 @@ const buildTagger = template(` })(); `); -module.exports = function(args) { +module.exports = function (args) { // This is a Babel plugin, but the user put it in the Webpack config. if (this && this.callback) { throw new Error( @@ -40,25 +40,25 @@ module.exports = function(args) { function shouldRegisterBinding(binding) { let { type, node } = binding.path; switch (type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'VariableDeclaration': - return true; - case 'VariableDeclarator': - const { init } = node; - if (t.isCallExpression(init) && init.callee.name === 'require') { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'VariableDeclaration': + return true; + case 'VariableDeclarator': + const { init } = node; + if (t.isCallExpression(init) && init.callee.name === 'require') { + return false; + } + return true; + default: return false; - } - return true; - default: - return false; } } const REGISTRATIONS = Symbol(); return { visitor: { - ExportDefaultDeclaration(path, { file }) { + ExportDefaultDeclaration(path, { file }) { // Default exports with names are going // to be in scope anyway so no need to bother. if (path.node.declaration.id) { @@ -73,9 +73,9 @@ module.exports = function(args) { t.toExpression(path.node.declaration); path.insertBefore( t.variableDeclaration('const', [ - t.variableDeclarator(id, expression) + t.variableDeclarator(id, expression), ]) - ) + ); path.node.declaration = id; // It won't appear in scope.bindings @@ -84,7 +84,7 @@ module.exports = function(args) { buildRegistration({ ID: id, NAME: t.stringLiteral('default'), - FILENAME: t.stringLiteral(file.opts.filename) + FILENAME: t.stringLiteral(file.opts.filename), }) ); }, @@ -101,7 +101,7 @@ module.exports = function(args) { node[REGISTRATIONS].push(buildRegistration({ ID: binding.identifier, NAME: t.stringLiteral(id), - FILENAME: t.stringLiteral(file.opts.filename) + FILENAME: t.stringLiteral(file.opts.filename), })); } } @@ -116,12 +116,12 @@ module.exports = function(args) { node.body.push(buildSemi()); node.body.push( buildTagger({ - REGISTRATIONS: registrations + REGISTRATIONS: registrations, }) ); node.body.push(buildSemi()); - } - } - } + }, + }, + }, }; -} +}; diff --git a/src/index.js b/src/index.js index 560d947ac..d1c8b0e4d 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,6 @@ module.exports = function warnAboutIncorrectUsage(arg) { 'require("react-hot-loader").AppContainer.' ); } -} +}; module.exports.AppContainer = AppContainer; diff --git a/src/patch.dev.js b/src/patch.dev.js index 7b4bf7b32..edf69be2b 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -24,14 +24,16 @@ class ComponentMap { get(type) { if (this.wm) { return this.wm.get(type); - } else { - const slot = this.getSlot(type); - for (let i = 0; i < slot.length; i++) { - if (slot[i].key === type) { - return slot[i].value; - } + } + + const slot = this.getSlot(type); + for (let i = 0; i < slot.length; i++) { + if (slot[i].key === type) { + return slot[i].value; } } + + return undefined; } set(type, value) { @@ -52,15 +54,15 @@ class ComponentMap { has(type) { if (this.wm) { return this.wm.has(type); - } else { - const slot = this.getSlot(type); - for (let i = 0; i < slot.length; i++) { - if (slot[i].key === type) { - return true; - } + } + + const slot = this.getSlot(type); + for (let i = 0; i < slot.length; i++) { + if (slot[i].key === type) { + return true; } - return false; } + return false; } } @@ -85,7 +87,7 @@ const hooks = { if (!didWarnAboutID[id]) { didWarnAboutID[id] = true; const baseName = fileName.replace(/^.*[\\\/]/, ''); - console.error( + console.error( // eslint-disable-line no-console `React Hot Loader: ${uniqueLocalName} in ${fileName} will not hot reload ` + `correctly because ${baseName} uses <${uniqueLocalName} /> during ` + `module definition. For hot reloading to work, move ${uniqueLocalName} ` + @@ -113,7 +115,7 @@ const hooks = { didWarnAboutID = {}; hasCreatedElementsByType = new ComponentMap(useWeakMap); idsByType = new ComponentMap(useWeakMap); - } + }, }; hooks.reset(typeof WeakMap === 'function'); @@ -127,12 +129,12 @@ function resolveType(type) { hasCreatedElementsByType.set(type, true); // When available, give proxy class to React instead of the real class. - var id = idsByType.get(type); + const id = idsByType.get(type); if (!id) { return type; } - var proxy = proxiesByID[id]; + const proxy = proxiesByID[id]; if (!proxy) { return type; } diff --git a/src/patch.js b/src/patch.js index 74b298fe3..706c20290 100644 --- a/src/patch.js +++ b/src/patch.js @@ -1,3 +1,5 @@ +/* eslint-disable global-require */ + 'use strict'; if (process.env.NODE_ENV === 'production') { diff --git a/src/webpack/index.js b/src/webpack/index.js index ea371f17a..6f84f44d5 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -48,7 +48,7 @@ function transform(source, map) { if (this.sourceMap === false) { return this.callback(null, [ source, - appendText + appendText, ].join(separator)); } @@ -57,11 +57,11 @@ function transform(source, map) { } const node = new SourceNode(null, null, null, [ SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), - new SourceNode(null, null, this.resourcePath, appendText) + new SourceNode(null, null, this.resourcePath, appendText), ]).join(separator); const result = node.toStringWithSourceMap(); this.callback(null, result.code, result.map.toString()); -}; +} module.exports = transform; diff --git a/src/webpack/makeIdentitySourceMap.js b/src/webpack/makeIdentitySourceMap.js index f6db242bf..58b5d1a49 100644 --- a/src/webpack/makeIdentitySourceMap.js +++ b/src/webpack/makeIdentitySourceMap.js @@ -11,16 +11,16 @@ function makeIdentitySourceMap(content, resourcePath) { source: resourcePath, original: { line: index + 1, - column: 0 + column: 0, }, generated: { line: index + 1, - column: 0 - } + column: 0, + }, }); }); return map.toJSON(); } -module.exports = makeIdentitySourceMap; \ No newline at end of file +module.exports = makeIdentitySourceMap; diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index bb82e658c..e0900c92b 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,4 +1,4 @@ -;(function () { +(function () { /* react-hot-loader/webpack */ if (process.env.NODE_ENV !== 'production') { if (typeof __REACT_HOT_LOADER__ === 'undefined') { @@ -25,4 +25,4 @@ __REACT_HOT_LOADER__.register(namedExport, key, __FILENAME__); } } -})(); \ No newline at end of file +})(); diff --git a/test/AppContainer/index.js b/test/AppContainer/index.js index 920ccbf6a..710492e23 100644 --- a/test/AppContainer/index.js +++ b/test/AppContainer/index.js @@ -1 +1 @@ -import './AppContainer.dev' +import './AppContainer.dev'; diff --git a/test/AppContainer/setup.js b/test/AppContainer/setup.js index d923573d7..700a28b27 100644 --- a/test/AppContainer/setup.js +++ b/test/AppContainer/setup.js @@ -1,7 +1,7 @@ require('../../src/patch.dev'); -var jsdom = require('jsdom').jsdom; -var exposedProperties = ['window', 'navigator', 'document']; +const jsdom = require('jsdom').jsdom; +const exposedProperties = ['window', 'navigator', 'document']; global.document = jsdom(''); global.window = document.defaultView; Object.keys(document.defaultView).forEach((property) => { @@ -11,5 +11,5 @@ Object.keys(document.defaultView).forEach((property) => { } }); global.navigator = { - userAgent: 'node.js' + userAgent: 'node.js', }; diff --git a/test/index.js b/test/index.js index 87c34185a..16e77c39f 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,3 @@ import './babel'; import './AppContainer'; -import './patch'; \ No newline at end of file +import './patch'; diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index 8232f5a9a..5d906c1a4 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -33,6 +33,7 @@ function runAllTests(useWeakMap) { const spy = spyOn(console, 'error'); try { + /* eslint-disable no-console */ RHL.register(Kanye, 'Yeezy', '/wow/test.js'); expect(console.error.calls.length).toBe(1); expect(console.error.calls[0].arguments[0]).toBe( @@ -48,6 +49,7 @@ function runAllTests(useWeakMap) { expect(console.error.calls.length).toBe(1); expect(.type).toBe(Kanye); expect(.type).toBe(Kanye2); + /* eslint-enable no-console */ } finally { spy.restore(); } @@ -109,44 +111,44 @@ function runAllTests(useWeakMap) { it('passes props through', () => { expect(
.props).toEqual({ x: 42, - y: 'lol' + y: 'lol', }); expect(.props).toEqual({ x: 42, - y: 'lol' + y: 'lol', }); RHL.register(B1, 'b', 'test.js'); expect(.props).toEqual({ x: 42, - y: 'lol' + y: 'lol', }); RHL.register(B2, 'b', 'test.js'); expect(.props).toEqual({ x: 42, - y: 'lol' + y: 'lol', }); }); it('passes children through', () => { expect(
{'Hi'}{'Bye'}
.props.children).toEqual([ 'Hi', - 'Bye' + 'Bye', ]); expect({'Hi'}{'Bye'}.props.children).toEqual([ 'Hi', - 'Bye' + 'Bye', ]); RHL.register(B1, 'b', 'test.js'); expect({'Hi'}{'Bye'}.props.children).toEqual([ 'Hi', - 'Bye' + 'Bye', ]); RHL.register(B2, 'b', 'test.js'); expect({'Hi'}{'Bye'}.props.children).toEqual([ 'Hi', - 'Bye' + 'Bye', ]); }); }); diff --git a/webpack.js b/webpack.js index ca7a6f582..1e5ce3163 100644 --- a/webpack.js +++ b/webpack.js @@ -1 +1 @@ -module.exports = require('./lib/webpack'); \ No newline at end of file +module.exports = require('./lib/webpack'); From 2926afc181aa9590e06086fa815f320e2871e7f8 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 22:30:11 +0100 Subject: [PATCH 100/161] added run lint to travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 56a566c75..5b96ea765 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,6 @@ language: node_js node_js: - "5" - "6" +script: + - npm run lint + - npm test From e4c1b8199da04991b74468f8724070b6dd9401ac Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 May 2016 23:07:42 +0100 Subject: [PATCH 101/161] jsx double quotes --- .eslintrc | 3 +-- test/patch/patch.dev.js | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.eslintrc b/.eslintrc index a090590b2..d8bd92c0b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,6 @@ }, }, "rules": { - "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }], - "jsx-quotes": ["error", "prefer-single"] + "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }] } } diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index 5d906c1a4..cd8301726 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -109,22 +109,22 @@ function runAllTests(useWeakMap) { }); it('passes props through', () => { - expect(
.props).toEqual({ + expect(
.props).toEqual({ x: 42, y: 'lol', }); - expect(.props).toEqual({ + expect(.props).toEqual({ x: 42, y: 'lol', }); RHL.register(B1, 'b', 'test.js'); - expect(.props).toEqual({ + expect(.props).toEqual({ x: 42, y: 'lol', }); RHL.register(B2, 'b', 'test.js'); - expect(.props).toEqual({ + expect(.props).toEqual({ x: 42, y: 'lol', }); From b8e81fb8bfbbcd2cc0fe48f902ed48efbec2e3b2 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 May 2016 00:06:01 +0100 Subject: [PATCH 102/161] fixed some more --- .eslintrc | 3 + src/webpack/tagCommonJSExports.js | 8 +- test/AppContainer/AppContainer.dev.js | 686 +++++++++++++------------- 3 files changed, 351 insertions(+), 346 deletions(-) diff --git a/.eslintrc b/.eslintrc index d8bd92c0b..33223ef62 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,5 +7,8 @@ }, "rules": { "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }] + }, + globals: { + "__REACT_HOT_LOADER__": true } } diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index e0900c92b..b220b3574 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -1,4 +1,6 @@ -(function () { +/* global __FILENAME__ */ + +;(function register() { // eslint-disable-line no-extra-semi /* react-hot-loader/webpack */ if (process.env.NODE_ENV !== 'production') { if (typeof __REACT_HOT_LOADER__ === 'undefined') { @@ -10,7 +12,7 @@ return; } - for (let key in module.exports) { + for (const key in module.exports) { // eslint-disable-line no-restricted-syntax if (!Object.prototype.hasOwnProperty.call(module.exports, key)) { continue; } @@ -25,4 +27,4 @@ __REACT_HOT_LOADER__.register(namedExport, key, __FILENAME__); } } -})(); +}()); diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 5189795fe..fe1a152ec 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -1,7 +1,8 @@ import './setup'; -import React, {Component} from 'react'; -import expect, {createSpy} from 'expect'; -import {mount} from 'enzyme'; +import React, { Component } from 'react'; +import expect, { createSpy } from 'expect'; +import { mount } from 'enzyme'; +import { mapProps } from 'recompose'; import AppContainer from '../../src/AppContainer.dev'; const RHL = global.__REACT_HOT_LOADER__; @@ -34,12 +35,12 @@ function runAllTests(useWeakMap) { class App extends Component { shouldComponentUpdate() { - return false + return false; } render() { - spy() - return
hey
+ spy(); + return
hey
; } } RHL.register(App, 'App', 'test.js'); @@ -50,16 +51,16 @@ function runAllTests(useWeakMap) { { class App extends Component { shouldComponentUpdate() { - return false + return false; } render() { - spy() - return
ho
+ spy(); + return
ho
; } } RHL.register(App, 'App', 'test.js'); - wrapper.setProps({children: }); + wrapper.setProps({ children: }); } expect(spy.calls.length).toBe(2); @@ -97,7 +98,7 @@ function runAllTests(useWeakMap) { } } RHL.register(App, 'App', 'test.js'); - wrapper.setProps({children: element}); + wrapper.setProps({ children: element }); } expect(spy.calls.length).toBe(2); @@ -113,647 +114,646 @@ function runAllTests(useWeakMap) { } render() { - spy() - return
hey
+ spy(); + return
hey
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const element = - let wrapper + const element = ; + let wrapper; { class App extends Component { shouldComponentUpdate() { - return false + return false; } render() { - spy() - return
ho
+ spy(); + return
ho
; } } - RHL.register(App, 'App', 'test.js') - wrapper = mount({element}) + RHL.register(App, 'App', 'test.js'); + wrapper = mount({element}); } - expect(spy.calls.length).toBe(1) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(1); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('hot-reloads children without losing state', () => { class App extends Component { componentWillMount() { - this.state = 'old' + this.state = 'old'; } shouldComponentUpdate() { - return false + return false; } render() { - return
old render + {this.state} state
+ return
old render + {this.state} state
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + const wrapper = mount(); + expect(wrapper.text()).toBe('old render + old state'); { class App extends Component { componentWillMount() { - this.state = 'new' + this.state = 'new'; } shouldComponentUpdate() { - return false + return false; } render() { - return
new render + {this.state} state
+ return
new render + {this.state} state
; } } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); } - expect(wrapper.text()).toBe('new render + old state') - }) - }) + expect(wrapper.text()).toBe('new render + old state'); + }); + }); describe('with createClass root', () => { it('renders children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + const wrapper = mount(); + expect(wrapper.find('App').length).toBe(1); + expect(wrapper.contains(
hey
)).toBe(true); + expect(spy.calls.length).toBe(1); + }); it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); - const wrapper = mount() - expect(spy.calls.length).toBe(1) + const wrapper = mount(); + expect(spy.calls.length).toBe(1); { const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) + spy(); + return
ho
; + }, + }); + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + const element = ; + const wrapper = mount({element}); + expect(spy.calls.length).toBe(1); { const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + spy(); + return
ho
; + }, + }); + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: element }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('renders latest children on receiving cached never-rendered children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); - const element = - let wrapper + const element = ; + let wrapper; { const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper = mount({element}) + spy(); + return
ho
; + }, + }); + RHL.register(App, 'App', 'test.js'); + wrapper = mount({element}); } - expect(spy.calls.length).toBe(1) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(1); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('hot-reloads children without losing state', () => { const App = React.createClass({ componentWillMount() { - this.state = 'old' + this.state = 'old'; }, shouldComponentUpdate() { - return false + return false; }, render() { - return
old render + {this.state} state
- } - }) - RHL.register(App, 'App', 'test.js') + return
old render + {this.state} state
; + }, + }); + RHL.register(App, 'App', 'test.js'); - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + const wrapper = mount(); + expect(wrapper.text()).toBe('old render + old state'); { const App = React.createClass({ componentWillMount() { - this.state = 'new' + this.state = 'new'; }, shouldComponentUpdate() { - return false + return false; }, render() { - return
new render + {this.state} state
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) + return
new render + {this.state} state
; + }, + }); + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); } - expect(wrapper.text()).toBe('new render + old state') - }) - }) + expect(wrapper.text()).toBe('new render + old state'); + }); + }); describe('with createFactory root', () => { it('renders children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); - const wrapper = mount({AppF()}) - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + const wrapper = mount({AppF()}); + expect(wrapper.find('App').length).toBe(1); + expect(wrapper.contains(
hey
)).toBe(true); + expect(spy.calls.length).toBe(1); + }); it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); - const wrapper = mount({AppF()}) - expect(spy.calls.length).toBe(1) + const wrapper = mount({AppF()}); + expect(spy.calls.length).toBe(1); { const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) - wrapper.setProps({children: AppF()}) + spy(); + return
ho
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); + wrapper.setProps({ children: AppF() }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); - const element = AppF() - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + const element = AppF(); + const wrapper = mount({element}); + expect(spy.calls.length).toBe(1); { const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + spy(); + return
ho
; + }, + }); + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: element }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('renders latest children on receiving cached never-rendered children', () => { - const spy = createSpy() + const spy = createSpy(); const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
hey
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + spy(); + return
hey
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); - const element = AppF() - let wrapper + const element = AppF(); + let wrapper; { const App = React.createClass({ shouldComponentUpdate() { - return false + return false; }, render() { - spy() - return
ho
- } - }) - RHL.register(App, 'App', 'test.js') - wrapper = mount({element}) + spy(); + return
ho
; + }, + }); + RHL.register(App, 'App', 'test.js'); + wrapper = mount({element}); } - expect(spy.calls.length).toBe(1) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(1); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('hot-reloads children without losing state', () => { const App = React.createClass({ componentWillMount() { - this.state = 'old' + this.state = 'old'; }, shouldComponentUpdate() { - return false + return false; }, render() { - return
old render + {this.state} state
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) + return
old render + {this.state} state
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); - const wrapper = mount({AppF()}) - expect(wrapper.text()).toBe('old render + old state') + const wrapper = mount({AppF()}); + expect(wrapper.text()).toBe('old render + old state'); { const App = React.createClass({ componentWillMount() { - this.state = 'new' + this.state = 'new'; }, shouldComponentUpdate() { - return false + return false; }, render() { - return
new render + {this.state} state
- } - }) - RHL.register(App, 'App', 'test.js') - const AppF = React.createFactory(App) - wrapper.setProps({children: AppF()}) + return
new render + {this.state} state
; + }, + }); + RHL.register(App, 'App', 'test.js'); + const AppF = React.createFactory(App); + wrapper.setProps({ children: AppF() }); } - expect(wrapper.text()).toBe('new render + old state') - }) - }) + expect(wrapper.text()).toBe('new render + old state'); + }); + }); describe('with SFC root', () => { it('renders children', () => { - const spy = createSpy() + const spy = createSpy(); const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }; + RHL.register(App, 'App', 'test.js'); - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(spy.calls.length).toBe(1) - }) + const wrapper = mount(); + expect(wrapper.find('App').length).toBe(1); + expect(wrapper.contains(
hey
)).toBe(true); + expect(spy.calls.length).toBe(1); + }); it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const spy = createSpy(); const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }; + RHL.register(App, 'App', 'test.js'); - const wrapper = mount() - expect(spy.calls.length).toBe(1) + const wrapper = mount(); + expect(spy.calls.length).toBe(1); { const App = () => { - spy() - return
ho
- } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: }) + spy(); + return
ho
; + }; + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const spy = createSpy(); const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }; + RHL.register(App, 'App', 'test.js'); - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + const element = ; + const wrapper = mount({element}); + expect(spy.calls.length).toBe(1); { const App = () => { - spy() - return
ho
- } - RHL.register(App, 'App', 'test.js') - wrapper.setProps({children: element}) + spy(); + return
ho
; + }; + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: element }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('renders latest children on receiving cached never-rendered children', () => { - const spy = createSpy() + const spy = createSpy(); const App = () => { - spy() - return
hey
- } - RHL.register(App, 'App', 'test.js') + spy(); + return
hey
; + }; + RHL.register(App, 'App', 'test.js'); - const element = - let wrapper + const element = ; + let wrapper; { const App = () => { - spy() - return
ho
- } - RHL.register(App, 'App', 'test.js') - wrapper = mount({element}) + spy(); + return
ho
; + }; + RHL.register(App, 'App', 'test.js'); + wrapper = mount({element}); } - expect(spy.calls.length).toBe(1) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(1); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('hot-reloads children without losing state', () => { class App extends Component { componentWillMount() { - this.state = 'old' + this.state = 'old'; } - shouldComponentUpdate() { return false } + shouldComponentUpdate() { return false; } render() { - return
old render + {this.state} state
+ return
old render + {this.state} state
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Root = () => - RHL.register(Root, 'Root', 'test.js') + const Root = () => ; + RHL.register(Root, 'Root', 'test.js'); - const wrapper = mount() - expect(wrapper.text()).toBe('old render + old state') + const wrapper = mount(); + expect(wrapper.text()).toBe('old render + old state'); { class App extends Component { componentWillMount() { - this.state = 'new' + this.state = 'new'; } - shouldComponentUpdate() { return false } + shouldComponentUpdate() { return false; } render() { - return
new render + {this.state} state
+ return
new render + {this.state} state
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Root = () => - RHL.register(Root, 'Root', 'test.js') - wrapper.setProps({children: }) + const Root = () => ; + RHL.register(Root, 'Root', 'test.js'); + wrapper.setProps({ children: }); } - expect(wrapper.text()).toBe('new render + old state') - }) - }) + expect(wrapper.text()).toBe('new render + old state'); + }); + }); describe('with HOC-wrapped root', () => { - const mapProps = require('recompose').mapProps it('renders children', () => { - const spy = createSpy() + const spy = createSpy(); class App extends React.Component { render() { - spy() - return
hey
+ spy(); + return
hey
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); - const wrapper = mount() - expect(wrapper.find('App').length).toBe(1) - expect(wrapper.contains(
hey
)).toBe(true) - expect(wrapper.find('App').prop('n')).toBe(15) - expect(spy.calls.length).toBe(1) - }) + const wrapper = mount(); + expect(wrapper.find('App').length).toBe(1); + expect(wrapper.contains(
hey
)).toBe(true); + expect(wrapper.find('App').prop('n')).toBe(15); + expect(spy.calls.length).toBe(1); + }); it('force updates the tree on receiving new children', () => { - const spy = createSpy() + const spy = createSpy(); class App extends React.Component { render() { - spy() - return
hey
+ spy(); + return
hey
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); - const wrapper = mount() - expect(spy.calls.length).toBe(1) + const wrapper = mount(); + expect(spy.calls.length).toBe(1); { class App extends React.Component { render() { - spy() - return
ho
+ spy(); + return
ho
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: }) + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); + wrapper.setProps({ children: }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('force updates the tree on receiving cached children', () => { - const spy = createSpy() + const spy = createSpy(); class App extends React.Component { render() { - spy() - return
hey
+ spy(); + return
hey
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); - const element = - const wrapper = mount({element}) - expect(spy.calls.length).toBe(1) + const element = ; + const wrapper = mount({element}); + expect(spy.calls.length).toBe(1); { class App extends React.Component { render() { - spy() - return
ho
+ spy(); + return
ho
; } } - RHL.register(App, 'App', 'test.js') + RHL.register(App, 'App', 'test.js'); - const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App) - RHL.register(Enhanced, 'Enhanced', 'test.js') - wrapper.setProps({children: element}) + const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); + RHL.register(Enhanced, 'Enhanced', 'test.js'); + wrapper.setProps({ children: element }); } - expect(spy.calls.length).toBe(2) - expect(wrapper.contains(
ho
)).toBe(true) - }) + expect(spy.calls.length).toBe(2); + expect(wrapper.contains(
ho
)).toBe(true); + }); it('renders latest children on receiving cached never-rendered children', () => { const spy = createSpy(); @@ -825,14 +825,14 @@ function runAllTests(useWeakMap) { const Enhanced = mapProps(props => ({ n: props.n * 5 }))(App); RHL.register(Enhanced, 'Enhanced', 'test.js'); - wrapper.setProps({children: }); + wrapper.setProps({ children: }); } expect(wrapper.text()).toBe('new render + old state + 20'); }); }); }); -}; +} runAllTests(true); runAllTests(false); From 6f2c583b56c72250fc5283df76b1b235fe926138 Mon Sep 17 00:00:00 2001 From: Gadi Cohen Date: Tue, 3 May 2016 14:58:37 +0200 Subject: [PATCH 103/161] consoleErrorReporter: correct error proptype & npm i redbox-react --- docs/TipsAndTricks.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/TipsAndTricks.md b/docs/TipsAndTricks.md index a47e9eff7..7a8f21a6b 100644 --- a/docs/TipsAndTricks.md +++ b/docs/TipsAndTricks.md @@ -13,7 +13,7 @@ const consoleErrorReporter = ({error}) => { }; consoleErrorReporter.propTypes = { - error: React.PropTypes.error + error: React.PropTypes.instanceOf(Error).isRequired }; render(( @@ -21,4 +21,6 @@ render(( ), document.getElementById('react-root')); -``` \ No newline at end of file +``` + +You'll also need to `npm install --save-dev redbox-react`. From 13f73229cad6bbc7f4537396c6f6d7f14f33f658 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 May 2016 21:44:49 +0100 Subject: [PATCH 104/161] finished linting all the files --- .eslintrc | 4 +++- src/AppContainer.dev.js | 2 +- src/babel/index.js | 21 ++++++++++++--------- src/patch.dev.js | 4 ++-- src/webpack/index.js | 4 ++-- src/webpack/makeIdentitySourceMap.js | 2 +- test/babel/index.js | 27 +++++++++++++-------------- test/patch/patch.dev.js | 2 -- 8 files changed, 34 insertions(+), 32 deletions(-) diff --git a/.eslintrc b/.eslintrc index 33223ef62..074a2605a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,9 @@ }, }, "rules": { - "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }] + "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }], + "no-console": ["error", { allow: ["error"] }], + "strict": 0, }, globals: { "__REACT_HOT_LOADER__": true diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index f659a77d1..26e2f09b9 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -13,7 +13,7 @@ class AppContainer extends Component { componentDidMount() { if (typeof __REACT_HOT_LOADER__ === 'undefined') { - console.error( // eslint-disable-line no-console + console.error( 'React Hot Loader: It appears that "react-hot-loader/patch" ' + 'did not run immediately before the app started. Make sure that it ' + 'runs before any other code. For example, if you use Webpack, ' + diff --git a/src/babel/index.js b/src/babel/index.js index bcaed0e82..fb9bf1efe 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,7 +14,7 @@ const buildTagger = template(` })(); `); -module.exports = function (args) { +module.exports = function plugin(args) { // This is a Babel plugin, but the user put it in the Webpack config. if (this && this.callback) { throw new Error( @@ -38,18 +38,19 @@ module.exports = function (args) { // Try our best to avoid variables from require(). // Ideally we only want to find components defined by the user. function shouldRegisterBinding(binding) { - let { type, node } = binding.path; + const { type, node } = binding.path; switch (type) { case 'FunctionDeclaration': case 'ClassDeclaration': case 'VariableDeclaration': return true; - case 'VariableDeclarator': + case 'VariableDeclarator': { const { init } = node; if (t.isCallExpression(init) && init.callee.name === 'require') { return false; } return true; + } default: return false; } @@ -76,7 +77,7 @@ module.exports = function (args) { t.variableDeclarator(id, expression), ]) ); - path.node.declaration = id; + path.node.declaration = id; // eslint-disable-line no-param-reassign // It won't appear in scope.bindings // so we'll manually remember it exists. @@ -91,11 +92,12 @@ module.exports = function (args) { Program: { enter({ node, scope }, { file }) { - node[REGISTRATIONS] = []; + node[REGISTRATIONS] = []; // eslint-disable-line no-param-reassign // Everything in the top level scope, when reasonable, // is going to get tagged with __source. - for (let id in scope.bindings) { + /* eslint-disable guard-for-in,no-restricted-syntax */ + for (const id in scope.bindings) { const binding = scope.bindings[id]; if (shouldRegisterBinding(binding)) { node[REGISTRATIONS].push(buildRegistration({ @@ -105,11 +107,12 @@ module.exports = function (args) { })); } } + /* eslint-enable */ }, - exit({ node, scope }) { - let registrations = node[REGISTRATIONS]; - node[REGISTRATIONS] = null; + exit({ node }) { + const registrations = node[REGISTRATIONS]; + node[REGISTRATIONS] = null; // eslint-disable-line no-param-reassign // Inject the generated tagging code at the very end // so that it is as minimally intrusive as possible. diff --git a/src/patch.dev.js b/src/patch.dev.js index edf69be2b..f5a79b6b2 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -82,12 +82,12 @@ const hooks = { if (typeof uniqueLocalName !== 'string' || typeof fileName !== 'string') { return; } - const id = fileName + '#' + uniqueLocalName; + const id = fileName + '#' + uniqueLocalName; // eslint-disable-line prefer-template if (!idsByType.has(type) && hasCreatedElementsByType.has(type)) { if (!didWarnAboutID[id]) { didWarnAboutID[id] = true; const baseName = fileName.replace(/^.*[\\\/]/, ''); - console.error( // eslint-disable-line no-console + console.error( `React Hot Loader: ${uniqueLocalName} in ${fileName} will not hot reload ` + `correctly because ${baseName} uses <${uniqueLocalName} /> during ` + `module definition. For hot reloading to work, move ${uniqueLocalName} ` + diff --git a/src/webpack/index.js b/src/webpack/index.js index 6f84f44d5..6aacda56f 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -53,7 +53,7 @@ function transform(source, map) { } if (!map) { - map = makeIdentitySourceMap(source, this.resourcePath); + map = makeIdentitySourceMap(source, this.resourcePath); // eslint-disable-line no-param-reassign } const node = new SourceNode(null, null, null, [ SourceNode.fromStringWithSourceMap(source, new SourceMapConsumer(map)), @@ -61,7 +61,7 @@ function transform(source, map) { ]).join(separator); const result = node.toStringWithSourceMap(); - this.callback(null, result.code, result.map.toString()); + return this.callback(null, result.code, result.map.toString()); } module.exports = transform; diff --git a/src/webpack/makeIdentitySourceMap.js b/src/webpack/makeIdentitySourceMap.js index 58b5d1a49..649a05ef3 100644 --- a/src/webpack/makeIdentitySourceMap.js +++ b/src/webpack/makeIdentitySourceMap.js @@ -6,7 +6,7 @@ function makeIdentitySourceMap(content, resourcePath) { const map = new SourceMapGenerator(); map.setSourceContent(resourcePath, content); - content.split('\n').map((line, index) => { + content.split('\n').forEach((line, index) => { map.addMapping({ source: resourcePath, original: { diff --git a/test/babel/index.js b/test/babel/index.js index acfc26a9b..6ac011222 100644 --- a/test/babel/index.js +++ b/test/babel/index.js @@ -9,21 +9,20 @@ function trim(str) { describe('tags potential React components', () => { const fixturesDir = path.join(__dirname, 'fixtures'); - fs.readdirSync(fixturesDir).map(fixtureName => { + fs.readdirSync(fixturesDir).forEach(fixtureName => { const fixtureDir = path.join(fixturesDir, fixtureName); - if (!fs.statSync(fixtureDir).isDirectory()) { - return; + if (fs.statSync(fixtureDir).isDirectory()) { + it(fixtureName.split('-').join(' '), () => { + const actualPath = path.join(fixtureDir, 'actual.js'); + const actual = transformFileSync(actualPath).code; + const templatePath = path.sep === '\\' ? + actualPath.replace(/\\/g, '/') : + actualPath; + const expected = fs.readFileSync( + path.join(fixtureDir, 'expected.js') + ).toString().replace(/__FILENAME__/g, JSON.stringify(templatePath)); + expect(trim(actual)).toEqual(trim(expected)); + }); } - it(fixtureName.split('-').join(' '), () => { - const actualPath = path.join(fixtureDir, 'actual.js'); - const actual = transformFileSync(actualPath).code; - const templatePath = path.sep === '\\' ? - actualPath.replace(/\\/g, '/') : - actualPath; - const expected = fs.readFileSync( - path.join(fixtureDir, 'expected.js') - ).toString().replace(/__FILENAME__/g, JSON.stringify(templatePath)); - expect(trim(actual)).toEqual(trim(expected)); - }); }); }); diff --git a/test/patch/patch.dev.js b/test/patch/patch.dev.js index cd8301726..ed08b1777 100644 --- a/test/patch/patch.dev.js +++ b/test/patch/patch.dev.js @@ -33,7 +33,6 @@ function runAllTests(useWeakMap) { const spy = spyOn(console, 'error'); try { - /* eslint-disable no-console */ RHL.register(Kanye, 'Yeezy', '/wow/test.js'); expect(console.error.calls.length).toBe(1); expect(console.error.calls[0].arguments[0]).toBe( @@ -49,7 +48,6 @@ function runAllTests(useWeakMap) { expect(console.error.calls.length).toBe(1); expect(.type).toBe(Kanye); expect(.type).toBe(Kanye2); - /* eslint-enable no-console */ } finally { spy.restore(); } From 49a91331e3cb7e4f8b7b948802669d6d9ee49dd5 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 May 2016 00:37:45 +0100 Subject: [PATCH 105/161] fix error message spacing --- src/AppContainer.dev.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 26e2f09b9..8875650b2 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -84,8 +84,10 @@ AppContainer.propTypes = { }, children(props) { if (React.Children.count(props.children) !== 1) { - return new Error('Invalid prop "children" supplied to AppContainer.' + - 'Expected a single React element with your app’s root component, e.g. .'); + return new Error( + 'Invalid prop "children" supplied to AppContainer. ' + + 'Expected a single React element with your app’s root component, e.g. .' + ); } return undefined; From 2482e327f5416f84bc0dff450305648dfce6c601 Mon Sep 17 00:00:00 2001 From: John Leidegren Date: Wed, 4 May 2016 11:29:41 +0200 Subject: [PATCH 106/161] Fixed typo this.props.prop -> this.props.props (#285) --- src/AppContainer.prod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AppContainer.prod.js b/src/AppContainer.prod.js index 569ba0954..bef9d97a8 100644 --- a/src/AppContainer.prod.js +++ b/src/AppContainer.prod.js @@ -8,7 +8,7 @@ const { Component } = React; class AppContainer extends Component { render() { if (this.props.component) { - return ; + return ; } return React.Children.only(this.props.children); From a7191b5bb30157acc38efad0beef5ea8c0ff2159 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 May 2016 23:19:34 +0100 Subject: [PATCH 107/161] remove eslint comments from output of webpack loader --- src/webpack/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/webpack/index.js b/src/webpack/index.js index 6aacda56f..1bb3d13aa 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -34,6 +34,9 @@ function transform(source, map) { // Babel inserts these. // Ideally we'd opt out for one file but this is simpler. .replace(/['"]use strict['"];/, '') + // eslint comments don't need to end up in the output + .replace(/\/\/ eslint-disable-line .*\n/g, '\n') + .replace(/\/\* global.*\*\//, '') .split(/\n\s*/) .join(' '); } From ad50110f731a4ed7e2f87c6641659c6082fcd560 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 May 2016 23:25:47 +0100 Subject: [PATCH 108/161] patch createFactory --- src/patch.dev.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/patch.dev.js b/src/patch.dev.js index f5a79b6b2..6a026031f 100644 --- a/src/patch.dev.js +++ b/src/patch.dev.js @@ -152,7 +152,18 @@ function patchedCreateElement(type, ...args) { } patchedCreateElement.isPatchedByReactHotLoader = true; +function patchedCreateFactory(type) { + // Patch React.createFactory to use patched createElement + // because the original implementation uses the internal, + // unpatched ReactElement.createElement + const factory = patchedCreateElement.bind(null, type); + factory.type = type; + return factory; +} +patchedCreateFactory.isPatchedByReactHotLoader = true; + if (typeof global.__REACT_HOT_LOADER__ === 'undefined') { React.createElement = patchedCreateElement; + React.createFactory = patchedCreateFactory; global.__REACT_HOT_LOADER__ = hooks; } From 62881d97ca74907bc6de86399eb6b022c7bbc591 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 5 May 2016 03:06:19 +0100 Subject: [PATCH 109/161] 3.0.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 116420e65..8ec26f916 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-beta.1", + "version": "3.0.0-beta.2", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From df188ba2c339c692cdb83d8873b423c07f087ca9 Mon Sep 17 00:00:00 2001 From: Jiri Spac Date: Thu, 19 May 2016 11:16:24 +0200 Subject: [PATCH 110/161] bumped redbox-react dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ec26f916..092c28805 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "global": "^4.3.0", "react-deep-force-update": "^2.0.1", "react-proxy": "^3.0.0-alpha.0", - "redbox-react": "^1.2.3", + "redbox-react": "^1.2.5", "source-map": "^0.4.4" }, "repository": { From 71abb6354314b42278b4fd10112a3003253388a4 Mon Sep 17 00:00:00 2001 From: Mike Meyer Date: Sun, 22 May 2016 17:21:42 -0700 Subject: [PATCH 111/161] Change `NODE_ENV === production` to `module.hot` --- src/AppContainer.js | 6 +++--- src/babel/index.js | 2 +- src/patch.js | 6 +++--- src/webpack/tagCommonJSExports.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AppContainer.js b/src/AppContainer.js index 940f98071..a7f13e4a7 100644 --- a/src/AppContainer.js +++ b/src/AppContainer.js @@ -2,8 +2,8 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { - module.exports = require('./AppContainer.prod'); -} else { +if (module.hot) { module.exports = require('./AppContainer.dev'); +} else { + module.exports = require('./AppContainer.prod'); } diff --git a/src/babel/index.js b/src/babel/index.js index fb9bf1efe..aece3e5b0 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -30,7 +30,7 @@ module.exports = function plugin(args) { const { types: t } = args; // No-op in production. - if (process.env.NODE_ENV === 'production') { + if (!module.hot) { return { visitor: {} }; } diff --git a/src/patch.js b/src/patch.js index 706c20290..1df23185a 100644 --- a/src/patch.js +++ b/src/patch.js @@ -2,8 +2,8 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { - module.exports = require('./patch.prod'); -} else { +if (module.hot) { module.exports = require('./patch.dev'); +} else { + module.exports = require('./patch.prod'); } diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index b220b3574..3f84fde3f 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -2,7 +2,7 @@ ;(function register() { // eslint-disable-line no-extra-semi /* react-hot-loader/webpack */ - if (process.env.NODE_ENV !== 'production') { + if (module.hot) { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } From c28bfa2f9dfec747f64f473d8594272a59cdfe02 Mon Sep 17 00:00:00 2001 From: Mike Meyer Date: Mon, 23 May 2016 15:12:51 -0700 Subject: [PATCH 112/161] Make requested changes from https://github.com/gaearon/react-hot-loader/pull/302/files#r64205976 --- src/AppContainer.js | 6 +++--- src/babel/index.js | 2 +- src/patch.js | 6 +++--- src/webpack/tagCommonJSExports.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AppContainer.js b/src/AppContainer.js index a7f13e4a7..cbf62b72b 100644 --- a/src/AppContainer.js +++ b/src/AppContainer.js @@ -2,8 +2,8 @@ 'use strict'; -if (module.hot) { - module.exports = require('./AppContainer.dev'); -} else { +if (!module.hot && process.env.NODE_ENV === 'production') { module.exports = require('./AppContainer.prod'); +} else { + module.exports = require('./AppContainer.dev'); } diff --git a/src/babel/index.js b/src/babel/index.js index aece3e5b0..fb9bf1efe 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -30,7 +30,7 @@ module.exports = function plugin(args) { const { types: t } = args; // No-op in production. - if (!module.hot) { + if (process.env.NODE_ENV === 'production') { return { visitor: {} }; } diff --git a/src/patch.js b/src/patch.js index 1df23185a..eeaff0792 100644 --- a/src/patch.js +++ b/src/patch.js @@ -2,8 +2,8 @@ 'use strict'; -if (module.hot) { - module.exports = require('./patch.dev'); -} else { +if (!module.hot && process.env.NODE_ENV === 'production') { module.exports = require('./patch.prod'); +} else { + module.exports = require('./patch.dev'); } diff --git a/src/webpack/tagCommonJSExports.js b/src/webpack/tagCommonJSExports.js index 3f84fde3f..b220b3574 100644 --- a/src/webpack/tagCommonJSExports.js +++ b/src/webpack/tagCommonJSExports.js @@ -2,7 +2,7 @@ ;(function register() { // eslint-disable-line no-extra-semi /* react-hot-loader/webpack */ - if (module.hot) { + if (process.env.NODE_ENV !== 'production') { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } From 47d53e674fd38ad89c97f60adb24e72d3832f12e Mon Sep 17 00:00:00 2001 From: Nathan Marks Date: Wed, 29 Jun 2016 11:30:33 -0400 Subject: [PATCH 113/161] Fix redbox require and add test --- src/AppContainer.dev.js | 2 +- test/AppContainer/AppContainer.dev.js | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index 8875650b2..af9e197b0 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -2,7 +2,7 @@ const React = require('react'); const deepForceUpdate = require('react-deep-force-update'); -const Redbox = require('redbox-react'); +const Redbox = require('redbox-react').default; const { Component } = React; class AppContainer extends Component { diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index fe1a152ec..96388c498 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -1,7 +1,7 @@ import './setup'; import React, { Component } from 'react'; import expect, { createSpy } from 'expect'; -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { mapProps } from 'recompose'; import AppContainer from '../../src/AppContainer.dev'; @@ -831,6 +831,15 @@ function runAllTests(useWeakMap) { expect(wrapper.text()).toBe('new render + old state + 20'); }); }); + + describe('should use Redbox as the default errorReporter', () => { + const wrapper = shallow(
hey
); + const error = new Error('Something is wrong!'); + wrapper.setState({ error }); + const errorReporter = wrapper.find('RedBox'); + expect(errorReporter.length).toBe(1); + expect(errorReporter.prop('error')).toBe(error); + }); }); } From 019cd0815ea8912f8ef227596af9356a7ddbb465 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 13 Jul 2016 00:32:48 -0500 Subject: [PATCH 114/161] enable stage-1 transforms and add babel-eslint --- .babelrc | 4 ++-- .eslintrc | 6 +----- package.json | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.babelrc b/.babelrc index facd18092..562a5e32f 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["es2015", "react"] -} \ No newline at end of file + "presets": ["es2015", "react", "stage-1"] +} diff --git a/.eslintrc b/.eslintrc index 074a2605a..769349d8d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,6 @@ { "extends": "airbnb", - "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true - }, - }, + "parser": "babel-eslint", "rules": { "no-underscore-dangle": ["error", { "allow": ["__REACT_HOT_LOADER__"] }], "no-console": ["error", { allow: ["error"] }], diff --git a/package.json b/package.json index 8ec26f916..270681ba0 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,10 @@ "devDependencies": { "babel-cli": "^6.7.5", "babel-core": "^6.7.6", + "babel-eslint": "^6.1.2", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", + "babel-preset-stage-1": "^6.5.0", "enzyme": "^2.2.0", "eslint": "^2.9.0", "eslint-config-airbnb": "^8.0.0", From c07dcb07a665da075df21016396f6c449c893bce Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 13 Jul 2016 00:38:49 -0500 Subject: [PATCH 115/161] Add a passing test showing that component class methods get replaced properly when the method is changed, as well as a failing test demonstrating that class properties do not get replaced. --- test/AppContainer/AppContainer.dev.js | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index fe1a152ec..5152bbeb4 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -181,6 +181,112 @@ function runAllTests(useWeakMap) { expect(wrapper.text()).toBe('new render + old state'); }); + + it('replaces children class methods', () => { + const spy = createSpy(); + + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick() { + spy('foo'); + } + + render() { + return old render; + } + } + RHL.register(App, 'App', 'test.js'); + + const wrapper = mount(); + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('foo'); + + spy.reset(); + { + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick() { + spy('bar'); + } + + render() { + return new render; + } + } + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); + } + + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('bar'); + }); + + it('replaces children class properties', () => { + const spy = createSpy(); + + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = () => { + spy('foo'); + }; + + render() { + return old render; + } + } + RHL.register(App, 'App', 'test.js'); + + const wrapper = mount(); + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('foo'); + + spy.reset(); + { + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = () => { + spy('bar'); + }; + + render() { + return new render; + } + } + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); + } + + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('bar'); + }); }); describe('with createClass root', () => { From 40e5e364b00a874014b09ab6e47a399b4e9e9a09 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 13 Jul 2016 03:43:03 -0500 Subject: [PATCH 116/161] extend the existing babel plugin to transform class property arrow functions into hot-reloadable class methods (#242) --- src/babel/index.js | 34 +++++++++++++++++ test/AppContainer/.babelrc | 4 ++ test/babel/fixtures/class-properties/.babelrc | 4 ++ .../babel/fixtures/class-properties/actual.js | 5 +++ .../fixtures/class-properties/expected.js | 38 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 test/AppContainer/.babelrc create mode 100644 test/babel/fixtures/class-properties/.babelrc create mode 100644 test/babel/fixtures/class-properties/actual.js create mode 100644 test/babel/fixtures/class-properties/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index fb9bf1efe..55a319050 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,6 +14,16 @@ const buildTagger = template(` })(); `); +const buildNewClassProperty = (t, key, identifier, params) => { + const returnExpression = t.callExpression( + t.memberExpression(t.thisExpression(), identifier), + params + ); + const blockStatement = t.blockStatement([t.returnStatement(returnExpression)]); + const newArrowFunction = t.arrowFunctionExpression(params, blockStatement); + return t.classProperty(key, newArrowFunction); +}; + module.exports = function plugin(args) { // This is a Babel plugin, but the user put it in the Webpack config. if (this && this.callback) { @@ -125,6 +135,30 @@ module.exports = function plugin(args) { node.body.push(buildSemi()); }, }, + + Class(classPath) { + const classBody = classPath.get('body'); + + classBody.get('body').forEach(path => { + if (path.isClassProperty()) { + const { node } = path; + + if (node.value.type === 'ArrowFunctionExpression') { + const { params } = node.value; + const newIdentifier = t.identifier(`__${node.key.name}__REACT_HOT_LOADER__`); + + // create a new method on the class that the original class property function + // calls, since the method is able to be replaced by RHL + const newMethod = t.classMethod('method', newIdentifier, params, node.value.body); + path.insertAfter(newMethod); + + // replace the original class property function with a function that calls + // the new class method created above + path.replaceWith(buildNewClassProperty(t, node.key, newIdentifier, params)); + } + } + }); + }, }, }; }; diff --git a/test/AppContainer/.babelrc b/test/AppContainer/.babelrc new file mode 100644 index 000000000..f91228920 --- /dev/null +++ b/test/AppContainer/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1", "react"], + "plugins": ["../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/.babelrc b/test/babel/fixtures/class-properties/.babelrc new file mode 100644 index 000000000..665cc9b04 --- /dev/null +++ b/test/babel/fixtures/class-properties/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1"], + "plugins": ["../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/actual.js b/test/babel/fixtures/class-properties/actual.js new file mode 100644 index 000000000..e05af95b0 --- /dev/null +++ b/test/babel/fixtures/class-properties/actual.js @@ -0,0 +1,5 @@ +class Foo { + bar = (a, b) => { + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/expected.js b/test/babel/fixtures/class-properties/expected.js new file mode 100644 index 000000000..69263adce --- /dev/null +++ b/test/babel/fixtures/class-properties/expected.js @@ -0,0 +1,38 @@ +"use strict"; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Foo = function () { + function Foo() { + var _this = this; + + _classCallCheck(this, Foo); + + this.bar = function (a, b) { + return _this.__bar__REACT_HOT_LOADER__(a, b); + }; + } + + _createClass(Foo, [{ + key: "__bar__REACT_HOT_LOADER__", + value: function __bar__REACT_HOT_LOADER__(a, b) { + return a(b); + } + }]); + + return Foo; +}(); + +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; From 2e957fa855b3f0c8f1bac930dfb876e2548b0c62 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 13 Jul 2016 19:18:13 -0500 Subject: [PATCH 117/161] support arrow functions without block statement bodies, and add matching tests. also restructure the babel plugin tests --- src/babel/index.js | 12 +++-- test/AppContainer/AppContainer.dev.js | 51 ++++++++++++++++++- test/babel/fixtures/class-properties/.babelrc | 4 -- .../class-properties/block-body/.babelrc | 4 ++ .../{ => block-body}/actual.js | 0 .../{ => block-body}/expected.js | 0 .../class-properties/expression-body/.babelrc | 4 ++ .../expression-body/actual.js | 3 ++ .../expression-body/expected.js | 38 ++++++++++++++ test/babel/index.js | 22 ++++++++ 10 files changed, 130 insertions(+), 8 deletions(-) delete mode 100644 test/babel/fixtures/class-properties/.babelrc create mode 100644 test/babel/fixtures/class-properties/block-body/.babelrc rename test/babel/fixtures/class-properties/{ => block-body}/actual.js (100%) rename test/babel/fixtures/class-properties/{ => block-body}/expected.js (100%) create mode 100644 test/babel/fixtures/class-properties/expression-body/.babelrc create mode 100644 test/babel/fixtures/class-properties/expression-body/actual.js create mode 100644 test/babel/fixtures/class-properties/expression-body/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index 55a319050..fbe421337 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -143,13 +143,19 @@ module.exports = function plugin(args) { if (path.isClassProperty()) { const { node } = path; - if (node.value.type === 'ArrowFunctionExpression') { - const { params } = node.value; + // class property node value is nullable + if (node.value && node.value.type === 'ArrowFunctionExpression') { + const params = node.value.params; const newIdentifier = t.identifier(`__${node.key.name}__REACT_HOT_LOADER__`); + // arrow function body can either be a block statement or a returned expression + const newMethodBody = node.value.body.type === 'BlockStatement' ? + node.value.body : + t.blockStatement([t.returnStatement(node.value.body)]); + // create a new method on the class that the original class property function // calls, since the method is able to be replaced by RHL - const newMethod = t.classMethod('method', newIdentifier, params, node.value.body); + const newMethod = t.classMethod('method', newIdentifier, params, newMethodBody); path.insertAfter(newMethod); // replace the original class property function with a function that calls diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 5152bbeb4..fb9534ce4 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -235,7 +235,7 @@ function runAllTests(useWeakMap) { expect(spy).toHaveBeenCalledWith('bar'); }); - it('replaces children class properties', () => { + it('replaces children class property functions', () => { const spy = createSpy(); class App extends Component { @@ -289,6 +289,55 @@ function runAllTests(useWeakMap) { }); }); + it('replaces children class property arrow functions without block statement bodies', () => { + const spy = createSpy(); + + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = () => spy('foo'); + + render() { + return old render; + } + } + RHL.register(App, 'App', 'test.js'); + + const wrapper = mount(); + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('foo'); + + spy.reset(); + { + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = () => spy('bar'); + + render() { + return new render; + } + } + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); + } + + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('bar'); + }); + describe('with createClass root', () => { it('renders children', () => { const spy = createSpy(); diff --git a/test/babel/fixtures/class-properties/.babelrc b/test/babel/fixtures/class-properties/.babelrc deleted file mode 100644 index 665cc9b04..000000000 --- a/test/babel/fixtures/class-properties/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["es2015", "stage-1"], - "plugins": ["../../../../src/babel"] -} diff --git a/test/babel/fixtures/class-properties/block-body/.babelrc b/test/babel/fixtures/class-properties/block-body/.babelrc new file mode 100644 index 000000000..0e51598c6 --- /dev/null +++ b/test/babel/fixtures/class-properties/block-body/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1"], + "plugins": ["../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/actual.js b/test/babel/fixtures/class-properties/block-body/actual.js similarity index 100% rename from test/babel/fixtures/class-properties/actual.js rename to test/babel/fixtures/class-properties/block-body/actual.js diff --git a/test/babel/fixtures/class-properties/expected.js b/test/babel/fixtures/class-properties/block-body/expected.js similarity index 100% rename from test/babel/fixtures/class-properties/expected.js rename to test/babel/fixtures/class-properties/block-body/expected.js diff --git a/test/babel/fixtures/class-properties/expression-body/.babelrc b/test/babel/fixtures/class-properties/expression-body/.babelrc new file mode 100644 index 000000000..0e51598c6 --- /dev/null +++ b/test/babel/fixtures/class-properties/expression-body/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1"], + "plugins": ["../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/expression-body/actual.js b/test/babel/fixtures/class-properties/expression-body/actual.js new file mode 100644 index 000000000..0f55b7c52 --- /dev/null +++ b/test/babel/fixtures/class-properties/expression-body/actual.js @@ -0,0 +1,3 @@ +class Foo { + onClick = (e) => e.target.value +} diff --git a/test/babel/fixtures/class-properties/expression-body/expected.js b/test/babel/fixtures/class-properties/expression-body/expected.js new file mode 100644 index 000000000..6071e0c2c --- /dev/null +++ b/test/babel/fixtures/class-properties/expression-body/expected.js @@ -0,0 +1,38 @@ +"use strict"; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Foo = function () { + function Foo() { + var _this = this; + + _classCallCheck(this, Foo); + + this.onClick = function (e) { + return _this.__onClick__REACT_HOT_LOADER__(e); + }; + } + + _createClass(Foo, [{ + key: "__onClick__REACT_HOT_LOADER__", + value: function __onClick__REACT_HOT_LOADER__(e) { + return e.target.value; + } + }]); + + return Foo; +}(); + +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; diff --git a/test/babel/index.js b/test/babel/index.js index 6ac011222..51a20e00c 100644 --- a/test/babel/index.js +++ b/test/babel/index.js @@ -9,6 +9,28 @@ function trim(str) { describe('tags potential React components', () => { const fixturesDir = path.join(__dirname, 'fixtures'); + fs.readdirSync(fixturesDir).forEach(fixtureName => { + if (fixtureName !== 'class-properties') { + const fixtureDir = path.join(fixturesDir, fixtureName); + if (fs.statSync(fixtureDir).isDirectory()) { + it(fixtureName.split('-').join(' '), () => { + const actualPath = path.join(fixtureDir, 'actual.js'); + const actual = transformFileSync(actualPath).code; + const templatePath = path.sep === '\\' ? + actualPath.replace(/\\/g, '/') : + actualPath; + const expected = fs.readFileSync( + path.join(fixtureDir, 'expected.js') + ).toString().replace(/__FILENAME__/g, JSON.stringify(templatePath)); + expect(trim(actual)).toEqual(trim(expected)); + }); + } + } + }); +}); + +describe('copies arrow function body block onto hidden class methods', () => { + const fixturesDir = path.join(__dirname, 'fixtures/class-properties'); fs.readdirSync(fixturesDir).forEach(fixtureName => { const fixtureDir = path.join(fixturesDir, fixtureName); if (fs.statSync(fixtureDir).isDirectory()) { From 61c0fa9dd7d2584833f0ff6fe2c17a35a735d3a0 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 26 Jul 2016 20:54:52 -0500 Subject: [PATCH 118/161] only copy the parameter identifier on the class properties transform, since the parameters in the source code can have default values, which don't need to be copied when calling the new generated method --- src/babel/index.js | 13 +++++- .../class-properties/default-params/.babelrc | 4 ++ .../class-properties/default-params/actual.js | 5 +++ .../default-params/expected.js | 40 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/babel/fixtures/class-properties/default-params/.babelrc create mode 100644 test/babel/fixtures/class-properties/default-params/actual.js create mode 100644 test/babel/fixtures/class-properties/default-params/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index fbe421337..33d9073dd 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,6 +14,15 @@ const buildTagger = template(` })(); `); +const cleanUpParams = params => ( + params.map(param => { + if (param.type === 'AssignmentPattern') { + return param.left; + } + return param; + }) +); + const buildNewClassProperty = (t, key, identifier, params) => { const returnExpression = t.callExpression( t.memberExpression(t.thisExpression(), identifier), @@ -160,7 +169,9 @@ module.exports = function plugin(args) { // replace the original class property function with a function that calls // the new class method created above - path.replaceWith(buildNewClassProperty(t, node.key, newIdentifier, params)); + path.replaceWith( + buildNewClassProperty(t, node.key, newIdentifier, cleanUpParams(params)) + ); } } }); diff --git a/test/babel/fixtures/class-properties/default-params/.babelrc b/test/babel/fixtures/class-properties/default-params/.babelrc new file mode 100644 index 000000000..0e51598c6 --- /dev/null +++ b/test/babel/fixtures/class-properties/default-params/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1"], + "plugins": ["../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/default-params/actual.js b/test/babel/fixtures/class-properties/default-params/actual.js new file mode 100644 index 000000000..ed43931df --- /dev/null +++ b/test/babel/fixtures/class-properties/default-params/actual.js @@ -0,0 +1,5 @@ +class Foo { + bar = (a = "foo") => { + return `${a}bar`; + }; +} diff --git a/test/babel/fixtures/class-properties/default-params/expected.js b/test/babel/fixtures/class-properties/default-params/expected.js new file mode 100644 index 000000000..c8e93e1af --- /dev/null +++ b/test/babel/fixtures/class-properties/default-params/expected.js @@ -0,0 +1,40 @@ +"use strict"; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Foo = function () { + function Foo() { + var _this = this; + + _classCallCheck(this, Foo); + + this.bar = function (a) { + return _this.__bar__REACT_HOT_LOADER__(a); + }; + } + + _createClass(Foo, [{ + key: "__bar__REACT_HOT_LOADER__", + value: function __bar__REACT_HOT_LOADER__() { + var a = arguments.length <= 0 || arguments[0] === undefined ? "foo" : arguments[0]; + + return a + "bar"; + } + }]); + + return Foo; +}(); + +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; From b4e6dd0d904a95b9043a0aad86714547419d3931 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 26 Jul 2016 22:34:32 -0500 Subject: [PATCH 119/161] move test into its proper `describe()` block --- test/AppContainer/AppContainer.dev.js | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index fb9534ce4..5aa713f9a 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -235,7 +235,7 @@ function runAllTests(useWeakMap) { expect(spy).toHaveBeenCalledWith('bar'); }); - it('replaces children class property functions', () => { + it('replaces children class property arrow functions', () => { const spy = createSpy(); class App extends Component { @@ -287,34 +287,10 @@ function runAllTests(useWeakMap) { wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('bar'); }); - }); - - it('replaces children class property arrow functions without block statement bodies', () => { - const spy = createSpy(); - - class App extends Component { - componentWillMount() { - this.state = 'new'; - } - - shouldComponentUpdate() { - return false; - } - - handleClick = () => spy('foo'); - render() { - return old render; - } - } - RHL.register(App, 'App', 'test.js'); - - const wrapper = mount(); - wrapper.find('span').simulate('click'); - expect(spy).toHaveBeenCalledWith('foo'); + it('replaces children class property arrow functions without block statement bodies', () => { + const spy = createSpy(); - spy.reset(); - { class App extends Component { componentWillMount() { this.state = 'new'; @@ -324,18 +300,42 @@ function runAllTests(useWeakMap) { return false; } - handleClick = () => spy('bar'); + handleClick = () => spy('foo'); render() { - return new render; + return old render; } } RHL.register(App, 'App', 'test.js'); - wrapper.setProps({ children: }); - } - wrapper.find('span').simulate('click'); - expect(spy).toHaveBeenCalledWith('bar'); + const wrapper = mount(); + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('foo'); + + spy.reset(); + { + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = () => spy('bar'); + + render() { + return new render; + } + } + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); + } + + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('bar'); + }); }); describe('with createClass root', () => { From f474b69d60355961cf5343e950fd571251051db8 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 26 Jul 2016 23:11:26 -0500 Subject: [PATCH 120/161] for the class properties transform, if the class properties function has destructured parameters, instead of copying the destructuring expression, create new identifiers for each param, and call the generated method with those new parameters --- src/babel/index.js | 16 ++++---- .../destructured-params/.babelrc | 4 ++ .../destructured-params/actual.js | 5 +++ .../destructured-params/expected.js | 41 +++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 test/babel/fixtures/class-properties/destructured-params/.babelrc create mode 100644 test/babel/fixtures/class-properties/destructured-params/actual.js create mode 100644 test/babel/fixtures/class-properties/destructured-params/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index 33d9073dd..bce38bbe7 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,22 +14,24 @@ const buildTagger = template(` })(); `); -const cleanUpParams = params => ( - params.map(param => { +const cleanUpParams = (t, params) => ( + params.map((param, i) => { if (param.type === 'AssignmentPattern') { return param.left; + } else if (param.type === 'ObjectPattern') { + return t.identifier(`param${i}`); } return param; }) ); const buildNewClassProperty = (t, key, identifier, params) => { - const returnExpression = t.callExpression( + const returnStatement = t.returnStatement(t.callExpression( t.memberExpression(t.thisExpression(), identifier), params - ); - const blockStatement = t.blockStatement([t.returnStatement(returnExpression)]); - const newArrowFunction = t.arrowFunctionExpression(params, blockStatement); + )); + const arrowFunctionBody = t.blockStatement([returnStatement]); + const newArrowFunction = t.arrowFunctionExpression(params, arrowFunctionBody); return t.classProperty(key, newArrowFunction); }; @@ -170,7 +172,7 @@ module.exports = function plugin(args) { // replace the original class property function with a function that calls // the new class method created above path.replaceWith( - buildNewClassProperty(t, node.key, newIdentifier, cleanUpParams(params)) + buildNewClassProperty(t, node.key, newIdentifier, cleanUpParams(t, params)) ); } } diff --git a/test/babel/fixtures/class-properties/destructured-params/.babelrc b/test/babel/fixtures/class-properties/destructured-params/.babelrc new file mode 100644 index 000000000..0e51598c6 --- /dev/null +++ b/test/babel/fixtures/class-properties/destructured-params/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1"], + "plugins": ["../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/destructured-params/actual.js b/test/babel/fixtures/class-properties/destructured-params/actual.js new file mode 100644 index 000000000..463e529c1 --- /dev/null +++ b/test/babel/fixtures/class-properties/destructured-params/actual.js @@ -0,0 +1,5 @@ +class Foo { + bar = ({ a }, { b }) => { + return `${a}${b}`; + }; +} diff --git a/test/babel/fixtures/class-properties/destructured-params/expected.js b/test/babel/fixtures/class-properties/destructured-params/expected.js new file mode 100644 index 000000000..a1708a5b1 --- /dev/null +++ b/test/babel/fixtures/class-properties/destructured-params/expected.js @@ -0,0 +1,41 @@ +"use strict"; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Foo = function () { + function Foo() { + var _this = this; + + _classCallCheck(this, Foo); + + this.bar = function (param0, param1) { + return _this.__bar__REACT_HOT_LOADER__(param0, param1); + }; + } + + _createClass(Foo, [{ + key: "__bar__REACT_HOT_LOADER__", + value: function __bar__REACT_HOT_LOADER__(_ref, _ref2) { + var a = _ref.a; + var b = _ref2.b; + + return "" + a + b; + } + }]); + + return Foo; +}(); + +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; From caf6d51de31af2e7542af25eb2e9f8f83fde9a8c Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 27 Jul 2016 00:42:36 -0500 Subject: [PATCH 121/161] Change class property transform to use rest params/spread operator This allows the class property functions to be hot-reloaded properly when changing the number of arguments --- src/babel/index.js | 34 +++++-------- test/AppContainer/AppContainer.dev.js | 50 +++++++++++++++++++ .../class-properties/block-body/expected.js | 4 +- .../default-params/expected.js | 4 +- .../destructured-params/expected.js | 4 +- .../expression-body/expected.js | 4 +- 6 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index bce38bbe7..e81b827c1 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,25 +14,17 @@ const buildTagger = template(` })(); `); -const cleanUpParams = (t, params) => ( - params.map((param, i) => { - if (param.type === 'AssignmentPattern') { - return param.left; - } else if (param.type === 'ObjectPattern') { - return t.identifier(`param${i}`); - } - return param; - }) -); - -const buildNewClassProperty = (t, key, identifier, params) => { - const returnStatement = t.returnStatement(t.callExpression( - t.memberExpression(t.thisExpression(), identifier), - params - )); - const arrowFunctionBody = t.blockStatement([returnStatement]); - const newArrowFunction = t.arrowFunctionExpression(params, arrowFunctionBody); - return t.classProperty(key, newArrowFunction); +const buildNewClassProperty = (t, classPropertyName, newMethodName) => { + const returnExpression = t.callExpression( + t.memberExpression(t.thisExpression(), newMethodName), + [t.spreadElement(t.identifier('params'))] + ); + + const newArrowFunction = t.arrowFunctionExpression( + [t.restElement(t.identifier('params'))], + returnExpression + ); + return t.classProperty(classPropertyName, newArrowFunction); }; module.exports = function plugin(args) { @@ -171,9 +163,7 @@ module.exports = function plugin(args) { // replace the original class property function with a function that calls // the new class method created above - path.replaceWith( - buildNewClassProperty(t, node.key, newIdentifier, cleanUpParams(t, params)) - ); + path.replaceWith(buildNewClassProperty(t, node.key, newIdentifier)); } } }); diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index 5aa713f9a..beb8e0ce3 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -336,6 +336,56 @@ function runAllTests(useWeakMap) { wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('bar'); }); + + it('replaces children with class property arrow ' + + 'functions with different numbers of arguments', () => { + const spy = createSpy(); + + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = () => spy('foo'); + + render() { + return old render; + } + } + RHL.register(App, 'App', 'test.js'); + + const wrapper = mount(); + wrapper.find('span').simulate('click'); + expect(spy).toHaveBeenCalledWith('foo'); + + spy.reset(); + { + class App extends Component { + componentWillMount() { + this.state = 'new'; + } + + shouldComponentUpdate() { + return false; + } + + handleClick = ({ target }) => spy(target.value); + + render() { + return new render; + } + } + RHL.register(App, 'App', 'test.js'); + wrapper.setProps({ children: }); + } + + wrapper.find('span').simulate('click', { target: { value: 'bar' } }); + expect(spy).toHaveBeenCalledWith('bar'); + }); }); describe('with createClass root', () => { diff --git a/test/babel/fixtures/class-properties/block-body/expected.js b/test/babel/fixtures/class-properties/block-body/expected.js index 69263adce..f401f2e7c 100644 --- a/test/babel/fixtures/class-properties/block-body/expected.js +++ b/test/babel/fixtures/class-properties/block-body/expected.js @@ -10,8 +10,8 @@ var Foo = function () { _classCallCheck(this, Foo); - this.bar = function (a, b) { - return _this.__bar__REACT_HOT_LOADER__(a, b); + this.bar = function () { + return _this.__bar__REACT_HOT_LOADER__.apply(_this, arguments); }; } diff --git a/test/babel/fixtures/class-properties/default-params/expected.js b/test/babel/fixtures/class-properties/default-params/expected.js index c8e93e1af..b66cb0b02 100644 --- a/test/babel/fixtures/class-properties/default-params/expected.js +++ b/test/babel/fixtures/class-properties/default-params/expected.js @@ -10,8 +10,8 @@ var Foo = function () { _classCallCheck(this, Foo); - this.bar = function (a) { - return _this.__bar__REACT_HOT_LOADER__(a); + this.bar = function () { + return _this.__bar__REACT_HOT_LOADER__.apply(_this, arguments); }; } diff --git a/test/babel/fixtures/class-properties/destructured-params/expected.js b/test/babel/fixtures/class-properties/destructured-params/expected.js index a1708a5b1..7fcb1bb78 100644 --- a/test/babel/fixtures/class-properties/destructured-params/expected.js +++ b/test/babel/fixtures/class-properties/destructured-params/expected.js @@ -10,8 +10,8 @@ var Foo = function () { _classCallCheck(this, Foo); - this.bar = function (param0, param1) { - return _this.__bar__REACT_HOT_LOADER__(param0, param1); + this.bar = function () { + return _this.__bar__REACT_HOT_LOADER__.apply(_this, arguments); }; } diff --git a/test/babel/fixtures/class-properties/expression-body/expected.js b/test/babel/fixtures/class-properties/expression-body/expected.js index 6071e0c2c..57d0f37e6 100644 --- a/test/babel/fixtures/class-properties/expression-body/expected.js +++ b/test/babel/fixtures/class-properties/expression-body/expected.js @@ -10,8 +10,8 @@ var Foo = function () { _classCallCheck(this, Foo); - this.onClick = function (e) { - return _this.__onClick__REACT_HOT_LOADER__(e); + this.onClick = function () { + return _this.__onClick__REACT_HOT_LOADER__.apply(_this, arguments); }; } From 26f4c14abb6e6eb91c99e3e9a4cf0fbfd8b6b278 Mon Sep 17 00:00:00 2001 From: jameskraus Date: Tue, 23 Aug 2016 15:18:57 -0400 Subject: [PATCH 122/161] Fixing the broken V3 example in the docs --- docs/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index a3abe338a..a0df195e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,11 +50,12 @@ ReactDOM.render( // Hot Module Replacement API if (module.hot) { module.hot.accept('./containers/App', () => { - render( + const NextApp = require('./containers/App').default; + ReactDOM.render( - + - />, + , document.getElementById('root') ); }); From ffb51558cf81ce391f88a6e13109e2120d78784d Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Sat, 3 Sep 2016 09:46:28 -0500 Subject: [PATCH 123/161] 3.0.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 092c28805..2e58165a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-beta.2", + "version": "3.0.0-beta.3", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 752e32ddafa8fa3ce76a1fbdcd1a29210dac5720 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 6 Sep 2016 19:52:36 -0500 Subject: [PATCH 124/161] Bump stage-1 dependency to stage-2 Since the class properties proposal was moved to stage 2 at TC39, we no longer need the stage-1 dependency. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 270681ba0..db3bb8b77 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "babel-eslint": "^6.1.2", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", - "babel-preset-stage-1": "^6.5.0", + "babel-preset-stage-2": "^6.5.0", "enzyme": "^2.2.0", "eslint": "^2.9.0", "eslint-config-airbnb": "^8.0.0", From 669b9a575d2f4bd272bcf280583bbdf946e44315 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 6 Sep 2016 19:53:59 -0500 Subject: [PATCH 125/161] Remove unneeded transforms from babel plugin tests We only care about what the plugin does, and not about how Babel transpiles ES2015/class properties transforms. This makes the fixtures more immediately understandable. --- .../class-properties/block-body/.babelrc | 3 +- .../class-properties/block-body/expected.js | 29 ++++------------- .../class-properties/default-params/.babelrc | 3 +- .../default-params/expected.js | 31 ++++-------------- .../destructured-params/.babelrc | 3 +- .../destructured-params/expected.js | 32 ++++--------------- .../class-properties/expression-body/.babelrc | 3 +- .../expression-body/expected.js | 29 ++++------------- 8 files changed, 28 insertions(+), 105 deletions(-) diff --git a/test/babel/fixtures/class-properties/block-body/.babelrc b/test/babel/fixtures/class-properties/block-body/.babelrc index 0e51598c6..4075556b7 100644 --- a/test/babel/fixtures/class-properties/block-body/.babelrc +++ b/test/babel/fixtures/class-properties/block-body/.babelrc @@ -1,4 +1,3 @@ { - "presets": ["es2015", "stage-1"], - "plugins": ["../../../../../src/babel"] + "plugins": ["syntax-class-properties", "../../../../../src/babel"] } diff --git a/test/babel/fixtures/class-properties/block-body/expected.js b/test/babel/fixtures/class-properties/block-body/expected.js index f401f2e7c..9d4018dcf 100644 --- a/test/babel/fixtures/class-properties/block-body/expected.js +++ b/test/babel/fixtures/class-properties/block-body/expected.js @@ -1,30 +1,13 @@ -"use strict"; +var _this = this; -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); +class Foo { + bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Foo = function () { - function Foo() { - var _this = this; - - _classCallCheck(this, Foo); - - this.bar = function () { - return _this.__bar__REACT_HOT_LOADER__.apply(_this, arguments); - }; + __bar__REACT_HOT_LOADER__(a, b) { + return a(b); } - _createClass(Foo, [{ - key: "__bar__REACT_HOT_LOADER__", - value: function __bar__REACT_HOT_LOADER__(a, b) { - return a(b); - } - }]); - - return Foo; -}(); - +} ; (function () { diff --git a/test/babel/fixtures/class-properties/default-params/.babelrc b/test/babel/fixtures/class-properties/default-params/.babelrc index 0e51598c6..4075556b7 100644 --- a/test/babel/fixtures/class-properties/default-params/.babelrc +++ b/test/babel/fixtures/class-properties/default-params/.babelrc @@ -1,4 +1,3 @@ { - "presets": ["es2015", "stage-1"], - "plugins": ["../../../../../src/babel"] + "plugins": ["syntax-class-properties", "../../../../../src/babel"] } diff --git a/test/babel/fixtures/class-properties/default-params/expected.js b/test/babel/fixtures/class-properties/default-params/expected.js index b66cb0b02..4fe43f8c5 100644 --- a/test/babel/fixtures/class-properties/default-params/expected.js +++ b/test/babel/fixtures/class-properties/default-params/expected.js @@ -1,32 +1,13 @@ -"use strict"; +var _this = this; -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); +class Foo { + bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Foo = function () { - function Foo() { - var _this = this; - - _classCallCheck(this, Foo); - - this.bar = function () { - return _this.__bar__REACT_HOT_LOADER__.apply(_this, arguments); - }; + __bar__REACT_HOT_LOADER__(a = "foo") { + return `${ a }bar`; } - _createClass(Foo, [{ - key: "__bar__REACT_HOT_LOADER__", - value: function __bar__REACT_HOT_LOADER__() { - var a = arguments.length <= 0 || arguments[0] === undefined ? "foo" : arguments[0]; - - return a + "bar"; - } - }]); - - return Foo; -}(); - +} ; (function () { diff --git a/test/babel/fixtures/class-properties/destructured-params/.babelrc b/test/babel/fixtures/class-properties/destructured-params/.babelrc index 0e51598c6..4075556b7 100644 --- a/test/babel/fixtures/class-properties/destructured-params/.babelrc +++ b/test/babel/fixtures/class-properties/destructured-params/.babelrc @@ -1,4 +1,3 @@ { - "presets": ["es2015", "stage-1"], - "plugins": ["../../../../../src/babel"] + "plugins": ["syntax-class-properties", "../../../../../src/babel"] } diff --git a/test/babel/fixtures/class-properties/destructured-params/expected.js b/test/babel/fixtures/class-properties/destructured-params/expected.js index 7fcb1bb78..07f4f9c21 100644 --- a/test/babel/fixtures/class-properties/destructured-params/expected.js +++ b/test/babel/fixtures/class-properties/destructured-params/expected.js @@ -1,33 +1,13 @@ -"use strict"; +var _this = this; -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); +class Foo { + bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Foo = function () { - function Foo() { - var _this = this; - - _classCallCheck(this, Foo); - - this.bar = function () { - return _this.__bar__REACT_HOT_LOADER__.apply(_this, arguments); - }; + __bar__REACT_HOT_LOADER__({ a }, { b }) { + return `${ a }${ b }`; } - _createClass(Foo, [{ - key: "__bar__REACT_HOT_LOADER__", - value: function __bar__REACT_HOT_LOADER__(_ref, _ref2) { - var a = _ref.a; - var b = _ref2.b; - - return "" + a + b; - } - }]); - - return Foo; -}(); - +} ; (function () { diff --git a/test/babel/fixtures/class-properties/expression-body/.babelrc b/test/babel/fixtures/class-properties/expression-body/.babelrc index 0e51598c6..4075556b7 100644 --- a/test/babel/fixtures/class-properties/expression-body/.babelrc +++ b/test/babel/fixtures/class-properties/expression-body/.babelrc @@ -1,4 +1,3 @@ { - "presets": ["es2015", "stage-1"], - "plugins": ["../../../../../src/babel"] + "plugins": ["syntax-class-properties", "../../../../../src/babel"] } diff --git a/test/babel/fixtures/class-properties/expression-body/expected.js b/test/babel/fixtures/class-properties/expression-body/expected.js index 57d0f37e6..303ed2a28 100644 --- a/test/babel/fixtures/class-properties/expression-body/expected.js +++ b/test/babel/fixtures/class-properties/expression-body/expected.js @@ -1,30 +1,13 @@ -"use strict"; +var _this = this; -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); +class Foo { + onClick = (...params) => _this.__onClick__REACT_HOT_LOADER__(...params); -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Foo = function () { - function Foo() { - var _this = this; - - _classCallCheck(this, Foo); - - this.onClick = function () { - return _this.__onClick__REACT_HOT_LOADER__.apply(_this, arguments); - }; + __onClick__REACT_HOT_LOADER__(e) { + return e.target.value; } - _createClass(Foo, [{ - key: "__onClick__REACT_HOT_LOADER__", - value: function __onClick__REACT_HOT_LOADER__(e) { - return e.target.value; - } - }]); - - return Foo; -}(); - +} ; (function () { From 698d59292954052f410af8bdd847ed8cd6044e26 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 6 Sep 2016 20:05:53 -0500 Subject: [PATCH 126/161] extend AppContainer class property tests verifying that the class property transform doesn't break the other transform and cause components to remount. --- test/AppContainer/AppContainer.dev.js | 48 ++++++++++++++++++++------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/test/AppContainer/AppContainer.dev.js b/test/AppContainer/AppContainer.dev.js index beb8e0ce3..59d735a33 100644 --- a/test/AppContainer/AppContainer.dev.js +++ b/test/AppContainer/AppContainer.dev.js @@ -187,7 +187,7 @@ function runAllTests(useWeakMap) { class App extends Component { componentWillMount() { - this.state = 'new'; + this.state = 'old'; } shouldComponentUpdate() { @@ -199,7 +199,9 @@ function runAllTests(useWeakMap) { } render() { - return old render; + return ( + old render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -207,6 +209,7 @@ function runAllTests(useWeakMap) { const wrapper = mount(); wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('foo'); + expect(wrapper.text()).toBe('old render + old state'); spy.reset(); { @@ -224,7 +227,9 @@ function runAllTests(useWeakMap) { } render() { - return new render; + return ( + new render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -233,6 +238,7 @@ function runAllTests(useWeakMap) { wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('bar'); + expect(wrapper.text()).toBe('new render + old state'); }); it('replaces children class property arrow functions', () => { @@ -240,7 +246,7 @@ function runAllTests(useWeakMap) { class App extends Component { componentWillMount() { - this.state = 'new'; + this.state = 'old'; } shouldComponentUpdate() { @@ -252,7 +258,9 @@ function runAllTests(useWeakMap) { }; render() { - return old render; + return ( + old render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -260,6 +268,7 @@ function runAllTests(useWeakMap) { const wrapper = mount(); wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('foo'); + expect(wrapper.text()).toBe('old render + old state'); spy.reset(); { @@ -277,7 +286,9 @@ function runAllTests(useWeakMap) { }; render() { - return new render; + return ( + new render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -286,6 +297,7 @@ function runAllTests(useWeakMap) { wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('bar'); + expect(wrapper.text()).toBe('new render + old state'); }); it('replaces children class property arrow functions without block statement bodies', () => { @@ -293,7 +305,7 @@ function runAllTests(useWeakMap) { class App extends Component { componentWillMount() { - this.state = 'new'; + this.state = 'old'; } shouldComponentUpdate() { @@ -303,7 +315,9 @@ function runAllTests(useWeakMap) { handleClick = () => spy('foo'); render() { - return old render; + return ( + old render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -311,6 +325,7 @@ function runAllTests(useWeakMap) { const wrapper = mount(); wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('foo'); + expect(wrapper.text()).toBe('old render + old state'); spy.reset(); { @@ -326,7 +341,9 @@ function runAllTests(useWeakMap) { handleClick = () => spy('bar'); render() { - return new render; + return ( + new render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -335,6 +352,7 @@ function runAllTests(useWeakMap) { wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('bar'); + expect(wrapper.text()).toBe('new render + old state'); }); it('replaces children with class property arrow ' + @@ -343,7 +361,7 @@ function runAllTests(useWeakMap) { class App extends Component { componentWillMount() { - this.state = 'new'; + this.state = 'old'; } shouldComponentUpdate() { @@ -353,7 +371,9 @@ function runAllTests(useWeakMap) { handleClick = () => spy('foo'); render() { - return old render; + return ( + old render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -361,6 +381,7 @@ function runAllTests(useWeakMap) { const wrapper = mount(); wrapper.find('span').simulate('click'); expect(spy).toHaveBeenCalledWith('foo'); + expect(wrapper.text()).toBe('old render + old state'); spy.reset(); { @@ -376,7 +397,9 @@ function runAllTests(useWeakMap) { handleClick = ({ target }) => spy(target.value); render() { - return new render; + return ( + new render + {this.state} state + ); } } RHL.register(App, 'App', 'test.js'); @@ -385,6 +408,7 @@ function runAllTests(useWeakMap) { wrapper.find('span').simulate('click', { target: { value: 'bar' } }); expect(spy).toHaveBeenCalledWith('bar'); + expect(wrapper.text()).toBe('new render + old state'); }); }); From 28e9332b5b0cec3db089f1bd9404ad17f5d9f70d Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 6 Sep 2016 20:14:31 -0500 Subject: [PATCH 127/161] update stage-1 references to stage-2 --- .babelrc | 2 +- test/AppContainer/.babelrc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.babelrc b/.babelrc index 562a5e32f..27b5c0dc7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["es2015", "react", "stage-1"] + "presets": ["es2015", "react", "stage-2"] } diff --git a/test/AppContainer/.babelrc b/test/AppContainer/.babelrc index f91228920..96a75de7e 100644 --- a/test/AppContainer/.babelrc +++ b/test/AppContainer/.babelrc @@ -1,4 +1,4 @@ { - "presets": ["es2015", "stage-1", "react"], + "presets": ["es2015", "stage-2", "react"], "plugins": ["../../src/babel"] } From 02b1e6dc2d8068fce8bbe63cd2344935d0093909 Mon Sep 17 00:00:00 2001 From: tanguylebarzic Date: Wed, 14 Sep 2016 03:07:22 +0200 Subject: [PATCH 128/161] Fix the import of a component from a component with the same base name (#347) --- src/webpack/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/index.js b/src/webpack/index.js index 1bb3d13aa..745d18dd5 100644 --- a/src/webpack/index.js +++ b/src/webpack/index.js @@ -45,7 +45,7 @@ function transform(source, map) { const separator = '\n\n'; const appendText = tagCommonJSExportsSource.replace( /__FILENAME__/g, - JSON.stringify(path.basename(this.resourcePath)) + JSON.stringify(this.resourcePath) ); if (this.sourceMap === false) { From 7c78ec8742f60525fb83e8b28789e5919ec6c9ad Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 14 Sep 2016 21:38:08 +0100 Subject: [PATCH 129/161] opt out of class properties transpile on use of arguments, new.target inside --- src/babel/index.js | 28 +++++++++++++++++++ .../class-properties/arguments/.babelrc | 3 ++ .../class-properties/arguments/actual.js | 7 +++++ .../class-properties/arguments/expected.js | 19 +++++++++++++ .../nested-arguments/.babelrc | 3 ++ .../nested-arguments/actual.js | 9 ++++++ .../nested-arguments/expected.js | 21 ++++++++++++++ .../nested-new.target/.babelrc | 3 ++ .../nested-new.target/actual.js | 9 ++++++ .../nested-new.target/expected.js | 20 +++++++++++++ .../class-properties/new.target/.babelrc | 3 ++ .../class-properties/new.target/actual.js | 7 +++++ .../class-properties/new.target/expected.js | 18 ++++++++++++ 13 files changed, 150 insertions(+) create mode 100644 test/babel/fixtures/class-properties/arguments/.babelrc create mode 100644 test/babel/fixtures/class-properties/arguments/actual.js create mode 100644 test/babel/fixtures/class-properties/arguments/expected.js create mode 100644 test/babel/fixtures/class-properties/nested-arguments/.babelrc create mode 100644 test/babel/fixtures/class-properties/nested-arguments/actual.js create mode 100644 test/babel/fixtures/class-properties/nested-arguments/expected.js create mode 100644 test/babel/fixtures/class-properties/nested-new.target/.babelrc create mode 100644 test/babel/fixtures/class-properties/nested-new.target/actual.js create mode 100644 test/babel/fixtures/class-properties/nested-new.target/expected.js create mode 100644 test/babel/fixtures/class-properties/new.target/.babelrc create mode 100644 test/babel/fixtures/class-properties/new.target/actual.js create mode 100644 test/babel/fixtures/class-properties/new.target/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index e81b827c1..f0f998106 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -27,6 +27,24 @@ const buildNewClassProperty = (t, classPropertyName, newMethodName) => { return t.classProperty(classPropertyName, newArrowFunction); }; +const classPropertyOptOutVistor = { + MetaProperty(path, state) { + const { node } = path; + + if (node.meta.name === 'new' && node.property.name === 'target') { + state.optOut = true; // eslint-disable-line no-param-reassign + } + }, + + ReferencedIdentifier(path, state) { + const { node } = path; + + if (node.name === 'arguments') { + state.optOut = true; // eslint-disable-line no-param-reassign + } + }, +}; + module.exports = function plugin(args) { // This is a Babel plugin, but the user put it in the Webpack config. if (this && this.callback) { @@ -146,6 +164,16 @@ module.exports = function plugin(args) { if (path.isClassProperty()) { const { node } = path; + const state = { + optOut: false, + }; + + path.traverse(classPropertyOptOutVistor, state); + + if (state.optOut) { + return; + } + // class property node value is nullable if (node.value && node.value.type === 'ArrowFunctionExpression') { const params = node.value.params; diff --git a/test/babel/fixtures/class-properties/arguments/.babelrc b/test/babel/fixtures/class-properties/arguments/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/arguments/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/arguments/actual.js b/test/babel/fixtures/class-properties/arguments/actual.js new file mode 100644 index 000000000..72498f70c --- /dev/null +++ b/test/babel/fixtures/class-properties/arguments/actual.js @@ -0,0 +1,7 @@ +class Foo { + bar = (a, b) => { + arguments; + + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/arguments/expected.js b/test/babel/fixtures/class-properties/arguments/expected.js new file mode 100644 index 000000000..8717eedfc --- /dev/null +++ b/test/babel/fixtures/class-properties/arguments/expected.js @@ -0,0 +1,19 @@ +var _arguments = arguments; +class Foo { + bar = (a, b) => { + _arguments; + + return a(b); + }; +} +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; diff --git a/test/babel/fixtures/class-properties/nested-arguments/.babelrc b/test/babel/fixtures/class-properties/nested-arguments/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/nested-arguments/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/nested-arguments/actual.js b/test/babel/fixtures/class-properties/nested-arguments/actual.js new file mode 100644 index 000000000..ef223fdae --- /dev/null +++ b/test/babel/fixtures/class-properties/nested-arguments/actual.js @@ -0,0 +1,9 @@ +class Foo { + bar = (a, b) => { + () => { + arguments; + } + + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/nested-arguments/expected.js b/test/babel/fixtures/class-properties/nested-arguments/expected.js new file mode 100644 index 000000000..143c8d06c --- /dev/null +++ b/test/babel/fixtures/class-properties/nested-arguments/expected.js @@ -0,0 +1,21 @@ +var _arguments = arguments; +class Foo { + bar = (a, b) => { + () => { + _arguments; + }; + + return a(b); + }; +} +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; diff --git a/test/babel/fixtures/class-properties/nested-new.target/.babelrc b/test/babel/fixtures/class-properties/nested-new.target/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/nested-new.target/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/nested-new.target/actual.js b/test/babel/fixtures/class-properties/nested-new.target/actual.js new file mode 100644 index 000000000..d61409eb3 --- /dev/null +++ b/test/babel/fixtures/class-properties/nested-new.target/actual.js @@ -0,0 +1,9 @@ +class Foo { + bar = (a, b) => { + () => { + new.target; + } + + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/nested-new.target/expected.js b/test/babel/fixtures/class-properties/nested-new.target/expected.js new file mode 100644 index 000000000..83c85f891 --- /dev/null +++ b/test/babel/fixtures/class-properties/nested-new.target/expected.js @@ -0,0 +1,20 @@ +class Foo { + bar = (a, b) => { + () => { + new.target; + }; + + return a(b); + }; +} +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; diff --git a/test/babel/fixtures/class-properties/new.target/.babelrc b/test/babel/fixtures/class-properties/new.target/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/new.target/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/new.target/actual.js b/test/babel/fixtures/class-properties/new.target/actual.js new file mode 100644 index 000000000..a4b098773 --- /dev/null +++ b/test/babel/fixtures/class-properties/new.target/actual.js @@ -0,0 +1,7 @@ +class Foo { + bar = (a, b) => { + new.target; + + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/new.target/expected.js b/test/babel/fixtures/class-properties/new.target/expected.js new file mode 100644 index 000000000..c77ea847e --- /dev/null +++ b/test/babel/fixtures/class-properties/new.target/expected.js @@ -0,0 +1,18 @@ +class Foo { + bar = (a, b) => { + new.target; + + return a(b); + }; +} +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; From bc263de6d123f9f12b5eccfe3054feb948e7a3fd Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 14 Sep 2016 21:04:36 -0500 Subject: [PATCH 130/161] 3.0.0-beta.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c7f96e3e..c0d2966b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-beta.3", + "version": "3.0.0-beta.4", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 2ee9e977670c2d0a7356cd20ced794da66ad129a Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Tue, 20 Sep 2016 11:12:54 -0400 Subject: [PATCH 131/161] fix async arrow functions --- src/babel/index.js | 15 +++++++++---- test/babel/fixtures/async-functions/.babelrc | 6 ++++++ test/babel/fixtures/async-functions/actual.js | 5 +++++ .../fixtures/async-functions/expected.js | 21 +++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 test/babel/fixtures/async-functions/.babelrc create mode 100644 test/babel/fixtures/async-functions/actual.js create mode 100644 test/babel/fixtures/async-functions/expected.js diff --git a/src/babel/index.js b/src/babel/index.js index f0f998106..569ff7a7d 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -14,15 +14,20 @@ const buildTagger = template(` })(); `); -const buildNewClassProperty = (t, classPropertyName, newMethodName) => { - const returnExpression = t.callExpression( +const buildNewClassProperty = (t, classPropertyName, newMethodName, isAsync) => { + let returnExpression = t.callExpression( t.memberExpression(t.thisExpression(), newMethodName), [t.spreadElement(t.identifier('params'))] ); + if (isAsync) { + returnExpression = t.awaitExpression(returnExpression); + } + const newArrowFunction = t.arrowFunctionExpression( [t.restElement(t.identifier('params'))], - returnExpression + returnExpression, + isAsync ); return t.classProperty(classPropertyName, newArrowFunction); }; @@ -176,6 +181,7 @@ module.exports = function plugin(args) { // class property node value is nullable if (node.value && node.value.type === 'ArrowFunctionExpression') { + const isAsync = node.value.async; const params = node.value.params; const newIdentifier = t.identifier(`__${node.key.name}__REACT_HOT_LOADER__`); @@ -187,11 +193,12 @@ module.exports = function plugin(args) { // create a new method on the class that the original class property function // calls, since the method is able to be replaced by RHL const newMethod = t.classMethod('method', newIdentifier, params, newMethodBody); + newMethod.async = isAsync; path.insertAfter(newMethod); // replace the original class property function with a function that calls // the new class method created above - path.replaceWith(buildNewClassProperty(t, node.key, newIdentifier)); + path.replaceWith(buildNewClassProperty(t, node.key, newIdentifier, isAsync)); } } }); diff --git a/test/babel/fixtures/async-functions/.babelrc b/test/babel/fixtures/async-functions/.babelrc new file mode 100644 index 000000000..e4d66502a --- /dev/null +++ b/test/babel/fixtures/async-functions/.babelrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + "syntax-class-properties", + "../../../../src/babel" + ] +} diff --git a/test/babel/fixtures/async-functions/actual.js b/test/babel/fixtures/async-functions/actual.js new file mode 100644 index 000000000..7307e91b7 --- /dev/null +++ b/test/babel/fixtures/async-functions/actual.js @@ -0,0 +1,5 @@ +class Foo { + bar = async (a, b) => { + return await a(b); + }; +} diff --git a/test/babel/fixtures/async-functions/expected.js b/test/babel/fixtures/async-functions/expected.js new file mode 100644 index 000000000..6684b19e2 --- /dev/null +++ b/test/babel/fixtures/async-functions/expected.js @@ -0,0 +1,21 @@ +var _this = this; + +class Foo { + bar = async (...params) => await _this.__bar__REACT_HOT_LOADER__(...params); + + async __bar__REACT_HOT_LOADER__(a, b) { + return await a(b); + } + +} +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; From 1058ad9c015af94a77fb33b0a3497039a77efb48 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 21 Sep 2016 20:57:41 -0500 Subject: [PATCH 132/161] Prevent the Babel plugin from breaking the output of babel-node (#374) * prevent the babel plugin from breaking the output of babel-node the tagger code inserted by the babel plugin at the end of every file is an IIFE, which happens to break babel-node when it's loaded. by changing it to a variable declaration (that is still immediately invoked), it works the same way and doesn't break the plugin. * comment why we assigned the tagger function to an unused variable --- src/babel/index.js | 8 ++++++-- test/babel/fixtures/bindings/expected.js | 4 ++-- .../babel/fixtures/class-properties/arguments/expected.js | 4 ++-- .../fixtures/class-properties/block-body/expected.js | 4 ++-- .../fixtures/class-properties/default-params/expected.js | 4 ++-- .../class-properties/destructured-params/expected.js | 4 ++-- .../fixtures/class-properties/expression-body/expected.js | 4 ++-- .../class-properties/nested-arguments/expected.js | 4 ++-- .../class-properties/nested-new.target/expected.js | 4 ++-- .../fixtures/class-properties/new.target/expected.js | 4 ++-- test/babel/fixtures/counter/expected.js | 4 ++-- test/babel/fixtures/issue-246/expected.js | 4 ++-- test/babel/fixtures/name-clash/expected.js | 4 ++-- 13 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index f0f998106..39b51dd0d 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -4,8 +4,11 @@ const buildRegistration = template( '__REACT_HOT_LOADER__.register(ID, NAME, FILENAME);' ); const buildSemi = template(';'); + +// We're making the IIFE we insert at the end of the file an unused variable +// because it otherwise breaks the output of the babel-node REPL (#359). const buildTagger = template(` -(function () { +var UNUSED = (function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } @@ -141,7 +144,7 @@ module.exports = function plugin(args) { /* eslint-enable */ }, - exit({ node }) { + exit({ node, scope }) { const registrations = node[REGISTRATIONS]; node[REGISTRATIONS] = null; // eslint-disable-line no-param-reassign @@ -150,6 +153,7 @@ module.exports = function plugin(args) { node.body.push(buildSemi()); node.body.push( buildTagger({ + UNUSED: scope.generateUidIdentifier(), REGISTRATIONS: registrations, }) ); diff --git a/test/babel/fixtures/bindings/expected.js b/test/babel/fixtures/bindings/expected.js index 6e8494e7f..6117668a1 100644 --- a/test/babel/fixtures/bindings/expected.js +++ b/test/babel/fixtures/bindings/expected.js @@ -22,7 +22,7 @@ const _default = React.createClass({}); export default _default; ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } @@ -38,6 +38,6 @@ export default _default; __REACT_HOT_LOADER__.register(E, "E", __FILENAME__); __REACT_HOT_LOADER__.register(_default, "default", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/arguments/expected.js b/test/babel/fixtures/class-properties/arguments/expected.js index 8717eedfc..985d899a1 100644 --- a/test/babel/fixtures/class-properties/arguments/expected.js +++ b/test/babel/fixtures/class-properties/arguments/expected.js @@ -8,12 +8,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/block-body/expected.js b/test/babel/fixtures/class-properties/block-body/expected.js index 9d4018dcf..894278fc2 100644 --- a/test/babel/fixtures/class-properties/block-body/expected.js +++ b/test/babel/fixtures/class-properties/block-body/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/default-params/expected.js b/test/babel/fixtures/class-properties/default-params/expected.js index 4fe43f8c5..19f8f34c6 100644 --- a/test/babel/fixtures/class-properties/default-params/expected.js +++ b/test/babel/fixtures/class-properties/default-params/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/destructured-params/expected.js b/test/babel/fixtures/class-properties/destructured-params/expected.js index 07f4f9c21..7916cc25f 100644 --- a/test/babel/fixtures/class-properties/destructured-params/expected.js +++ b/test/babel/fixtures/class-properties/destructured-params/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/expression-body/expected.js b/test/babel/fixtures/class-properties/expression-body/expected.js index 303ed2a28..4ac9d1755 100644 --- a/test/babel/fixtures/class-properties/expression-body/expected.js +++ b/test/babel/fixtures/class-properties/expression-body/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/nested-arguments/expected.js b/test/babel/fixtures/class-properties/nested-arguments/expected.js index 143c8d06c..ceaf67be1 100644 --- a/test/babel/fixtures/class-properties/nested-arguments/expected.js +++ b/test/babel/fixtures/class-properties/nested-arguments/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/nested-new.target/expected.js b/test/babel/fixtures/class-properties/nested-new.target/expected.js index 83c85f891..fa479c403 100644 --- a/test/babel/fixtures/class-properties/nested-new.target/expected.js +++ b/test/babel/fixtures/class-properties/nested-new.target/expected.js @@ -9,12 +9,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/new.target/expected.js b/test/babel/fixtures/class-properties/new.target/expected.js index c77ea847e..16311e162 100644 --- a/test/babel/fixtures/class-properties/new.target/expected.js +++ b/test/babel/fixtures/class-properties/new.target/expected.js @@ -7,12 +7,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/counter/expected.js b/test/babel/fixtures/counter/expected.js index 5658a7158..ad0a85e17 100644 --- a/test/babel/fixtures/counter/expected.js +++ b/test/babel/fixtures/counter/expected.js @@ -21,7 +21,7 @@ var _default = function _default() { exports.default = _default; ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } @@ -29,6 +29,6 @@ exports.default = _default; __REACT_HOT_LOADER__.register(Counter, "Counter", __FILENAME__); __REACT_HOT_LOADER__.register(_default, "default", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/issue-246/expected.js b/test/babel/fixtures/issue-246/expected.js index 95de82c72..39f682363 100644 --- a/test/babel/fixtures/issue-246/expected.js +++ b/test/babel/fixtures/issue-246/expected.js @@ -13,12 +13,12 @@ function spread() { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(spread, "spread", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/name-clash/expected.js b/test/babel/fixtures/name-clash/expected.js index ebb8b38a0..773c271a8 100644 --- a/test/babel/fixtures/name-clash/expected.js +++ b/test/babel/fixtures/name-clash/expected.js @@ -3,7 +3,7 @@ const _default2 = 42; export default _default2; ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } @@ -11,6 +11,6 @@ export default _default2; __REACT_HOT_LOADER__.register(_default, "_default", __FILENAME__); __REACT_HOT_LOADER__.register(_default2, "default", __FILENAME__); -})(); +}(); ; From 1df66507f580a3b272bc08b93054a7476f7087cb Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 21 Sep 2016 21:17:46 -0500 Subject: [PATCH 133/161] add async arrow class property test with expression body also move the async function tests under fixtures/class-properties --- .../async-functions-expression-body}/.babelrc | 2 +- .../async-functions-expression-body/actual.js | 3 +++ .../expected.js | 21 +++++++++++++++++++ .../class-properties/async-functions/.babelrc | 6 ++++++ .../async-functions/actual.js | 0 .../async-functions/expected.js | 0 6 files changed, 31 insertions(+), 1 deletion(-) rename test/babel/fixtures/{async-functions => class-properties/async-functions-expression-body}/.babelrc (63%) create mode 100644 test/babel/fixtures/class-properties/async-functions-expression-body/actual.js create mode 100644 test/babel/fixtures/class-properties/async-functions-expression-body/expected.js create mode 100644 test/babel/fixtures/class-properties/async-functions/.babelrc rename test/babel/fixtures/{ => class-properties}/async-functions/actual.js (100%) rename test/babel/fixtures/{ => class-properties}/async-functions/expected.js (100%) diff --git a/test/babel/fixtures/async-functions/.babelrc b/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc similarity index 63% rename from test/babel/fixtures/async-functions/.babelrc rename to test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc index e4d66502a..5c0b59e95 100644 --- a/test/babel/fixtures/async-functions/.babelrc +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc @@ -1,6 +1,6 @@ { "plugins": [ "syntax-class-properties", - "../../../../src/babel" + "../../../../../src/babel" ] } diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/actual.js b/test/babel/fixtures/class-properties/async-functions-expression-body/actual.js new file mode 100644 index 000000000..4652d980f --- /dev/null +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/actual.js @@ -0,0 +1,3 @@ +class Foo { + bar = async (a, b) => await b(a) +} diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js new file mode 100644 index 000000000..6be54d0a5 --- /dev/null +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js @@ -0,0 +1,21 @@ +var _this = this; + +class Foo { + bar = async (...params) => await _this.__bar__REACT_HOT_LOADER__(...params); + + async __bar__REACT_HOT_LOADER__(a, b) { + return await b(a); + } + +} +; + +(function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +})(); + +; diff --git a/test/babel/fixtures/class-properties/async-functions/.babelrc b/test/babel/fixtures/class-properties/async-functions/.babelrc new file mode 100644 index 000000000..5c0b59e95 --- /dev/null +++ b/test/babel/fixtures/class-properties/async-functions/.babelrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + "syntax-class-properties", + "../../../../../src/babel" + ] +} diff --git a/test/babel/fixtures/async-functions/actual.js b/test/babel/fixtures/class-properties/async-functions/actual.js similarity index 100% rename from test/babel/fixtures/async-functions/actual.js rename to test/babel/fixtures/class-properties/async-functions/actual.js diff --git a/test/babel/fixtures/async-functions/expected.js b/test/babel/fixtures/class-properties/async-functions/expected.js similarity index 100% rename from test/babel/fixtures/async-functions/expected.js rename to test/babel/fixtures/class-properties/async-functions/expected.js From 38781b3e6c06570cf40b03c7d12179ea18e87fe3 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 21 Sep 2016 21:22:58 -0500 Subject: [PATCH 134/161] update tests --- .../async-functions-expression-body/expected.js | 4 ++-- .../fixtures/class-properties/async-functions/expected.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js index 6be54d0a5..81b2e5e7c 100644 --- a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; diff --git a/test/babel/fixtures/class-properties/async-functions/expected.js b/test/babel/fixtures/class-properties/async-functions/expected.js index 6684b19e2..ba0c41e8b 100644 --- a/test/babel/fixtures/class-properties/async-functions/expected.js +++ b/test/babel/fixtures/class-properties/async-functions/expected.js @@ -10,12 +10,12 @@ class Foo { } ; -(function () { +var _temp = function () { if (typeof __REACT_HOT_LOADER__ === 'undefined') { return; } __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); -})(); +}(); ; From 3f97daf96fc271d38cbc3ad0ac9bfcb51d3884d7 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 21 Sep 2016 21:29:51 -0500 Subject: [PATCH 135/161] 3.0.0-beta.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0d2966b5..4e75eead4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-beta.4", + "version": "3.0.0-beta.5", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 794c02b0aeef3d13ec09a939824c5ed8ec836a09 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 21 Sep 2016 21:52:07 -0500 Subject: [PATCH 136/161] Update CHANGELOG.md --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa3971bc2..fe7d06716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ ## Changelog +### 3.0.0-beta.5 + +* Makes the class properties portion of the Babel plugin work with async functions. (#372) +* Change the output of the tagger code in the Babel plugin so that it doesn't break the output of `babel-node`. (#374) + +### 3.0.0-beta.4 + +* Extends the Babel plugin to enable hot reloading of class properties. (#322) +* Fixes a bug in the Webpack loader from a component importing a module with the same basename. (#347) + +### 3.0.0-beta.3 + +* Fixes broken import of RedBox, which led to confusing stack traces when applications threw errors. (#314) +* Add `module.hot` checks to conditional `require()`s to remove unnecessary warnings when using server rendering. (#302) + +### 3.0.0-beta.2 + +* Patch `React.createFactory` (#287) +* Fix props typo (#285) + +### 3.0.0-beta.1 + +* Adds complete React Router support. Async routes should work fine now. (#272) +* Fixes a nasty bug which caused unwrapped component to render. (#266, #272) +* Fixes an issue that caused components with `shouldComponentUpdate` optimizations not getting redrawn (#269, 2a1e384d54e1919117f70f75dd20ad2490b1d9f5) +* Internal: a rewrite and much better test coverage. + +### 3.0.0-beta.0 + +* Fixes an issue when used in Webpack 2 (https://github.com/gaearon/react-hot-loader/issues/263) +* **Breaking change:** instead of + + ```js + +``` + + you now need to write + + ```js + + + + ``` + + (#250) + + **See [this commit](https://github.com/gaearon/react-hot-boilerplate/commit/b52c727937a499f3efdc5dceb74ae952aa318c3a) as an update reference!** + ### 3.0.0-alpha Big changes both to internals and usage. No docs yet but you can look at https://github.com/gaearon/react-hot-boilerplate/pull/61 for an example. From 4f346653ac8693fdb0db89625411d3e6d33025a0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Sep 2016 09:40:41 +0100 Subject: [PATCH 137/161] more passing tests for class properties transform --- .../class-properties/not-a-function/.babelrc | 3 +++ .../class-properties/not-a-function/actual.js | 3 +++ .../class-properties/not-a-function/expected.js | 14 ++++++++++++++ .../not-an-arrow-function/.babelrc | 3 +++ .../not-an-arrow-function/actual.js | 5 +++++ .../not-an-arrow-function/expected.js | 16 ++++++++++++++++ 6 files changed, 44 insertions(+) create mode 100644 test/babel/fixtures/class-properties/not-a-function/.babelrc create mode 100644 test/babel/fixtures/class-properties/not-a-function/actual.js create mode 100644 test/babel/fixtures/class-properties/not-a-function/expected.js create mode 100644 test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc create mode 100644 test/babel/fixtures/class-properties/not-an-arrow-function/actual.js create mode 100644 test/babel/fixtures/class-properties/not-an-arrow-function/expected.js diff --git a/test/babel/fixtures/class-properties/not-a-function/.babelrc b/test/babel/fixtures/class-properties/not-a-function/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/not-a-function/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/not-a-function/actual.js b/test/babel/fixtures/class-properties/not-a-function/actual.js new file mode 100644 index 000000000..d4c4a0e33 --- /dev/null +++ b/test/babel/fixtures/class-properties/not-a-function/actual.js @@ -0,0 +1,3 @@ +class Foo { + bar = 2; +} diff --git a/test/babel/fixtures/class-properties/not-a-function/expected.js b/test/babel/fixtures/class-properties/not-a-function/expected.js new file mode 100644 index 000000000..6e2cf5432 --- /dev/null +++ b/test/babel/fixtures/class-properties/not-a-function/expected.js @@ -0,0 +1,14 @@ +class Foo { + bar = 2; +} +; + +var _temp = function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +}(); + +; diff --git a/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc b/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/not-an-arrow-function/actual.js b/test/babel/fixtures/class-properties/not-an-arrow-function/actual.js new file mode 100644 index 000000000..1341d81af --- /dev/null +++ b/test/babel/fixtures/class-properties/not-an-arrow-function/actual.js @@ -0,0 +1,5 @@ +class Foo { + bar = function (a, b) { + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js b/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js new file mode 100644 index 000000000..3799c87a6 --- /dev/null +++ b/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js @@ -0,0 +1,16 @@ +class Foo { + bar = function (a, b) { + return a(b); + }; +} +; + +var _temp = function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +}(); + +; From 46b0d312a1f2025d2471a02755047819a163ea4f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Sep 2016 09:49:00 +0100 Subject: [PATCH 138/161] added tests for class props with same name as class method --- .../same-name-as-class-method/.babelrc | 3 +++ .../same-name-as-class-method/actual.js | 9 +++++++ .../same-name-as-class-method/expected.js | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc create mode 100644 test/babel/fixtures/class-properties/same-name-as-class-method/actual.js create mode 100644 test/babel/fixtures/class-properties/same-name-as-class-method/expected.js diff --git a/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc b/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/same-name-as-class-method/actual.js b/test/babel/fixtures/class-properties/same-name-as-class-method/actual.js new file mode 100644 index 000000000..e800a15e4 --- /dev/null +++ b/test/babel/fixtures/class-properties/same-name-as-class-method/actual.js @@ -0,0 +1,9 @@ +class Foo { + bar = (a, b) => { + return a(b); + }; + + bar() { + return 2 + } +} diff --git a/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js b/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js new file mode 100644 index 000000000..50173acae --- /dev/null +++ b/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js @@ -0,0 +1,24 @@ +var _this = this; + +class Foo { + bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); + + __bar__REACT_HOT_LOADER__(a, b) { + return a(b); + } + + bar() { + return 2; + } +} +; + +var _temp = function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +}(); + +; From 12f3fdd0a32ef263fd8f28807a2111ef1a1af465 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Sep 2016 09:33:52 +0100 Subject: [PATCH 139/161] added failing test for static class properties --- .../class-properties/static-property/.babelrc | 3 +++ .../class-properties/static-property/actual.js | 5 +++++ .../class-properties/static-property/expected.js | 16 ++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 test/babel/fixtures/class-properties/static-property/.babelrc create mode 100644 test/babel/fixtures/class-properties/static-property/actual.js create mode 100644 test/babel/fixtures/class-properties/static-property/expected.js diff --git a/test/babel/fixtures/class-properties/static-property/.babelrc b/test/babel/fixtures/class-properties/static-property/.babelrc new file mode 100644 index 000000000..4075556b7 --- /dev/null +++ b/test/babel/fixtures/class-properties/static-property/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-class-properties", "../../../../../src/babel"] +} diff --git a/test/babel/fixtures/class-properties/static-property/actual.js b/test/babel/fixtures/class-properties/static-property/actual.js new file mode 100644 index 000000000..67bfc4652 --- /dev/null +++ b/test/babel/fixtures/class-properties/static-property/actual.js @@ -0,0 +1,5 @@ +class Foo { + static bar = (a, b) => { + return a(b); + }; +} diff --git a/test/babel/fixtures/class-properties/static-property/expected.js b/test/babel/fixtures/class-properties/static-property/expected.js new file mode 100644 index 000000000..c3da49e95 --- /dev/null +++ b/test/babel/fixtures/class-properties/static-property/expected.js @@ -0,0 +1,16 @@ +class Foo { + static bar = (a, b) => { + return a(b); + }; +} +; + +var _temp = function () { + if (typeof __REACT_HOT_LOADER__ === 'undefined') { + return; + } + + __REACT_HOT_LOADER__.register(Foo, "Foo", __FILENAME__); +}(); + +; From 44f9902fa61932dba682f62e4980a19e13c4ad03 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Sep 2016 09:35:15 +0100 Subject: [PATCH 140/161] opt out of class property transform for static class properties --- src/babel/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/babel/index.js b/src/babel/index.js index b7e75e9d5..0a5be5e12 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -173,6 +173,11 @@ module.exports = function plugin(args) { if (path.isClassProperty()) { const { node } = path; + // don't apply transform to static class properties + if (node.static) { + return; + } + const state = { optOut: false, }; From 4e912990a1d0344219b8e16945ff92e4445323a3 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 5 Oct 2016 12:23:08 +0100 Subject: [PATCH 141/161] added babel-plugin-syntax-async-functions to babelrc of async functions test fixtures --- .../class-properties/async-functions-expression-body/.babelrc | 1 + test/babel/fixtures/class-properties/async-functions/.babelrc | 1 + 2 files changed, 2 insertions(+) diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc b/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc index 5c0b59e95..a86136fec 100644 --- a/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc @@ -1,5 +1,6 @@ { "plugins": [ + "syntax-async-functions", "syntax-class-properties", "../../../../../src/babel" ] diff --git a/test/babel/fixtures/class-properties/async-functions/.babelrc b/test/babel/fixtures/class-properties/async-functions/.babelrc index 5c0b59e95..a86136fec 100644 --- a/test/babel/fixtures/class-properties/async-functions/.babelrc +++ b/test/babel/fixtures/class-properties/async-functions/.babelrc @@ -1,5 +1,6 @@ { "plugins": [ + "syntax-async-functions", "syntax-class-properties", "../../../../../src/babel" ] From 8fab49ed2f229b6ec9858fe3000204b600b1cc73 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 7 Oct 2016 15:24:11 +0100 Subject: [PATCH 142/161] add babel-plugin-transform-class-properties to class-properties babel fixtures - using only babel-plugin-syntax-class-properties produces invalid output --- .../fixtures/class-properties/arguments/.babelrc | 2 +- .../class-properties/arguments/expected.js | 12 +++++++----- .../async-functions-expression-body/.babelrc | 4 ++-- .../async-functions-expression-body/expected.js | 6 +++--- .../class-properties/async-functions/.babelrc | 4 ++-- .../class-properties/async-functions/expected.js | 6 +++--- .../fixtures/class-properties/block-body/.babelrc | 2 +- .../class-properties/block-body/expected.js | 6 +++--- .../class-properties/default-params/.babelrc | 2 +- .../class-properties/default-params/expected.js | 6 +++--- .../class-properties/destructured-params/.babelrc | 2 +- .../destructured-params/expected.js | 6 +++--- .../class-properties/expression-body/.babelrc | 2 +- .../class-properties/expression-body/expected.js | 6 +++--- .../class-properties/nested-arguments/.babelrc | 2 +- .../class-properties/nested-arguments/expected.js | 14 ++++++++------ .../class-properties/nested-new.target/.babelrc | 2 +- .../class-properties/nested-new.target/expected.js | 13 ++++++++----- .../fixtures/class-properties/new.target/.babelrc | 2 +- .../class-properties/new.target/expected.js | 11 +++++++---- .../class-properties/not-a-function/.babelrc | 2 +- .../class-properties/not-a-function/expected.js | 5 ++++- .../not-an-arrow-function/.babelrc | 2 +- .../not-an-arrow-function/expected.js | 9 ++++++--- .../same-name-as-class-method/.babelrc | 2 +- .../same-name-as-class-method/expected.js | 6 +++--- .../class-properties/static-property/.babelrc | 2 +- .../class-properties/static-property/expected.js | 11 ++++++----- 28 files changed, 83 insertions(+), 66 deletions(-) diff --git a/test/babel/fixtures/class-properties/arguments/.babelrc b/test/babel/fixtures/class-properties/arguments/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/arguments/.babelrc +++ b/test/babel/fixtures/class-properties/arguments/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/arguments/expected.js b/test/babel/fixtures/class-properties/arguments/expected.js index 985d899a1..99835ee0a 100644 --- a/test/babel/fixtures/class-properties/arguments/expected.js +++ b/test/babel/fixtures/class-properties/arguments/expected.js @@ -1,10 +1,12 @@ -var _arguments = arguments; class Foo { - bar = (a, b) => { - _arguments; + constructor() { + this.bar = (a, b) => { + arguments; + + return a(b); + }; + } - return a(b); - }; } ; diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc b/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc index a86136fec..9f1e1af4b 100644 --- a/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/.babelrc @@ -1,7 +1,7 @@ { "plugins": [ + "../../../../../src/babel", + "transform-class-properties", "syntax-async-functions", - "syntax-class-properties", - "../../../../../src/babel" ] } diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js index 81b2e5e7c..5033e6987 100644 --- a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - bar = async (...params) => await _this.__bar__REACT_HOT_LOADER__(...params); + constructor() { + this.bar = async (...params) => await this.__bar__REACT_HOT_LOADER__(...params); + } async __bar__REACT_HOT_LOADER__(a, b) { return await b(a); diff --git a/test/babel/fixtures/class-properties/async-functions/.babelrc b/test/babel/fixtures/class-properties/async-functions/.babelrc index a86136fec..9f1e1af4b 100644 --- a/test/babel/fixtures/class-properties/async-functions/.babelrc +++ b/test/babel/fixtures/class-properties/async-functions/.babelrc @@ -1,7 +1,7 @@ { "plugins": [ + "../../../../../src/babel", + "transform-class-properties", "syntax-async-functions", - "syntax-class-properties", - "../../../../../src/babel" ] } diff --git a/test/babel/fixtures/class-properties/async-functions/expected.js b/test/babel/fixtures/class-properties/async-functions/expected.js index ba0c41e8b..4bc37895a 100644 --- a/test/babel/fixtures/class-properties/async-functions/expected.js +++ b/test/babel/fixtures/class-properties/async-functions/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - bar = async (...params) => await _this.__bar__REACT_HOT_LOADER__(...params); + constructor() { + this.bar = async (...params) => await this.__bar__REACT_HOT_LOADER__(...params); + } async __bar__REACT_HOT_LOADER__(a, b) { return await a(b); diff --git a/test/babel/fixtures/class-properties/block-body/.babelrc b/test/babel/fixtures/class-properties/block-body/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/block-body/.babelrc +++ b/test/babel/fixtures/class-properties/block-body/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/block-body/expected.js b/test/babel/fixtures/class-properties/block-body/expected.js index 894278fc2..fa7ebdee5 100644 --- a/test/babel/fixtures/class-properties/block-body/expected.js +++ b/test/babel/fixtures/class-properties/block-body/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); + constructor() { + this.bar = (...params) => this.__bar__REACT_HOT_LOADER__(...params); + } __bar__REACT_HOT_LOADER__(a, b) { return a(b); diff --git a/test/babel/fixtures/class-properties/default-params/.babelrc b/test/babel/fixtures/class-properties/default-params/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/default-params/.babelrc +++ b/test/babel/fixtures/class-properties/default-params/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/default-params/expected.js b/test/babel/fixtures/class-properties/default-params/expected.js index 19f8f34c6..ddd573435 100644 --- a/test/babel/fixtures/class-properties/default-params/expected.js +++ b/test/babel/fixtures/class-properties/default-params/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); + constructor() { + this.bar = (...params) => this.__bar__REACT_HOT_LOADER__(...params); + } __bar__REACT_HOT_LOADER__(a = "foo") { return `${ a }bar`; diff --git a/test/babel/fixtures/class-properties/destructured-params/.babelrc b/test/babel/fixtures/class-properties/destructured-params/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/destructured-params/.babelrc +++ b/test/babel/fixtures/class-properties/destructured-params/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/destructured-params/expected.js b/test/babel/fixtures/class-properties/destructured-params/expected.js index 7916cc25f..7c4dac820 100644 --- a/test/babel/fixtures/class-properties/destructured-params/expected.js +++ b/test/babel/fixtures/class-properties/destructured-params/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); + constructor() { + this.bar = (...params) => this.__bar__REACT_HOT_LOADER__(...params); + } __bar__REACT_HOT_LOADER__({ a }, { b }) { return `${ a }${ b }`; diff --git a/test/babel/fixtures/class-properties/expression-body/.babelrc b/test/babel/fixtures/class-properties/expression-body/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/expression-body/.babelrc +++ b/test/babel/fixtures/class-properties/expression-body/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/expression-body/expected.js b/test/babel/fixtures/class-properties/expression-body/expected.js index 4ac9d1755..ced1724a1 100644 --- a/test/babel/fixtures/class-properties/expression-body/expected.js +++ b/test/babel/fixtures/class-properties/expression-body/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - onClick = (...params) => _this.__onClick__REACT_HOT_LOADER__(...params); + constructor() { + this.onClick = (...params) => this.__onClick__REACT_HOT_LOADER__(...params); + } __onClick__REACT_HOT_LOADER__(e) { return e.target.value; diff --git a/test/babel/fixtures/class-properties/nested-arguments/.babelrc b/test/babel/fixtures/class-properties/nested-arguments/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/nested-arguments/.babelrc +++ b/test/babel/fixtures/class-properties/nested-arguments/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/nested-arguments/expected.js b/test/babel/fixtures/class-properties/nested-arguments/expected.js index ceaf67be1..86cf53ea8 100644 --- a/test/babel/fixtures/class-properties/nested-arguments/expected.js +++ b/test/babel/fixtures/class-properties/nested-arguments/expected.js @@ -1,12 +1,14 @@ -var _arguments = arguments; class Foo { - bar = (a, b) => { - () => { - _arguments; + constructor() { + this.bar = (a, b) => { + () => { + arguments; + }; + + return a(b); }; + } - return a(b); - }; } ; diff --git a/test/babel/fixtures/class-properties/nested-new.target/.babelrc b/test/babel/fixtures/class-properties/nested-new.target/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/nested-new.target/.babelrc +++ b/test/babel/fixtures/class-properties/nested-new.target/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/nested-new.target/expected.js b/test/babel/fixtures/class-properties/nested-new.target/expected.js index fa479c403..cf4ad1552 100644 --- a/test/babel/fixtures/class-properties/nested-new.target/expected.js +++ b/test/babel/fixtures/class-properties/nested-new.target/expected.js @@ -1,11 +1,14 @@ class Foo { - bar = (a, b) => { - () => { - new.target; + constructor() { + this.bar = (a, b) => { + () => { + new.target; + }; + + return a(b); }; + } - return a(b); - }; } ; diff --git a/test/babel/fixtures/class-properties/new.target/.babelrc b/test/babel/fixtures/class-properties/new.target/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/new.target/.babelrc +++ b/test/babel/fixtures/class-properties/new.target/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/new.target/expected.js b/test/babel/fixtures/class-properties/new.target/expected.js index 16311e162..8fe6cc9ca 100644 --- a/test/babel/fixtures/class-properties/new.target/expected.js +++ b/test/babel/fixtures/class-properties/new.target/expected.js @@ -1,9 +1,12 @@ class Foo { - bar = (a, b) => { - new.target; + constructor() { + this.bar = (a, b) => { + new.target; + + return a(b); + }; + } - return a(b); - }; } ; diff --git a/test/babel/fixtures/class-properties/not-a-function/.babelrc b/test/babel/fixtures/class-properties/not-a-function/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/not-a-function/.babelrc +++ b/test/babel/fixtures/class-properties/not-a-function/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/not-a-function/expected.js b/test/babel/fixtures/class-properties/not-a-function/expected.js index 6e2cf5432..8eebee8ab 100644 --- a/test/babel/fixtures/class-properties/not-a-function/expected.js +++ b/test/babel/fixtures/class-properties/not-a-function/expected.js @@ -1,5 +1,8 @@ class Foo { - bar = 2; + constructor() { + this.bar = 2; + } + } ; diff --git a/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc b/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc +++ b/test/babel/fixtures/class-properties/not-an-arrow-function/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js b/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js index 3799c87a6..ea5674d51 100644 --- a/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js +++ b/test/babel/fixtures/class-properties/not-an-arrow-function/expected.js @@ -1,7 +1,10 @@ class Foo { - bar = function (a, b) { - return a(b); - }; + constructor() { + this.bar = function (a, b) { + return a(b); + }; + } + } ; diff --git a/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc b/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc +++ b/test/babel/fixtures/class-properties/same-name-as-class-method/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js b/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js index 50173acae..9bcc53586 100644 --- a/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js +++ b/test/babel/fixtures/class-properties/same-name-as-class-method/expected.js @@ -1,7 +1,7 @@ -var _this = this; - class Foo { - bar = (...params) => _this.__bar__REACT_HOT_LOADER__(...params); + constructor() { + this.bar = (...params) => this.__bar__REACT_HOT_LOADER__(...params); + } __bar__REACT_HOT_LOADER__(a, b) { return a(b); diff --git a/test/babel/fixtures/class-properties/static-property/.babelrc b/test/babel/fixtures/class-properties/static-property/.babelrc index 4075556b7..3d663018a 100644 --- a/test/babel/fixtures/class-properties/static-property/.babelrc +++ b/test/babel/fixtures/class-properties/static-property/.babelrc @@ -1,3 +1,3 @@ { - "plugins": ["syntax-class-properties", "../../../../../src/babel"] + "plugins": ["../../../../../src/babel", "transform-class-properties"] } diff --git a/test/babel/fixtures/class-properties/static-property/expected.js b/test/babel/fixtures/class-properties/static-property/expected.js index c3da49e95..d4ded03aa 100644 --- a/test/babel/fixtures/class-properties/static-property/expected.js +++ b/test/babel/fixtures/class-properties/static-property/expected.js @@ -1,8 +1,9 @@ -class Foo { - static bar = (a, b) => { - return a(b); - }; -} +class Foo {} + +Foo.bar = (a, b) => { + return a(b); +}; + ; var _temp = function () { From b4de995ed0af09ee5f449b4ee26c179623771549 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Thu, 13 Oct 2016 20:40:34 -0500 Subject: [PATCH 143/161] Use production patch/AppContainer if no module.hot Prevents cases where NODE_ENV=test imports the development AppContainer and patching code. We can leave the NODE_ENV=production check in for manually opting out. Closes #396. --- src/AppContainer.js | 2 +- src/patch.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AppContainer.js b/src/AppContainer.js index cbf62b72b..f103d226d 100644 --- a/src/AppContainer.js +++ b/src/AppContainer.js @@ -2,7 +2,7 @@ 'use strict'; -if (!module.hot && process.env.NODE_ENV === 'production') { +if (!module.hot || process.env.NODE_ENV === 'production') { module.exports = require('./AppContainer.prod'); } else { module.exports = require('./AppContainer.dev'); diff --git a/src/patch.js b/src/patch.js index eeaff0792..53c7c5efb 100644 --- a/src/patch.js +++ b/src/patch.js @@ -2,7 +2,7 @@ 'use strict'; -if (!module.hot && process.env.NODE_ENV === 'production') { +if (!module.hot || process.env.NODE_ENV === 'production') { module.exports = require('./patch.prod'); } else { module.exports = require('./patch.dev'); From 03687ecb27027bf5baf5e0c625dc0356f96cd900 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Thu, 13 Oct 2016 20:54:10 -0500 Subject: [PATCH 144/161] 3.0.0-beta.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e75eead4..6375aa867 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-hot-loader", - "version": "3.0.0-beta.5", + "version": "3.0.0-beta.6", "description": "Tweak React components in real time.", "main": "index.js", "files": [ From 284269f2bda4a02ec27a06977e324d73f0e7d435 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Thu, 13 Oct 2016 21:04:25 -0500 Subject: [PATCH 145/161] update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe7d06716..98f2edc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Changelog +### 3.0.0-beta.6 + +* Use production versions of `patch` and `AppContainer` if no `module.hot` available, so it doesn't break people using `NODE_ENV=test`. (#398) +* Opt out of transforming static class properties. (#381) + ### 3.0.0-beta.5 * Makes the class properties portion of the Babel plugin work with async functions. (#372) From 574a4107304f6b01ce5140536e58422f6632fa96 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 18 Oct 2016 19:41:59 -0500 Subject: [PATCH 146/161] Add "Known Limitations" doc This shows some common problems with using RHL 3, along with workarounds (since they don't currently have great solutions) --- docs/Known Limitations.md | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/Known Limitations.md diff --git a/docs/Known Limitations.md b/docs/Known Limitations.md new file mode 100644 index 000000000..ffdf08c31 --- /dev/null +++ b/docs/Known Limitations.md @@ -0,0 +1,55 @@ +## Known Limitations + +### require.ensure +If you want to use Webpack code splitting via `require.ensure`, you'll need to add an additional `module.hot.accept` callback within the `require.ensure` block, like this: + +```js +require.ensure([], (require) => { + if (module.hot) { + module.hot.accept('../components/App', () => { + loadComponent(require('../components/App').default); + }) + } + loadComponent(require('../components/App').default); +}); +``` + +Note that if you're using React Router (pre-4.0), this will only work with `getChildRoutes`, but not `getComponent`, since `getComponent`'s callback will only load a component once. + +### Checking Element `type`s +Because React Hot Loader creates proxied versions of your components, comparing reference types of elements won't work: + +```js +const element = ; +console.log(element.type === Component); // false +``` + +One workaround is to create an element (that will have the `type` of the proxied component): + +```js +const ComponentType = ().type; +const element = ; +console.log(element.type === ComponentType); // true +``` + +### Reassigning Components +React Hot Loader will only try to reload the original component reference, so if you reassign it to another variable like this: + +```js +let App = () => (
hello
); +App = connect()(App); +export default App; +``` + +RHL won't reload it. Instead, you'll need to define it once: + +```js +const App = () => (
hello
); +export default connect()(App); +``` + +### Using Non-Transformed Classes +Unfortunately, right now classes need to be compiled by either Babel or TypeScript to the ES5 equivalent (see [#313](https://github.com/gaearon/react-hot-loader/issues/313)). + +### Decorators +Components that are decorated (using something like [`@autobind`](https://github.com/andreypopp/autobind-decorator)) currently do not retain state when being hot-reloaded. (see [#279](https://github.com/gaearon/react-hot-loader/issues/279)) From 142201d0fdc56dc99604855f4c8289f3ec24a033 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Tue, 18 Oct 2016 21:56:50 -0500 Subject: [PATCH 147/161] Add System.import section on known limitations Also, remove the section about transforming ES2015 classes because we want to fix that soon. --- docs/Known Limitations.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/Known Limitations.md b/docs/Known Limitations.md index ffdf08c31..e2c5c943b 100644 --- a/docs/Known Limitations.md +++ b/docs/Known Limitations.md @@ -1,6 +1,6 @@ ## Known Limitations -### require.ensure +### Code Splitting If you want to use Webpack code splitting via `require.ensure`, you'll need to add an additional `module.hot.accept` callback within the `require.ensure` block, like this: ```js @@ -16,6 +16,8 @@ require.ensure([], (require) => { Note that if you're using React Router (pre-4.0), this will only work with `getChildRoutes`, but not `getComponent`, since `getComponent`'s callback will only load a component once. +Also, if you're using the Webpack 2 beta, you can use `System.import` without extra `module.hot.accept` calls, although there are still a [few issues with it](https://github.com/gaearon/react-hot-loader/issues/303). + ### Checking Element `type`s Because React Hot Loader creates proxied versions of your components, comparing reference types of elements won't work: @@ -41,15 +43,12 @@ App = connect()(App); export default App; ``` -RHL won't reload it. Instead, you'll need to define it once: +React Hot Loader won't reload it. Instead, you'll need to define it once: ```js const App = () => (
hello
); export default connect()(App); ``` -### Using Non-Transformed Classes -Unfortunately, right now classes need to be compiled by either Babel or TypeScript to the ES5 equivalent (see [#313](https://github.com/gaearon/react-hot-loader/issues/313)). - ### Decorators Components that are decorated (using something like [`@autobind`](https://github.com/andreypopp/autobind-decorator)) currently do not retain state when being hot-reloaded. (see [#279](https://github.com/gaearon/react-hot-loader/issues/279)) From 475049f262646d92d9e641945eb2c885e4331498 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 26 Oct 2016 12:24:59 -0500 Subject: [PATCH 148/161] Remove deprecations from AppContainer In the alpha versions of 3.0, AppContainer accepted the child component and its props as props, but we moved to passing a child in the beta. The beta's been out for a while, and I haven't seen any examples recently using the alpha examples, so I'd say it's safe to remove these deprecations. --- src/AppContainer.dev.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/AppContainer.dev.js b/src/AppContainer.dev.js index af9e197b0..87f8f4a56 100644 --- a/src/AppContainer.dev.js +++ b/src/AppContainer.dev.js @@ -52,36 +52,11 @@ class AppContainer extends Component { return ; } - if (this.props.component) { - return ; - } - return React.Children.only(this.props.children); } } AppContainer.propTypes = { - component(props) { - if (props.component) { - return new Error( - 'Passing "component" prop to is deprecated. ' + - 'Replace with .' - ); - } - - return undefined; - }, - props(props) { - if (props.props) { - return new Error( - 'Passing "props" prop to is deprecated. ' + - 'Replace ' + - 'with .' - ); - } - - return undefined; - }, children(props) { if (React.Children.count(props.children) !== 1) { return new Error( From 2ce0b3c27520777ae78b6050efbf5891577fb4c8 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Thu, 27 Oct 2016 16:20:37 -0500 Subject: [PATCH 149/161] add Node >= 4 to package.json "engines" --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 6375aa867..22faf8413 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,8 @@ "react-dom": "^15.0.2", "recompose": "^0.17.0", "rimraf": "^2.5.2" + }, + "engines": { + "node": ">= 4" } } From 9da8e84bdef6ee8b67e409063ec0c9a5b013ebbc Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Sun, 30 Oct 2016 19:44:04 -0500 Subject: [PATCH 150/161] document migrating from create-react-app --- docs/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/README.md b/docs/README.md index a0df195e3..9cfb8c594 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,6 +64,40 @@ if (module.hot) { You can also check out [this commit for the migration of a TodoMVC app from 1.0 to 3.0.](https://github.com/gaearon/redux-devtools/commit/64f58b7010a1b2a71ad16716eb37ac1031f93915) +## Migrating from [create-react-app](https://github.com/facebookincubator/create-react-app) + +* Run `npm run eject` +* Install React Hot Loader (`npm install --save-dev react-hot-loader@3.0.0-beta.6`) +* In `config/webpack.config.dev.js`: + 1. Add `'react-hot-loader/patch'` to entry array (anywhere before `paths.appIndexJs`). It should now look like (excluding comments): + ```js + entry: [ + 'react-hot-loader/patch', + require.resolve('react-dev-utils/webpackHotDevClient'), + require.resolve('./polyfills'), + paths.appIndexJs + ] + ``` + + 2. Add `'react-hot-loader/babel'` to Babel loader configuration. The loader should now look like: + ```js + { + test: /\.(js|jsx)$/, + include: paths.appSrc, + loader: 'babel', + query: { + cacheDirectory: findCacheDir({ + name: 'react-scripts' + }), + plugins: [ + 'react-hot-loader/babel' + ] + } + } + ``` + +* Add `AppContainer` to `src/index.js` (see `AppContainer` section in [Migration to 3.0 above](https://github.com/gaearon/react-hot-loader/blob/next-docs/docs/README.md#migration-to-30)) + ### Source Maps If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. From d2706c69c7e9baca69be497b3c2d33ca8c91dfbe Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Wed, 2 Nov 2016 11:25:32 -0500 Subject: [PATCH 151/161] Update README.md --- docs/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 9cfb8c594..d041fa5eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@ ### Starter Kit -* [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate) (Bare minimum) +* [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate/tree/next) (Bare minimum) ### Migration to 3.0 - If you're using Babel and ES6, remove the `react-hot` loader from any loaders in your Webpack config, and add `react-hot-loader/babel` to the `plugins` section of your `.babelrc`: @@ -30,11 +30,11 @@ - 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any code runs before `react-hot-loader/patch` has, so put it in the first position. -- `` - AppContainer is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. +- `` is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. - React Hot Loader 3 does not hide the hot module replacement API, so the following needs to be added below wherever you call `ReactDOM.render` in your app: -```js +```jsx import React from 'react' import ReactDOM from 'react-dom' import { AppContainer } from 'react-hot-loader' @@ -54,8 +54,7 @@ if (module.hot) { ReactDOM.render( - - , + , document.getElementById('root') ); }); From 61582ed4f771048c3eaafcc589b196773168a99e Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Sat, 5 Nov 2016 16:25:08 -0500 Subject: [PATCH 152/161] update Known Limitations doc add JSX syntax highlighting and add another workaround for comparing React element types. --- docs/Known Limitations.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/Known Limitations.md b/docs/Known Limitations.md index e2c5c943b..13ba477d8 100644 --- a/docs/Known Limitations.md +++ b/docs/Known Limitations.md @@ -21,23 +21,31 @@ Also, if you're using the Webpack 2 beta, you can use `System.import` without ex ### Checking Element `type`s Because React Hot Loader creates proxied versions of your components, comparing reference types of elements won't work: -```js +```jsx const element = ; console.log(element.type === Component); // false ``` One workaround is to create an element (that will have the `type` of the proxied component): -```js +```jsx const ComponentType = ().type; const element = ; console.log(element.type === ComponentType); // true ``` +You can also set a property on the component class: + +```jsx +const Widget = () =>
hi
; +Widget.isWidgetType = true; +console.log(.type.isWidgetType); // true +``` + ### Reassigning Components React Hot Loader will only try to reload the original component reference, so if you reassign it to another variable like this: -```js +```jsx let App = () => (
hello
); App = connect()(App); export default App; @@ -45,7 +53,7 @@ export default App; React Hot Loader won't reload it. Instead, you'll need to define it once: -```js +```jsx const App = () => (
hello
); export default connect()(App); ``` From 8365f09f7446c9607a51ae35ab781880ac19fc59 Mon Sep 17 00:00:00 2001 From: Cale Newman Date: Sat, 12 Nov 2016 15:45:08 -0600 Subject: [PATCH 153/161] Update README.md --- docs/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/README.md b/docs/README.md index d041fa5eb..7d6b953e9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -97,6 +97,25 @@ You can also check out [this commit for the migration of a TodoMVC app from 1.0 * Add `AppContainer` to `src/index.js` (see `AppContainer` section in [Migration to 3.0 above](https://github.com/gaearon/react-hot-loader/blob/next-docs/docs/README.md#migration-to-30)) +## Webpack 2 + +Because Webpack 2 has built-in support for ES2015 modules, you won't need to re-require your app root in `module.hot.accept`. The example above becomes: + +```jsx +if (module.hot) { + module.hot.accept('./containers/App', () => { + ReactDOM.render( + + + , + document.getElementById('root') + ); + }); +} +``` + +To make this work, you'll need to opt out of Babel transpiling ES2015 modules by changing the Babel ES2015 preset to be `["es2015", { "modules": false }]` + ### Source Maps If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. From 51dae3f4c86f21c143db838ff5d880433e4bc739 Mon Sep 17 00:00:00 2001 From: Cale-Newman Date: Sun, 27 Nov 2016 10:44:41 -0600 Subject: [PATCH 154/161] Add environment-specific importing for index.js This removes a few of the error messages that are only applicable in development, which saves a few bytes in production builds. --- src/index.dev.js | 39 +++++++++++++++++++++++++++++++++++++++ src/index.js | 44 +++++++------------------------------------- src/index.prod.js | 3 +++ 3 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 src/index.dev.js create mode 100644 src/index.prod.js diff --git a/src/index.dev.js b/src/index.dev.js new file mode 100644 index 000000000..d1c8b0e4d --- /dev/null +++ b/src/index.dev.js @@ -0,0 +1,39 @@ +'use strict'; + +const AppContainer = require('./AppContainer'); + +module.exports = function warnAboutIncorrectUsage(arg) { + if (this && this.callback) { + throw new Error( + 'React Hot Loader: The Webpack loader is now exported separately. ' + + 'If you use Babel, we recommend that you remove "react-hot-loader" ' + + 'from the "loaders" section of your Webpack configuration altogether, ' + + 'and instead add "react-hot-loader/babel" to the "plugins" section ' + + 'of your .babelrc file. ' + + 'If you prefer not to use Babel, replace "react-hot-loader" or ' + + '"react-hot" with "react-hot-loader/webpack" in the "loaders" section ' + + 'of your Webpack configuration.' + ); + } else if (arg && arg.types && arg.types.IfStatement) { + throw new Error( + 'React Hot Loader: The Babel plugin is exported separately. ' + + 'Replace "react-hot-loader" with "react-hot-loader/babel" ' + + 'in the "plugins" section of your .babelrc file. ' + + 'While we recommend the above, if you prefer not to use Babel, ' + + 'you may remove "react-hot-loader" from the "plugins" section of ' + + 'your .babelrc file altogether, and instead add ' + + '"react-hot-loader/webpack" to the "loaders" section of your Webpack ' + + 'configuration.' + ); + } else { + throw new Error( + 'React Hot Loader does not have a default export. ' + + 'If you use the import statement, make sure to include the ' + + 'curly braces: import { AppContainer } from "react-hot-loader". ' + + 'If you use CommonJS, make sure to read the named export: ' + + 'require("react-hot-loader").AppContainer.' + ); + } +}; + +module.exports.AppContainer = AppContainer; diff --git a/src/index.js b/src/index.js index d1c8b0e4d..f55258913 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,9 @@ -'use strict'; - -const AppContainer = require('./AppContainer'); +/* eslint-disable global-require */ -module.exports = function warnAboutIncorrectUsage(arg) { - if (this && this.callback) { - throw new Error( - 'React Hot Loader: The Webpack loader is now exported separately. ' + - 'If you use Babel, we recommend that you remove "react-hot-loader" ' + - 'from the "loaders" section of your Webpack configuration altogether, ' + - 'and instead add "react-hot-loader/babel" to the "plugins" section ' + - 'of your .babelrc file. ' + - 'If you prefer not to use Babel, replace "react-hot-loader" or ' + - '"react-hot" with "react-hot-loader/webpack" in the "loaders" section ' + - 'of your Webpack configuration.' - ); - } else if (arg && arg.types && arg.types.IfStatement) { - throw new Error( - 'React Hot Loader: The Babel plugin is exported separately. ' + - 'Replace "react-hot-loader" with "react-hot-loader/babel" ' + - 'in the "plugins" section of your .babelrc file. ' + - 'While we recommend the above, if you prefer not to use Babel, ' + - 'you may remove "react-hot-loader" from the "plugins" section of ' + - 'your .babelrc file altogether, and instead add ' + - '"react-hot-loader/webpack" to the "loaders" section of your Webpack ' + - 'configuration.' - ); - } else { - throw new Error( - 'React Hot Loader does not have a default export. ' + - 'If you use the import statement, make sure to include the ' + - 'curly braces: import { AppContainer } from "react-hot-loader". ' + - 'If you use CommonJS, make sure to read the named export: ' + - 'require("react-hot-loader").AppContainer.' - ); - } -}; +'use strict'; -module.exports.AppContainer = AppContainer; +if (!module.hot || process.env.NODE_ENV === 'production') { + module.exports = require('./index.prod'); +} else { + module.exports = require('./index.dev'); +} diff --git a/src/index.prod.js b/src/index.prod.js new file mode 100644 index 000000000..b9d446430 --- /dev/null +++ b/src/index.prod.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports.AppContainer = require('./AppContainer'); From 7eeceab29f45a7b42c215462bb03a71bde5c16c9 Mon Sep 17 00:00:00 2001 From: Dave Leaver Date: Sun, 12 Feb 2017 09:58:44 +1300 Subject: [PATCH 155/161] Add typescript note to README (#478) * Add typescript note to README * Move TypeScript to its own section --- docs/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/README.md b/docs/README.md index 7d6b953e9..d733c6c29 100644 --- a/docs/README.md +++ b/docs/README.md @@ -123,3 +123,16 @@ If you use `devtool: 'source-map'` (or its equivalent), source maps will be emit Source maps slow down your project. Use `devtool: 'eval'` for best build performance. Hot reloading code is just one line in the beginning and one line in the end of each module so you might not need source maps at all. + +## TypeScript + +When using TypeScript, Babel is not required, so your config should look like ([demo](https://github.com/Glavin001/react-hot-ts)): + +```js +{ + test: /\.tsx?$/, + loaders: ['react-hot-loader/webpack', 'ts-loader'], // (or awesome-typescript-loader) + include: path.join(__dirname, '..', '..', 'src') +} +``` + From 6606942fbc6d6ac560fff3f97922b11aba511cb2 Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Wed, 22 Feb 2017 09:44:23 +0100 Subject: [PATCH 156/161] Improve docs installation steps --- README.md | 36 +++++++++++-------------------- docs/README.md | 58 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index cbb430898..7e790eb33 100644 --- a/README.md +++ b/README.md @@ -16,28 +16,13 @@ Some nice things about it: Check out [the Migration to 3.0 guide](https://github.com/gaearon/react-hot-loader/tree/master/docs#migration-to-30) to learn how to migrate your app to 3.0. -### Learn - -This is a **stable for daily use in development** implementation of [React live code editing](https://www.youtube.com/watch?v=pw4fKkyPPg8). - -* Get inspired by a **[demo video](https://vimeo.com/100010922)** and **[try the live demo](http://gaearon.github.io/react-hot-loader/)**. - -* Read **[the Getting Started guide](http://gaearon.github.io/react-hot-loader/getstarted/).** - -* Use **[one of the starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)** for your next React project. - -## The Talk - -React Hot Loader was demoed together with **[Redux](https://github.com/gaearon/redux)** at React Europe. -Watch **[Dan Abramov's talk on Hot Reloading with Time Travel](https://www.youtube.com/watch?v=xsSnOQynTHs).** - ## Installation -`npm install --save-dev react-hot-loader` +`npm install --save-dev react-hot-loader@next` ## Usage -If you want to try hot reloading in a new project, try **[one of the starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)**, **[React Hot Boilerplate](https://github.com/gaearon/react-hot-boilerplate)** being the most minimal one. +If you want to try hot reloading in a new project, try **[one of the starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)**, **[React Hot Boilerplate](https://github.com/gaearon/react-hot-boilerplate/tree/next)** being the most minimal one. To use React Hot Loader in an existing project, you need to @@ -45,10 +30,19 @@ To use React Hot Loader in an existing project, you need to * enable Hot Module Replacement, which is a Webpack feature; * configure Webpack to use React Hot Loader for JS or JSX files. -These steps are covered by **[the Getting Started guide](http://gaearon.github.io/react-hot-loader/getstarted/)**. +These steps are covered by **[the Migration to 3.0 guide](https://github.com/gaearon/react-hot-loader/tree/master/docs#migration-to-30)**. If you'd rather stay with **Browserify**, check out **[LiveReactload](https://github.com/milankinen/livereactload)** by Matti Lankinen. +## Known limitations + +- React Router v3 is not fully supported. If you want to get most of React Hot Loader, consider switching to [React Router v4](https://reacttraining.com/react-router/). If you want to understand the reasoning, it's good to start in [React Router v4 FAQ](https://github.com/ReactTraining/react-router/blob/v4/README.md#v4-faq) + +## The Talk + +React Hot Loader was demoed together with **[Redux](https://github.com/gaearon/redux)** at React Europe. +Watch **[Dan Abramov's talk on Hot Reloading with Time Travel](https://www.youtube.com/watch?v=xsSnOQynTHs).** + ## React Native React Native **[supports hot reloading natively](https://facebook.github.io/react-native/blog/2016/03/24/introducing-hot-reloading.html)** as of version 0.22. @@ -59,11 +53,7 @@ If something doesn't work, in 99% cases it's a configuration issue. A missing op ## Documentation -Docs are in a bit of a flux right now because I'm in the process of updating everything to document the major 1.0 release. - -If you just learned about React Hot Loader and want to find out more, **[check out the walkthrough](http://gaearon.github.io/react-hot-loader/getstarted/)** and then try one of the **[starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)**. - -If you've been with us for a while, read **[1.0 release notes and migration guide](https://github.com/gaearon/react-hot-loader/blob/master/docs/README.md#migrating-to-10)**. +Check out the [docs directory](docs). ## Got Questions? diff --git a/docs/README.md b/docs/README.md index d733c6c29..5ac1e5151 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,7 +28,7 @@ } ``` -- 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any code runs before `react-hot-loader/patch` has, so put it in the first position. +- 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any app code runs before `react-hot-loader/patch` has, so put it in the first position (note: if you use any polyfills then it should go right after them). - `` is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. @@ -63,10 +63,37 @@ if (module.hot) { You can also check out [this commit for the migration of a TodoMVC app from 1.0 to 3.0.](https://github.com/gaearon/redux-devtools/commit/64f58b7010a1b2a71ad16716eb37ac1031f93915) +## Webpack 2 + +Because Webpack 2 has built-in support for ES2015 modules, you won't need to re-require your app root in `module.hot.accept`. The example above becomes: + +```jsx +if (module.hot) { + module.hot.accept('./containers/App', () => { + ReactDOM.render( + + + , + document.getElementById('root') + ); + }); +} +``` + +To make this work, you'll need to opt out of Babel transpiling ES2015 modules by changing the Babel ES2015 preset to be `["es2015", { "modules": false }]` + +### Source Maps + +If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. + +Source maps slow down your project. Use `devtool: 'eval'` for best build performance. + +Hot reloading code is just one line in the beginning and one line in the end of each module so you might not need source maps at all. + ## Migrating from [create-react-app](https://github.com/facebookincubator/create-react-app) * Run `npm run eject` -* Install React Hot Loader (`npm install --save-dev react-hot-loader@3.0.0-beta.6`) +* Install React Hot Loader (`npm install --save-dev react-hot-loader@next`) * In `config/webpack.config.dev.js`: 1. Add `'react-hot-loader/patch'` to entry array (anywhere before `paths.appIndexJs`). It should now look like (excluding comments): ```js @@ -97,33 +124,6 @@ You can also check out [this commit for the migration of a TodoMVC app from 1.0 * Add `AppContainer` to `src/index.js` (see `AppContainer` section in [Migration to 3.0 above](https://github.com/gaearon/react-hot-loader/blob/next-docs/docs/README.md#migration-to-30)) -## Webpack 2 - -Because Webpack 2 has built-in support for ES2015 modules, you won't need to re-require your app root in `module.hot.accept`. The example above becomes: - -```jsx -if (module.hot) { - module.hot.accept('./containers/App', () => { - ReactDOM.render( - - - , - document.getElementById('root') - ); - }); -} -``` - -To make this work, you'll need to opt out of Babel transpiling ES2015 modules by changing the Babel ES2015 preset to be `["es2015", { "modules": false }]` - -### Source Maps - -If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. - -Source maps slow down your project. Use `devtool: 'eval'` for best build performance. - -Hot reloading code is just one line in the beginning and one line in the end of each module so you might not need source maps at all. - ## TypeScript When using TypeScript, Babel is not required, so your config should look like ([demo](https://github.com/Glavin001/react-hot-ts)): From c327cd3edb06067f858bbd557cb8ed52dd786485 Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Wed, 22 Feb 2017 11:54:55 +0100 Subject: [PATCH 157/161] Workaround reference error when using async arrow functions in code (#486) --- src/babel/index.js | 10 ++++++++++ .../async-functions-expression-body/expected.js | 6 +----- .../class-properties/async-functions/expected.js | 8 +++----- .../class-properties/default-params/expected.js | 2 +- .../class-properties/destructured-params/expected.js | 2 +- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/babel/index.js b/src/babel/index.js index 0a5be5e12..72cfc45b0 100644 --- a/src/babel/index.js +++ b/src/babel/index.js @@ -191,6 +191,16 @@ module.exports = function plugin(args) { // class property node value is nullable if (node.value && node.value.type === 'ArrowFunctionExpression') { const isAsync = node.value.async; + + // TODO: + // Remove this check when babel issue is resolved: https://github.com/babel/babel/issues/5078 + // RHL Issue: https://github.com/gaearon/react-hot-loader/issues/391 + // This code makes async arrow functions not reloadable, + // but doesn't break code any more when using 'this' inside AAF + if (isAsync) { + return; + } + const params = node.value.params; const newIdentifier = t.identifier(`__${node.key.name}__REACT_HOT_LOADER__`); diff --git a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js index 5033e6987..cfc3e60cc 100644 --- a/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js +++ b/test/babel/fixtures/class-properties/async-functions-expression-body/expected.js @@ -1,10 +1,6 @@ class Foo { constructor() { - this.bar = async (...params) => await this.__bar__REACT_HOT_LOADER__(...params); - } - - async __bar__REACT_HOT_LOADER__(a, b) { - return await b(a); + this.bar = async (a, b) => await b(a); } } diff --git a/test/babel/fixtures/class-properties/async-functions/expected.js b/test/babel/fixtures/class-properties/async-functions/expected.js index 4bc37895a..34da2af6f 100644 --- a/test/babel/fixtures/class-properties/async-functions/expected.js +++ b/test/babel/fixtures/class-properties/async-functions/expected.js @@ -1,10 +1,8 @@ class Foo { constructor() { - this.bar = async (...params) => await this.__bar__REACT_HOT_LOADER__(...params); - } - - async __bar__REACT_HOT_LOADER__(a, b) { - return await a(b); + this.bar = async (a, b) => { + return await a(b); + }; } } diff --git a/test/babel/fixtures/class-properties/default-params/expected.js b/test/babel/fixtures/class-properties/default-params/expected.js index ddd573435..879a42eab 100644 --- a/test/babel/fixtures/class-properties/default-params/expected.js +++ b/test/babel/fixtures/class-properties/default-params/expected.js @@ -4,7 +4,7 @@ class Foo { } __bar__REACT_HOT_LOADER__(a = "foo") { - return `${ a }bar`; + return `${a}bar`; } } diff --git a/test/babel/fixtures/class-properties/destructured-params/expected.js b/test/babel/fixtures/class-properties/destructured-params/expected.js index 7c4dac820..1cf23da22 100644 --- a/test/babel/fixtures/class-properties/destructured-params/expected.js +++ b/test/babel/fixtures/class-properties/destructured-params/expected.js @@ -4,7 +4,7 @@ class Foo { } __bar__REACT_HOT_LOADER__({ a }, { b }) { - return `${ a }${ b }`; + return `${a}${b}`; } } From be4a7854c8f6adb5953ef4f5e849e2e1226c9281 Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Thu, 23 Feb 2017 09:45:14 +0100 Subject: [PATCH 158/161] Add example into docs for patch order. Mention webpack guide in docs. --- README.md | 4 +++- docs/README.md | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e790eb33..621bdc442 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ If you'd rather stay with **Browserify**, check out **[LiveReactload](https://gi ## Known limitations -- React Router v3 is not fully supported. If you want to get most of React Hot Loader, consider switching to [React Router v4](https://reacttraining.com/react-router/). If you want to understand the reasoning, it's good to start in [React Router v4 FAQ](https://github.com/ReactTraining/react-router/blob/v4/README.md#v4-faq) +- React Router v3 is not fully supported. If you want to get most of React Hot Loader, consider switching to [React Router v4](https://reacttraining.com/react-router/) (Note: it's currently in beta!). If you want to understand the reasoning, it's good to start in [React Router v4 FAQ](https://github.com/ReactTraining/react-router/blob/v4/README.md#v4-faq) ## The Talk @@ -55,6 +55,8 @@ If something doesn't work, in 99% cases it's a configuration issue. A missing op Check out the [docs directory](docs). +You can also check out a great [webpack guide to React hot module replacement](https://webpack.js.org/guides/hmr-react/). + ## Got Questions? [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gaearon/react-hot-loader?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/docs/README.md b/docs/README.md index 5ac1e5151..fec715350 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,7 +28,19 @@ } ``` -- 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any app code runs before `react-hot-loader/patch` has, so put it in the first position (note: if you use any polyfills then it should go right after them). +- 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any app code runs before `react-hot-loader/patch` has, so put it in the first position. However, if you're using polyfills put them before patch: + +```js +{ + entry: { + 'app': [ + 'babel-polyfill', + 'react-hot-loader/patch', + './src/index' + ] + } +} +``` - `` is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. From 9fa3350cc3a22ce9369471e5328a3b0917345198 Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Fri, 24 Feb 2017 09:57:06 +0100 Subject: [PATCH 159/161] Add starter kits --- README.md | 6 +++++- docs/README.md | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 621bdc442..2bdc50a2b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,11 @@ Check out [the Migration to 3.0 guide](https://github.com/gaearon/react-hot-load ## Usage -If you want to try hot reloading in a new project, try **[one of the starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)**, **[React Hot Boilerplate](https://github.com/gaearon/react-hot-boilerplate/tree/next)** being the most minimal one. +If you want to try hot reloading in a new project, try **[one of the starter kits](https://github.com/gaearon/react-hot-loader/tree/master/docs#starter-kits)**. + +Provided by owner and collaborators: +- **[React Hot Boilerplate](https://github.com/gaearon/react-hot-boilerplate/tree/next)** +- **[React Hot Loader Minimal Boilerplate](https://github.com/wkwiatek/react-hot-loader-minimal-boilerplate)** To use React Hot Loader in an existing project, you need to diff --git a/docs/README.md b/docs/README.md index fec715350..6816a19ec 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,31 @@ ### Starter Kit + +Provided by collaborators: * [react-hot-boilerplate](https://github.com/gaearon/react-hot-boilerplate/tree/next) (Bare minimum) +* [react-hot-loader-minimal-boilerplate](https://github.com/wkwiatek/react-hot-loader-minimal-boilerplate)* (Bare minimum) + +Provided by community: +* [webpack-react-redux](https://github.com/jpsierens/webpack-react-redux) (redux, react-router, hmr) +* [react-lego](https://github.com/peter-mouland/react-lego) (universal, react-router, other optional techs) +* [react-static-boilerplate](https://github.com/koistya/react-static-boilerplate) (static site generator; React, PostCSS, Webpack, BrowserSync) +* [react-cool-starter](https://github.com/wellyshen/react-cool-starter) (universal, redux, react-router, webpack 2, Babel, PostCSS, and more...) +* [react-redux-saga-boilerplate](https://github.com/gilbarbara/react-redux-saga-boilerplate) (react-router, redux, saga, webpack 2, jest w/ coverage, enzyme) +* [react-universal-boiler](https://github.com/strues/react-universal-boiler) (webpack 2, universal, react-router, redux, happypack) +* [apollo-fullstack-starter-kit](https://github.com/sysgears/apollo-fullstack-starter-kit) (universal, apollo, graphql, react, react-router, knex) +* [react-universally](https://github.com/ctrlplusb/react-universally) (universal, react, react router, express, seo, full stack webpack 2, babel) +* [meatier](https://github.com/mattkrick/meatier) (webpack 2 + hmr, universal, redux, graphql, react, react-router-redux, ssr) +* [react-hot-ts](https://github.com/Glavin001/react-hot-ts) (React, Webpack, TypeScript) +* [react-boilerplate-app](https://github.com/vebjorni/react-boilerplate-app) (react (duh), router, webpack with dev server, babel, hot reloading) +* [react-native-web](https://github.com/agrcrobles/react-native-web-webpack-starter) (react-native-web, webpack with dev server, hot reloading and flow soon...) +* [react-starter-kit](https://github.com/elios264/react-starter) (webpack 2 + htr + react + redux + router + babel + sass) +* [redux-react-starter](https://github.com/didierfranc/redux-react-starter) (webpack 2 + redux + react-redux 5 + react-router 4 + styled-component ...) +* [react-redux-universal-boilerplate](https://github.com/kiki-le-singe/react-redux-universal-boilerplate) (redux, react-router, universal, koa, webpack 2, babel, PostCSS, sass or cssnext, hot reloading, ...) +* [ARc](https://arc.js.org) (React, Jest, Storybook and other optional feature branches) +* [webpack-react-redux-starter](https://github.com/stsiarzhanau/webpack-react-redux-starter) (webpack 2, browsersync, babel, eslint, mocha, enzyme, jsdom, production config, detailed readme, and more...) +* [trowel](https://github.com/frux/trowel) (universal/ssr, redux, react-router 4, webpack 2, postcss) +* [react-navigation-web](https://github.com/agrcrobles/react-navigation-web) (react-navigation in web + redux, hot reloading!) +* [react-universal-hot-loader-starter-kit](https://github.com/earnubs/react-hot-loader-starter-kit) (universal express app with webpack 2, react-router 4, redux and react-hot-loader 3) +* [bare-minimum-react-hot-rr4-redux](https://github.com/nganbread/bare-minimum-react-hot-rr4-redux) (Bare minimum webpack 2, react-router 4, redux) ### Migration to 3.0 - If you're using Babel and ES6, remove the `react-hot` loader from any loaders in your Webpack config, and add `react-hot-loader/babel` to the `plugins` section of your `.babelrc`: From 29016a611b855140654f8b2d95c747c6df7847a9 Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Fri, 24 Feb 2017 10:05:55 +0100 Subject: [PATCH 160/161] Changes in docs tutorial --- docs/README.md | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6816a19ec..f2b0f1e95 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,6 +31,7 @@ Provided by community: - If you're using Babel and ES6, remove the `react-hot` loader from any loaders in your Webpack config, and add `react-hot-loader/babel` to the `plugins` section of your `.babelrc`: ```js +// .babelrc { "presets": ["es2015-loose", "stage-0", "react"], "plugins": ["react-hot-loader/babel"] @@ -40,23 +41,24 @@ Provided by community: - If you're *not* using Babel, or you're using Babel without ES6, replace the `react-hot` loader in your Webpack config with `react-hot-loader/webpack`: ```js +// webpack.config.js { test: /\.js$/, - loaders: ['react-hot', 'babel'], - include: path.join(__dirname, '..', '..', 'src') + loaders: ['react-hot', 'babel'] } // becomes +// webpack.config.js { test: /\.js$/, - loaders: ['react-hot-loader/webpack', 'babel'], - include: path.join(__dirname, '..', '..', 'src') + loaders: ['react-hot-loader/webpack', 'babel'] } ``` - 'react-hot-loader/patch' should be placed at the top of the `entry` section in your Webpack config. An error will occur if any app code runs before `react-hot-loader/patch` has, so put it in the first position. However, if you're using polyfills put them before patch: ```js +// webpack.config.js { entry: { 'app': [ @@ -68,7 +70,7 @@ Provided by community: } ``` -- `` is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. +- `` is a component that handles module reloading, as well as error handling. The root component of your app should be nested in AppContainer as a child. When in production, AppContainer is automatically disabled, and simply returns its children. - React Hot Loader 3 does not hide the hot module replacement API, so the following needs to be added below wherever you call `ReactDOM.render` in your app: @@ -105,21 +107,31 @@ You can also check out [this commit for the migration of a TodoMVC app from 1.0 Because Webpack 2 has built-in support for ES2015 modules, you won't need to re-require your app root in `module.hot.accept`. The example above becomes: +> Note: To make this work, you'll need to opt out of Babel transpiling ES2015 modules by changing the Babel ES2015 preset to be `["es2015", { "modules": false }]` + ```jsx +import React from 'react' +import ReactDom from 'react-dom' +import { AppContainer } from 'react-hot-loader' + +import App from './containers/App' + +const render = Component => { + ReactDOM.render( + + + , + document.getElementById('root') + ) +} + +render(App) + if (module.hot) { - module.hot.accept('./containers/App', () => { - ReactDOM.render( - - - , - document.getElementById('root') - ); - }); + module.hot.accept('./containers/App', () => { render(App) }) } ``` -To make this work, you'll need to opt out of Babel transpiling ES2015 modules by changing the Babel ES2015 preset to be `["es2015", { "modules": false }]` - ### Source Maps If you use `devtool: 'source-map'` (or its equivalent), source maps will be emitted to hide hot reloading code. @@ -169,8 +181,7 @@ When using TypeScript, Babel is not required, so your config should look like ([ ```js { test: /\.tsx?$/, - loaders: ['react-hot-loader/webpack', 'ts-loader'], // (or awesome-typescript-loader) - include: path.join(__dirname, '..', '..', 'src') + loaders: ['react-hot-loader/webpack', 'ts-loader'] // (or awesome-typescript-loader) } ``` From 460b39a155c58919706546f62ea5b918a2bdb02f Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Mon, 27 Feb 2017 19:49:01 +0100 Subject: [PATCH 161/161] Add beta to name in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2bdc50a2b..8dc312a6a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # React Hot Loader 3 [![npm package](https://img.shields.io/npm/v/react-hot-loader.svg?style=flat-square)](https://www.npmjs.org/package/react-hot-loader) -### React Hot Loader 3 has arrived! +### React Hot Loader 3 beta has arrived! It fixes some long-standing issues with both React Hot Loader and React Transform. @@ -40,7 +40,7 @@ If you'd rather stay with **Browserify**, check out **[LiveReactload](https://gi ## Known limitations -- React Router v3 is not fully supported. If you want to get most of React Hot Loader, consider switching to [React Router v4](https://reacttraining.com/react-router/) (Note: it's currently in beta!). If you want to understand the reasoning, it's good to start in [React Router v4 FAQ](https://github.com/ReactTraining/react-router/blob/v4/README.md#v4-faq) +- React Router v3 is not fully supported (e.g. async routes). If you want to get most of React Hot Loader, consider switching to [React Router v4](https://reacttraining.com/react-router/) (Note: it's currently in beta!). If you want to understand the reasoning, it's good to start in [React Router v4 FAQ](https://github.com/ReactTraining/react-router/blob/v4/README.md#v4-faq) ## The Talk