From f7ae4030e2519d989f6cacc5f7e56b88266285b8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 21 May 2017 02:13:40 +0100 Subject: [PATCH 01/17] Proof of concept: hot reloading --- packages/react-scripts/config/polyfills.js | 60 +++++++++++++++++++++ packages/react-scripts/template/src/App.js | 2 +- packages/react-scripts/template/src/Logo.js | 40 ++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/react-scripts/template/src/Logo.js diff --git a/packages/react-scripts/config/polyfills.js b/packages/react-scripts/config/polyfills.js index 8d97fb4ac39..a99311fc95c 100644 --- a/packages/react-scripts/config/polyfills.js +++ b/packages/react-scripts/config/polyfills.js @@ -28,3 +28,63 @@ Object.assign = require('object-assign'); if (process.env.NODE_ENV === 'test') { require('raf').polyfill(global); } + +// TODO: make this dev-only +// and move to a better place: + +let forceUpdateCallbacks = []; +let forceUpdateTimeout = null; +window.__enqueueForceUpdate = function(onSuccess) { + forceUpdateCallbacks.push(onSuccess); + if (forceUpdateTimeout) { + return; + } + forceUpdateTimeout = setTimeout(() => { + forceUpdateTimeout = null; + let callbacks = forceUpdateCallbacks; + forceUpdateCallbacks = []; + forceUpdateAll(); + callbacks.forEach(cb => cb()); + }); +}; +function forceUpdateAll() { + const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; + if (!hook) { + return; + } + const renderersById = hook._renderers; + const ids = Object.keys(renderersById); + const renderers = ids.map(id => renderersById[id]); + // TODO: support Fiber + renderers.forEach(renderer => { + const roots = renderer.Mount && renderer.Mount._instancesByReactRootID; + if (!roots) { + return; + } + Object.keys(roots).forEach(key => { + function traverseDeep(ins, cb) { + cb(ins); + if (ins._renderedComponent) { + traverseDeep(ins._renderedComponent, cb); + } else { + for (let key in ins._renderedChildren) { + if (ins._renderedChildren.hasOwnProperty(key)) { + traverseDeep(ins._renderedChildren[key], cb); + } + } + } + } + const root = roots[key]; + traverseDeep(root, inst => { + if (!inst._instance) { + return; + } + const updater = inst._instance.updater; + if (!updater || typeof updater.enqueueForceUpdate !== 'function') { + return; + } + updater.enqueueForceUpdate(inst._instance); + }); + }); + }); +} diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 7e261ca47e6..21bd6eeddf0 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import logo from './logo.svg'; +import Logo from './Logo'; import './App.css'; class App extends Component { diff --git a/packages/react-scripts/template/src/Logo.js b/packages/react-scripts/template/src/Logo.js new file mode 100644 index 00000000000..1818fb7e180 --- /dev/null +++ b/packages/react-scripts/template/src/Logo.js @@ -0,0 +1,40 @@ +import React from 'react'; +import logo from './logo.svg'; + +// TODO: Babel transform that does this +// in files that only export what appears to be +// functional components. +window.__Logo__ = function __Logo__(props) { + return ( +
+ logo +

Welcome to React?

+ {props.children} +
+ ); +}; + +// TODO: generate this +try { + Object.defineProperty(window.__Logo__, 'name', { value: 'Logo' }); +} catch (err) { + // ignore +} +export default function Logo() { + return window.__Logo__.apply(this, arguments); +} +// The accept dance. +// TODO: generate it for each module that looks like a functional component. +if (!module.hot.data) { + // Always accept first update + module.hot.accept(); +} else { + // Defer updating next updates until we know if they threw + module.hot.data.acceptNext = () => module.hot.accept(); +} +module.hot.dispose(data => { + window.__enqueueForceUpdate(() => { + // Only accept next if we haven't crashed + data.acceptNext(); + }); +}); From 3ae3ce60858e3d7ab8d60ca3bc6a22abe8056565 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 01:36:05 -0400 Subject: [PATCH 02/17] babel plugin :tada: --- packages/babel-plugin-react-app/index.js | 179 +++++++++++++++++++ packages/babel-plugin-react-app/package.json | 10 ++ packages/babel-preset-react-app/index.js | 17 +- packages/babel-preset-react-app/package.json | 3 +- packages/react-scripts/template/src/Logo.js | 30 +--- 5 files changed, 197 insertions(+), 42 deletions(-) create mode 100644 packages/babel-plugin-react-app/index.js create mode 100644 packages/babel-plugin-react-app/package.json diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js new file mode 100644 index 00000000000..de0d62e5b7e --- /dev/null +++ b/packages/babel-plugin-react-app/index.js @@ -0,0 +1,179 @@ +'use strict'; + +function functionReturnsElement(path) { + const { body } = path.body; + const last = body[body.length - 1]; + if (typeof last !== 'object' || last.type !== 'ReturnStatement') { + return false; + } + const { type: returnType } = last.argument; + if (returnType !== 'JSXElement') { + return false; + } + return true; +} + +function createMemberExpression(t, path) { + if (path.length > 2) { + return t.MemberExpression( + createMemberExpression(t, path.slice(0, -1)), + t.Identifier(path[path.length - 1]) + ); + } else if (path.length === 2) { + return t.MemberExpression(t.Identifier(path[0]), t.Identifier(path[1])); + } else { + return t.Identifier(path[0]); + } +} + +function createFunctionCall(t, path, args = []) { + return t.CallExpression(createMemberExpression(t, path), args); +} + +function hoistFunctionalComponentToWindow(t, name, params, body) { + return t.ExpressionStatement( + t.AssignmentExpression( + '=', + createMemberExpression(t, ['window', `__${name}__`]), + t.FunctionExpression(t.Identifier(`__${name}__`), params, body) + ) + ); +} + +function decorateFunctionName(t, name) { + return t.TryStatement( + t.BlockStatement([ + t.ExpressionStatement( + createFunctionCall( + t, + ['Object', 'defineProperty'], + [ + createMemberExpression(t, ['window', `__${name}__`]), + t.StringLiteral('name'), + t.ObjectExpression([ + t.ObjectProperty(t.Identifier('value'), t.StringLiteral(name)), + ]), + ] + ) + ), + ]), + t.CatchClause(t.Identifier('_ignored'), t.BlockStatement([])) + ); +} + +function exportHoistedFunctionCallProxy(t, name) { + return t.ExportDefaultDeclaration( + t.FunctionDeclaration( + t.Identifier(name), + [], + t.BlockStatement([ + t.ReturnStatement( + createFunctionCall( + t, + ['window', `__${name}__`, 'apply'], + [t.ThisExpression(), t.Identifier('arguments')] + ) + ), + ]) + ) + ); +} + +module.exports = function({ types: t }) { + return { + visitor: { + ExportDefaultDeclaration(path) { + const { type } = path.node.declaration; + if ( + type !== 'FunctionDeclaration' || + !functionReturnsElement(path.node.declaration) + ) { + return; + } + const { id: { name }, params, body } = path.node.declaration; + + path.replaceWithMultiple([ + hoistFunctionalComponentToWindow(t, name, params, body), + decorateFunctionName(t, name), + exportHoistedFunctionCallProxy(t, name), + t.IfStatement( + t.UnaryExpression( + '!', + createMemberExpression(t, ['module', 'hot', 'data']) + ), + t.BlockStatement([ + t.ExpressionStatement( + createFunctionCall(t, ['module', 'hot', 'accept']) + ), + ]), + t.BlockStatement([ + t.ExpressionStatement( + t.AssignmentExpression( + '=', + createMemberExpression(t, [ + 'module', + 'hot', + 'data', + 'acceptNext', + ]), + t.ArrowFunctionExpression( + [], + createFunctionCall(t, ['module', 'hot', 'accept']) + ) + ) + ), + ]) + ), + t.ExpressionStatement( + createFunctionCall( + t, + ['module', 'hot', 'dispose'], + [ + t.ArrowFunctionExpression( + [t.Identifier('data')], + t.BlockStatement([ + t.ExpressionStatement( + createFunctionCall( + t, + ['window', '__enqueueForceUpdate'], + [ + t.ArrowFunctionExpression( + [], + t.BlockStatement([ + t.IfStatement( + t.BinaryExpression( + '===', + t.UnaryExpression( + 'typeof', + createMemberExpression(t, [ + 'data', + 'acceptNext', + ]) + ), + t.StringLiteral('function') + ), + t.BlockStatement([ + t.ExpressionStatement( + createFunctionCall(t, [ + 'data', + 'acceptNext', + ]) + ), + ]), + null + ), + ]) + ), + ] + ) + ), + ]) + ), + ] + ) + ), + ]); + }, + }, + }; +}; diff --git a/packages/babel-plugin-react-app/package.json b/packages/babel-plugin-react-app/package.json new file mode 100644 index 00000000000..efd5d3eefa2 --- /dev/null +++ b/packages/babel-plugin-react-app/package.json @@ -0,0 +1,10 @@ +{ + "name": "babel-plugin-react-app", + "version": "0.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "BSD-3-Clause" +} diff --git a/packages/babel-preset-react-app/index.js b/packages/babel-preset-react-app/index.js index 6ec10b5a0cd..da80accb4e2 100644 --- a/packages/babel-preset-react-app/index.js +++ b/packages/babel-preset-react-app/index.js @@ -23,18 +23,6 @@ module.exports = function(api, opts) { opts = {}; } - // This is similar to how `env` works in Babel: - // https://babeljs.io/docs/usage/babelrc/#env-option - // We are not using `env` because it’s ignored in versions > babel-core@6.10.4: - // https://github.com/babel/babel/issues/4539 - // https://github.com/facebook/create-react-app/issues/720 - // It’s also nice that we can enforce `NODE_ENV` being specified. - var env = process.env.BABEL_ENV || process.env.NODE_ENV; - var isEnvDevelopment = env === 'development'; - var isEnvProduction = env === 'production'; - var isEnvTest = env === 'test'; - var isFlowEnabled = validateBoolOption('flow', opts.flow, true); - if (!isEnvDevelopment && !isEnvProduction && !isEnvTest) { throw new Error( 'Using `babel-preset-react-app` requires that you specify `NODE_ENV` or ' + @@ -135,6 +123,11 @@ module.exports = function(api, opts) { isEnvTest && // Transform dynamic import to require require('babel-plugin-transform-dynamic-import').default, + + // TODO + isEnvDevelopment && + // Transform for functional hot reloading + require.resolve('babel-plugin-react-app'), ].filter(Boolean), }; }; diff --git a/packages/babel-preset-react-app/package.json b/packages/babel-preset-react-app/package.json index 5e3f3b0a5ff..c8acd2a243a 100644 --- a/packages/babel-preset-react-app/package.json +++ b/packages/babel-preset-react-app/package.json @@ -27,6 +27,7 @@ "@babel/preset-react": "7.0.0-beta.38", "babel-plugin-macros": "2.0.0", "babel-plugin-transform-dynamic-import": "2.0.0", - "babel-plugin-transform-react-remove-prop-types": "0.4.12" + "babel-plugin-transform-react-remove-prop-types": "0.4.12", + "babel-plugin-react-app": "^0.0.0" } } diff --git a/packages/react-scripts/template/src/Logo.js b/packages/react-scripts/template/src/Logo.js index 1818fb7e180..bf83a0e6963 100644 --- a/packages/react-scripts/template/src/Logo.js +++ b/packages/react-scripts/template/src/Logo.js @@ -1,10 +1,7 @@ import React from 'react'; import logo from './logo.svg'; -// TODO: Babel transform that does this -// in files that only export what appears to be -// functional components. -window.__Logo__ = function __Logo__(props) { +export default function Logo(props) { return (
logo @@ -12,29 +9,4 @@ window.__Logo__ = function __Logo__(props) { {props.children}
); -}; - -// TODO: generate this -try { - Object.defineProperty(window.__Logo__, 'name', { value: 'Logo' }); -} catch (err) { - // ignore -} -export default function Logo() { - return window.__Logo__.apply(this, arguments); -} -// The accept dance. -// TODO: generate it for each module that looks like a functional component. -if (!module.hot.data) { - // Always accept first update - module.hot.accept(); -} else { - // Defer updating next updates until we know if they threw - module.hot.data.acceptNext = () => module.hot.accept(); } -module.hot.dispose(data => { - window.__enqueueForceUpdate(() => { - // Only accept next if we haven't crashed - data.acceptNext(); - }); -}); From 22b28f583896c33d1ed5d314c97cbbd6c97973fc Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 09:46:31 -0400 Subject: [PATCH 03/17] Add file hash --- packages/babel-plugin-react-app/index.js | 31 +++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js index de0d62e5b7e..acb13723d9c 100644 --- a/packages/babel-plugin-react-app/index.js +++ b/packages/babel-plugin-react-app/index.js @@ -1,5 +1,7 @@ 'use strict'; +const crypto = require('crypto'); + function functionReturnsElement(path) { const { body } = path.body; const last = body[body.length - 1]; @@ -30,17 +32,17 @@ function createFunctionCall(t, path, args = []) { return t.CallExpression(createMemberExpression(t, path), args); } -function hoistFunctionalComponentToWindow(t, name, params, body) { +function hoistFunctionalComponentToWindow(t, generatedName, params, body) { return t.ExpressionStatement( t.AssignmentExpression( '=', - createMemberExpression(t, ['window', `__${name}__`]), - t.FunctionExpression(t.Identifier(`__${name}__`), params, body) + createMemberExpression(t, ['window', generatedName]), + t.FunctionExpression(t.Identifier(generatedName), params, body) ) ); } -function decorateFunctionName(t, name) { +function decorateFunctionName(t, name, generatedName) { return t.TryStatement( t.BlockStatement([ t.ExpressionStatement( @@ -48,7 +50,7 @@ function decorateFunctionName(t, name) { t, ['Object', 'defineProperty'], [ - createMemberExpression(t, ['window', `__${name}__`]), + createMemberExpression(t, ['window', generatedName]), t.StringLiteral('name'), t.ObjectExpression([ t.ObjectProperty(t.Identifier('value'), t.StringLiteral(name)), @@ -61,7 +63,7 @@ function decorateFunctionName(t, name) { ); } -function exportHoistedFunctionCallProxy(t, name) { +function exportHoistedFunctionCallProxy(t, name, generatedName) { return t.ExportDefaultDeclaration( t.FunctionDeclaration( t.Identifier(name), @@ -70,7 +72,7 @@ function exportHoistedFunctionCallProxy(t, name) { t.ReturnStatement( createFunctionCall( t, - ['window', `__${name}__`, 'apply'], + ['window', generatedName, 'apply'], [t.ThisExpression(), t.Identifier('arguments')] ) ), @@ -82,7 +84,7 @@ function exportHoistedFunctionCallProxy(t, name) { module.exports = function({ types: t }) { return { visitor: { - ExportDefaultDeclaration(path) { + ExportDefaultDeclaration(path, state) { const { type } = path.node.declaration; if ( type !== 'FunctionDeclaration' || @@ -92,10 +94,17 @@ module.exports = function({ types: t }) { } const { id: { name }, params, body } = path.node.declaration; + const fileHash = crypto + .createHash('md5') + .update(state.file.opts.filename) + .digest('hex'); + + const generatedName = `${name}$$${fileHash}`; + path.replaceWithMultiple([ - hoistFunctionalComponentToWindow(t, name, params, body), - decorateFunctionName(t, name), - exportHoistedFunctionCallProxy(t, name), + hoistFunctionalComponentToWindow(t, generatedName, params, body), + decorateFunctionName(t, name, generatedName), + exportHoistedFunctionCallProxy(t, name, generatedName), t.IfStatement( t.UnaryExpression( '!', From 26b9f4ee959367956ec0e3a29d3c6421cf8699cd Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 10:28:03 -0400 Subject: [PATCH 04/17] Use string literals --- packages/babel-plugin-react-app/index.js | 47 ++++++++++++++++-------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js index acb13723d9c..af08aaecf8f 100644 --- a/packages/babel-plugin-react-app/index.js +++ b/packages/babel-plugin-react-app/index.js @@ -1,7 +1,5 @@ 'use strict'; -const crypto = require('crypto'); - function functionReturnsElement(path) { const { body } = path.body; const last = body[body.length - 1]; @@ -16,13 +14,20 @@ function functionReturnsElement(path) { } function createMemberExpression(t, path) { + const last = path[path.length - 1]; + const computed = last.type === 'StringLiteral'; if (path.length > 2) { return t.MemberExpression( createMemberExpression(t, path.slice(0, -1)), - t.Identifier(path[path.length - 1]) + computed ? last : t.Identifier(last), + computed ); } else if (path.length === 2) { - return t.MemberExpression(t.Identifier(path[0]), t.Identifier(path[1])); + return t.MemberExpression( + t.Identifier(path[0]), + computed ? last : t.Identifier(last), + computed + ); } else { return t.Identifier(path[0]); } @@ -32,12 +37,18 @@ function createFunctionCall(t, path, args = []) { return t.CallExpression(createMemberExpression(t, path), args); } -function hoistFunctionalComponentToWindow(t, generatedName, params, body) { +function hoistFunctionalComponentToWindow( + t, + name, + generatedName, + params, + body +) { return t.ExpressionStatement( t.AssignmentExpression( '=', - createMemberExpression(t, ['window', generatedName]), - t.FunctionExpression(t.Identifier(generatedName), params, body) + createMemberExpression(t, ['window', t.StringLiteral(generatedName)]), + t.FunctionExpression(t.Identifier(`__hot__${name}__`), params, body) ) ); } @@ -50,7 +61,10 @@ function decorateFunctionName(t, name, generatedName) { t, ['Object', 'defineProperty'], [ - createMemberExpression(t, ['window', generatedName]), + createMemberExpression(t, [ + 'window', + t.StringLiteral(generatedName), + ]), t.StringLiteral('name'), t.ObjectExpression([ t.ObjectProperty(t.Identifier('value'), t.StringLiteral(name)), @@ -72,7 +86,7 @@ function exportHoistedFunctionCallProxy(t, name, generatedName) { t.ReturnStatement( createFunctionCall( t, - ['window', generatedName, 'apply'], + ['window', t.StringLiteral(generatedName), 'apply'], [t.ThisExpression(), t.Identifier('arguments')] ) ), @@ -94,15 +108,16 @@ module.exports = function({ types: t }) { } const { id: { name }, params, body } = path.node.declaration; - const fileHash = crypto - .createHash('md5') - .update(state.file.opts.filename) - .digest('hex'); - - const generatedName = `${name}$$${fileHash}`; + const generatedName = `__hot__${state.file.opts.filename}$$${name}`; path.replaceWithMultiple([ - hoistFunctionalComponentToWindow(t, generatedName, params, body), + hoistFunctionalComponentToWindow( + t, + name, + generatedName, + params, + body + ), decorateFunctionName(t, name, generatedName), exportHoistedFunctionCallProxy(t, name, generatedName), t.IfStatement( From 70c0e795a2476d9fbd6ef4cbc6a00545d019c2d3 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 11:31:01 -0400 Subject: [PATCH 05/17] Use templates --- packages/babel-plugin-react-app/index.js | 200 ++++++------------- packages/babel-plugin-react-app/package.json | 5 +- 2 files changed, 61 insertions(+), 144 deletions(-) diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js index af08aaecf8f..c44f7bf2302 100644 --- a/packages/babel-plugin-react-app/index.js +++ b/packages/babel-plugin-react-app/index.js @@ -1,5 +1,7 @@ 'use strict'; +const template = require('babel-template'); + function functionReturnsElement(path) { const { body } = path.body; const last = body[body.length - 1]; @@ -13,30 +15,6 @@ function functionReturnsElement(path) { return true; } -function createMemberExpression(t, path) { - const last = path[path.length - 1]; - const computed = last.type === 'StringLiteral'; - if (path.length > 2) { - return t.MemberExpression( - createMemberExpression(t, path.slice(0, -1)), - computed ? last : t.Identifier(last), - computed - ); - } else if (path.length === 2) { - return t.MemberExpression( - t.Identifier(path[0]), - computed ? last : t.Identifier(last), - computed - ); - } else { - return t.Identifier(path[0]); - } -} - -function createFunctionCall(t, path, args = []) { - return t.CallExpression(createMemberExpression(t, path), args); -} - function hoistFunctionalComponentToWindow( t, name, @@ -44,55 +22,47 @@ function hoistFunctionalComponentToWindow( params, body ) { - return t.ExpressionStatement( - t.AssignmentExpression( - '=', - createMemberExpression(t, ['window', t.StringLiteral(generatedName)]), - t.FunctionExpression(t.Identifier(`__hot__${name}__`), params, body) - ) - ); + return template( + ` + window[GEN_NAME] = function NAME(PARAMS) { + BODY + } + ` + )({ + GEN_NAME: t.StringLiteral(generatedName), + NAME: t.Identifier(`__hot__${name}__`), + PARAMS: params, + BODY: body, + }); } function decorateFunctionName(t, name, generatedName) { - return t.TryStatement( - t.BlockStatement([ - t.ExpressionStatement( - createFunctionCall( - t, - ['Object', 'defineProperty'], - [ - createMemberExpression(t, [ - 'window', - t.StringLiteral(generatedName), - ]), - t.StringLiteral('name'), - t.ObjectExpression([ - t.ObjectProperty(t.Identifier('value'), t.StringLiteral(name)), - ]), - ] - ) - ), - ]), - t.CatchClause(t.Identifier('_ignored'), t.BlockStatement([])) - ); + return template( + ` + try { + Object.defineProperty(window[GEN_NAME], 'name', { + value: NAME + }); + } catch (_ignored) {} + ` + )({ + GEN_NAME: t.StringLiteral(generatedName), + NAME: t.StringLiteral(name), + }); } function exportHoistedFunctionCallProxy(t, name, generatedName) { - return t.ExportDefaultDeclaration( - t.FunctionDeclaration( - t.Identifier(name), - [], - t.BlockStatement([ - t.ReturnStatement( - createFunctionCall( - t, - ['window', t.StringLiteral(generatedName), 'apply'], - [t.ThisExpression(), t.Identifier('arguments')] - ) - ), - ]) - ) - ); + return template( + ` + export default function NAME() { + return window[GEN_NAME].apply(this, arguments); + } + `, + { sourceType: 'module' } + )({ + GEN_NAME: t.StringLiteral(generatedName), + NAME: t.Identifier(name), + }); } module.exports = function({ types: t }) { @@ -120,82 +90,26 @@ module.exports = function({ types: t }) { ), decorateFunctionName(t, name, generatedName), exportHoistedFunctionCallProxy(t, name, generatedName), - t.IfStatement( - t.UnaryExpression( - '!', - createMemberExpression(t, ['module', 'hot', 'data']) - ), - t.BlockStatement([ - t.ExpressionStatement( - createFunctionCall(t, ['module', 'hot', 'accept']) - ), - ]), - t.BlockStatement([ - t.ExpressionStatement( - t.AssignmentExpression( - '=', - createMemberExpression(t, [ - 'module', - 'hot', - 'data', - 'acceptNext', - ]), - t.ArrowFunctionExpression( - [], - createFunctionCall(t, ['module', 'hot', 'accept']) - ) - ) - ), - ]) - ), - t.ExpressionStatement( - createFunctionCall( - t, - ['module', 'hot', 'dispose'], - [ - t.ArrowFunctionExpression( - [t.Identifier('data')], - t.BlockStatement([ - t.ExpressionStatement( - createFunctionCall( - t, - ['window', '__enqueueForceUpdate'], - [ - t.ArrowFunctionExpression( - [], - t.BlockStatement([ - t.IfStatement( - t.BinaryExpression( - '===', - t.UnaryExpression( - 'typeof', - createMemberExpression(t, [ - 'data', - 'acceptNext', - ]) - ), - t.StringLiteral('function') - ), - t.BlockStatement([ - t.ExpressionStatement( - createFunctionCall(t, [ - 'data', - 'acceptNext', - ]) - ), - ]), - null - ), - ]) - ), - ] - ) - ), - ]) - ), - ] - ) - ), + template( + ` + if (!module.hot.data) { + module.hot.accept(); + } else { + module.hot.data.acceptNext = () => module.hot.accept(); + } + ` + )(), + template( + ` + module.hot.dispose(data => { + window.__enqueueForceUpdate(() => { + if (typeof data.acceptNext === 'function') { + data.acceptNext(); + } + }); + }); + ` + )({}), ]); }, }, diff --git a/packages/babel-plugin-react-app/package.json b/packages/babel-plugin-react-app/package.json index efd5d3eefa2..372ca87e368 100644 --- a/packages/babel-plugin-react-app/package.json +++ b/packages/babel-plugin-react-app/package.json @@ -6,5 +6,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "dependencies": { + "babel-template": "6.24.1" + } } From ee56c264db004babea376fe19893cff65c37e5cc Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 11:45:12 -0400 Subject: [PATCH 06/17] Add noop flashNodes function --- packages/babel-plugin-react-app/index.js | 4 ++-- packages/react-scripts/config/polyfills.js | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js index c44f7bf2302..ea9a0c45818 100644 --- a/packages/babel-plugin-react-app/index.js +++ b/packages/babel-plugin-react-app/index.js @@ -106,10 +106,10 @@ module.exports = function({ types: t }) { if (typeof data.acceptNext === 'function') { data.acceptNext(); } - }); + }, NAME); }); ` - )({}), + )({ NAME: t.Identifier(name) }), ]); }, }, diff --git a/packages/react-scripts/config/polyfills.js b/packages/react-scripts/config/polyfills.js index a99311fc95c..b2e3f9daa75 100644 --- a/packages/react-scripts/config/polyfills.js +++ b/packages/react-scripts/config/polyfills.js @@ -32,10 +32,18 @@ if (process.env.NODE_ENV === 'test') { // TODO: make this dev-only // and move to a better place: +let nodes = []; +function flashNodes() { + //TODO: flash nodes + nodes = []; +} + let forceUpdateCallbacks = []; +let forceUpdateTypes = []; let forceUpdateTimeout = null; -window.__enqueueForceUpdate = function(onSuccess) { +window.__enqueueForceUpdate = function(onSuccess, type) { forceUpdateCallbacks.push(onSuccess); + forceUpdateTypes.push(type); if (forceUpdateTimeout) { return; } @@ -43,11 +51,14 @@ window.__enqueueForceUpdate = function(onSuccess) { forceUpdateTimeout = null; let callbacks = forceUpdateCallbacks; forceUpdateCallbacks = []; - forceUpdateAll(); + let types = forceUpdateTypes; + forceUpdateTypes = []; + forceUpdateAll(types); callbacks.forEach(cb => cb()); + flashNodes(); }); }; -function forceUpdateAll() { +function forceUpdateAll(types) { const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (!hook) { return; @@ -58,6 +69,7 @@ function forceUpdateAll() { // TODO: support Fiber renderers.forEach(renderer => { const roots = renderer.Mount && renderer.Mount._instancesByReactRootID; + const { getNodeFromInstance } = renderer.ComponentTree; if (!roots) { return; } @@ -79,6 +91,9 @@ function forceUpdateAll() { if (!inst._instance) { return; } + if (types.indexOf(inst._currentElement.type) !== -1) { + nodes.push(getNodeFromInstance(inst)); + } const updater = inst._instance.updater; if (!updater || typeof updater.enqueueForceUpdate !== 'function') { return; From 627ed980286238a1321730f71b3a6382737049da Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 12:39:34 -0400 Subject: [PATCH 07/17] Steal some react-dev-utils code --- packages/react-scripts/config/polyfills.js | 96 +++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/react-scripts/config/polyfills.js b/packages/react-scripts/config/polyfills.js index b2e3f9daa75..c2ba4ca4337 100644 --- a/packages/react-scripts/config/polyfills.js +++ b/packages/react-scripts/config/polyfills.js @@ -32,9 +32,100 @@ if (process.env.NODE_ENV === 'test') { // TODO: make this dev-only // and move to a better place: +const CANVAS_NODE_ID = 'ReactHotReloadUpdateTrace'; + +function createCanvas() { + let canvas = window.document.getElementById(CANVAS_NODE_ID) || + window.document.createElement('canvas'); + canvas.id = CANVAS_NODE_ID; + canvas.width = window.screen.availWidth; + canvas.height = window.screen.availHeight; + canvas.style.cssText = ` + xx-background-color: red; + xx-opacity: 0.5; + bottom: 0; + left: 0; + pointer-events: none; + position: fixed; + right: 0; + top: 0; + z-index: 1000000000; + `; + if (!canvas.parentNode) { + const root = window.document.documentElement; + root.insertBefore(canvas, root.firstChild); + } + return canvas; +} + +function removeCanvas(canvas) { + canvas.parentNode.removeChild(canvas); +} + +const OUTLINE_COLOR = '#f0f0f0'; + +const COLORS = [ + // coolest + '#55cef6', + '#55f67b', + '#a5f655', + '#f4f655', + '#f6a555', + '#f66855', + // hottest + '#ff0000', +]; + +function drawBorder(ctx, measurement, borderWidth, borderColor) { + // outline + ctx.lineWidth = 1; + ctx.strokeStyle = OUTLINE_COLOR; + + ctx.strokeRect( + measurement.left - 1, + measurement.top - 1, + measurement.width + 2, + measurement.height + 2 + ); + + // inset + ctx.lineWidth = 1; + ctx.strokeStyle = OUTLINE_COLOR; + ctx.strokeRect( + measurement.left + borderWidth, + measurement.top + borderWidth, + measurement.width - borderWidth, + measurement.height - borderWidth + ); + ctx.strokeStyle = borderColor; + + ctx.setLineDash([0]); + + // border + ctx.lineWidth = '' + borderWidth; + ctx.strokeRect( + measurement.left + Math.floor(borderWidth / 2), + measurement.top + Math.floor(borderWidth / 2), + measurement.width - borderWidth, + measurement.height - borderWidth + ); + + ctx.setLineDash([0]); +} + let nodes = []; function flashNodes() { - //TODO: flash nodes + const canvas = createCanvas(); + let count = 0; + for (const node of nodes) { + drawBorder( + canvas.getContext('2d'), + node.getBoundingClientRect(), + 3, + COLORS[count++ % COLORS.length] + ); + } + setTimeout(() => removeCanvas(canvas), 250); nodes = []; } @@ -91,7 +182,8 @@ function forceUpdateAll(types) { if (!inst._instance) { return; } - if (types.indexOf(inst._currentElement.type) !== -1) { + const { type, type: { name } } = inst._currentElement; + if (types.find(t => t === type || t.name === name)) { nodes.push(getNodeFromInstance(inst)); } const updater = inst._instance.updater; From def1afb8a34b9861d46adcf993ce0a2692308759 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 12:56:07 -0400 Subject: [PATCH 08/17] Extract code into files --- packages/react-dev-utils/forceUpdateHook.js | 81 ++++++++++ packages/react-dev-utils/nodeHighlighter.js | 103 ++++++++++++ packages/react-dev-utils/package.json | 2 + packages/react-scripts/config/polyfills.js | 165 +------------------- 4 files changed, 187 insertions(+), 164 deletions(-) create mode 100644 packages/react-dev-utils/forceUpdateHook.js create mode 100644 packages/react-dev-utils/nodeHighlighter.js diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js new file mode 100644 index 00000000000..11c45064681 --- /dev/null +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +const highlight = require('./nodeHighlighter'); + +let forceUpdateCallbacks = []; +let forceUpdateTypes = []; +let forceUpdateTimeout = null; +let nodes = []; +window.__enqueueForceUpdate = function(onSuccess, type) { + forceUpdateCallbacks.push(onSuccess); + forceUpdateTypes.push(type); + if (forceUpdateTimeout) { + return; + } + forceUpdateTimeout = setTimeout(() => { + forceUpdateTimeout = null; + let callbacks = forceUpdateCallbacks; + forceUpdateCallbacks = []; + let types = forceUpdateTypes; + forceUpdateTypes = []; + forceUpdateAll(types); + callbacks.forEach(cb => cb()); + highlight(nodes); + nodes = []; + }); +}; +function forceUpdateAll(types) { + const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; + if (!hook) { + return; + } + const renderersById = hook._renderers; + const ids = Object.keys(renderersById); + const renderers = ids.map(id => renderersById[id]); + // TODO: support Fiber + renderers.forEach(renderer => { + const roots = renderer.Mount && renderer.Mount._instancesByReactRootID; + const { getNodeFromInstance } = renderer.ComponentTree; + if (!roots) { + return; + } + Object.keys(roots).forEach(key => { + function traverseDeep(ins, cb) { + cb(ins); + if (ins._renderedComponent) { + traverseDeep(ins._renderedComponent, cb); + } else { + for (let key in ins._renderedChildren) { + if (ins._renderedChildren.hasOwnProperty(key)) { + traverseDeep(ins._renderedChildren[key], cb); + } + } + } + } + const root = roots[key]; + traverseDeep(root, inst => { + if (!inst._instance) { + return; + } + const { type, type: { name } } = inst._currentElement; + if (types.find(t => t === type || t.name === name)) { + nodes.push(getNodeFromInstance(inst)); + } + const updater = inst._instance.updater; + if (!updater || typeof updater.enqueueForceUpdate !== 'function') { + return; + } + updater.enqueueForceUpdate(inst._instance); + }); + }); + }); +} diff --git a/packages/react-dev-utils/nodeHighlighter.js b/packages/react-dev-utils/nodeHighlighter.js new file mode 100644 index 00000000000..2f82cae3d8a --- /dev/null +++ b/packages/react-dev-utils/nodeHighlighter.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +const OUTLINE_COLOR = '#f0f0f0'; + +const COLORS = [ + // coolest + '#55cef6', + '#55f67b', + '#a5f655', + '#f4f655', + '#f6a555', + '#f66855', + // hottest + '#ff0000', +]; + +function mount() { + let canvas = window.document.createElement('canvas'); + canvas.width = window.screen.availWidth; + canvas.height = window.screen.availHeight; + canvas.style.cssText = ` + bottom: 0; + left: 0; + pointer-events: none; + position: fixed; + right: 0; + top: 0; + z-index: 1000000000; + `; + + const root = window.document.documentElement; + root.insertBefore(canvas, root.firstChild); + return canvas; +} + +function unmount(canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.parentNode.removeChild(canvas); +} + +function drawBorder(ctx, measurement, borderWidth, borderColor) { + // outline + ctx.lineWidth = 1; + ctx.strokeStyle = OUTLINE_COLOR; + + ctx.strokeRect( + measurement.left - 1, + measurement.top - 1, + measurement.width + 2, + measurement.height + 2 + ); + + // inset + ctx.lineWidth = 1; + ctx.strokeStyle = OUTLINE_COLOR; + ctx.strokeRect( + measurement.left + borderWidth, + measurement.top + borderWidth, + measurement.width - borderWidth, + measurement.height - borderWidth + ); + ctx.strokeStyle = borderColor; + + ctx.setLineDash([0]); + + // border + ctx.lineWidth = '' + borderWidth; + ctx.strokeRect( + measurement.left + Math.floor(borderWidth / 2), + measurement.top + Math.floor(borderWidth / 2), + measurement.width - borderWidth, + measurement.height - borderWidth + ); + + ctx.setLineDash([0]); +} + +function highlight(nodes, { borderWidth = 3, duration = 250 } = {}) { + const canvas = mount(); + const ctx = canvas.getContext('2d'); + let count = 0; + for (const node of nodes) { + drawBorder( + ctx, + node.getBoundingClientRect(), + borderWidth, + COLORS[count++ % COLORS.length] + ); + } + setTimeout(() => unmount(canvas), duration); +} + +module.exports = highlight; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 05bddedae05..fb0d4e0d4cc 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -19,6 +19,7 @@ "eslintFormatter.js", "errorOverlayMiddleware.js", "FileSizeReporter.js", + "forceUpdateHook.js", "printBuildError.js", "formatWebpackMessages.js", "getProcessForPort.js", @@ -28,6 +29,7 @@ "launchEditor.js", "launchEditorEndpoint.js", "ModuleScopePlugin.js", + "nodeHighlighter.js", "noopServiceWorkerMiddleware.js", "openBrowser.js", "openChrome.applescript", diff --git a/packages/react-scripts/config/polyfills.js b/packages/react-scripts/config/polyfills.js index c2ba4ca4337..b9315f8842b 100644 --- a/packages/react-scripts/config/polyfills.js +++ b/packages/react-scripts/config/polyfills.js @@ -31,167 +31,4 @@ if (process.env.NODE_ENV === 'test') { // TODO: make this dev-only // and move to a better place: - -const CANVAS_NODE_ID = 'ReactHotReloadUpdateTrace'; - -function createCanvas() { - let canvas = window.document.getElementById(CANVAS_NODE_ID) || - window.document.createElement('canvas'); - canvas.id = CANVAS_NODE_ID; - canvas.width = window.screen.availWidth; - canvas.height = window.screen.availHeight; - canvas.style.cssText = ` - xx-background-color: red; - xx-opacity: 0.5; - bottom: 0; - left: 0; - pointer-events: none; - position: fixed; - right: 0; - top: 0; - z-index: 1000000000; - `; - if (!canvas.parentNode) { - const root = window.document.documentElement; - root.insertBefore(canvas, root.firstChild); - } - return canvas; -} - -function removeCanvas(canvas) { - canvas.parentNode.removeChild(canvas); -} - -const OUTLINE_COLOR = '#f0f0f0'; - -const COLORS = [ - // coolest - '#55cef6', - '#55f67b', - '#a5f655', - '#f4f655', - '#f6a555', - '#f66855', - // hottest - '#ff0000', -]; - -function drawBorder(ctx, measurement, borderWidth, borderColor) { - // outline - ctx.lineWidth = 1; - ctx.strokeStyle = OUTLINE_COLOR; - - ctx.strokeRect( - measurement.left - 1, - measurement.top - 1, - measurement.width + 2, - measurement.height + 2 - ); - - // inset - ctx.lineWidth = 1; - ctx.strokeStyle = OUTLINE_COLOR; - ctx.strokeRect( - measurement.left + borderWidth, - measurement.top + borderWidth, - measurement.width - borderWidth, - measurement.height - borderWidth - ); - ctx.strokeStyle = borderColor; - - ctx.setLineDash([0]); - - // border - ctx.lineWidth = '' + borderWidth; - ctx.strokeRect( - measurement.left + Math.floor(borderWidth / 2), - measurement.top + Math.floor(borderWidth / 2), - measurement.width - borderWidth, - measurement.height - borderWidth - ); - - ctx.setLineDash([0]); -} - -let nodes = []; -function flashNodes() { - const canvas = createCanvas(); - let count = 0; - for (const node of nodes) { - drawBorder( - canvas.getContext('2d'), - node.getBoundingClientRect(), - 3, - COLORS[count++ % COLORS.length] - ); - } - setTimeout(() => removeCanvas(canvas), 250); - nodes = []; -} - -let forceUpdateCallbacks = []; -let forceUpdateTypes = []; -let forceUpdateTimeout = null; -window.__enqueueForceUpdate = function(onSuccess, type) { - forceUpdateCallbacks.push(onSuccess); - forceUpdateTypes.push(type); - if (forceUpdateTimeout) { - return; - } - forceUpdateTimeout = setTimeout(() => { - forceUpdateTimeout = null; - let callbacks = forceUpdateCallbacks; - forceUpdateCallbacks = []; - let types = forceUpdateTypes; - forceUpdateTypes = []; - forceUpdateAll(types); - callbacks.forEach(cb => cb()); - flashNodes(); - }); -}; -function forceUpdateAll(types) { - const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; - if (!hook) { - return; - } - const renderersById = hook._renderers; - const ids = Object.keys(renderersById); - const renderers = ids.map(id => renderersById[id]); - // TODO: support Fiber - renderers.forEach(renderer => { - const roots = renderer.Mount && renderer.Mount._instancesByReactRootID; - const { getNodeFromInstance } = renderer.ComponentTree; - if (!roots) { - return; - } - Object.keys(roots).forEach(key => { - function traverseDeep(ins, cb) { - cb(ins); - if (ins._renderedComponent) { - traverseDeep(ins._renderedComponent, cb); - } else { - for (let key in ins._renderedChildren) { - if (ins._renderedChildren.hasOwnProperty(key)) { - traverseDeep(ins._renderedChildren[key], cb); - } - } - } - } - const root = roots[key]; - traverseDeep(root, inst => { - if (!inst._instance) { - return; - } - const { type, type: { name } } = inst._currentElement; - if (types.find(t => t === type || t.name === name)) { - nodes.push(getNodeFromInstance(inst)); - } - const updater = inst._instance.updater; - if (!updater || typeof updater.enqueueForceUpdate !== 'function') { - return; - } - updater.enqueueForceUpdate(inst._instance); - }); - }); - }); -} +require('react-dev-utils/forceUpdateHook'); From de751b1c7d4d5d2f4fd65fc1f02d7b1a13c1e053 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 13:04:48 -0400 Subject: [PATCH 09/17] Add hot id --- packages/babel-plugin-react-app/index.js | 16 ++++++++++++++++ packages/react-dev-utils/forceUpdateHook.js | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js index ea9a0c45818..58818ddeb54 100644 --- a/packages/babel-plugin-react-app/index.js +++ b/packages/babel-plugin-react-app/index.js @@ -65,6 +65,21 @@ function exportHoistedFunctionCallProxy(t, name, generatedName) { }); } +function decorateFunctionId(t, name, generatedName) { + return template( + ` + try { + Object.defineProperty(NAME, '__hot__id', { + value: GEN_NAME + }); + } catch (_ignored) {} + ` + )({ + GEN_NAME: t.StringLiteral(generatedName), + NAME: t.Identifier(name), + }); +} + module.exports = function({ types: t }) { return { visitor: { @@ -90,6 +105,7 @@ module.exports = function({ types: t }) { ), decorateFunctionName(t, name, generatedName), exportHoistedFunctionCallProxy(t, name, generatedName), + decorateFunctionId(t, name, generatedName), template( ` if (!module.hot.data) { diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js index 11c45064681..e94d74ba195 100644 --- a/packages/react-dev-utils/forceUpdateHook.js +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -66,8 +66,12 @@ function forceUpdateAll(types) { if (!inst._instance) { return; } - const { type, type: { name } } = inst._currentElement; - if (types.find(t => t === type || t.name === name)) { + const { type, type: { __hot__id } } = inst._currentElement; + if ( + types.find( + t => t === type || (__hot__id && t.__hot__id === __hot__id) + ) + ) { nodes.push(getNodeFromInstance(inst)); } const updater = inst._instance.updater; From b5fc2f876d57aaafa1b37679c820ce08818aaeff Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 14:18:06 -0400 Subject: [PATCH 10/17] Ease canvas out --- packages/react-dev-utils/nodeHighlighter.js | 30 ++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/react-dev-utils/nodeHighlighter.js b/packages/react-dev-utils/nodeHighlighter.js index 2f82cae3d8a..a47970cfbce 100644 --- a/packages/react-dev-utils/nodeHighlighter.js +++ b/packages/react-dev-utils/nodeHighlighter.js @@ -9,6 +9,8 @@ 'use strict'; +const REFRESH_RATE = 1000 / 60; + const OUTLINE_COLOR = '#f0f0f0'; const COLORS = [ @@ -34,7 +36,7 @@ function mount() { position: fixed; right: 0; top: 0; - z-index: 1000000000; + z-index: ${2147483647 - 2}; `; const root = window.document.documentElement; @@ -43,8 +45,6 @@ function mount() { } function unmount(canvas) { - const ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.parentNode.removeChild(canvas); } @@ -85,9 +85,9 @@ function drawBorder(ctx, measurement, borderWidth, borderColor) { ctx.setLineDash([0]); } -function highlight(nodes, { borderWidth = 3, duration = 250 } = {}) { - const canvas = mount(); - const ctx = canvas.getContext('2d'); +function draw(canvas, ctx, nodes, borderWidth, duration, elapsed = 0) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.globalAlpha = Math.max(0, (duration - elapsed) / duration); let count = 0; for (const node of nodes) { drawBorder( @@ -97,7 +97,23 @@ function highlight(nodes, { borderWidth = 3, duration = 250 } = {}) { COLORS[count++ % COLORS.length] ); } - setTimeout(() => unmount(canvas), duration); + + if (elapsed >= duration) { + return; + } + + setTimeout( + () => { + draw(canvas, ctx, nodes, borderWidth, duration, elapsed + REFRESH_RATE); + }, + REFRESH_RATE + ); +} + +function highlight(nodes, { borderWidth = 4, duration = 1500 } = {}) { + const canvas = mount(); + const ctx = canvas.getContext('2d'); + draw(canvas, ctx, nodes, borderWidth, duration); } module.exports = highlight; From 90c291ecbdb8f1c0e935d495a94650b6b2f3719d Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 May 2017 14:19:12 -0400 Subject: [PATCH 11/17] Unmount canvas --- packages/react-dev-utils/nodeHighlighter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-dev-utils/nodeHighlighter.js b/packages/react-dev-utils/nodeHighlighter.js index a47970cfbce..320d968aadc 100644 --- a/packages/react-dev-utils/nodeHighlighter.js +++ b/packages/react-dev-utils/nodeHighlighter.js @@ -99,6 +99,7 @@ function draw(canvas, ctx, nodes, borderWidth, duration, elapsed = 0) { } if (elapsed >= duration) { + unmount(canvas); return; } From cc3b9c4a90def72d0780652209218ec9b45913c2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 25 May 2017 20:50:55 +0100 Subject: [PATCH 12/17] Make it work in all browsers --- packages/react-dev-utils/forceUpdateHook.js | 8 +++++--- packages/react-dev-utils/package.json | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js index e94d74ba195..1fb0c348e30 100644 --- a/packages/react-dev-utils/forceUpdateHook.js +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -9,6 +9,11 @@ 'use strict'; +// TODO: this is noisy when client is not running +// but it still gets the job done. Add a silent mode +// that won't attempt to connect maybe? +require('react-devtools-core').connectToDevTools(); + const highlight = require('./nodeHighlighter'); let forceUpdateCallbacks = []; @@ -35,9 +40,6 @@ window.__enqueueForceUpdate = function(onSuccess, type) { }; function forceUpdateAll(types) { const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; - if (!hook) { - return; - } const renderersById = hook._renderers; const ids = Object.keys(renderersById); const renderers = ids.map(id => renderersById[id]); diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index fb0d4e0d4cc..48f2557c8c5 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -49,6 +49,7 @@ "filesize": "3.5.11", "global-modules": "1.0.0", "gzip-size": "4.1.0", + "html-entities": "1.2.1", "inquirer": "5.0.0", "is-root": "1.0.0", "opn": "5.2.0", From bf7b87a8ad0fe0f4052bce6ff8f0c5cf8f75e4d3 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 29 May 2017 20:11:19 +0100 Subject: [PATCH 13/17] Remove noisy connection message and add logs --- packages/react-dev-utils/forceUpdateHook.js | 17 ++++++++++++++++- packages/react-scripts/template/src/Logo.js | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js index 1fb0c348e30..da331ff739b 100644 --- a/packages/react-dev-utils/forceUpdateHook.js +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -12,7 +12,7 @@ // TODO: this is noisy when client is not running // but it still gets the job done. Add a silent mode // that won't attempt to connect maybe? -require('react-devtools-core').connectToDevTools(); +require('react-devtools-core'); const highlight = require('./nodeHighlighter'); @@ -36,6 +36,21 @@ window.__enqueueForceUpdate = function(onSuccess, type) { callbacks.forEach(cb => cb()); highlight(nodes); nodes = []; + if (console != null) { + if (typeof console.clear === 'function') { + console.clear(); + } + if (typeof console.info === 'function') { + const names = types + .map(type => type.displayName || type.name) + .filter(Boolean); + if (names.length > 0) { + console.info( + 'Components have been reloaded: ' + names.join(',') + '.' + ); + } + } + } }); }; function forceUpdateAll(types) { diff --git a/packages/react-scripts/template/src/Logo.js b/packages/react-scripts/template/src/Logo.js index bf83a0e6963..24787ec3d97 100644 --- a/packages/react-scripts/template/src/Logo.js +++ b/packages/react-scripts/template/src/Logo.js @@ -5,7 +5,7 @@ export default function Logo(props) { return (
logo -

Welcome to React?

+

Welcome to React!

{props.children}
); From 42511c732d98a4707ecdb0933cd9d57d599613b1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 29 May 2017 20:17:20 +0100 Subject: [PATCH 14/17] Better message --- packages/react-dev-utils/forceUpdateHook.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js index da331ff739b..39bc9959c1d 100644 --- a/packages/react-dev-utils/forceUpdateHook.js +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -45,9 +45,7 @@ window.__enqueueForceUpdate = function(onSuccess, type) { .map(type => type.displayName || type.name) .filter(Boolean); if (names.length > 0) { - console.info( - 'Components have been reloaded: ' + names.join(',') + '.' - ); + console.info('Reloaded components: ' + names.join(',') + '.'); } } } From 31d5eca066ab1bb4c506d467ee36f5ba3071e2a4 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 11 May 2018 20:04:03 +0100 Subject: [PATCH 15/17] Fix bad merge --- packages/babel-preset-react-app/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/babel-preset-react-app/index.js b/packages/babel-preset-react-app/index.js index da80accb4e2..dc612d9e38a 100644 --- a/packages/babel-preset-react-app/index.js +++ b/packages/babel-preset-react-app/index.js @@ -23,6 +23,18 @@ module.exports = function(api, opts) { opts = {}; } + // This is similar to how `env` works in Babel: + // https://babeljs.io/docs/usage/babelrc/#env-option + // We are not using `env` because it’s ignored in versions > babel-core@6.10.4: + // https://github.com/babel/babel/issues/4539 + // https://github.com/facebook/create-react-app/issues/720 + // It’s also nice that we can enforce `NODE_ENV` being specified. + var env = process.env.BABEL_ENV || process.env.NODE_ENV; + var isEnvDevelopment = env === 'development'; + var isEnvProduction = env === 'production'; + var isEnvTest = env === 'test'; + var isFlowEnabled = validateBoolOption('flow', opts.flow, true); + if (!isEnvDevelopment && !isEnvProduction && !isEnvTest) { throw new Error( 'Using `babel-preset-react-app` requires that you specify `NODE_ENV` or ' + From 2d8eb246eda11267f0ac0f95d614d5fd62536a00 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 11 May 2018 21:07:51 +0100 Subject: [PATCH 16/17] Update --- packages/react-dev-utils/forceUpdateHook.js | 74 +++++++++++-------- packages/react-dev-utils/package.json | 1 + packages/react-scripts/template/src/App.js | 48 ++++++------ .../react-scripts/template/src/Counter.js | 14 ++++ packages/react-scripts/template/src/Logo.js | 12 --- 5 files changed, 81 insertions(+), 68 deletions(-) create mode 100644 packages/react-scripts/template/src/Counter.js delete mode 100644 packages/react-scripts/template/src/Logo.js diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js index 39bc9959c1d..4dbcf097d29 100644 --- a/packages/react-dev-utils/forceUpdateHook.js +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -51,50 +51,62 @@ window.__enqueueForceUpdate = function(onSuccess, type) { } }); }; + +function traverseDeep(root, onUpdate) { + let node = root; + while (true) { + node.expirationTime = 1; + if (node.alternate) { + node.alternate.expirationTime = 1; + } + if (node.tag === 1) { + onUpdate(node); + } + if (node.child) { + node.child.return = node; + node = node.child; + continue; + } + if (node === root) { + return; + } + while (!node.sibling) { + if (!node.return || node.return === root) { + return; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + } +} + function forceUpdateAll(types) { const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; const renderersById = hook._renderers; const ids = Object.keys(renderersById); - const renderers = ids.map(id => renderersById[id]); - // TODO: support Fiber - renderers.forEach(renderer => { - const roots = renderer.Mount && renderer.Mount._instancesByReactRootID; - const { getNodeFromInstance } = renderer.ComponentTree; - if (!roots) { + ids.forEach(id => { + const renderer = renderersById[id]; + const roots = hook.getFiberRoots(id); + if (!roots.size) { return; } - Object.keys(roots).forEach(key => { - function traverseDeep(ins, cb) { - cb(ins); - if (ins._renderedComponent) { - traverseDeep(ins._renderedComponent, cb); - } else { - for (let key in ins._renderedChildren) { - if (ins._renderedChildren.hasOwnProperty(key)) { - traverseDeep(ins._renderedChildren[key], cb); - } - } - } - } - const root = roots[key]; - traverseDeep(root, inst => { - if (!inst._instance) { - return; - } - const { type, type: { __hot__id } } = inst._currentElement; + // TODO: this is WAY too brittle. + roots.forEach(root => { + const reactRoot = root.containerInfo._reactRootContainer; + traverseDeep(root.current, node => { + const type = node.type; + const { __hot__id } = type; if ( types.find( t => t === type || (__hot__id && t.__hot__id === __hot__id) ) ) { - nodes.push(getNodeFromInstance(inst)); - } - const updater = inst._instance.updater; - if (!updater || typeof updater.enqueueForceUpdate !== 'function') { - return; + nodes.push(renderer.findHostInstanceByFiber(node)); } - updater.enqueueForceUpdate(inst._instance); + node.memoizedProps = Object.assign({}, node.memoizedProps); }); + reactRoot.render(root.current.memoizedState.element); }); }); } diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 48f2557c8c5..3d1e1eac87d 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -54,6 +54,7 @@ "is-root": "1.0.0", "opn": "5.2.0", "pkg-up": "2.0.0", + "react-devtools-core": "2.1.9", "react-error-overlay": "^4.0.0", "recursive-readdir": "2.2.1", "shell-quote": "1.6.1", diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 21bd6eeddf0..d57eb416758 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -1,28 +1,26 @@ -import React, { Component } from 'react'; -import Logo from './Logo'; +import React from 'react'; +import Counter from './Counter'; +import logo from './logo.svg'; import './App.css'; -class App extends Component { - render() { - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); - } +export default function App() { + return ( +
+
+ logo +

+ Edit src/App.js and save to reload. +

+ + + Learn React + +
+
+ ); } - -export default App; diff --git a/packages/react-scripts/template/src/Counter.js b/packages/react-scripts/template/src/Counter.js new file mode 100644 index 00000000000..8efb5e7036d --- /dev/null +++ b/packages/react-scripts/template/src/Counter.js @@ -0,0 +1,14 @@ +import React, { Component } from 'react'; + +export default class Counter extends Component { + state = { value: 0 }; + componentDidMount() { + this.interval = setInterval( + () => this.setState(s => ({ value: s.value + 1 })), + 1000 + ); + } + render() { + return

{this.state.value}

; + } +} diff --git a/packages/react-scripts/template/src/Logo.js b/packages/react-scripts/template/src/Logo.js deleted file mode 100644 index 24787ec3d97..00000000000 --- a/packages/react-scripts/template/src/Logo.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import logo from './logo.svg'; - -export default function Logo(props) { - return ( -
- logo -

Welcome to React!

- {props.children} -
- ); -} From abe07e9538f0d548eefc753949a80376c9b89f8c Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 21 Oct 2018 11:56:28 -0400 Subject: [PATCH 17/17] format --- packages/babel-plugin-react-app/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js index 58818ddeb54..e0ac87fcf8d 100644 --- a/packages/babel-plugin-react-app/index.js +++ b/packages/babel-plugin-react-app/index.js @@ -91,7 +91,11 @@ module.exports = function({ types: t }) { ) { return; } - const { id: { name }, params, body } = path.node.declaration; + const { + id: { name }, + params, + body, + } = path.node.declaration; const generatedName = `__hot__${state.file.opts.filename}$$${name}`;