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

Customize scope for arrow functions #679

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"dependencies": {
"global": "^4.3.0",
"react-deep-force-update": "^2.1.1",
"react-proxy": "^3.0.0-alpha.0",
"react-stand-in": "^1.0.2",
"redbox-react": "^1.3.6",
"source-map": "^0.6.1"
},
Expand Down
179 changes: 18 additions & 161 deletions src/babel/index.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,5 @@
const replaced = Symbol('replaced')

const buildNewClassProperty = (
t,
classPropertyName,
newMethodName,
isAsync,
) => {
let returnExpression = t.callExpression(
t.memberExpression(t.thisExpression(), newMethodName),
[t.spreadElement(t.identifier('params'))],
)

if (isAsync) {
returnExpression = t.awaitExpression(returnExpression)
}

const newArrowFunction = t.arrowFunctionExpression(
[t.restElement(t.identifier('params'))],
returnExpression,
isAsync,
)
return t.classProperty(classPropertyName, newArrowFunction)
}

const buildNewAssignmentExpression = (
t,
classPropertyName,
newMethodName,
isAsync,
) => {
let returnExpression = t.callExpression(
t.memberExpression(t.thisExpression(), newMethodName),
[t.spreadElement(t.identifier('params'))],
)

if (isAsync) {
returnExpression = t.awaitExpression(returnExpression)
}

const newArrowFunction = t.arrowFunctionExpression(
[t.restElement(t.identifier('params'))],
returnExpression,
isAsync,
)
const left = t.memberExpression(
t.thisExpression(),
t.identifier(classPropertyName.name),
)

const replacement = t.assignmentExpression('=', left, newArrowFunction)
replacement[replaced] = true

return replacement
}

const classPropertyOptOutVistor = {
MetaProperty(path, state) {
Expand Down Expand Up @@ -91,6 +38,10 @@ module.exports = function plugin(args) {
'__REACT_HOT_LOADER__.register(ID, NAME, FILENAME);',
)

var evalTemplate = template('this[key]=eval(code);')

var rewireSuperTemplate = template('ID=superClass;');

// We're making the IIFE we insert at the end of the file an unused variable
// because it otherwise breaks the output of the babel-node REPL (#359).
const buildTagger = template(`
Expand Down Expand Up @@ -205,114 +156,20 @@ module.exports = function plugin(args) {
Class(classPath) {
const classBody = classPath.get('body')

classBody.get('body').forEach(path => {
if (path.isClassProperty()) {
const { node } = path

// don't apply transform to static class properties
if (node.static) {
return
}

const state = {
optOut: false,
}

path.traverse(classPropertyOptOutVistor, state)

if (state.optOut) {
return
}

// class property node value is nullable
if (node.value && node.value.type === 'ArrowFunctionExpression') {
const isAsync = node.value.async

// TODO:
// Remove this check when babel issue is resolved: https://github.com/babel/babel/issues/5078
// RHL Issue: https://github.com/gaearon/react-hot-loader/issues/391
// This code makes async arrow functions not reloadable,
// but doesn't break code any more when using 'this' inside AAF
if (isAsync) {
return
}

const { params } = node.value
const newIdentifier = t.identifier(
`__${node.key.name}__REACT_HOT_LOADER__`,
)

// arrow function body can either be a block statement or a returned expression
const newMethodBody =
node.value.body.type === 'BlockStatement'
? node.value.body
: t.blockStatement([t.returnStatement(node.value.body)])

// create a new method on the class that the original class property function
// calls, since the method is able to be replaced by RHL
const newMethod = t.classMethod(
'method',
newIdentifier,
params,
newMethodBody,
)
newMethod.async = isAsync
path.insertAfter(newMethod)

// replace the original class property function with a function that calls
// the new class method created above
path.replaceWith(
buildNewClassProperty(t, node.key, newIdentifier, isAsync),
)
}
} else if (!path.node[replaced] && path.node.kind === 'constructor') {
path.traverse({
AssignmentExpression(exp) {
if (
!exp.node[replaced] &&
exp.node.left.type === 'MemberExpression' &&
exp.node.left.object.type === 'ThisExpression' &&
exp.node.right.type === 'ArrowFunctionExpression'
) {
const key = exp.node.left.property
const node = exp.node.right

const isAsync = node.async
const { params } = node
const newIdentifier = t.identifier(
`__${key.name}__REACT_HOT_LOADER__`,
)

// arrow function body can either be a block statement or a returned expression
const newMethodBody =
node.body.type === 'BlockStatement'
? node.body
: t.blockStatement([t.returnStatement(node.body)])

const newMethod = t.classMethod(
'method',
newIdentifier,
params,
newMethodBody,
)
newMethod.async = isAsync
newMethod[replaced] = true
path.insertAfter(newMethod)

// replace assignment exp
exp.replaceWith(
buildNewAssignmentExpression(
t,
key,
newIdentifier,
isAsync,
),
)
}
},
})
}
})
var newMethod = t.classMethod(
'method',
t.identifier('__facade__regenerateByEval'),
[t.identifier('key'), t.identifier('code')],
t.blockStatement([evalTemplate()])
)
classBody.pushContainer('body',newMethod)

var newMethod = t.classMethod(
'method',
t.identifier('__facade__rewireSuper'),
[t.identifier('superClass')],
t.blockStatement([rewireSuperTemplate({ ID: t.identifier(classPath.node.id.name) })]));
classBody.pushContainer('body', newMethod);
},
},
}
Expand Down
25 changes: 18 additions & 7 deletions src/patch.dev.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const React = require('react')
const createProxy = require('react-proxy').default
const createProxy = require('react-stand-in').default
const global = require('global')

class ComponentMap {
Expand Down Expand Up @@ -68,11 +68,18 @@ let proxiesByID
let didWarnAboutID
let hasCreatedElementsByType
let idsByType
let firstInstances
let knownSignatures
let didUpdateProxy

function replaceFirstInstance (id, type) {
try {
firstInstances[id].prototype.__facade__rewireSuper(type)
} catch (e) {}
}

const hooks = {
register(type, uniqueLocalName, fileName) {
register(type, uniqueLocalName, fileName, replaceCallback) {
if (typeof type !== 'function') {
return
}
Expand All @@ -82,16 +89,17 @@ const hooks = {
if (typeof uniqueLocalName !== 'string' || typeof fileName !== 'string') {
return
}

const id = fileName + '#' + uniqueLocalName // eslint-disable-line prefer-template
if (!idsByType.has(type) && hasCreatedElementsByType.has(type)) {
if (!didWarnAboutID[id]) {
didWarnAboutID[id] = true
const baseName = fileName.replace(/^.*[\\/]/, '')
console.error(
`React Hot Loader: ${uniqueLocalName} in ${fileName} will not hot reload ` +
`correctly because ${baseName} uses <${uniqueLocalName} /> during ` +
`module definition. For hot reloading to work, move ${uniqueLocalName} ` +
`into a separate file and import it from ${baseName}.`,
`correctly because ${baseName} uses <${uniqueLocalName} /> during ` +
`module definition. For hot reloading to work, move ${uniqueLocalName} ` +
`into a separate file and import it from ${baseName}.`,
)
}
return
Expand All @@ -104,8 +112,10 @@ const hooks = {
// the same way as the original classes but are updatable with
// new versions without destroying original instances.
if (!proxiesByID[id]) {
firstInstances[id] = type;
proxiesByID[id] = createProxy(type)
} else {
replaceFirstInstance(id, type)
proxiesByID[id].update(type)
didUpdateProxy = true
}
Expand All @@ -116,6 +126,7 @@ const hooks = {
didWarnAboutID = {}
hasCreatedElementsByType = new ComponentMap(useWeakMap)
idsByType = new ComponentMap(useWeakMap)
firstInstances = {}
knownSignatures = {}
didUpdateProxy = false
},
Expand All @@ -129,8 +140,8 @@ function warnAboutUnnacceptedClass(typeSignature) {
if (didUpdateProxy && global.__REACT_HOT_LOADER__.warnings !== false) {
console.warn(
'React Hot Loader: this component is not accepted by Hot Loader. \n' +
'Please check is it extracted as a top level class, a function or a variable. \n' +
'Click below to reveal the source location: \n',
'Please check is it extracted as a top level class, a function or a variable. \n' +
'Click below to reveal the source location: \n',
typeSignature,
)
}
Expand Down
Loading