Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept: simple hot reloading #2304

Closed
wants to merge 19 commits into from
Closed
137 changes: 137 additions & 0 deletions packages/babel-plugin-react-app/index.js
Original file line number Diff line number Diff line change
@@ -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) }),
]);
},
},
};
};
13 changes: 13 additions & 0 deletions packages/babel-plugin-react-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "babel-plugin-react-app",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "BSD-3-Clause",
"dependencies": {
"babel-template": "6.24.1"
}
}
3 changes: 3 additions & 0 deletions packages/babel-preset-react-app/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ module.exports = function(api, opts, env) {
isEnvTest &&
// Transform dynamic import to require
require('babel-plugin-dynamic-import-node'),
isEnvDevelopment &&
// Transform for functional hot reloading
require('babel-plugin-react-app'),
].filter(Boolean),
};
};
3 changes: 2 additions & 1 deletion packages/babel-preset-react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"babel-loader": "8.0.4",
"babel-plugin-dynamic-import-node": "2.2.0",
"babel-plugin-macros": "2.4.2",
"babel-plugin-transform-react-remove-prop-types": "0.4.18"
"babel-plugin-transform-react-remove-prop-types": "0.4.18",
"babel-plugin-react-app": "^0.0.0"
}
}
112 changes: 112 additions & 0 deletions packages/react-dev-utils/forceUpdateHook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

'use strict';

// TODO: this is noisy when client is not running
// but it still gets the job done. Add a silent mode
// that won't attempt to connect maybe?
require('react-devtools-core');

const highlight = require('./nodeHighlighter');

let forceUpdateCallbacks = [];
let forceUpdateTypes = [];
let forceUpdateTimeout = null;
let nodes = [];
window.__enqueueForceUpdate = function(onSuccess, type) {
forceUpdateCallbacks.push(onSuccess);
forceUpdateTypes.push(type);
if (forceUpdateTimeout) {
return;
}
forceUpdateTimeout = setTimeout(() => {
forceUpdateTimeout = null;
let callbacks = forceUpdateCallbacks;
forceUpdateCallbacks = [];
let types = forceUpdateTypes;
forceUpdateTypes = [];
forceUpdateAll(types);
callbacks.forEach(cb => cb());
highlight(nodes);
nodes = [];
if (console != null) {
if (typeof console.clear === 'function') {
console.clear();
}
if (typeof console.info === 'function') {
const names = types
.map(type => type.displayName || type.name)
.filter(Boolean);
if (names.length > 0) {
console.info('Reloaded components: ' + names.join(',') + '.');
}
}
}
});
};

function traverseDeep(root, onUpdate) {
let node = root;
while (true) {
node.expirationTime = 1;
if (node.alternate) {
node.alternate.expirationTime = 1;
}
if (node.tag === 1) {
onUpdate(node);
}
if (node.child) {
node.child.return = node;
node = node.child;
continue;
}
if (node === root) {
return;
}
while (!node.sibling) {
if (!node.return || node.return === root) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}

function forceUpdateAll(types) {
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
const renderersById = hook._renderers;
const ids = Object.keys(renderersById);
ids.forEach(id => {
const renderer = renderersById[id];
const roots = hook.getFiberRoots(id);
if (!roots.size) {
return;
}
// TODO: this is WAY too brittle.
roots.forEach(root => {
const reactRoot = root.containerInfo._reactRootContainer;
traverseDeep(root.current, node => {
const type = node.type;
const { __hot__id } = type;
if (
types.find(
t => t === type || (__hot__id && t.__hot__id === __hot__id)
)
) {
nodes.push(renderer.findHostInstanceByFiber(node));
}
node.memoizedProps = Object.assign({}, node.memoizedProps);
});
reactRoot.render(root.current.memoizedState.element);
});
});
}
Loading