diff --git a/packages/babel-plugin-hot-reload/index.js b/packages/babel-plugin-hot-reload/index.js new file mode 100644 index 00000000000..587aa6cf6ac --- /dev/null +++ b/packages/babel-plugin-hot-reload/index.js @@ -0,0 +1,208 @@ +'use strict'; +const sysPath = require('path'); +const fs = require('fs'); + +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 hotAssign(types, name, func) { + return [ + types.variableDeclaration('var', [ + types.variableDeclarator( + types.identifier(name), + hotRegister(types, name, func) + ), + ]), + 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 isIdentifierCandidate(identifier) { + return ( + identifier.type === 'Identifier' && + identifier.name[0] >= 'A' && + identifier.name[0] <= 'Z' + ); +} + +function isVariableCandidate(declaration) { + return ( + isIdentifierCandidate(declaration.id) && + declaration.init && + !isHotCall(declaration.init) + ); +} + +function isAssignmentCandidate(assignment) { + return isIdentifierCandidate(assignment.left) && assignment.operator === '='; +} + +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] + ); +} + +function hotDeclare(types, path) { + path.replaceWith( + types.variableDeclarator( + types.identifier(path.node.id.name), + hotRegister(types, path.node.id.name, path.node.init) + ) + ); +} + +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 (this.file.code.includes('no-hot')) { + return; + } + + 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; + } + + const { type } = path.node.declaration; + if ( + type !== 'FunctionDeclaration' || + !functionReturnsElement(path.node.declaration) + ) { + return; + } + const { + id: { 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(types, path); + } + }, + }); + }, + 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)) { + path.node.right = hotRegister( + types, + path.node.left.name, + path.node.right + ); + } + } + }, + }); + }, + }, + }; +}; diff --git a/packages/babel-plugin-hot-reload/package.json b/packages/babel-plugin-hot-reload/package.json new file mode 100644 index 00000000000..0c50959c06b --- /dev/null +++ b/packages/babel-plugin-hot-reload/package.json @@ -0,0 +1,18 @@ +{ + "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": {}, + "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/__fixtures__/assignment.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js new file mode 100644 index 00000000000..f1c8fdc57d8 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/assignment.js @@ -0,0 +1,4 @@ +let LazyCC; +LazyCC = 5; + +let DoubleLazyCC = LazyCC; 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 new file mode 100644 index 00000000000..44a35fed54f --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export-class.js @@ -0,0 +1,8 @@ +import React from 'react'; +import Hello from './components/Hello'; + +export default class App extends React.Component { + render() { + return ; + } +} diff --git a/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js new file mode 100644 index 00000000000..5a51123bfae --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__fixtures__/default-export.js @@ -0,0 +1,12 @@ +import React from 'react'; +import Hello from './components/Hello'; +import Layout from './components/Layout'; +import './App.css'; + +export default function App({ children }) { + return ( + + + + ); +} 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 new file mode 100644 index 00000000000..fd283c38a11 --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/__snapshots__/fixtures.test.js.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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); +" +`; + +exports[`hot-reload default export class: default export class 1`] = ` +" +import React from 'react'; +import Hello from './components/Hello'; + +export default class App extends React.Component { + render() { + return ; + } +} + + ↓ ↓ ↓ ↓ ↓ ↓ + +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 ; + } + +} +" +`; + +exports[`hot-reload default export: default export 1`] = ` +" +import React from 'react'; +import Hello from './components/Hello'; +import Layout from './components/Layout'; +import './App.css'; + +export default function App({ children }) { + return ( + + + + ); +} + + ↓ ↓ ↓ ↓ ↓ ↓ + +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({ + children +}) { + return + + ; +}); + +export default App; +" +`; + +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 new file mode 100644 index 00000000000..9c0e81d692d --- /dev/null +++ b/packages/babel-plugin-hot-reload/tests/fixtures.test.js @@ -0,0 +1,26 @@ +'use strict'; + +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', 'dynamicImport'] }, + generatorOpts: {}, + babelrc: false, + }, + 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/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" } 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" diff --git a/packages/react-scripts/template/src/App.js b/packages/react-scripts/template/src/App.js index 7e261ca47e6..6a387218dea 100644 --- a/packages/react-scripts/template/src/App.js +++ b/packages/react-scripts/template/src/App.js @@ -1,28 +1,12 @@ -import React, { Component } from 'react'; -import logo from './logo.svg'; +import React from 'react'; +import Hello from './Hello'; +import Layout from './Layout'; import './App.css'; -class App extends Component { - render() { - return ( -
-
- logo -

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

- - Learn React - -
-
- ); - } +export default function App({ children }) { + return ( + + + + ); } - -export default App; diff --git a/packages/react-scripts/template/src/CounterClass.js b/packages/react-scripts/template/src/CounterClass.js new file mode 100644 index 00000000000..ddd8d7fff8f --- /dev/null +++ b/packages/react-scripts/template/src/CounterClass.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import CounterFunction from './CounterFunction'; +import HOCFunction from './HOCFunction'; + +let HFF = HOCFunction(CounterFunction); + +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}{' '} + {this.props.hocChild && ( + <> + (inner HOC: {HFF.field}) + + )} + + ); + } +} diff --git a/packages/react-scripts/template/src/CounterFunction.js b/packages/react-scripts/template/src/CounterFunction.js new file mode 100644 index 00000000000..2b6d41accd0 --- /dev/null +++ b/packages/react-scripts/template/src/CounterFunction.js @@ -0,0 +1,35 @@ +import React, { forwardRef, memo, useReducer, useLayoutEffect } from 'react'; +import HOCFunction from './HOCFunction'; + +let Fwd = forwardRef((props, ref) => ( + + + +)); +let HFF = HOCFunction(Fwd); + +let 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}) + + )} + + ); + }) +); + +export let N = 10; +export default Fwd; diff --git a/packages/react-scripts/template/src/HOCClass.js b/packages/react-scripts/template/src/HOCClass.js new file mode 100644 index 00000000000..5f442b854e3 --- /dev/null +++ b/packages/react-scripts/template/src/HOCClass.js @@ -0,0 +1,14 @@ +import React, { Component } from 'react'; + +export default function withStuff(Wrapped, color) { + return class extends Component { + static field = 42; + 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..029790888e1 --- /dev/null +++ b/packages/react-scripts/template/src/HOCFunction.js @@ -0,0 +1,13 @@ +import React from 'react'; + +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 new file mode 100644 index 00000000000..ea1d0e53ec6 --- /dev/null +++ b/packages/react-scripts/template/src/Hello.js @@ -0,0 +1,37 @@ +import React, { Suspense, lazy, useState } from 'react'; +import HOCClass from './HOCClass'; +import HOCFunction from './HOCFunction'; +import CounterClass from './CounterClass'; +import CounterFunction, { N } from './CounterFunction'; + +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()); + return ( + }> +

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

+
+ ); +} 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/TODO.md b/packages/react-scripts/template/src/TODO.md new file mode 100644 index 00000000000..88964b96a45 --- /dev/null +++ b/packages/react-scripts/template/src/TODO.md @@ -0,0 +1,49 @@ +- 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 + https://mobile.twitter.com/wSokra/status/1071503979014692865 + +- effects can change arg num + +* frags +* error handling + - reenable overlay + - offer a way to reset? + - suppress error dialog? + - suppress messages? + - check types? debug tools? + +- highlight updates + +* directives? + - ! +* hooks + - remount on error? + - compare introspection? +* 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 +* 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 new file mode 100644 index 00000000000..1d523ef143b --- /dev/null +++ b/packages/react-scripts/template/src/hot.js @@ -0,0 +1,272 @@ +// no-hot +import React from 'react'; + +let HotContext = React.createContext(); + +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); + __setInc = setInc; + 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(); + +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'; + } else if (type.$$typeof === Symbol.for('react.lazy')) { + return 'lazy'; + } else if (type.$$typeof === Symbol.for('react.forward_ref')) { + return 'forward_ref'; + } + } + return 'other'; +} + +function init(rawType, id) { + let kind = getKind(rawType); + switch (kind) { + case 'function': { + if (proxies.has(rawType)) { + 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) { + readContext(HotContext); + + let realDispatcher = CurrentOwner.currentDispatcher; + let freshRawType = idToRawFunction.get(id); + let currentHooks = []; + + function callNoopHook([hook, data]) { + 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: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } + realDispatcher.useLayoutEffect(() => {}, inputs); + break; + } + case realDispatcher.useEffect: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } + realDispatcher.useEffect(() => {}, inputs); + break; + } + case realDispatcher.useMemo: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } + realDispatcher.useMemo(() => {}, inputs); + break; + } + case realDispatcher.useCallback: { + let inputs; + if (data) { + inputs = new Array(data).fill(NOOP); + } + 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) { + let prevHook = previousHooks[currentHooks.length]; + if (prevHook && prevHook[0] !== hook) { + throw new Error('Hook mismatch.'); + } + 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: + 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(); + // Shift Hooks to recover. This leaks memory. Ideallly we'd reset. + previousHooks.push([realDispatcher.useState]); + abandonedHooks.push(...previousHooks); + previousHooks = []; + reset(); + } + + return ( + {ret} + ); + }, + }); + proxies.add(proxy); + return proxy; + } + case 'memo': { + rawType.type = init(rawType.type, id); + return rawType; + } + case 'lazy': { + return rawType; + } + case 'forward_ref': { + rawType.render = init(rawType.render, 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': { + idToRawFunction.set(id, nextRawType); + const forceRemount = + nextRawType.toString().indexOf('//!') !== -1 || + nextRawType.toString().indexOf('// !') !== -1; + return !forceRemount; + } + case 'memo': { + return accept(type.type, nextRawType.type, id); + } + case 'forward_ref': { + return accept(type.render, nextRawType.render, id); + } + case 'lazy': { + return true; + } + default: { + return false; + } + } +} + +window.__assign = function(webpackModule, localId, nextRawType) { + const id = webpackModule.i + '$' + localId; + let type = idToPersistentType.get(id); + if (!accept(type, nextRawType, id)) { + type = init(nextRawType, id); + idToPersistentType.set(id, type); + } + return type; +}; diff --git a/packages/react-scripts/template/src/index.js b/packages/react-scripts/template/src/index.js index 0c5e75da1cd..b86dd84a690 100644 --- a/packages/react-scripts/template/src/index.js +++ b/packages/react-scripts/template/src/index.js @@ -1,10 +1,16 @@ 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'; -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.