diff --git a/packages/babel-plugin-react-app/index.js b/packages/babel-plugin-react-app/index.js new file mode 100644 index 00000000000..e0ac87fcf8d --- /dev/null +++ b/packages/babel-plugin-react-app/index.js @@ -0,0 +1,137 @@ +'use strict'; + +const template = require('babel-template'); + +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 hoistFunctionalComponentToWindow( + t, + name, + generatedName, + 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 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 template( + ` + export default function NAME() { + return window[GEN_NAME].apply(this, arguments); + } + `, + { sourceType: 'module' } + )({ + GEN_NAME: t.StringLiteral(generatedName), + NAME: t.Identifier(name), + }); +} + +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: { + ExportDefaultDeclaration(path, state) { + const { type } = path.node.declaration; + if ( + type !== 'FunctionDeclaration' || + !functionReturnsElement(path.node.declaration) + ) { + return; + } + const { + id: { name }, + params, + body, + } = path.node.declaration; + + const generatedName = `__hot__${state.file.opts.filename}$$${name}`; + + path.replaceWithMultiple([ + hoistFunctionalComponentToWindow( + t, + name, + generatedName, + params, + body + ), + decorateFunctionName(t, name, generatedName), + exportHoistedFunctionCallProxy(t, name, generatedName), + decorateFunctionId(t, name, generatedName), + 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(); + } + }, NAME); + }); + ` + )({ NAME: t.Identifier(name) }), + ]); + }, + }, + }; +}; diff --git a/packages/babel-plugin-react-app/package.json b/packages/babel-plugin-react-app/package.json new file mode 100644 index 00000000000..372ca87e368 --- /dev/null +++ b/packages/babel-plugin-react-app/package.json @@ -0,0 +1,13 @@ +{ + "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", + "dependencies": { + "babel-template": "6.24.1" + } +} diff --git a/packages/babel-preset-react-app/create.js b/packages/babel-preset-react-app/create.js index cdf94a63243..123641c4d68 100644 --- a/packages/babel-preset-react-app/create.js +++ b/packages/babel-preset-react-app/create.js @@ -159,6 +159,9 @@ module.exports = function(api, opts, env) { isEnvTest && // Transform dynamic import to require require('babel-plugin-dynamic-import-node'), + isEnvDevelopment && + // Transform for functional hot reloading + require('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 f6fa2ea4466..cb69607a5fd 100644 --- a/packages/babel-preset-react-app/package.json +++ b/packages/babel-preset-react-app/package.json @@ -33,6 +33,7 @@ "babel-loader": "8.0.4", "babel-plugin-dynamic-import-node": "2.2.0", "babel-plugin-macros": "2.4.2", - "babel-plugin-transform-react-remove-prop-types": "0.4.18" + "babel-plugin-transform-react-remove-prop-types": "0.4.18", + "babel-plugin-react-app": "^0.0.0" } } diff --git a/packages/react-dev-utils/forceUpdateHook.js b/packages/react-dev-utils/forceUpdateHook.js new file mode 100644 index 00000000000..4dbcf097d29 --- /dev/null +++ b/packages/react-dev-utils/forceUpdateHook.js @@ -0,0 +1,112 @@ +/** + * 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'; + +// 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'); + +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 = []; + 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('Reloaded components: ' + names.join(',') + '.'); + } + } + } + }); +}; + +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); + ids.forEach(id => { + const renderer = renderersById[id]; + const roots = hook.getFiberRoots(id); + if (!roots.size) { + return; + } + // 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(renderer.findHostInstanceByFiber(node)); + } + node.memoizedProps = Object.assign({}, node.memoizedProps); + }); + reactRoot.render(root.current.memoizedState.element); + }); + }); +} diff --git a/packages/react-dev-utils/nodeHighlighter.js b/packages/react-dev-utils/nodeHighlighter.js new file mode 100644 index 00000000000..320d968aadc --- /dev/null +++ b/packages/react-dev-utils/nodeHighlighter.js @@ -0,0 +1,120 @@ +/** + * 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 REFRESH_RATE = 1000 / 60; + +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: ${2147483647 - 2}; + `; + + const root = window.document.documentElement; + root.insertBefore(canvas, root.firstChild); + return canvas; +} + +function unmount(canvas) { + 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 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( + ctx, + node.getBoundingClientRect(), + borderWidth, + COLORS[count++ % COLORS.length] + ); + } + + if (elapsed >= duration) { + unmount(canvas); + 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; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index e70d4b57ac8..66bbae7c775 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -19,6 +19,7 @@ "eslintFormatter.js", "evalSourceMapMiddleware.js", "FileSizeReporter.js", + "forceUpdateHook.js", "formatWebpackMessages.js", "getCacheIdentifier.js", "getCSSModuleLocalIdent.js", @@ -31,6 +32,7 @@ "launchEditorEndpoint.js", "ModuleNotFoundPlugin.js", "ModuleScopePlugin.js", + "nodeHighlighter.js", "noopServiceWorkerMiddleware.js", "openBrowser.js", "openChrome.applescript", @@ -57,6 +59,7 @@ "loader-utils": "1.1.0", "opn": "5.4.0", "pkg-up": "2.0.0", + "react-devtools-core": "2.1.9", "react-error-overlay": "^5.0.5", "recursive-readdir": "2.2.2", "shell-quote": "1.6.1", diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 29c7a8621f2..b7ed6f10ee4 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -98,6 +98,8 @@ module.exports = { // require.resolve('webpack-dev-server/client') + '?/', // require.resolve('webpack/hot/dev-server'), require.resolve('react-dev-utils/webpackHotDevClient'), + // Include a hook which allows components to be forcibly updated + require.resolve('react-dev-utils/forceUpdateHook'), // Finally, this is your app's code: paths.appIndexJs, // We include the app code last so that if there is a runtime error during diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 7e261ca47e6..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 React from 'react'; +import Counter from './Counter'; import logo from './logo.svg'; import './App.css'; -class App extends Component { - render() { - return ( -
- Edit src/App.js
and save to reload.
-
+ Edit src/App.js
and save to reload.
+