From ae65f65fbc24ca0ecdf40a73d20e42f12b1dc004 Mon Sep 17 00:00:00 2001 From: Timer Date: Mon, 3 Dec 2018 20:20:57 +0000 Subject: [PATCH 01/47] Add initial Babel plugin impl --- packages/babel-plugin-hot-reload/index.js | 137 ++++++++++++++++++ packages/babel-plugin-hot-reload/package.json | 13 ++ packages/babel-preset-react-app/create.js | 3 + packages/babel-preset-react-app/package.json | 1 + 4 files changed, 154 insertions(+) create mode 100644 packages/babel-plugin-hot-reload/index.js create mode 100644 packages/babel-plugin-hot-reload/package.json diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js new file mode 100644 index 00000000000..e0ac87fcf8d --- /dev/null +++ b/packages/babel-plugin-hot-reload/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-hot-reload/package.json b/packages/babel-plugin-hot-reload/package.json new file mode 100644 index 00000000000..9a9ad66c653 --- /dev/null +++ b/packages/babel-plugin-hot-reload/package.json @@ -0,0 +1,13 @@ +{ + "name": "babel-plugin-hot-reload", + "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 a0423f2551c..0f928b5bb65 100644 --- a/packages/babel-preset-react-app/create.js +++ b/packages/babel-preset-react-app/create.js @@ -180,6 +180,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-hot-reload'), ].filter(Boolean), overrides: [ isFlowEnabled && { diff --git a/packages/babel-preset-react-app/package.json b/packages/babel-preset-react-app/package.json index 753ce324f87..57fb54da0ff 100644 --- a/packages/babel-preset-react-app/package.json +++ b/packages/babel-preset-react-app/package.json @@ -34,6 +34,7 @@ "@babel/runtime": "7.1.5", "babel-loader": "8.0.4", "babel-plugin-dynamic-import-node": "2.2.0", + "babel-plugin-hot-reload": "^0.0.0", "babel-plugin-macros": "2.4.2", "babel-plugin-transform-react-remove-prop-types": "0.4.20" } From baa9051b9e409a526058da0d2a48a8738eec08a7 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 20:38:25 +0000 Subject: [PATCH 02/47] Bump React to alpha --- packages/react-scripts/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 1f6b8cfafec..65cf676b12b 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -73,8 +73,8 @@ "workbox-webpack-plugin": "3.6.3" }, "devDependencies": { - "react": "^16.3.2", - "react-dom": "^16.3.2" + "react": "^16.7.0-alpha.2", + "react-dom": "^16.7.0-alpha.2" }, "optionalDependencies": { "fsevents": "1.2.4" From 47ca2c4dfe488a74369b1c164714dc03ce40496c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 21:41:37 +0000 Subject: [PATCH 03/47] MVP --- packages/babel-plugin-hot-reload/index.js | 14 +++----------- packages/react-scripts/template/src/App.js | 4 +++- .../react-scripts/template/src/CounterClass.js | 17 +++++++++++++++++ packages/react-scripts/template/src/Hello.js | 14 ++++++++++++++ packages/react-scripts/template/src/index.js | 4 ++++ 5 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 packages/react-scripts/template/src/CounterClass.js create mode 100644 packages/react-scripts/template/src/Hello.js diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index e0ac87fcf8d..4bc03645a0d 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -112,21 +112,13 @@ module.exports = function({ types: t }) { decorateFunctionId(t, name, generatedName), template( ` - if (!module.hot.data) { - module.hot.accept(); - } else { - module.hot.data.acceptNext = () => module.hot.accept(); - } + module.hot.accept(); ` )(), template( ` - module.hot.dispose(data => { - window.__enqueueForceUpdate(() => { - if (typeof data.acceptNext === 'function') { - data.acceptNext(); - } - }, NAME); + module.hot.dispose(() => { + window.__enqueueForceUpdate(NAME); }); ` )({ NAME: t.Identifier(name) }), diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 7e261ca47e6..353eee64604 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -1,13 +1,15 @@ import React, { Component } from 'react'; +import Hello from './Hello'; import logo from './logo.svg'; import './App.css'; class App extends Component { render() { return ( -
+
this.forceUpdate()}>
logo +

Edit src/App.js and save to reload.

diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js new file mode 100644 index 00000000000..6ba114f0e4e --- /dev/null +++ b/packages/react-scripts/template/src/CounterClass.js @@ -0,0 +1,17 @@ +import { Component } from 'react'; + +export default class Counter extends Component { + state = { value: 0 }; + componentDidMount() { + this.interval = setInterval( + () => this.setState(s => ({ value: s.value + 1 })), + 1000 + ); + } + componentWillUnmount() { + clearInterval(this.interval); + } + render() { + return this.state.value; + } +} diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js new file mode 100644 index 00000000000..06de8481bef --- /dev/null +++ b/packages/react-scripts/template/src/Hello.js @@ -0,0 +1,14 @@ +import React, { useState } from 'react'; +import CounterClass from './CounterClass'; + +export default function Hello() { + const [value] = useState(Math.random()); + return ( +

+ {value.toString().slice(0, 5)} +
+ b + +

+ ); +} diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index 0c5e75da1cd..ea211432f76 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -4,6 +4,10 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +window.__enqueueForceUpdate = function(type) { + console.log(type); +}; + ReactDOM.render(, document.getElementById('root')); // If you want your app to work offline and load faster, you can change From 1d272a9bccf84b25fb9401705c21d8ff708e23b1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 22:30:01 +0000 Subject: [PATCH 04/47] Test stubs --- .../template/src/CounterFunction.js | 10 ++++++++++ packages/react-scripts/template/src/HOCClass.js | 9 +++++++++ .../react-scripts/template/src/HOCFunction.js | 7 +++++++ packages/react-scripts/template/src/Hello.js | 17 +++++++++++++++-- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 packages/react-scripts/template/src/CounterFunction.js create mode 100644 packages/react-scripts/template/src/HOCClass.js create mode 100644 packages/react-scripts/template/src/HOCFunction.js diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js new file mode 100644 index 00000000000..eea9e578247 --- /dev/null +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -0,0 +1,10 @@ +import { useState, useRef, useLayoutEffect } from 'react'; + +export default function Counter() { + const [value, setValue] = useState(0); + useLayoutEffect(() => { + const id = setInterval(() => setValue(c => c + 1), 1000); + return () => clearInterval(id); + }, []); + return value; +} diff --git a/packages/react-scripts/template/src/HOCClass.js b/packages/react-scripts/template/src/HOCClass.js new file mode 100644 index 00000000000..6e6b77a1281 --- /dev/null +++ b/packages/react-scripts/template/src/HOCClass.js @@ -0,0 +1,9 @@ +import React, { Component } from 'react'; + +export default function withStuff(Wrapped) { + return class extends Component { + render() { + return ; + } + }; +} diff --git a/packages/react-scripts/template/src/HOCFunction.js b/packages/react-scripts/template/src/HOCFunction.js new file mode 100644 index 00000000000..ac6e64d68c9 --- /dev/null +++ b/packages/react-scripts/template/src/HOCFunction.js @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function withStuff(Wrapped) { + return function(props) { + return ; + }; +} diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 06de8481bef..90fffb5639f 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -1,5 +1,13 @@ import React, { useState } from 'react'; import CounterClass from './CounterClass'; +import CounterFunction from './CounterFunction'; +import HOCClass from './HOCClass'; +import HOCFunction from './HOCFunction'; + +const HCC = HOCClass(CounterClass); +const HCF = HOCClass(CounterFunction); +const HFC = HOCFunction(CounterClass); +const HFF = HOCFunction(CounterFunction); export default function Hello() { const [value] = useState(Math.random()); @@ -7,8 +15,13 @@ export default function Hello() {

{value.toString().slice(0, 5)}
- b - + hello +
+ class: +
+ function: +
+ hocs:

); } From 9c0e3f37088c0db4d026e62cd83c82f97573f46a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 22:46:08 +0000 Subject: [PATCH 05/47] More test stubs --- .../react-scripts/template/src/CounterClass.js | 17 +++++++++++++++-- .../template/src/CounterFunction.js | 18 +++++++++++++++--- packages/react-scripts/template/src/Hello.js | 8 ++++---- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 6ba114f0e4e..0e7dd381015 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -1,4 +1,8 @@ -import { Component } from 'react'; +import React, { Component } from 'react'; +import CounterFunction from './CounterFunction'; +import HOCFunction from './HOCFunction'; + +const HFF = HOCFunction(CounterFunction); export default class Counter extends Component { state = { value: 0 }; @@ -12,6 +16,15 @@ export default class Counter extends Component { clearInterval(this.interval); } render() { - return this.state.value; + return ( + <> + {this.state.value}{' '} + {this.props.hocChild && ( + <> + (inner HOC: ) + + )} + + ); } } diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index eea9e578247..6e6a58cb0f1 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,10 +1,22 @@ -import { useState, useRef, useLayoutEffect } from 'react'; +import React, { useState, useRef, useLayoutEffect } from 'react'; +import HOCFunction from './HOCFunction'; -export default function Counter() { +const HFF = HOCFunction(Counter); + +export default function Counter({ hocChild }) { const [value, setValue] = useState(0); useLayoutEffect(() => { const id = setInterval(() => setValue(c => c + 1), 1000); return () => clearInterval(id); }, []); - return value; + return ( + <> + {value} + {hocChild && ( + <> + (inner HOC: ) + + )} + + ); } diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 90fffb5639f..e25cac5a378 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -12,16 +12,16 @@ const HFF = HOCFunction(CounterFunction); export default function Hello() { const [value] = useState(Math.random()); return ( -

+

{value.toString().slice(0, 5)}
hello
- class: + class:
- function: + function:
hocs: -

+ ); } From 4489ebe575221fe8515ca95ebe9c4441d13275dd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 22:56:33 +0000 Subject: [PATCH 06/47] More test stubs --- packages/react-scripts/template/src/CounterClass.js | 4 ++-- packages/react-scripts/template/src/CounterFunction.js | 10 +++++----- packages/react-scripts/template/src/Hello.js | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 0e7dd381015..c074cf19430 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -17,14 +17,14 @@ export default class Counter extends Component { } render() { return ( - <> + {this.state.value}{' '} {this.props.hocChild && ( <> (inner HOC: ) )} - + ); } } diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 6e6a58cb0f1..14250b34482 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,22 +1,22 @@ -import React, { useState, useRef, useLayoutEffect } from 'react'; +import React, { useState, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; const HFF = HOCFunction(Counter); -export default function Counter({ hocChild }) { +export default function Counter(props) { const [value, setValue] = useState(0); useLayoutEffect(() => { const id = setInterval(() => setValue(c => c + 1), 1000); return () => clearInterval(id); }, []); return ( - <> + {value} - {hocChild && ( + {props.hocChild && ( <> (inner HOC: ) )} - + ); } diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index e25cac5a378..cf3101ed7b6 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -15,7 +15,7 @@ export default function Hello() {

{value.toString().slice(0, 5)}
- hello + hello!!!!
class:
From 1e798cfd20a57cfb040a08c82894bbd4faae61f9 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 22:56:39 +0000 Subject: [PATCH 07/47] todos --- packages/react-scripts/template/src/TODO.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/react-scripts/template/src/TODO.md diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md new file mode 100644 index 00000000000..0b881584ca7 --- /dev/null +++ b/packages/react-scripts/template/src/TODO.md @@ -0,0 +1,4 @@ +- frags +- error handling +- hocs? +- render props From 7316feefa650e0ade1db29374294009fb82f30a0 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 23:26:00 +0000 Subject: [PATCH 08/47] Add render hook --- packages/babel-plugin-hot-reload/index.js | 4 +++- packages/react-scripts/template/src/CounterClass.js | 1 - packages/react-scripts/template/src/CounterFunction.js | 1 - packages/react-scripts/template/src/Hello.js | 3 +-- packages/react-scripts/template/src/index.js | 6 +++++- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 4bc03645a0d..419153c7787 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -55,7 +55,9 @@ function exportHoistedFunctionCallProxy(t, name, generatedName) { return template( ` export default function NAME() { - return window[GEN_NAME].apply(this, arguments); + var result = window[GEN_NAME].apply(this, arguments); + window.__renderHook(NAME); + return result; } `, { sourceType: 'module' } diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index c074cf19430..6245c908dd4 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import CounterFunction from './CounterFunction'; import HOCFunction from './HOCFunction'; - const HFF = HOCFunction(CounterFunction); export default class Counter extends Component { diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 14250b34482..1243a5d454b 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,6 +1,5 @@ import React, { useState, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; - const HFF = HOCFunction(Counter); export default function Counter(props) { diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index cf3101ed7b6..3deb6c8c86a 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -3,7 +3,6 @@ import CounterClass from './CounterClass'; import CounterFunction from './CounterFunction'; import HOCClass from './HOCClass'; import HOCFunction from './HOCFunction'; - const HCC = HOCClass(CounterClass); const HCF = HOCClass(CounterFunction); const HFC = HOCFunction(CounterClass); @@ -15,7 +14,7 @@ export default function Hello() {

{value.toString().slice(0, 5)}
- hello!!!! + hello!!
class:
diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index ea211432f76..fbcfd5342ef 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -5,7 +5,11 @@ import App from './App'; import * as serviceWorker from './serviceWorker'; window.__enqueueForceUpdate = function(type) { - console.log(type); + console.log('force', type); +}; + +window.__renderHook = function(type) { + console.log('render', type); }; ReactDOM.render(, document.getElementById('root')); From a423c31e424993a6619b05a75a08e51e80a00904 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 3 Dec 2018 23:40:01 +0000 Subject: [PATCH 09/47] Add basic force update mechanism --- packages/babel-plugin-hot-reload/index.js | 2 +- packages/react-scripts/template/src/Hello.js | 3 ++- packages/react-scripts/template/src/index.js | 17 +++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 419153c7787..e91f38671da 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -120,7 +120,7 @@ module.exports = function({ types: t }) { template( ` module.hot.dispose(() => { - window.__enqueueForceUpdate(NAME); + setTimeout(() => window.__enqueueForceUpdate(NAME)); }); ` )({ NAME: t.Identifier(name) }), diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 3deb6c8c86a..c9e4ae2924d 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -10,11 +10,12 @@ const HFF = HOCFunction(CounterFunction); export default function Hello() { const [value] = useState(Math.random()); + return (

{value.toString().slice(0, 5)}
- hello!! + hello!
class:
diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index fbcfd5342ef..c6955c572d5 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -4,12 +4,25 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; +let setStates = new Map(); + window.__enqueueForceUpdate = function(type) { - console.log('force', type); + console.log('force', type.name); + const id = type.__hot__id; + (setStates.get(id) || []).forEach(setState => setState({})); }; window.__renderHook = function(type) { - console.log('render', type); + console.log('render', type.name); + const [, setState] = React.useState(); + React.useLayoutEffect(() => { + const id = type.__hot__id; + if (!setStates.has(id)) { + setStates.set(id, new Set()); + } + setStates.get(id).add(setState); + return () => setStates.get(id).delete(setState); + }, []); }; ReactDOM.render(, document.getElementById('root')); From 8d8f2060bfa968572cf052937a5e170c4d76b5fa Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 4 Dec 2018 00:32:28 +0000 Subject: [PATCH 10/47] Just use context --- packages/react-scripts/template/src/index.js | 28 +++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index c6955c572d5..203b08138df 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -4,28 +4,30 @@ import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; -let setStates = new Map(); +let HotContext = React.createContext(); +let _invalidate; +function HotContainer({ children }) { + const [inc, setInc] = React.useState(0); + _invalidate = () => setInc(c => c + 1); + return {children}; +} window.__enqueueForceUpdate = function(type) { console.log('force', type.name); - const id = type.__hot__id; - (setStates.get(id) || []).forEach(setState => setState({})); + _invalidate(); }; window.__renderHook = function(type) { console.log('render', type.name); - const [, setState] = React.useState(); - React.useLayoutEffect(() => { - const id = type.__hot__id; - if (!setStates.has(id)) { - setStates.set(id, new Set()); - } - setStates.get(id).add(setState); - return () => setStates.get(id).delete(setState); - }, []); + React.useContext(HotContext); }; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , + document.getElementById('root') +); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. From 4c1133f281df89dcf5f6abacf1f0b1e29a2965d6 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 4 Dec 2018 09:41:26 +0000 Subject: [PATCH 11/47] Revert to vanilla and fix HOCs --- packages/babel-plugin-hot-reload/index.js | 2 + .../template/src/CounterClass.js | 4 +- .../template/src/CounterFunction.js | 19 ++++++--- packages/react-scripts/template/src/Hello.js | 23 ++++++---- packages/react-scripts/template/src/TODO.md | 15 +++++++ packages/react-scripts/template/src/hot.js | 42 +++++++++++++++++++ packages/react-scripts/template/src/index.js | 19 +-------- 7 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 packages/react-scripts/template/src/hot.js diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index e91f38671da..60f4b00d6c0 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -83,6 +83,8 @@ function decorateFunctionId(t, name, generatedName) { } module.exports = function({ types: t }) { + return { visitor: {} }; + return { visitor: { ExportDefaultDeclaration(path, state) { diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 6245c908dd4..2e6081a4bd6 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -1,7 +1,9 @@ import React, { Component } from 'react'; import CounterFunction from './CounterFunction'; import HOCFunction from './HOCFunction'; -const HFF = HOCFunction(CounterFunction); + +const HFF = window.__createProxy(module, 'HFF'); +HFF.__setImpl(HOCFunction(CounterFunction)); export default class Counter extends Component { state = { value: 0 }; diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 1243a5d454b..26378a9f645 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,11 +1,16 @@ -import React, { useState, useLayoutEffect } from 'react'; +import React, { useReducer, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; -const HFF = HOCFunction(Counter); -export default function Counter(props) { - const [value, setValue] = useState(0); +let Counter = window.__createProxy(module, 'Counter'); +let HFF = window.__createProxy(module, 'HFF'); + +HFF.__setImpl(HOCFunction(Counter)); +Counter.__setImpl(function CounterImpl(props) { + const [value, dispatch] = useReducer((v, a) => { + return a === 'inc' ? v + 1 : v; + }, 0); useLayoutEffect(() => { - const id = setInterval(() => setValue(c => c + 1), 1000); + const id = setInterval(() => dispatch('inc'), 1000); return () => clearInterval(id); }, []); return ( @@ -18,4 +23,6 @@ export default function Counter(props) { )} ); -} +}); + +export default Counter; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index c9e4ae2924d..04be3b30ace 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -3,19 +3,26 @@ import CounterClass from './CounterClass'; import CounterFunction from './CounterFunction'; import HOCClass from './HOCClass'; import HOCFunction from './HOCFunction'; -const HCC = HOCClass(CounterClass); -const HCF = HOCClass(CounterFunction); -const HFC = HOCFunction(CounterClass); -const HFF = HOCFunction(CounterFunction); -export default function Hello() { +let Hello = window.__createProxy(module, 'Hello'); +let HCC = window.__createProxy(module, 'CounterClass'); +let HCF = window.__createProxy(module, 'HCF'); +let HFC = window.__createProxy(module, 'HFC'); +let HFF = window.__createProxy(module, 'HFF'); + +HCC.__setImpl(HOCClass(CounterClass)); +HCF.__setImpl(HOCClass(CounterFunction)); +HFC.__setImpl(HOCFunction(CounterClass)); +HFF.__setImpl(HOCFunction(CounterFunction)); + +Hello.__setImpl(function HelloImpl() { const [value] = useState(Math.random()); return (

{value.toString().slice(0, 5)}
- hello! + world
class:
@@ -24,4 +31,6 @@ export default function Hello() { hocs:

); -} +}); + +export default Hello; diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index 0b881584ca7..f11e5a8dcb6 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -1,4 +1,19 @@ - frags - error handling - hocs? + - with self as input + - returning a class + - HOC itself by mistake + - protect against nested? +- type equality + - memo and friends + - nested / applied twice + - mutually recursive + - re-exports + - same HOC applied early, two callsites +- classes + - how to detect and reload? - render props +- directives? +- when to accept? +- test integrations diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js new file mode 100644 index 00000000000..8810b34d4cf --- /dev/null +++ b/packages/react-scripts/template/src/hot.js @@ -0,0 +1,42 @@ +import React from 'react'; + +let HotContext = React.createContext(); +let _invalidate; +export function HotContainer({ children }) { + const [inc, setInc] = React.useState(0); + _invalidate = () => setInc(c => c + 1); + return {children}; +} + +window.__createProxy = function(m, localId) { + m.hot.accept(); + m.hot.dispose(() => { + setTimeout(() => window.__enqueueForceUpdate()); + }); + const id = m.i + '$' + localId; + + if (!window[`${id}_proxy`]) { + function P(...args) { + let impl = window[`${id}_impl`]; + if (impl.prototype && impl.prototype.isReactComponent) { + return new impl(...args); + } + let res = impl.apply(this, arguments); + window.__renderHook(); + return res; + } + window[`${id}_proxy`] = P; + P.__setImpl = impl => { + window[`${id}_impl`] = impl; + }; + } + return window[`${id}_proxy`]; +}; + +window.__enqueueForceUpdate = function(type) { + _invalidate(); +}; + +window.__renderHook = function(type) { + React.useContext(HotContext); +}; diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index 203b08138df..b86dd84a690 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -1,27 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { HotContainer } from './hot'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; -let HotContext = React.createContext(); -let _invalidate; -function HotContainer({ children }) { - const [inc, setInc] = React.useState(0); - _invalidate = () => setInc(c => c + 1); - return {children}; -} - -window.__enqueueForceUpdate = function(type) { - console.log('force', type.name); - _invalidate(); -}; - -window.__renderHook = function(type) { - console.log('render', type.name); - React.useContext(HotContext); -}; - ReactDOM.render( From 25c69dc385bcbbe4a675b387be68c7a922856998 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 4 Dec 2018 10:42:19 +0000 Subject: [PATCH 12/47] More todos --- packages/react-scripts/template/src/Hello.js | 3 +-- packages/react-scripts/template/src/TODO.md | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 04be3b30ace..5872efb089e 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -14,7 +14,6 @@ HCC.__setImpl(HOCClass(CounterClass)); HCF.__setImpl(HOCClass(CounterFunction)); HFC.__setImpl(HOCFunction(CounterClass)); HFF.__setImpl(HOCFunction(CounterFunction)); - Hello.__setImpl(function HelloImpl() { const [value] = useState(Math.random()); @@ -22,7 +21,7 @@ Hello.__setImpl(function HelloImpl() {

{value.toString().slice(0, 5)}
- world + hello world!!!
class:
diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index f11e5a8dcb6..c9f2b455d12 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -1,5 +1,9 @@ - frags - error handling + - offer a way to reset? +- hooks + - remount on error? + - compare introspection? - hocs? - with self as input - returning a class From f570e93d40b7a2d49a7aa765a1cf299c60a1478d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 4 Dec 2018 15:38:10 +0000 Subject: [PATCH 13/47] More stuff to think about --- .../template/src/CounterFunction.js | 2 +- .../react-scripts/template/src/HOCClass.js | 9 +++++-- .../react-scripts/template/src/HOCFunction.js | 14 ++++++++--- packages/react-scripts/template/src/Hello.js | 10 ++++---- packages/react-scripts/template/src/TODO.md | 25 +++++++++++++++++++ 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 26378a9f645..06d1a89d495 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -18,7 +18,7 @@ Counter.__setImpl(function CounterImpl(props) { {value} {props.hocChild && ( <> - (inner HOC: ) + (inner HOC: {HFF.field}) )} diff --git a/packages/react-scripts/template/src/HOCClass.js b/packages/react-scripts/template/src/HOCClass.js index 6e6b77a1281..5f442b854e3 100644 --- a/packages/react-scripts/template/src/HOCClass.js +++ b/packages/react-scripts/template/src/HOCClass.js @@ -1,9 +1,14 @@ import React, { Component } from 'react'; -export default function withStuff(Wrapped) { +export default function withStuff(Wrapped, color) { return class extends Component { + static field = 42; render() { - return ; + return ( + + + + ); } }; } diff --git a/packages/react-scripts/template/src/HOCFunction.js b/packages/react-scripts/template/src/HOCFunction.js index ac6e64d68c9..029790888e1 100644 --- a/packages/react-scripts/template/src/HOCFunction.js +++ b/packages/react-scripts/template/src/HOCFunction.js @@ -1,7 +1,13 @@ import React from 'react'; -export default function withStuff(Wrapped) { - return function(props) { - return ; - }; +export default function withStuff(Wrapped, color) { + function Wrapper(props) { + return ( + + + + ); + } + Wrapper.field = 42; + return Wrapper; } diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 5872efb089e..14036c594d3 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -10,10 +10,10 @@ let HCF = window.__createProxy(module, 'HCF'); let HFC = window.__createProxy(module, 'HFC'); let HFF = window.__createProxy(module, 'HFF'); -HCC.__setImpl(HOCClass(CounterClass)); -HCF.__setImpl(HOCClass(CounterFunction)); -HFC.__setImpl(HOCFunction(CounterClass)); -HFF.__setImpl(HOCFunction(CounterFunction)); +HCC.__setImpl(HOCClass(CounterClass, 'red')); +HCF.__setImpl(HOCClass(CounterFunction, 'orange')); +HFC.__setImpl(HOCFunction(CounterClass, 'yellow')); +HFF.__setImpl(HOCFunction(CounterFunction, 'green')); Hello.__setImpl(function HelloImpl() { const [value] = useState(Math.random()); @@ -21,7 +21,7 @@ Hello.__setImpl(function HelloImpl() {

{value.toString().slice(0, 5)}
- hello world!!! + hello world!
class:
diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index c9f2b455d12..294c20fffc2 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -21,3 +21,28 @@ - directives? - when to accept? - test integrations + +lazy()? + +- maybe Proxy? +- do I want to preserve type? elementType? or what? + or lazy element? + +- what about static fields + + - we either break Foo.thing or .type? + - maybe we never wrap, but point to latest from current one? + - could also bail out + +- how to force update all (incl. inside memo) + +let A = createA(); +B() { + +} + +- two kinds of edits + - reeval child with new data + - remember new self + +* think in terms of "assign"? From 26872e8f420195b30058cc79d71a95ec3ef8eb84 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 4 Dec 2018 16:56:24 +0000 Subject: [PATCH 14/47] Reset again --- .../react-scripts/template/src/CounterClass.js | 5 ++--- .../template/src/CounterFunction.js | 8 +++----- packages/react-scripts/template/src/Hello.js | 17 ++++++----------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 2e6081a4bd6..eddb8497e7b 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -2,8 +2,7 @@ import React, { Component } from 'react'; import CounterFunction from './CounterFunction'; import HOCFunction from './HOCFunction'; -const HFF = window.__createProxy(module, 'HFF'); -HFF.__setImpl(HOCFunction(CounterFunction)); +const HFF = HOCFunction(CounterFunction); export default class Counter extends Component { state = { value: 0 }; @@ -22,7 +21,7 @@ export default class Counter extends Component { {this.state.value}{' '} {this.props.hocChild && ( <> - (inner HOC: ) + (inner HOC: {HFF.field}) )} diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 06d1a89d495..13ecb346971 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,11 +1,9 @@ import React, { useReducer, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; -let Counter = window.__createProxy(module, 'Counter'); -let HFF = window.__createProxy(module, 'HFF'); +let HFF = HOCFunction(Counter); -HFF.__setImpl(HOCFunction(Counter)); -Counter.__setImpl(function CounterImpl(props) { +function Counter(props) { const [value, dispatch] = useReducer((v, a) => { return a === 'inc' ? v + 1 : v; }, 0); @@ -23,6 +21,6 @@ Counter.__setImpl(function CounterImpl(props) { )} ); -}); +} export default Counter; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 14036c594d3..44e7f7fc794 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -4,17 +4,12 @@ import CounterFunction from './CounterFunction'; import HOCClass from './HOCClass'; import HOCFunction from './HOCFunction'; -let Hello = window.__createProxy(module, 'Hello'); -let HCC = window.__createProxy(module, 'CounterClass'); -let HCF = window.__createProxy(module, 'HCF'); -let HFC = window.__createProxy(module, 'HFC'); -let HFF = window.__createProxy(module, 'HFF'); +let HCC = HOCClass(CounterClass, 'red'); +let HCF = HOCClass(CounterFunction, 'orange'); +let HFC = HOCFunction(CounterClass, 'yellow'); +let HFF = HOCFunction(CounterFunction, 'green'); -HCC.__setImpl(HOCClass(CounterClass, 'red')); -HCF.__setImpl(HOCClass(CounterFunction, 'orange')); -HFC.__setImpl(HOCFunction(CounterClass, 'yellow')); -HFF.__setImpl(HOCFunction(CounterFunction, 'green')); -Hello.__setImpl(function HelloImpl() { +function Hello() { const [value] = useState(Math.random()); return ( @@ -30,6 +25,6 @@ Hello.__setImpl(function HelloImpl() { hocs:

); -}); +} export default Hello; From 13a8ffa1a10f41304f9492d9bda33930f093d637 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 4 Dec 2018 18:58:14 +0000 Subject: [PATCH 15/47] Make fields work --- .../template/src/CounterFunction.js | 10 +++-- packages/react-scripts/template/src/Hello.js | 17 +++++--- packages/react-scripts/template/src/hot.js | 42 +++++++------------ 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 13ecb346971..0b1bfa93a2d 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,9 +1,10 @@ import React, { useReducer, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; -let HFF = HOCFunction(Counter); +let HFF; +let Counter; -function Counter(props) { +Counter = window.__assign(module, 'Counter', function Counter(props) { const [value, dispatch] = useReducer((v, a) => { return a === 'inc' ? v + 1 : v; }, 0); @@ -13,7 +14,7 @@ function Counter(props) { }, []); return ( - {value} + {value}{' '} {props.hocChild && ( <> (inner HOC: {HFF.field}) @@ -21,6 +22,7 @@ function Counter(props) { )} ); -} +}); +HFF = window.__assign(module, 'HFF', HOCFunction(Counter)); export default Counter; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 44e7f7fc794..55e007355ce 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -4,12 +4,12 @@ import CounterFunction from './CounterFunction'; import HOCClass from './HOCClass'; import HOCFunction from './HOCFunction'; -let HCC = HOCClass(CounterClass, 'red'); -let HCF = HOCClass(CounterFunction, 'orange'); -let HFC = HOCFunction(CounterClass, 'yellow'); -let HFF = HOCFunction(CounterFunction, 'green'); +let HCC; +let HCF; +let HFC; +let HFF; -function Hello() { +let Hello = window.__assign(module, 'Hello', function Hello() { const [value] = useState(Math.random()); return ( @@ -25,6 +25,11 @@ function Hello() { hocs:

); -} +}); + +HCC = window.__assign(module, 'HCC', HOCClass(CounterClass, 'red')); +HCF = window.__assign(module, 'HCF', HOCClass(CounterFunction, 'orange')); +HFC = window.__assign(module, 'HFC', HOCFunction(CounterClass, 'yellow')); +HFF = window.__assign(module, 'HFF', HOCFunction(CounterFunction, 'green')); export default Hello; diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index 8810b34d4cf..1a47c995649 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -8,35 +8,25 @@ export function HotContainer({ children }) { return {children}; } -window.__createProxy = function(m, localId) { +window.__assign = function(m, localId, value) { m.hot.accept(); m.hot.dispose(() => { - setTimeout(() => window.__enqueueForceUpdate()); + setTimeout(() => _invalidate()); }); const id = m.i + '$' + localId; - - if (!window[`${id}_proxy`]) { - function P(...args) { - let impl = window[`${id}_impl`]; - if (impl.prototype && impl.prototype.isReactComponent) { - return new impl(...args); - } - let res = impl.apply(this, arguments); - window.__renderHook(); - return res; - } - window[`${id}_proxy`] = P; - P.__setImpl = impl => { - window[`${id}_impl`] = impl; - }; + window['latest_' + id] = value; + let orig = window['orig_' + id]; + if (!orig) { + // Can fall back to a custom convention, e.g. + // orig.__apply__ if React respects that. + orig = new Proxy(value, { + apply(target, thisArg, args) { + let ret = window['latest_' + id].apply(null, args); + React.useContext(HotContext); + return ret; + }, + }); + window['orig_' + id] = orig; } - return window[`${id}_proxy`]; -}; - -window.__enqueueForceUpdate = function(type) { - _invalidate(); -}; - -window.__renderHook = function(type) { - React.useContext(HotContext); + return orig; }; From 759f87c93bad2985af323f9735585cc232bb805d Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 5 Dec 2018 20:39:48 +0000 Subject: [PATCH 16/47] Make memo work --- .../template/src/CounterFunction.js | 46 +++++----- packages/react-scripts/template/src/hot.js | 88 +++++++++++++++---- 2 files changed, 96 insertions(+), 38 deletions(-) diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 0b1bfa93a2d..48e2b2f2495 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,28 +1,34 @@ -import React, { useReducer, useLayoutEffect } from 'react'; +import React, { memo, useReducer, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; let HFF; let Counter; -Counter = window.__assign(module, 'Counter', function Counter(props) { - const [value, dispatch] = useReducer((v, a) => { - return a === 'inc' ? v + 1 : v; - }, 0); - useLayoutEffect(() => { - const id = setInterval(() => dispatch('inc'), 1000); - return () => clearInterval(id); - }, []); - return ( - - {value}{' '} - {props.hocChild && ( - <> - (inner HOC: {HFF.field}) - - )} - - ); -}); +Counter = window.__assign( + module, + 'Counter', + memo( + memo(function Counter(props) { + const [value, dispatch] = useReducer((v, a) => { + return a === 'inc' ? v + 1 : v; + }, 0); + useLayoutEffect(() => { + const id = setInterval(() => dispatch('inc'), 1000); + return () => clearInterval(id); + }, []); + return ( + + {value}{' '} + {props.hocChild && ( + <> + (inner HOC: {HFF.field}) + + )} + + ); + }) + ) +); HFF = window.__assign(module, 'HFF', HOCFunction(Counter)); export default Counter; diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index 1a47c995649..cd1a149a55e 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -8,25 +8,77 @@ export function HotContainer({ children }) { return {children}; } -window.__assign = function(m, localId, value) { - m.hot.accept(); - m.hot.dispose(() => { +let storage = new Map(); +let latestRawFunctions = new Map(); + +function getKind(type) { + if (typeof type === 'function') { + if (type.prototype && type.prototype.isReactComponent) { + return 'class'; + } else { + return 'function'; + } + } else if (type !== null && typeof type === 'object') { + if (type.$$typeof === Symbol.for('react.memo')) { + return 'memo'; + } + } + return 'other'; +} + +function init(rawType, id) { + let kind = getKind(rawType); + switch (kind) { + case 'function': { + latestRawFunctions.set(id, rawType); + return new Proxy(rawType, { + apply(target, thisArg, args) { + let ret = latestRawFunctions.get(id).apply(null, args); + React.useContext(HotContext); + return ret; + }, + }); + } + case 'memo': { + rawType.type = init(rawType.type, id); + return rawType; + } + default: { + return rawType; + } + } +} + +function accept(type, nextRawType, id) { + let kind = getKind(type); + let nextKind = getKind(nextRawType); + if (kind !== nextKind) { + return false; + } + switch (kind) { + case 'function': { + latestRawFunctions.set(id, nextRawType); + return true; + } + case 'memo': { + return accept(type.type, nextRawType.type, id); + } + default: { + return false; + } + } +} + +window.__assign = function(webpackModule, localId, nextRawType) { + webpackModule.hot.accept(); + webpackModule.hot.dispose(() => { setTimeout(() => _invalidate()); }); - const id = m.i + '$' + localId; - window['latest_' + id] = value; - let orig = window['orig_' + id]; - if (!orig) { - // Can fall back to a custom convention, e.g. - // orig.__apply__ if React respects that. - orig = new Proxy(value, { - apply(target, thisArg, args) { - let ret = window['latest_' + id].apply(null, args); - React.useContext(HotContext); - return ret; - }, - }); - window['orig_' + id] = orig; + const id = webpackModule.i + '$' + localId; + let type = storage.get(id); + if (!accept(type, nextRawType, id)) { + type = init(nextRawType, id); + storage.set(id, type); } - return orig; + return type; }; From 63f71875ecce91a936f799c46866f1e441d50830 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 5 Dec 2018 21:44:42 +0000 Subject: [PATCH 17/47] Fix lazy and double wrapping --- packages/react-scripts/template/src/Hello.js | 50 ++++++++++++++------ packages/react-scripts/template/src/TODO.md | 2 + packages/react-scripts/template/src/hot.js | 30 ++++++++---- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 55e007355ce..0edca0776ae 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -1,9 +1,13 @@ -import React, { useState } from 'react'; -import CounterClass from './CounterClass'; -import CounterFunction from './CounterFunction'; +import React, { Suspense, lazy, useState } from 'react'; import HOCClass from './HOCClass'; import HOCFunction from './HOCFunction'; +import CounterClass from './CounterClass'; +import CounterFunction from './CounterFunction'; +let LazyCC; +let LazyCF; +let DblCC; +let DblCF; let HCC; let HCF; let HFC; @@ -13,20 +17,38 @@ let Hello = window.__assign(module, 'Hello', function Hello() { const [value] = useState(Math.random()); return ( -

- {value.toString().slice(0, 5)} -
- hello world! -
- class: -
- function: -
- hocs: -

+ }> +

+ {value.toString().slice(0, 5)} +
+ hello world! +
+ class: +
+ function: +
+ doublewrapped: +
+ lazy: +
+ hocs: +

+
); }); +LazyCC = window.__assign( + module, + 'LazyCC', + lazy(() => import('./CounterClass')) +); +LazyCF = window.__assign( + module, + 'LazyCF', + lazy(() => import('./CounterFunction')) +); +DblCC = window.__assign(module, 'DblCC', CounterClass); +DblCF = window.__assign(module, 'DblCF', CounterFunction); HCC = window.__assign(module, 'HCC', HOCClass(CounterClass, 'red')); HCF = window.__assign(module, 'HCF', HOCClass(CounterFunction, 'orange')); HFC = window.__assign(module, 'HFC', HOCFunction(CounterClass, 'yellow')); diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index 294c20fffc2..38f33bc16c3 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -24,6 +24,8 @@ lazy()? +- lazy of memo of proxy + - maybe Proxy? - do I want to preserve type? elementType? or what? or lazy element? diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index cd1a149a55e..ca579caf9e6 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -8,8 +8,9 @@ export function HotContainer({ children }) { return {children}; } -let storage = new Map(); -let latestRawFunctions = new Map(); +let idToPersistentType = new Map(); +let idToRawFunction = new Map(); +let proxies = new WeakSet(); function getKind(type) { if (typeof type === 'function') { @@ -21,6 +22,8 @@ function getKind(type) { } else if (type !== null && typeof type === 'object') { if (type.$$typeof === Symbol.for('react.memo')) { return 'memo'; + } else if (type.$$typeof === Symbol.for('react.lazy')) { + return 'lazy'; } } return 'other'; @@ -30,19 +33,27 @@ function init(rawType, id) { let kind = getKind(rawType); switch (kind) { case 'function': { - latestRawFunctions.set(id, rawType); - return new Proxy(rawType, { + if (proxies.has(rawType)) { + return rawType; + } + idToRawFunction.set(id, rawType); + const proxy = new Proxy(rawType, { apply(target, thisArg, args) { - let ret = latestRawFunctions.get(id).apply(null, args); + let ret = idToRawFunction.get(id).apply(null, args); React.useContext(HotContext); return ret; }, }); + proxies.add(proxy); + return proxy; } case 'memo': { rawType.type = init(rawType.type, id); return rawType; } + case 'lazy': { + return rawType; + } default: { return rawType; } @@ -57,12 +68,15 @@ function accept(type, nextRawType, id) { } switch (kind) { case 'function': { - latestRawFunctions.set(id, nextRawType); + idToRawFunction.set(id, nextRawType); return true; } case 'memo': { return accept(type.type, nextRawType.type, id); } + case 'lazy': { + return true; + } default: { return false; } @@ -75,10 +89,10 @@ window.__assign = function(webpackModule, localId, nextRawType) { setTimeout(() => _invalidate()); }); const id = webpackModule.i + '$' + localId; - let type = storage.get(id); + let type = idToPersistentType.get(id); if (!accept(type, nextRawType, id)) { type = init(nextRawType, id); - storage.set(id, type); + idToPersistentType.set(id, type); } return type; }; From 6fe5e6f5c22794b1a8e044274cea9b152b20a252 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 5 Dec 2018 22:05:21 +0000 Subject: [PATCH 18/47] Fix forwardRef --- .../template/src/CounterFunction.js | 18 +++++++++--- packages/react-scripts/template/src/TODO.md | 29 ++----------------- packages/react-scripts/template/src/hot.js | 17 ++++++++++- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 48e2b2f2495..d5f50898cd7 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,8 +1,9 @@ -import React, { memo, useReducer, useLayoutEffect } from 'react'; +import React, { forwardRef, memo, useReducer, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; let HFF; let Counter; +let Fwd; Counter = window.__assign( module, @@ -18,7 +19,7 @@ Counter = window.__assign( }, []); return ( - {value}{' '} + {value} {props.hocChild && ( <> (inner HOC: {HFF.field}) @@ -29,6 +30,15 @@ Counter = window.__assign( }) ) ); -HFF = window.__assign(module, 'HFF', HOCFunction(Counter)); +Fwd = window.__assign( + module, + 'FWD', + forwardRef((props, ref) => ( + + + + )) +); +HFF = window.__assign(module, 'HFF', HOCFunction(Fwd)); -export default Counter; +export default Fwd; diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index 38f33bc16c3..a6ed2b0ce94 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -21,30 +21,7 @@ - directives? - when to accept? - test integrations - -lazy()? - -- lazy of memo of proxy - -- maybe Proxy? -- do I want to preserve type? elementType? or what? - or lazy element? - -- what about static fields - - - we either break Foo.thing or .type? - - maybe we never wrap, but point to latest from current one? - - could also bail out - +- exotic (lazy, memo, fwd) - how to force update all (incl. inside memo) - -let A = createA(); -B() { -
-} - -- two kinds of edits - - reeval child with new data - - remember new self - -* think in terms of "assign"? +- don't accept module if either assignment failed +- displayName etc diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index ca579caf9e6..aaf51ed279d 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -8,6 +8,12 @@ export function HotContainer({ children }) { return {children}; } +let CurrentOwner = + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner; +function readContext(Ctx) { + CurrentOwner.currentDispatcher.readContext(Ctx); +} + let idToPersistentType = new Map(); let idToRawFunction = new Map(); let proxies = new WeakSet(); @@ -24,6 +30,8 @@ function getKind(type) { return 'memo'; } else if (type.$$typeof === Symbol.for('react.lazy')) { return 'lazy'; + } else if (type.$$typeof === Symbol.for('react.forward_ref')) { + return 'forward_ref'; } } return 'other'; @@ -40,7 +48,7 @@ function init(rawType, id) { const proxy = new Proxy(rawType, { apply(target, thisArg, args) { let ret = idToRawFunction.get(id).apply(null, args); - React.useContext(HotContext); + readContext(HotContext); return ret; }, }); @@ -54,6 +62,10 @@ function init(rawType, id) { case 'lazy': { return rawType; } + case 'forward_ref': { + rawType.render = init(rawType.render, id); + return rawType; + } default: { return rawType; } @@ -74,6 +86,9 @@ function accept(type, nextRawType, id) { case 'memo': { return accept(type.type, nextRawType.type, id); } + case 'forward_ref': { + return accept(type.render, nextRawType.render, id); + } case 'lazy': { return true; } From 43e5f2e13307acc0c1f073f95f679335aa5039e6 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 6 Dec 2018 00:04:14 +0000 Subject: [PATCH 19/47] Bump --- packages/react-scripts/template/src/Hello.js | 2 +- packages/react-scripts/template/src/hot.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 0edca0776ae..53b54ca761c 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -21,7 +21,7 @@ let Hello = window.__assign(module, 'Hello', function Hello() {

{value.toString().slice(0, 5)}
- hello world! + hello world!!
class:
diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index aaf51ed279d..c0101d7c9a8 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -47,7 +47,8 @@ function init(rawType, id) { idToRawFunction.set(id, rawType); const proxy = new Proxy(rawType, { apply(target, thisArg, args) { - let ret = idToRawFunction.get(id).apply(null, args); + let freshRawType = idToRawFunction.get(id); + let ret = freshRawType.apply(null, args); readContext(HotContext); return ret; }, From f499c8abe751cbb8b320bd7f8ae8e39f934bddaa Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 6 Dec 2018 21:42:32 +0000 Subject: [PATCH 20/47] Todos --- packages/react-scripts/template/src/TODO.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index a6ed2b0ce94..22c7c6fc67d 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -1,6 +1,18 @@ +- other strategies: + - seb suggested walking down the tree and reconciling +- write down constraints. + + - actually, tests! + - frags - error handling + - reenable overlay - offer a way to reset? + - suppress error dialog? + - suppress messages? + - check types? debug tools? +- directives? + - ! - hooks - remount on error? - compare introspection? @@ -18,10 +30,12 @@ - classes - how to detect and reload? - render props -- directives? - when to accept? - test integrations - exotic (lazy, memo, fwd) - how to force update all (incl. inside memo) - don't accept module if either assignment failed - displayName etc +- false positives for things getting wrapped +- forwardRef + - make sure refs _on_ proxies actually work From 24bc20b44a1365d7022df6019e22f9f95835a25b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 00:36:14 +0000 Subject: [PATCH 21/47] Support adding/removing hooks and error recovery --- .../template/src/CounterFunction.js | 4 +- packages/react-scripts/template/src/Hello.js | 3 +- packages/react-scripts/template/src/TODO.md | 32 ++--- packages/react-scripts/template/src/hot.js | 129 +++++++++++++++++- 4 files changed, 145 insertions(+), 23 deletions(-) diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index d5f50898cd7..43c819e4d49 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -17,9 +17,11 @@ Counter = window.__assign( const id = setInterval(() => dispatch('inc'), 1000); return () => clearInterval(id); }, []); + const [x] = React.useState(100); + return ( - {value} + {x} {value} {props.hocChild && ( <> (inner HOC: {HFF.field}) diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 53b54ca761c..f7e0687a73f 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -15,13 +15,12 @@ let HFF; let Hello = window.__assign(module, 'Hello', function Hello() { const [value] = useState(Math.random()); - return ( }>

{value.toString().slice(0, 5)}
- hello world!! + hello world!
class:
diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index 22c7c6fc67d..a715774d625 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -4,38 +4,38 @@ - actually, tests! -- frags -- error handling +* don't accept module if either assignment failed +* frags +* error handling - reenable overlay - offer a way to reset? - suppress error dialog? - suppress messages? - check types? debug tools? -- directives? +* directives? - ! -- hooks +* hooks - remount on error? - compare introspection? -- hocs? +* hocs? - with self as input - returning a class - HOC itself by mistake - protect against nested? -- type equality +* type equality - memo and friends - nested / applied twice - mutually recursive - re-exports - same HOC applied early, two callsites -- classes +* classes - how to detect and reload? -- render props -- when to accept? -- test integrations -- exotic (lazy, memo, fwd) -- how to force update all (incl. inside memo) -- don't accept module if either assignment failed -- displayName etc -- false positives for things getting wrapped -- forwardRef +* render props +* when to accept? +* test integrations +* exotic (lazy, memo, fwd) +* how to force update all (incl. inside memo) +* displayName etc +* false positives for things getting wrapped +* forwardRef - make sure refs _on_ proxies actually work diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index c0101d7c9a8..224266e3c09 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -45,12 +45,130 @@ function init(rawType, id) { return rawType; } idToRawFunction.set(id, rawType); + + let abandonedHooks = []; + let isAbandoningHooks = false; + let previousHooks = []; + const NEVER = {}; + const NOOP = {}; + const proxy = new Proxy(rawType, { apply(target, thisArg, args) { - let freshRawType = idToRawFunction.get(id); - let ret = freshRawType.apply(null, args); readContext(HotContext); - return ret; + + let realDispatcher = CurrentOwner.currentDispatcher; + let freshRawType = idToRawFunction.get(id); + let currentHooks = []; + + function callNoopHook([hook, inputLength]) { + let inputs; + if (inputLength) { + inputs = new Array(inputLength).fill(NOOP); + } + switch (hook) { + case realDispatcher.useState: + realDispatcher.useState(); + break; + case realDispatcher.useRef: + realDispatcher.useRef(); + break; + case realDispatcher.useReducer: + realDispatcher.useReducer(state => state); + break; + case realDispatcher.useLayoutEffect: + realDispatcher.useLayoutEffect(() => {}, inputs); + break; + case realDispatcher.useEffect: + realDispatcher.useEffect(() => {}, inputs); + break; + case realDispatcher.useMemo: + realDispatcher.useMemo(() => {}, inputs); + break; + case realDispatcher.useCallback: + realDispatcher.useCallback(() => {}, inputs); + break; + case realDispatcher.readContext: + case realDispatcher.useContext: + break; + default: + throw new Error('TODO'); + } + } + + // Pad left abandoned Hooks + for (let i = 0; i < abandonedHooks.length; i++) { + const hook = abandonedHooks[i]; + callNoopHook(hook); + } + + CurrentOwner.currentDispatcher = new Proxy(realDispatcher, { + get(target, prop, receiver) { + const hook = Reflect.get(...arguments); + return new Proxy(hook, { + apply(t, thisArg, argumentsList) { + // TODO: check if type matches up and throw + // TODO: reset individual state if primitive type differs + switch (hook) { + case realDispatcher.useLayoutEffect: + case realDispatcher.useEffect: + case realDispatcher.useMemo: + case realDispatcher.useCallback: + { + let inputs = argumentsList[1]; + if (inputs) { + if (inputs.length === 0) { + // Allows us to clean up if this Hook is removed later. + argumentsList[1] = inputs = [NEVER]; + } + currentHooks.push([hook, inputs.length]); + } else { + currentHooks.push([hook]); + } + } + break; + default: + currentHooks.push([hook]); + break; + } + return Reflect.apply(t, thisArg, argumentsList); + }, + }); + }, + }); + let ret; + try { + ret = freshRawType.apply(null, args); + isAbandoningHooks = false; + } catch (err) { + if (isAbandoningHooks) { + isAbandoningHooks = false; + throw err; + } + isAbandoningHooks = true; + } finally { + CurrentOwner.currentDispatcher = realDispatcher; + } + + // Pad right missing Hooks + for (let i = currentHooks.length; i < previousHooks.length; i++) { + const hook = previousHooks[i]; + callNoopHook(hook); + currentHooks.push(hook); + } + + previousHooks = currentHooks; + + if (isAbandoningHooks) { + const [, reset] = realDispatcher.useState(); + previousHooks.push([realDispatcher.useState]); + abandonedHooks = previousHooks; + previousHooks = []; + reset(); + } + + return ( + {ret} + ); }, }); proxies.add(proxy); @@ -82,7 +200,10 @@ function accept(type, nextRawType, id) { switch (kind) { case 'function': { idToRawFunction.set(id, nextRawType); - return true; + const forceRemount = + nextRawType.toString().indexOf('//!') !== -1 || + nextRawType.toString().indexOf('// !') !== -1; + return !forceRemount; } case 'memo': { return accept(type.type, nextRawType.type, id); From 9b5fb72242a1c1015576f3a5d18ad9978fa160ee Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 14:47:27 +0000 Subject: [PATCH 22/47] Support reordering Hooks --- packages/react-scripts/template/src/hot.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index 224266e3c09..81d3f84f65b 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -106,6 +106,10 @@ function init(rawType, id) { const hook = Reflect.get(...arguments); return new Proxy(hook, { apply(t, thisArg, argumentsList) { + let prevHook = previousHooks[currentHooks.length]; + if (prevHook && prevHook[0] !== hook) { + throw new Error('Hook mismatch.'); + } // TODO: check if type matches up and throw // TODO: reset individual state if primitive type differs switch (hook) { @@ -160,8 +164,9 @@ function init(rawType, id) { if (isAbandoningHooks) { const [, reset] = realDispatcher.useState(); + // Shift Hooks to recover. This leaks memory. Ideallly we'd reset. previousHooks.push([realDispatcher.useState]); - abandonedHooks = previousHooks; + abandonedHooks.push(...previousHooks); previousHooks = []; reset(); } From cc5a4700e9422d7dd454b44d6476871002dac678 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 15:27:30 +0000 Subject: [PATCH 23/47] Reset if primitive state type changes --- .../template/src/CounterFunction.js | 3 +- packages/react-scripts/template/src/hot.js | 45 ++++++++++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 43c819e4d49..4a4e4256a46 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -17,11 +17,10 @@ Counter = window.__assign( const id = setInterval(() => dispatch('inc'), 1000); return () => clearInterval(id); }, []); - const [x] = React.useState(100); return ( - {x} {value} + {value} {props.hocChild && ( <> (inner HOC: {HFF.field}) diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index 81d3f84f65b..c0f83dd9b92 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -60,11 +60,7 @@ function init(rawType, id) { let freshRawType = idToRawFunction.get(id); let currentHooks = []; - function callNoopHook([hook, inputLength]) { - let inputs; - if (inputLength) { - inputs = new Array(inputLength).fill(NOOP); - } + function callNoopHook([hook, data]) { switch (hook) { case realDispatcher.useState: realDispatcher.useState(); @@ -75,18 +71,38 @@ function init(rawType, id) { case realDispatcher.useReducer: realDispatcher.useReducer(state => state); break; - case realDispatcher.useLayoutEffect: + case realDispatcher.useLayoutEffect: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } realDispatcher.useLayoutEffect(() => {}, inputs); break; - case realDispatcher.useEffect: + } + case realDispatcher.useEffect: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } realDispatcher.useEffect(() => {}, inputs); break; - case realDispatcher.useMemo: + } + case realDispatcher.useMemo: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } realDispatcher.useMemo(() => {}, inputs); break; - case realDispatcher.useCallback: + } + case realDispatcher.useCallback: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } realDispatcher.useCallback(() => {}, inputs); break; + } case realDispatcher.readContext: case realDispatcher.useContext: break; @@ -110,9 +126,16 @@ function init(rawType, id) { if (prevHook && prevHook[0] !== hook) { throw new Error('Hook mismatch.'); } - // TODO: check if type matches up and throw - // TODO: reset individual state if primitive type differs switch (hook) { + case realDispatcher.useState: + if ( + prevHook && + typeof prevHook[1] !== typeof argumentsList[0] + ) { + throw new Error('State type mismatch.'); + } + currentHooks.push([hook, argumentsList[0]]); + break; case realDispatcher.useLayoutEffect: case realDispatcher.useEffect: case realDispatcher.useMemo: From 42d8a1d7dbae1f34b2d4db2f48a436bef27c7922 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 16:39:26 +0000 Subject: [PATCH 24/47] Add __commit phase --- packages/react-scripts/template/src/App.js | 37 ++++++------------- .../template/src/CounterFunction.js | 1 + packages/react-scripts/template/src/Hello.js | 3 ++ packages/react-scripts/template/src/Layout.js | 26 +++++++++++++ packages/react-scripts/template/src/hot.js | 17 ++++++--- 5 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 packages/react-scripts/template/src/Layout.js diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 353eee64604..5e1fed41f1b 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -1,30 +1,17 @@ -import React, { Component } from 'react'; +import React from 'react'; import Hello from './Hello'; -import logo from './logo.svg'; +import Layout from './Layout'; import './App.css'; -class App extends Component { - render() { - return ( -
- ); - } -} +console.log('running App'); + +let App = window.__assign(module, 'App', function App({ children }) { + return ( + + + + ); +}); +window.__commit(module); export default App; diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 4a4e4256a46..6683029c4bb 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -41,5 +41,6 @@ Fwd = window.__assign( )) ); HFF = window.__assign(module, 'HFF', HOCFunction(Fwd)); +window.__commit(module); export default Fwd; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index f7e0687a73f..34d2eec6548 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -13,6 +13,8 @@ let HCF; let HFC; let HFF; +console.log('running Hello'); + let Hello = window.__assign(module, 'Hello', function Hello() { const [value] = useState(Math.random()); return ( @@ -52,5 +54,6 @@ HCC = window.__assign(module, 'HCC', HOCClass(CounterClass, 'red')); HCF = window.__assign(module, 'HCF', HOCClass(CounterFunction, 'orange')); HFC = window.__assign(module, 'HFC', HOCFunction(CounterClass, 'yellow')); HFF = window.__assign(module, 'HFF', HOCFunction(CounterFunction, 'green')); +window.__commit(module); export default Hello; diff --git a/packages/react-scripts/template/src/Layout.js b/packages/react-scripts/template/src/Layout.js new file mode 100644 index 00000000000..0a50b3cf34f --- /dev/null +++ b/packages/react-scripts/template/src/Layout.js @@ -0,0 +1,26 @@ +import React, { Component } from 'react'; +import logo from './logo.svg'; + +export default class Layout extends Component { + render() { + return ( +
+
+ logo + {this.props.children} +

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

+ + Learn React + +
+
+ ); + } +} diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index c0f83dd9b92..53bf1973f45 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -1,10 +1,10 @@ import React from 'react'; let HotContext = React.createContext(); -let _invalidate; +let invalidate; export function HotContainer({ children }) { const [inc, setInc] = React.useState(0); - _invalidate = () => setInc(c => c + 1); + invalidate = () => setInc(c => c + 1); return {children}; } @@ -249,10 +249,6 @@ function accept(type, nextRawType, id) { } window.__assign = function(webpackModule, localId, nextRawType) { - webpackModule.hot.accept(); - webpackModule.hot.dispose(() => { - setTimeout(() => _invalidate()); - }); const id = webpackModule.i + '$' + localId; let type = idToPersistentType.get(id); if (!accept(type, nextRawType, id)) { @@ -261,3 +257,12 @@ window.__assign = function(webpackModule, localId, nextRawType) { } return type; }; + +window.__commit = function(webpackModule) { + // TODO: we should abort self-accept if one of types changed. + // but it doesn't seem like webpack lets us do this yet. + webpackModule.hot.accept(); + webpackModule.hot.dispose(() => { + setTimeout(() => invalidate()); + }); +}; From accf852cd67fd883321e16d98117e54758f3d527 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 16:40:23 +0000 Subject: [PATCH 25/47] todo --- packages/react-scripts/template/src/TODO.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index a715774d625..74787257a13 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -12,6 +12,9 @@ - suppress error dialog? - suppress messages? - check types? debug tools? + +- highlight updates + * directives? - ! * hooks From 8c3f39d262d2c4e0e2e6a38e89774d0acd52a30e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 21:50:35 +0000 Subject: [PATCH 26/47] Propagate acceptance --- packages/react-scripts/template/src/App.js | 4 +-- .../template/src/CounterClass.js | 5 +++- .../template/src/CounterFunction.js | 4 ++- packages/react-scripts/template/src/Hello.js | 11 ++++---- packages/react-scripts/template/src/hot.js | 25 +++++++++++-------- packages/react-scripts/template/src/index.js | 2 ++ 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 5e1fed41f1b..14a6a18462c 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -3,8 +3,6 @@ import Hello from './Hello'; import Layout from './Layout'; import './App.css'; -console.log('running App'); - let App = window.__assign(module, 'App', function App({ children }) { return ( @@ -12,6 +10,6 @@ let App = window.__assign(module, 'App', function App({ children }) { ); }); -window.__commit(module); +module.hot.accept(['./Layout', './Hello'], window.__invalidate); export default App; diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index eddb8497e7b..68d60238cbe 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import CounterFunction from './CounterFunction'; import HOCFunction from './HOCFunction'; -const HFF = HOCFunction(CounterFunction); +let HFF; export default class Counter extends Component { state = { value: 0 }; @@ -28,3 +28,6 @@ export default class Counter extends Component { ); } } + +HFF = window.__assign(module, 'HFF', HOCFunction(CounterFunction)); +module.hot.accept(['./CounterFunction', './HOCFunction'], window.__invalidate); diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 6683029c4bb..bc694dc993b 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -41,6 +41,8 @@ Fwd = window.__assign( )) ); HFF = window.__assign(module, 'HFF', HOCFunction(Fwd)); -window.__commit(module); +module.hot.accept(['./HOCFunction'], window.__invalidate); + +export let N = 10; export default Fwd; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 34d2eec6548..0d3a07b1062 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -2,7 +2,7 @@ import React, { Suspense, lazy, useState } from 'react'; import HOCClass from './HOCClass'; import HOCFunction from './HOCFunction'; import CounterClass from './CounterClass'; -import CounterFunction from './CounterFunction'; +import CounterFunction, { N } from './CounterFunction'; let LazyCC; let LazyCF; @@ -13,14 +13,12 @@ let HCF; let HFC; let HFF; -console.log('running Hello'); - let Hello = window.__assign(module, 'Hello', function Hello() { const [value] = useState(Math.random()); return ( }>

- {value.toString().slice(0, 5)} + {N} - {value.toString().slice(0, 5)}
hello world!
@@ -54,6 +52,9 @@ HCC = window.__assign(module, 'HCC', HOCClass(CounterClass, 'red')); HCF = window.__assign(module, 'HCF', HOCClass(CounterFunction, 'orange')); HFC = window.__assign(module, 'HFC', HOCFunction(CounterClass, 'yellow')); HFF = window.__assign(module, 'HFF', HOCFunction(CounterFunction, 'green')); -window.__commit(module); +module.hot.accept( + ['./CounterFunction', './CounterClass', './HOCFunction', './HOCClass'], + window.__invalidate +); export default Hello; diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index 53bf1973f45..40cee12ff9a 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -1,10 +1,22 @@ import React from 'react'; let HotContext = React.createContext(); -let invalidate; + +let invalidated; +let __setInc; +window.__invalidate = () => { + if (!invalidated) { + invalidated = true; + setTimeout(() => { + invalidated = false; + __setInc(c => c + 1); + }); + } +}; + export function HotContainer({ children }) { const [inc, setInc] = React.useState(0); - invalidate = () => setInc(c => c + 1); + __setInc = setInc; return {children}; } @@ -257,12 +269,3 @@ window.__assign = function(webpackModule, localId, nextRawType) { } return type; }; - -window.__commit = function(webpackModule) { - // TODO: we should abort self-accept if one of types changed. - // but it doesn't seem like webpack lets us do this yet. - webpackModule.hot.accept(); - webpackModule.hot.dispose(() => { - setTimeout(() => invalidate()); - }); -}; diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index b86dd84a690..7e950026ed8 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -12,6 +12,8 @@ ReactDOM.render( document.getElementById('root') ); +module.hot.accept(['./App'], window.__invalidate); + // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA From 47750b4d4beaed74b8383c76fb100b68c9931769 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Dec 2018 23:16:39 +0000 Subject: [PATCH 27/47] Todos --- packages/react-scripts/template/src/TODO.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index 74787257a13..c1e354d14b9 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -1,10 +1,12 @@ - other strategies: - seb suggested walking down the tree and reconciling - write down constraints. - - actually, tests! * don't accept module if either assignment failed + + - write down why granular invalidation is useful + * frags * error handling - reenable overlay From b12c3f12c00eeb1bf635818f8e3538c355044050 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 8 Dec 2018 15:22:07 -0500 Subject: [PATCH 28/47] plugin: wrap export default function() in hot assignment --- packages/babel-plugin-hot-reload/index.js | 174 +++++++----------- packages/babel-plugin-hot-reload/package.json | 9 +- .../tests/__snapshots__/fixtures.test.js.snap | 38 ++++ .../tests/fixtures.test.js | 23 +++ .../tests/fixtures/single-default-export.js | 14 ++ 5 files changed, 149 insertions(+), 109 deletions(-) create mode 100644 packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap create mode 100644 packages/babel-plugin-hot-reload/tests/fixtures.test.js create mode 100644 packages/babel-plugin-hot-reload/tests/fixtures/single-default-export.js diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 60f4b00d6c0..4a9ee2a0390 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -1,7 +1,5 @@ 'use strict'; -const template = require('babel-template'); - function functionReturnsElement(path) { const { body } = path.body; const last = body[body.length - 1]; @@ -15,119 +13,81 @@ function functionReturnsElement(path) { 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 hotAssign(name, func) { + return [ + { + type: 'VariableDeclaration', + kind: 'let', + declarations: [ + { + type: 'VariableDeclarator', + id: { type: 'Identifier', name: name }, + init: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'window' }, + property: { type: 'Identifier', name: '__assign' }, + computed: false, + }, + arguments: [ + { type: 'Identifier', name: 'module' }, + { type: 'StringLiteral', value: name }, + func, + ], + }, + }, + ], + }, + { + type: 'ExportDefaultDeclaration', + declaration: { type: 'Identifier', name: name }, + }, + ]; } -function exportHoistedFunctionCallProxy(t, name, generatedName) { - return template( - ` - export default function NAME() { - var result = window[GEN_NAME].apply(this, arguments); - window.__renderHook(NAME); - return result; +function getExportDefaultDeclaration(t) { + return function ExportDefaultDeclaration(path) { + const { type } = path.node.declaration; + if ( + type !== 'FunctionDeclaration' || + !functionReturnsElement(path.node.declaration) + ) { + return; } - `, - { sourceType: 'module' } - )({ - GEN_NAME: t.StringLiteral(generatedName), - NAME: t.Identifier(name), - }); -} + const { + id: { name }, + } = path.node.declaration; + path.replaceWithMultiple(hotAssign(name, path.node.declaration)); -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), - }); -} + // const generatedName = `__hot__${state.file.opts.filename}$$${name}`; -module.exports = function({ types: t }) { - return { visitor: {} }; + // path.replaceWithMultiple([ + // hoistFunctionalComponentToWindow(t, name, generatedName, params, body), + // decorateFunctionName(t, name, generatedName), + // exportHoistedFunctionCallProxy(t, name, generatedName), + // decorateFunctionId(t, name, generatedName), + // template( + // ` + // module.hot.accept(); + // ` + // )(), + // template( + // ` + // module.hot.dispose(() => { + // setTimeout(() => window.__enqueueForceUpdate(NAME)); + // }); + // ` + // )({ NAME: t.Identifier(name) }), + // ]); + }; +} +module.exports = function({ types }) { return { + name: 'hot-reload', 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( - ` - module.hot.accept(); - ` - )(), - template( - ` - module.hot.dispose(() => { - setTimeout(() => window.__enqueueForceUpdate(NAME)); - }); - ` - )({ NAME: t.Identifier(name) }), - ]); - }, + ExportDefaultDeclaration: getExportDefaultDeclaration(types), }, }; }; diff --git a/packages/babel-plugin-hot-reload/package.json b/packages/babel-plugin-hot-reload/package.json index 9a9ad66c653..0c50959c06b 100644 --- a/packages/babel-plugin-hot-reload/package.json +++ b/packages/babel-plugin-hot-reload/package.json @@ -7,7 +7,12 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "license": "BSD-3-Clause", - "dependencies": { - "babel-template": "6.24.1" + "dependencies": {}, + "devDependencies": { + "@babel/core": "^7.2.0", + "@babel/template": "^7.1.2", + "babel-core": "7.0.0-bridge.0", + "babel-plugin-tester": "^5.5.2", + "jest": "^23.6.0" } } diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap new file mode 100644 index 00000000000..4402f2c36b4 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`hot-reload single default: single default 1`] = ` +" +import React from 'react'; +import Hello from './Hello'; +import Layout from './Layout'; +import './App.css'; + +export default function App({ children }) { + return ( + + + + ); +} + +module.hot.accept(['./Layout', './Hello'], window.__invalidate); + + ↓ ↓ ↓ ↓ ↓ ↓ + +import React from 'react'; +import Hello from './Hello'; +import Layout from './Layout'; +import './App.css'; + +let App = window.__assign(module, \\"App\\", function App({ + children +}) { + return + + ; +}); + +export default App; +module.hot.accept(['./Layout', './Hello'], window.__invalidate); +" +`; diff --git a/packages/babel-plugin-hot-reload/tests/fixtures.test.js b/packages/babel-plugin-hot-reload/tests/fixtures.test.js new file mode 100644 index 00000000000..c68cbda3835 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/fixtures.test.js @@ -0,0 +1,23 @@ +'use strict'; + +const pluginTester = require('babel-plugin-tester'); +const hotReload = require('..'); +const path = require('path'); + +pluginTester({ + plugin: hotReload, + filename: __filename, + babelOptions: { + parserOpts: { plugins: ['jsx'] }, + generatorOpts: {}, + babelrc: false, + }, + snapshot: true, + tests: [ + { + title: 'single default', + fixture: path.join(__dirname, 'fixtures', 'single-default-export.js'), + snapshot: true, + }, + ], +}); diff --git a/packages/babel-plugin-hot-reload/tests/fixtures/single-default-export.js b/packages/babel-plugin-hot-reload/tests/fixtures/single-default-export.js new file mode 100644 index 00000000000..79de8c953c0 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/fixtures/single-default-export.js @@ -0,0 +1,14 @@ +import React from 'react'; +import Hello from './Hello'; +import Layout from './Layout'; +import './App.css'; + +export default function App({ children }) { + return ( + + + + ); +} + +module.hot.accept(['./Layout', './Hello'], window.__invalidate); From c745cbc94cc178056669bfde9f912ec3a8a14ff0 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 8 Dec 2018 15:29:23 -0500 Subject: [PATCH 29/47] example: Export App and Hello normally --- packages/react-scripts/template/src/App.js | 7 +++---- packages/react-scripts/template/src/Hello.js | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 14a6a18462c..79de8c953c0 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -3,13 +3,12 @@ import Hello from './Hello'; import Layout from './Layout'; import './App.css'; -let App = window.__assign(module, 'App', function App({ children }) { +export default function App({ children }) { return ( ); -}); -module.hot.accept(['./Layout', './Hello'], window.__invalidate); +} -export default App; +module.hot.accept(['./Layout', './Hello'], window.__invalidate); diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index 0d3a07b1062..d033d6f7101 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -13,7 +13,7 @@ let HCF; let HFC; let HFF; -let Hello = window.__assign(module, 'Hello', function Hello() { +export default function Hello() { const [value] = useState(Math.random()); return ( }> @@ -34,7 +34,7 @@ let Hello = window.__assign(module, 'Hello', function Hello() {

); -}); +} LazyCC = window.__assign( module, @@ -56,5 +56,3 @@ module.hot.accept( ['./CounterFunction', './CounterClass', './HOCFunction', './HOCClass'], window.__invalidate ); - -export default Hello; From bca09edc6f0c1b5bf1d65a97e2ec2c09f86fd3d2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 8 Dec 2018 22:46:40 +0000 Subject: [PATCH 30/47] add link --- packages/react-scripts/template/src/TODO.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-scripts/template/src/TODO.md b/packages/react-scripts/template/src/TODO.md index c1e354d14b9..88964b96a45 100644 --- a/packages/react-scripts/template/src/TODO.md +++ b/packages/react-scripts/template/src/TODO.md @@ -6,6 +6,9 @@ * don't accept module if either assignment failed - write down why granular invalidation is useful + https://mobile.twitter.com/wSokra/status/1071503979014692865 + +- effects can change arg num * frags * error handling From b10fa81447c4778bbd0a6ee5e4456d2ecd9ff07f Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 8 Dec 2018 15:35:53 -0500 Subject: [PATCH 31/47] plugin: remove commented out section --- packages/babel-plugin-hot-reload/index.js | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 4a9ee2a0390..c580fea3f42 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -59,27 +59,6 @@ function getExportDefaultDeclaration(t) { id: { name }, } = path.node.declaration; path.replaceWithMultiple(hotAssign(name, 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( - // ` - // module.hot.accept(); - // ` - // )(), - // template( - // ` - // module.hot.dispose(() => { - // setTimeout(() => window.__enqueueForceUpdate(NAME)); - // }); - // ` - // )({ NAME: t.Identifier(name) }), - // ]); }; } From f0bb8874ae1ec9b52fb0419ff06e1ff41ec0e526 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sat, 8 Dec 2018 15:55:52 -0500 Subject: [PATCH 32/47] plugin: refactor --- packages/babel-plugin-hot-reload/index.js | 30 ++++++++++------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index c580fea3f42..1ff06854847 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -46,27 +46,23 @@ function hotAssign(name, func) { ]; } -function getExportDefaultDeclaration(t) { - return function ExportDefaultDeclaration(path) { - const { type } = path.node.declaration; - if ( - type !== 'FunctionDeclaration' || - !functionReturnsElement(path.node.declaration) - ) { - return; - } - const { - id: { name }, - } = path.node.declaration; - path.replaceWithMultiple(hotAssign(name, path.node.declaration)); - }; -} - module.exports = function({ types }) { return { name: 'hot-reload', visitor: { - ExportDefaultDeclaration: getExportDefaultDeclaration(types), + ExportDefaultDeclaration(path) { + const { type } = path.node.declaration; + if ( + type !== 'FunctionDeclaration' || + !functionReturnsElement(path.node.declaration) + ) { + return; + } + const { + id: { name }, + } = path.node.declaration; + path.replaceWithMultiple(hotAssign(name, path.node.declaration)); + }, }, }; }; From a4961e770600db0f19f7fab9f1b12f365dc8d320 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:01:06 -0500 Subject: [PATCH 33/47] plugin: wrap module scoped variables --- packages/babel-plugin-hot-reload/index.js | 74 +++++++++++++++++-- .../default-export.js} | 0 .../tests/__fixtures__/empty-variable.js | 2 + .../tests/__fixtures__/lazy-component.js | 2 + .../tests/__snapshots__/fixtures.test.js.snap | 15 +++- .../tests/fixtures.test.js | 19 +++-- packages/react-scripts/template/src/hot.js | 1 + 7 files changed, 98 insertions(+), 15 deletions(-) rename packages/babel-plugin-hot-reload/tests/{fixtures/single-default-export.js => __fixtures__/default-export.js} (100%) create mode 100644 packages/babel-plugin-hot-reload/tests/__fixtures__/empty-variable.js create mode 100644 packages/babel-plugin-hot-reload/tests/__fixtures__/lazy-component.js diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 1ff06854847..589da9a85f4 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -13,7 +13,7 @@ function functionReturnsElement(path) { return true; } -function hotAssign(name, func) { +function hotAssign(types, name, func) { return [ { type: 'VariableDeclaration', @@ -39,18 +39,63 @@ function hotAssign(name, func) { }, ], }, - { - type: 'ExportDefaultDeclaration', - declaration: { type: 'Identifier', name: name }, - }, + types.exportDefaultDeclaration({ type: 'Identifier', name: name }), ]; } +function isHotCall(call) { + return ( + call && + call.type === 'CallExpression' && + call.callee.type === 'MemberExpression' && + call.callee.object.name === 'window' && + call.callee.property.name === '__assign' + ); +} + +function isVariableCandidate(declaration) { + return ( + declaration.id.type === 'Identifier' && + declaration.id.name[0] >= 'A' && + declaration.id.name[0] <= 'Z' && + declaration.init && + !isHotCall(declaration.init) + ); +} + +function hotDeclare(path) { + path.replaceWith({ + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: path.node.id.name, + }, + init: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'window' }, + property: { type: 'Identifier', name: '__assign' }, + computed: false, + }, + arguments: [ + { type: 'Identifier', name: 'module' }, + { type: 'StringLiteral', value: path.node.id.name }, + path.node.init, + ], + }, + }); +} + module.exports = function({ types }) { return { name: 'hot-reload', visitor: { ExportDefaultDeclaration(path) { + if (this.file.code.includes('no-hot')) { + return; + } + const { type } = path.node.declaration; if ( type !== 'FunctionDeclaration' || @@ -61,7 +106,24 @@ module.exports = function({ types }) { const { id: { name }, } = path.node.declaration; - path.replaceWithMultiple(hotAssign(name, path.node.declaration)); + path.replaceWithMultiple(hotAssign(types, name, path.node.declaration)); + }, + VariableDeclaration(path) { + if (path.parent.type !== 'Program') { + // Only traverse top level variable declaration + return; + } + if (this.file.code.includes('no-hot')) { + return; + } + + path.traverse({ + VariableDeclarator(path) { + if (isVariableCandidate(path.node)) { + hotDeclare(path); + } + }, + }); }, }, }; diff --git a/packages/babel-plugin-hot-reload/tests/fixtures/single-default-export.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js similarity index 100% rename from packages/babel-plugin-hot-reload/tests/fixtures/single-default-export.js rename to packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/empty-variable.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/empty-variable.js new file mode 100644 index 00000000000..a4d53dfb049 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/empty-variable.js @@ -0,0 +1,2 @@ +// no-snap +let LazyCC; diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/lazy-component.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/lazy-component.js new file mode 100644 index 00000000000..359892823f2 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/lazy-component.js @@ -0,0 +1,2 @@ +import { lazy } from 'react'; +const LazyCC = lazy(() => import('./CounterClass')); diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index 4402f2c36b4..33e1e2f9fea 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`hot-reload single default: single default 1`] = ` +exports[`hot-reload default export: default export 1`] = ` " import React from 'react'; import Hello from './Hello'; @@ -36,3 +36,16 @@ export default App; module.hot.accept(['./Layout', './Hello'], window.__invalidate); " `; + +exports[`hot-reload lazy component: lazy component 1`] = ` +" +import { lazy } from 'react'; +const LazyCC = lazy(() => import('./CounterClass')); + + ↓ ↓ ↓ ↓ ↓ ↓ + +import { lazy } from 'react'; + +const LazyCC = window.__assign(module, \\"LazyCC\\", lazy(() => import('./CounterClass'))); +" +`; diff --git a/packages/babel-plugin-hot-reload/tests/fixtures.test.js b/packages/babel-plugin-hot-reload/tests/fixtures.test.js index c68cbda3835..9c0e81d692d 100644 --- a/packages/babel-plugin-hot-reload/tests/fixtures.test.js +++ b/packages/babel-plugin-hot-reload/tests/fixtures.test.js @@ -3,21 +3,24 @@ const pluginTester = require('babel-plugin-tester'); const hotReload = require('..'); const path = require('path'); +const fs = require('fs'); pluginTester({ plugin: hotReload, filename: __filename, babelOptions: { - parserOpts: { plugins: ['jsx'] }, + parserOpts: { plugins: ['jsx', 'dynamicImport'] }, generatorOpts: {}, babelrc: false, }, snapshot: true, - tests: [ - { - title: 'single default', - fixture: path.join(__dirname, 'fixtures', 'single-default-export.js'), - snapshot: true, - }, - ], + tests: fs + .readdirSync(path.join(__dirname, '__fixtures__')) + .map(entry => path.join(__dirname, '__fixtures__', entry)) + .filter(entry => fs.lstatSync(entry).isFile() && entry.endsWith('js')) + .map(file => ({ + title: path.basename(file, '.js').replace(/-/g, ' '), + snapshot: !fs.readFileSync(file, 'utf8').includes('no-snap'), + fixture: file, + })), }); diff --git a/packages/react-scripts/template/src/hot.js b/packages/react-scripts/template/src/hot.js index 40cee12ff9a..1d523ef143b 100644 --- a/packages/react-scripts/template/src/hot.js +++ b/packages/react-scripts/template/src/hot.js @@ -1,3 +1,4 @@ +// no-hot import React from 'react'; let HotContext = React.createContext(); From ffefe1e069b4c72a242fa4a41a07a3eb456ff971 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:04:35 -0500 Subject: [PATCH 34/47] plugin: ensure variables are wrapped --- .../tests/__fixtures__/assignment.js | 2 ++ .../tests/__snapshots__/fixtures.test.js.snap | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js new file mode 100644 index 00000000000..f5426e7f647 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js @@ -0,0 +1,2 @@ +let LazyCC; +let DoubleLazyCC = LazyCC; diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index 33e1e2f9fea..f4dd94a13b8 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -1,5 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`hot-reload assignment: assignment 1`] = ` +" +let LazyCC; +let DoubleLazyCC = LazyCC; + + ↓ ↓ ↓ ↓ ↓ ↓ + +let LazyCC; + +let DoubleLazyCC = window.__assign(module, \\"DoubleLazyCC\\", LazyCC); +" +`; + exports[`hot-reload default export: default export 1`] = ` " import React from 'react'; From 101e5e2d98ffff5f2bd3d4d865e26738e1c36b4b Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:10:04 -0500 Subject: [PATCH 35/47] plugin: refactor --- packages/babel-plugin-hot-reload/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 589da9a85f4..6c44c7b3dda 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -53,11 +53,17 @@ function isHotCall(call) { ); } +function isIdentifierCandidate(identifier) { + return ( + identifier.type === 'Identifier' && + identifier.name[0] >= 'A' && + identifier.name[0] <= 'Z' + ); +} + function isVariableCandidate(declaration) { return ( - declaration.id.type === 'Identifier' && - declaration.id.name[0] >= 'A' && - declaration.id.name[0] <= 'Z' && + isIdentifierCandidate(declaration.id) && declaration.init && !isHotCall(declaration.init) ); From 1d7f9a176c00c5994be35cbd630a75ab22164991 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:20:20 -0500 Subject: [PATCH 36/47] plugin: register assignments --- packages/babel-plugin-hot-reload/index.js | 45 +++++++++++++++++++ .../tests/__fixtures__/assignment.js | 2 + .../tests/__snapshots__/fixtures.test.js.snap | 3 ++ 3 files changed, 50 insertions(+) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 6c44c7b3dda..58de1974ae7 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -69,7 +69,29 @@ function isVariableCandidate(declaration) { ); } +function isAssignmentCandidate(assignment) { + return isIdentifierCandidate(assignment.left) && assignment.operator === '='; +} + +function hotRegister(name, content) { + return { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { type: 'Identifier', name: 'window' }, + property: { type: 'Identifier', name: '__assign' }, + computed: false, + }, + arguments: [ + { type: 'Identifier', name: 'module' }, + { type: 'StringLiteral', value: name }, + content, + ], + }; +} + function hotDeclare(path) { + console.log('replacing', path.node.id); path.replaceWith({ type: 'VariableDeclarator', id: { @@ -131,6 +153,29 @@ module.exports = function({ types }) { }, }); }, + ExpressionStatement(path) { + if (path.parent.type !== 'Program') { + // Only traverse top level variable declaration + return; + } + if (this.file.code.includes('no-hot')) { + return; + } + + path.traverse({ + AssignmentExpression(path) { + if (isAssignmentCandidate(path.node)) { + if (!isHotCall(path.node.right)) { + console.log('replacing', path.node.left.name); + path.node.right = hotRegister( + path.node.left.name, + path.node.right + ); + } + } + }, + }); + }, }, }; }; diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js index f5426e7f647..f1c8fdc57d8 100644 --- a/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js @@ -1,2 +1,4 @@ let LazyCC; +LazyCC = 5; + let DoubleLazyCC = LazyCC; diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index f4dd94a13b8..dd439b623dd 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -3,11 +3,14 @@ exports[`hot-reload assignment: assignment 1`] = ` " let LazyCC; +LazyCC = 5; + let DoubleLazyCC = LazyCC; ↓ ↓ ↓ ↓ ↓ ↓ let LazyCC; +LazyCC = window.__assign(module, \\"LazyCC\\", 5); let DoubleLazyCC = window.__assign(module, \\"DoubleLazyCC\\", LazyCC); " From 0391778ee93d873e1fcb436139f762f6e264c8b6 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:30:40 -0500 Subject: [PATCH 37/47] plugin: add class test --- .../__fixtures__/default-export-class.js | 10 +++++++ .../tests/__snapshots__/fixtures.test.js.snap | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js new file mode 100644 index 00000000000..fee6b055534 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js @@ -0,0 +1,10 @@ +import React from 'react'; +import Hello from './Hello'; + +export default class App extends React.Component { + render() { + return ; + } +} + +module.hot.accept(['./Hello'], window.__invalidate); diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index dd439b623dd..7730eb20eb3 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -16,6 +16,33 @@ let DoubleLazyCC = window.__assign(module, \\"DoubleLazyCC\\", LazyCC); " `; +exports[`hot-reload default export class: default export class 1`] = ` +" +import React from 'react'; +import Hello from './Hello'; + +export default class App extends React.Component { + render() { + return ; + } +} + +module.hot.accept(['./Hello'], window.__invalidate); + + ↓ ↓ ↓ ↓ ↓ ↓ + +import React from 'react'; +import Hello from './Hello'; +export default class App extends React.Component { + render() { + return ; + } + +} +module.hot.accept(['./Hello'], window.__invalidate); +" +`; + exports[`hot-reload default export: default export 1`] = ` " import React from 'react'; From 76325bd2add7814370727b26ba3a893feb40f57b Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:32:03 -0500 Subject: [PATCH 38/47] plugin: remove debug --- packages/babel-plugin-hot-reload/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 58de1974ae7..dd5197c26d1 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -91,7 +91,6 @@ function hotRegister(name, content) { } function hotDeclare(path) { - console.log('replacing', path.node.id); path.replaceWith({ type: 'VariableDeclarator', id: { From 8c26b3a9e39ef1c8b0188796b3a651151b70a21e Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:36:22 -0500 Subject: [PATCH 39/47] example: remove __assign wrapping --- .../template/src/CounterClass.js | 2 +- .../template/src/CounterFunction.js | 61 ++++++++----------- packages/react-scripts/template/src/Hello.js | 25 +++----- 3 files changed, 37 insertions(+), 51 deletions(-) diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 68d60238cbe..1b86a1cee69 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -29,5 +29,5 @@ export default class Counter extends Component { } } -HFF = window.__assign(module, 'HFF', HOCFunction(CounterFunction)); +HFF = HOCFunction(CounterFunction); module.hot.accept(['./CounterFunction', './HOCFunction'], window.__invalidate); diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index bc694dc993b..f9a4fdc69d0 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -5,42 +5,35 @@ let HFF; let Counter; let Fwd; -Counter = window.__assign( - module, - 'Counter', - memo( - memo(function Counter(props) { - const [value, dispatch] = useReducer((v, a) => { - return a === 'inc' ? v + 1 : v; - }, 0); - useLayoutEffect(() => { - const id = setInterval(() => dispatch('inc'), 1000); - return () => clearInterval(id); - }, []); +Counter = memo( + memo(function Counter(props) { + const [value, dispatch] = useReducer((v, a) => { + return a === 'inc' ? v + 1 : v; + }, 0); + useLayoutEffect(() => { + const id = setInterval(() => dispatch('inc'), 1000); + return () => clearInterval(id); + }, []); - return ( - - {value} - {props.hocChild && ( - <> - (inner HOC: {HFF.field}) - - )} - - ); - }) - ) + return ( + + {value} + {props.hocChild && ( + <> + (inner HOC: {HFF.field}) + + )} + + ); + }) ); -Fwd = window.__assign( - module, - 'FWD', - forwardRef((props, ref) => ( - - - - )) -); -HFF = window.__assign(module, 'HFF', HOCFunction(Fwd)); + +Fwd = forwardRef((props, ref) => ( + + + +)); +HFF = HOCFunction(Fwd); module.hot.accept(['./HOCFunction'], window.__invalidate); diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index d033d6f7101..a59d32d1733 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -36,22 +36,15 @@ export default function Hello() { ); } -LazyCC = window.__assign( - module, - 'LazyCC', - lazy(() => import('./CounterClass')) -); -LazyCF = window.__assign( - module, - 'LazyCF', - lazy(() => import('./CounterFunction')) -); -DblCC = window.__assign(module, 'DblCC', CounterClass); -DblCF = window.__assign(module, 'DblCF', CounterFunction); -HCC = window.__assign(module, 'HCC', HOCClass(CounterClass, 'red')); -HCF = window.__assign(module, 'HCF', HOCClass(CounterFunction, 'orange')); -HFC = window.__assign(module, 'HFC', HOCFunction(CounterClass, 'yellow')); -HFF = window.__assign(module, 'HFF', HOCFunction(CounterFunction, 'green')); +LazyCC = lazy(() => import('./CounterClass')); +LazyCF = lazy(() => import('./CounterFunction')); + +DblCC = CounterClass; +DblCF = CounterFunction; +HCC = HOCClass(CounterClass, 'red'); +HCF = HOCClass(CounterFunction, 'orange'); +HFC = HOCFunction(CounterClass, 'yellow'); +HFF = HOCFunction(CounterFunction, 'green'); module.hot.accept( ['./CounterFunction', './CounterClass', './HOCFunction', './HOCClass'], window.__invalidate From 49de62f4cd71a2d678e4dd776346d51cbb71dd4f Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:40:04 -0500 Subject: [PATCH 40/47] plugin: remove debugging --- packages/babel-plugin-hot-reload/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index dd5197c26d1..4fed6aceb14 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -165,7 +165,6 @@ module.exports = function({ types }) { AssignmentExpression(path) { if (isAssignmentCandidate(path.node)) { if (!isHotCall(path.node.right)) { - console.log('replacing', path.node.left.name); path.node.right = hotRegister( path.node.left.name, path.node.right From 2b5c6eb3c461ab06c819c917179714878d996d29 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:46:43 -0500 Subject: [PATCH 41/47] example: unshuffle code --- .../template/src/CounterClass.js | 3 +-- .../template/src/CounterFunction.js | 18 ++++++------- packages/react-scripts/template/src/Hello.js | 25 ++++++------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 1b86a1cee69..3c434d30a13 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import CounterFunction from './CounterFunction'; import HOCFunction from './HOCFunction'; -let HFF; +let HFF = HOCFunction(CounterFunction); export default class Counter extends Component { state = { value: 0 }; @@ -29,5 +29,4 @@ export default class Counter extends Component { } } -HFF = HOCFunction(CounterFunction); module.hot.accept(['./CounterFunction', './HOCFunction'], window.__invalidate); diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index f9a4fdc69d0..1ea4ac21fa9 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -1,11 +1,14 @@ import React, { forwardRef, memo, useReducer, useLayoutEffect } from 'react'; import HOCFunction from './HOCFunction'; -let HFF; -let Counter; -let Fwd; +let Fwd = forwardRef((props, ref) => ( + + + +)); +let HFF = HOCFunction(Fwd); -Counter = memo( +let Counter = memo( memo(function Counter(props) { const [value, dispatch] = useReducer((v, a) => { return a === 'inc' ? v + 1 : v; @@ -28,13 +31,6 @@ Counter = memo( }) ); -Fwd = forwardRef((props, ref) => ( - - - -)); -HFF = HOCFunction(Fwd); - module.hot.accept(['./HOCFunction'], window.__invalidate); export let N = 10; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index a59d32d1733..dba0107f0a4 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -4,14 +4,14 @@ import HOCFunction from './HOCFunction'; import CounterClass from './CounterClass'; import CounterFunction, { N } from './CounterFunction'; -let LazyCC; -let LazyCF; -let DblCC; -let DblCF; -let HCC; -let HCF; -let HFC; -let HFF; +let LazyCC = lazy(() => import('./CounterClass')), + LazyCF = lazy(() => import('./CounterFunction')); +let DblCC = CounterClass, + DblCF = CounterFunction; +let HCC = HOCClass(CounterClass, 'red'), + HCF = HOCClass(CounterFunction, 'orange'), + HFC = HOCFunction(CounterClass, 'yellow'), + HFF = HOCFunction(CounterFunction, 'green'); export default function Hello() { const [value] = useState(Math.random()); @@ -36,15 +36,6 @@ export default function Hello() { ); } -LazyCC = lazy(() => import('./CounterClass')); -LazyCF = lazy(() => import('./CounterFunction')); - -DblCC = CounterClass; -DblCF = CounterFunction; -HCC = HOCClass(CounterClass, 'red'); -HCF = HOCClass(CounterFunction, 'orange'); -HFC = HOCFunction(CounterClass, 'yellow'); -HFF = HOCFunction(CounterFunction, 'green'); module.hot.accept( ['./CounterFunction', './CounterClass', './HOCFunction', './HOCClass'], window.__invalidate From 9d42a3aa9a273c6534f6219255728cd6d4d1fb81 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:53:38 -0500 Subject: [PATCH 42/47] plugin: refactor --- packages/babel-plugin-hot-reload/index.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 4fed6aceb14..c3013f7fae5 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -73,21 +73,11 @@ function isAssignmentCandidate(assignment) { return isIdentifierCandidate(assignment.left) && assignment.operator === '='; } -function hotRegister(name, content) { - return { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { type: 'Identifier', name: 'window' }, - property: { type: 'Identifier', name: '__assign' }, - computed: false, - }, - arguments: [ - { type: 'Identifier', name: 'module' }, - { type: 'StringLiteral', value: name }, - content, - ], - }; +function hotRegister(t, name, content) { + return t.callExpression( + t.memberExpression(t.identifier('window'), t.identifier('__assign')), + [t.identifier('module'), t.stringLiteral(name), content] + ); } function hotDeclare(path) { @@ -166,6 +156,7 @@ module.exports = function({ types }) { if (isAssignmentCandidate(path.node)) { if (!isHotCall(path.node.right)) { path.node.right = hotRegister( + types, path.node.left.name, path.node.right ); From 0553a4e0a07de90d69d9ca5a87562cb0b2b1cbbc Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 13:54:44 -0500 Subject: [PATCH 43/47] plugin: refactor --- packages/babel-plugin-hot-reload/index.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index c3013f7fae5..5752e006da7 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -80,27 +80,14 @@ function hotRegister(t, name, content) { ); } -function hotDeclare(path) { +function hotDeclare(types, path) { path.replaceWith({ type: 'VariableDeclarator', id: { type: 'Identifier', name: path.node.id.name, }, - init: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { type: 'Identifier', name: 'window' }, - property: { type: 'Identifier', name: '__assign' }, - computed: false, - }, - arguments: [ - { type: 'Identifier', name: 'module' }, - { type: 'StringLiteral', value: path.node.id.name }, - path.node.init, - ], - }, + init: hotRegister(types, path.node.id.name, path.node.init), }); } @@ -137,7 +124,7 @@ module.exports = function({ types }) { path.traverse({ VariableDeclarator(path) { if (isVariableCandidate(path.node)) { - hotDeclare(path); + hotDeclare(types, path); } }, }); From 22a15ebec123f32ac82cc8a61772c172ba1d01f3 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 14:06:30 -0500 Subject: [PATCH 44/47] plugin: refactor --- packages/babel-plugin-hot-reload/index.js | 47 ++++++------------- .../tests/__snapshots__/fixtures.test.js.snap | 2 +- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 5752e006da7..4e6eafb7a5b 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -15,30 +15,12 @@ function functionReturnsElement(path) { function hotAssign(types, name, func) { return [ - { - type: 'VariableDeclaration', - kind: 'let', - declarations: [ - { - type: 'VariableDeclarator', - id: { type: 'Identifier', name: name }, - init: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { type: 'Identifier', name: 'window' }, - property: { type: 'Identifier', name: '__assign' }, - computed: false, - }, - arguments: [ - { type: 'Identifier', name: 'module' }, - { type: 'StringLiteral', value: name }, - func, - ], - }, - }, - ], - }, + types.variableDeclaration('var', [ + types.variableDeclarator( + types.identifier(name), + hotRegister(types, name, func) + ), + ]), types.exportDefaultDeclaration({ type: 'Identifier', name: name }), ]; } @@ -74,6 +56,9 @@ function isAssignmentCandidate(assignment) { } function hotRegister(t, name, content) { + if (t.isFunctionDeclaration(content)) { + content.type = 'FunctionExpression'; // TODO: why do we have to do this hack? + } return t.callExpression( t.memberExpression(t.identifier('window'), t.identifier('__assign')), [t.identifier('module'), t.stringLiteral(name), content] @@ -81,14 +66,12 @@ function hotRegister(t, name, content) { } function hotDeclare(types, path) { - path.replaceWith({ - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: path.node.id.name, - }, - init: hotRegister(types, path.node.id.name, path.node.init), - }); + path.replaceWith( + types.variableDeclarator( + types.identifier(path.node.id.name), + hotRegister(types, path.node.id.name, path.node.init) + ) + ); } module.exports = function({ types }) { diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index 7730eb20eb3..24a3c10b47a 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -67,7 +67,7 @@ import Hello from './Hello'; import Layout from './Layout'; import './App.css'; -let App = window.__assign(module, \\"App\\", function App({ +var App = window.__assign(module, \\"App\\", function App({ children }) { return From 3a437e6e2320ec3a7e1698480cd0650a4b3c11e2 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 19:22:18 -0500 Subject: [PATCH 45/47] plugin: adjust fixtures --- .../tests/__fixtures__/components/Hello.js | 5 +++++ .../tests/__fixtures__/components/Layout.js | 5 +++++ .../tests/__fixtures__/default-export-class.js | 2 +- .../tests/__fixtures__/default-export.js | 4 ++-- .../tests/__snapshots__/fixtures.test.js.snap | 12 ++++++------ 5 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 packages/babel-plugin-hot-reload/tests/__fixtures__/components/Hello.js create mode 100644 packages/babel-plugin-hot-reload/tests/__fixtures__/components/Layout.js diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/components/Hello.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/components/Hello.js new file mode 100644 index 00000000000..5ca10759334 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/components/Hello.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default class extends React.Component { + render() {} +} diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/components/Layout.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/components/Layout.js new file mode 100644 index 00000000000..5ca10759334 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/components/Layout.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default class extends React.Component { + render() {} +} diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js index fee6b055534..434d1e31ee0 100644 --- a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js @@ -1,5 +1,5 @@ import React from 'react'; -import Hello from './Hello'; +import Hello from './components/Hello'; export default class App extends React.Component { render() { diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js index 79de8c953c0..87b01af1014 100644 --- a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js @@ -1,6 +1,6 @@ import React from 'react'; -import Hello from './Hello'; -import Layout from './Layout'; +import Hello from './components/Hello'; +import Layout from './components/Layout'; import './App.css'; export default function App({ children }) { diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index 24a3c10b47a..82d0c2b832d 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -19,7 +19,7 @@ let DoubleLazyCC = window.__assign(module, \\"DoubleLazyCC\\", LazyCC); exports[`hot-reload default export class: default export class 1`] = ` " import React from 'react'; -import Hello from './Hello'; +import Hello from './components/Hello'; export default class App extends React.Component { render() { @@ -32,7 +32,7 @@ module.hot.accept(['./Hello'], window.__invalidate); ↓ ↓ ↓ ↓ ↓ ↓ import React from 'react'; -import Hello from './Hello'; +import Hello from './components/Hello'; export default class App extends React.Component { render() { return ; @@ -46,8 +46,8 @@ module.hot.accept(['./Hello'], window.__invalidate); exports[`hot-reload default export: default export 1`] = ` " import React from 'react'; -import Hello from './Hello'; -import Layout from './Layout'; +import Hello from './components/Hello'; +import Layout from './components/Layout'; import './App.css'; export default function App({ children }) { @@ -63,8 +63,8 @@ module.hot.accept(['./Layout', './Hello'], window.__invalidate); ↓ ↓ ↓ ↓ ↓ ↓ import React from 'react'; -import Hello from './Hello'; -import Layout from './Layout'; +import Hello from './components/Hello'; +import Layout from './components/Layout'; import './App.css'; var App = window.__assign(module, \\"App\\", function App({ From fbaa7e6cabcf9ffbb11781876425186f2db3c78d Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 19:48:42 -0500 Subject: [PATCH 46/47] *: inject module.hot.accept --- packages/babel-plugin-hot-reload/index.js | 64 +++++++++++++++++++ .../__fixtures__/default-export-class.js | 2 - .../tests/__fixtures__/default-export.js | 2 - .../tests/__snapshots__/fixtures.test.js.snap | 9 +-- packages/react-scripts/template/src/App.js | 2 - .../template/src/CounterClass.js | 2 - .../template/src/CounterFunction.js | 2 - packages/react-scripts/template/src/Hello.js | 5 -- packages/react-scripts/template/src/index.js | 2 - 9 files changed, 67 insertions(+), 23 deletions(-) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index 4e6eafb7a5b..f3ea80f3f9a 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -1,4 +1,6 @@ 'use strict'; +const sysPath = require('path'); +const fs = require('fs'); function functionReturnsElement(path) { const { body } = path.body; @@ -74,10 +76,72 @@ function hotDeclare(types, path) { ); } +function isFile(path) { + try { + const stats = fs.lstatSync(path); + return stats.isFile(); + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } +} + +function naiveResolve(path, exts = ['js', 'jsx', 'ts', 'tsx']) { + return [path, ...exts.map(ext => path + '.' + ext)].find(isFile) || null; +} + +function shouldReloadFile(file) { + // TODO: this is really naive + const contents = fs.readFileSync(file, 'utf8'); + return contents.includes('React'); +} + module.exports = function({ types }) { return { name: 'hot-reload', visitor: { + ImportDeclaration(path) { + if (!types.isStringLiteral(path.node.source)) { + return; + } + + const target = path.node.source.value; + if (!target.startsWith('.')) { + return; + } + + const file = naiveResolve( + sysPath.resolve(sysPath.dirname(this.file.opts.filename), target) + ); + if (file == null) { + return; + } + + if (shouldReloadFile(file)) { + path.insertAfter( + types.expressionStatement( + types.callExpression( + types.memberExpression( + types.memberExpression( + types.identifier('module'), + types.identifier('hot') + ), + types.identifier('accept') + ), + [ + types.stringLiteral(target), + types.memberExpression( + types.identifier('window'), + types.identifier('__invalidate') + ), + ] + ) + ) + ); + } + }, ExportDefaultDeclaration(path) { if (this.file.code.includes('no-hot')) { return; diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js index 434d1e31ee0..44a35fed54f 100644 --- a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js @@ -6,5 +6,3 @@ export default class App extends React.Component { return ; } } - -module.hot.accept(['./Hello'], window.__invalidate); diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js index 87b01af1014..5a51123bfae 100644 --- a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js @@ -10,5 +10,3 @@ export default function App({ children }) { ); } - -module.hot.accept(['./Layout', './Hello'], window.__invalidate); diff --git a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap index 82d0c2b832d..fd283c38a11 100644 --- a/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -27,19 +27,17 @@ export default class App extends React.Component { } } -module.hot.accept(['./Hello'], window.__invalidate); - ↓ ↓ ↓ ↓ ↓ ↓ import React from 'react'; import Hello from './components/Hello'; +module.hot.accept(\\"./components/Hello\\", window.__invalidate); export default class App extends React.Component { render() { return ; } } -module.hot.accept(['./Hello'], window.__invalidate); " `; @@ -58,13 +56,13 @@ export default function App({ children }) { ); } -module.hot.accept(['./Layout', './Hello'], window.__invalidate); - ↓ ↓ ↓ ↓ ↓ ↓ import React from 'react'; import Hello from './components/Hello'; +module.hot.accept(\\"./components/Hello\\", window.__invalidate); import Layout from './components/Layout'; +module.hot.accept(\\"./components/Layout\\", window.__invalidate); import './App.css'; var App = window.__assign(module, \\"App\\", function App({ @@ -76,7 +74,6 @@ var App = window.__assign(module, \\"App\\", function App({ }); export default App; -module.hot.accept(['./Layout', './Hello'], window.__invalidate); " `; diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 79de8c953c0..6a387218dea 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -10,5 +10,3 @@ export default function App({ children }) { ); } - -module.hot.accept(['./Layout', './Hello'], window.__invalidate); diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js index 3c434d30a13..ddd8d7fff8f 100644 --- a/packages/react-scripts/template/src/CounterClass.js +++ b/packages/react-scripts/template/src/CounterClass.js @@ -28,5 +28,3 @@ export default class Counter extends Component { ); } } - -module.hot.accept(['./CounterFunction', './HOCFunction'], window.__invalidate); diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js index 1ea4ac21fa9..2b6d41accd0 100644 --- a/packages/react-scripts/template/src/CounterFunction.js +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -31,7 +31,5 @@ let Counter = memo( }) ); -module.hot.accept(['./HOCFunction'], window.__invalidate); - export let N = 10; export default Fwd; diff --git a/packages/react-scripts/template/src/Hello.js b/packages/react-scripts/template/src/Hello.js index dba0107f0a4..ea1d0e53ec6 100644 --- a/packages/react-scripts/template/src/Hello.js +++ b/packages/react-scripts/template/src/Hello.js @@ -35,8 +35,3 @@ export default function Hello() { ); } - -module.hot.accept( - ['./CounterFunction', './CounterClass', './HOCFunction', './HOCClass'], - window.__invalidate -); diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index 7e950026ed8..b86dd84a690 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -12,8 +12,6 @@ ReactDOM.render( document.getElementById('root') ); -module.hot.accept(['./App'], window.__invalidate); - // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA From 9096348a0baff11df5c22f93fe0caaa0c6e0d937 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Sun, 9 Dec 2018 19:59:06 -0500 Subject: [PATCH 47/47] Add no hot marker --- packages/babel-plugin-hot-reload/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js index f3ea80f3f9a..587aa6cf6ac 100644 --- a/packages/babel-plugin-hot-reload/index.js +++ b/packages/babel-plugin-hot-reload/index.js @@ -103,6 +103,10 @@ module.exports = function({ types }) { name: 'hot-reload', visitor: { ImportDeclaration(path) { + if (this.file.code.includes('no-hot')) { + return; + } + if (!types.isStringLiteral(path.node.source)) { return; }