Skip to content

Commit

Permalink
feat: webpack patch/inject mode
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Nov 11, 2018
1 parent 65309fc commit 42d637b
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 97 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@
"standard-version": "^4.3.0"
},
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0"
"react": "^15.0.0 || ^16.0.0",
"react-dom": "^15.0.0 || ^16.0.0"
},
"dependencies": {
"fast-levenshtein": "^2.0.6",
"global": "^4.3.0",
"hoist-non-react-statics": "^2.5.0",
"loader-utils": "^1.1.0",
"prop-types": "^15.6.1",
"react-lifecycles-compat": "^3.0.4",
"shallowequal": "^1.0.2",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const commonPlugins = [

const getConfig = (input, dist, env) => ({
input,
external: ['react', 'fs', 'path'].concat(Object.keys(pkg.dependencies)),
external: ['react-dom','react', 'fs', 'path'].concat(Object.keys(pkg.dependencies)),
plugins: commonPlugins
.concat(env ? [
replace({
Expand Down
9 changes: 9 additions & 0 deletions src/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ const configuration = {
// Allows using SFC without changes. leading to some components not updated
pureSFC: false,

// keep render method unpatched, moving sideEffect to componentWillUpdate
pureRender: true,

// Allows SFC to be used, enables "intermediate" components used by Relay, should be disabled for Preact
allowSFC: true,

// Disable "hot-replacement-render"
disableHotRenderer: false,

// Disable "hot-replacement-render" when injection into react-dom are made
disableHotRendererWhenInjected: false,

// Hook on babel component register.
onComponentRegister: false,

Expand Down
14 changes: 13 additions & 1 deletion src/proxy/createClassProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
instancesCount++
},
)
const UNSAFE_componentWillUpdate = lifeCycleWrapperFactory(
'UNSAFE_componentWillUpdate',
() => ({}),
)
const componentWillUpdate = lifeCycleWrapperFactory(
'componentWillUpdate',
() => ({}),
)
const componentDidUpdate = lifeCycleWrapperFactory(
'componentDidUpdate',
renderOptions.componentDidUpdate,
Expand Down Expand Up @@ -225,7 +233,11 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
const defineProxyMethods = (Proxy, Base = {}) => {
defineClassMembers(Proxy, {
...fakeBasePrototype(Base),
render: proxiedRender,
...(proxyConfig.pureRender
? { render: proxiedRender }
: Base.componentWillUpdate
? componentWillUpdate
: UNSAFE_componentWillUpdate),
hotComponentRender,
componentDidMount,
componentDidUpdate,
Expand Down
49 changes: 41 additions & 8 deletions src/reactHotLoader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-use-before-define */
import React from 'react'
import ReactDOM from 'react-dom'
import {
isCompositeComponent,
getComponentDisplayName,
Expand All @@ -23,22 +24,27 @@ import configuration from './configuration'
import logger from './logger'

import { preactAdapter } from './adapters/preact'
import { areSwappable } from './reconciler/utils'
import { PROXY_KEY, UNWRAP_PROXY } from './proxy'

const forceSimpleSFC = { proxy: { allowSFC: false } }
const lazyConstructor = '_ctor'

const updateLazy = (target, type) => {
const ctor = type[lazyConstructor]
if (target[lazyConstructor] !== type[lazyConstructor]) {
ctor()
// ctor()
}
if (!target[lazyConstructor].isPatchedByReactHotLoader) {
target[lazyConstructor] = () =>
ctor().then(m => {
const C = resolveType(m.default)
return {
default: props => <C {...props} />,
}
})
target[lazyConstructor].isPatchedByReactHotLoader = true
}
target[lazyConstructor] = () =>
ctor().then(m => {
const C = resolveType(m.default)
return {
default: props => <C {...props} />,
}
})
}
const updateMemo = (target, { type }) => {
target.type = resolveType(type)
Expand All @@ -47,6 +53,28 @@ const updateForward = (target, { render }) => {
target.render = render
}

export const hotComponentCompare = (oldType, newType) => {
if (oldType === newType) {
return true
}

if (isForwardType({ type: oldType }) && isForwardType({ type: newType })) {
return areSwappable(oldType.render, newType.render)
}

if (areSwappable(newType, oldType)) {
const oldProxy = getProxyByType(newType[UNWRAP_PROXY]())
if (oldProxy) {
oldProxy.dereference()
updateProxyById(oldType[PROXY_KEY], newType[UNWRAP_PROXY]())
updateProxyById(newType[PROXY_KEY], oldType[UNWRAP_PROXY]())
}
return true
}

return false
}

const shouldNotPatchComponent = type =>
!isCompositeComponent(type) || isTypeBlacklisted(type) || isProxyType(type)

Expand Down Expand Up @@ -142,6 +170,11 @@ const reactHotLoader = {
},

patch(React) {
if (ReactDOM.setHotElementComparator) {
ReactDOM.setHotElementComparator(hotComponentCompare)
configuration.disableHotRenderer =
configuration.disableHotRendererWhenInjected
}
if (!React.createElement.isPatchedByReactHotLoader) {
const originalCreateElement = React.createElement
// Trick React into rendering a proxy so that
Expand Down
92 changes: 10 additions & 82 deletions src/reconciler/hotReplacementRender.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import levenshtein from 'fast-levenshtein'
import { PROXY_IS_MOUNTED, PROXY_KEY, UNWRAP_PROXY } from '../proxy'
import {
getIdByType,
getProxyByType,
isRegisteredComponent,
isTypeBlacklisted,
Expand All @@ -14,7 +12,6 @@ import {
isContextConsumer,
isContextProvider,
getContextProvider,
isReactClass,
isReactClassInstance,
CONTEXT_CURRENT_VALUE,
isMemoType,
Expand All @@ -23,12 +20,8 @@ import {
} from '../internal/reactUtils'
import reactHotLoader from '../reactHotLoader'
import logger from '../logger'

// some `empty` names, React can autoset display name to...
const UNDEFINED_NAMES = {
Unknown: true,
Component: true,
}
import configuration from '../configuration'
import { areSwappable } from './utils'

let renderStack = []

Expand All @@ -40,19 +33,12 @@ const stackReport = () => {
const emptyMap = new Map()
const stackContext = () =>
(renderStack[renderStack.length - 1] || {}).context || emptyMap
const areNamesEqual = (a, b) =>
a === b || (UNDEFINED_NAMES[a] && UNDEFINED_NAMES[b])

const shouldUseRenderMethod = fn =>
fn && (isReactClassInstance(fn) || fn.SFC_fake)

const isFunctional = fn => typeof fn === 'function'
const isArray = fn => Array.isArray(fn)
const asArray = a => (isArray(a) ? a : [a])
const getTypeOf = type => {
if (isReactClass(type)) return 'ReactComponent'
if (isFunctional(type)) return 'StatelessFunctional'
return 'Fragment' // ?
}
const getElementType = child =>
child.type[UNWRAP_PROXY] ? child.type[UNWRAP_PROXY]() : child.type

const filterNullArray = a => {
if (!a) return []
Expand All @@ -69,69 +55,8 @@ const unflatten = a =>
return acc
}, [])

const getElementType = child =>
child.type[UNWRAP_PROXY] ? child.type[UNWRAP_PROXY]() : child.type

const haveTextSimilarity = (a, b) =>
// equal or slight changed
a === b || levenshtein.get(a, b) < a.length * 0.2

const equalClasses = (a, b) => {
const prototypeA = a.prototype
const prototypeB = Object.getPrototypeOf(b.prototype)

let hits = 0
let misses = 0
let comparisons = 0
Object.getOwnPropertyNames(prototypeA).forEach(key => {
const descriptorA = Object.getOwnPropertyDescriptor(prototypeA, key)
const valueA =
descriptorA && (descriptorA.value || descriptorA.get || descriptorA.set)
const descriptorB = Object.getOwnPropertyDescriptor(prototypeB, key)
const valueB =
descriptorB && (descriptorB.value || descriptorB.get || descriptorB.set)

if (typeof valueA === 'function' && key !== 'constructor') {
comparisons++
if (haveTextSimilarity(String(valueA), String(valueB))) {
hits++
} else {
misses++
if (key === 'render') {
misses++
}
}
}
})
// allow to add or remove one function
return (hits > 0 && misses <= 1) || comparisons === 0
}

export const areSwappable = (a, b) => {
// both are registered components and have the same name
if (getIdByType(b) && getIdByType(a) === getIdByType(b)) {
return true
}
if (getTypeOf(a) !== getTypeOf(b)) {
return false
}
if (isReactClass(a)) {
return (
areNamesEqual(getComponentDisplayName(a), getComponentDisplayName(b)) &&
equalClasses(a, b)
)
}

if (isFunctional(a)) {
const nameA = getComponentDisplayName(a)
return (
(areNamesEqual(nameA, getComponentDisplayName(b)) &&
nameA !== 'Component') ||
haveTextSimilarity(String(a), String(b))
)
}
return false
}
const isArray = fn => Array.isArray(fn)
const asArray = a => (isArray(a) ? a : [a])

const render = component => {
if (!component) {
Expand Down Expand Up @@ -481,6 +406,9 @@ export const hotComponentCompare = (oldType, newType) => {
}

export default (instance, stack) => {
if (configuration.disableHotRenderer) {
return
}
try {
// disable reconciler to prevent upcoming components from proxying.
reactHotLoader.disableProxyCreation = true
Expand Down
81 changes: 81 additions & 0 deletions src/reconciler/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import levenshtein from 'fast-levenshtein'
import { getIdByType } from './proxies'
import { getComponentDisplayName, isReactClass } from '../internal/reactUtils'
import { UNWRAP_PROXY } from '../proxy'

// some `empty` names, React can autoset display name to...
const UNDEFINED_NAMES = {
Unknown: true,
Component: true,
}

const areNamesEqual = (a, b) =>
a === b || (UNDEFINED_NAMES[a] && UNDEFINED_NAMES[b])

const isFunctional = fn => typeof fn === 'function'
const getTypeOf = type => {
if (isReactClass(type)) return 'ReactComponent'
if (isFunctional(type)) return 'StatelessFunctional'
return 'Fragment' // ?
}

const haveTextSimilarity = (a, b) =>
// equal or slight changed
a === b || levenshtein.get(a, b) < a.length * 0.2

const equalClasses = (a, b) => {
const prototypeA = a.prototype
const prototypeB = Object.getPrototypeOf(b.prototype)

let hits = 0
let misses = 0
let comparisons = 0
Object.getOwnPropertyNames(prototypeA).forEach(key => {
const descriptorA = Object.getOwnPropertyDescriptor(prototypeA, key)
const valueA =
descriptorA && (descriptorA.value || descriptorA.get || descriptorA.set)
const descriptorB = Object.getOwnPropertyDescriptor(prototypeB, key)
const valueB =
descriptorB && (descriptorB.value || descriptorB.get || descriptorB.set)

if (typeof valueA === 'function' && key !== 'constructor') {
comparisons++
if (haveTextSimilarity(String(valueA), String(valueB))) {
hits++
} else {
misses++
if (key === 'render') {
misses++
}
}
}
})
// allow to add or remove one function
return (hits > 0 && misses <= 1) || comparisons === 0
}

export const areSwappable = (a, b) => {
// both are registered components and have the same name
if (getIdByType(b) && getIdByType(a) === getIdByType(b)) {
return true
}
if (getTypeOf(a) !== getTypeOf(b)) {
return false
}
if (isReactClass(a)) {
return (
areNamesEqual(getComponentDisplayName(a), getComponentDisplayName(b)) &&
equalClasses(a, b)
)
}

if (isFunctional(a)) {
const nameA = getComponentDisplayName(a)
return (
(areNamesEqual(nameA, getComponentDisplayName(b)) &&
nameA !== 'Component') ||
haveTextSimilarity(String(a), String(b))
)
}
return false
}
Loading

0 comments on commit 42d637b

Please sign in to comment.