Skip to content

Commit 42d637b

Browse files
committed
feat: webpack patch/inject mode
1 parent 65309fc commit 42d637b

10 files changed

+284
-97
lines changed

Diff for: package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,14 @@
9393
"standard-version": "^4.3.0"
9494
},
9595
"peerDependencies": {
96-
"react": "^15.0.0 || ^16.0.0"
96+
"react": "^15.0.0 || ^16.0.0",
97+
"react-dom": "^15.0.0 || ^16.0.0"
9798
},
9899
"dependencies": {
99100
"fast-levenshtein": "^2.0.6",
100101
"global": "^4.3.0",
101102
"hoist-non-react-statics": "^2.5.0",
103+
"loader-utils": "^1.1.0",
102104
"prop-types": "^15.6.1",
103105
"react-lifecycles-compat": "^3.0.4",
104106
"shallowequal": "^1.0.2",

Diff for: rollup.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const commonPlugins = [
1616

1717
const getConfig = (input, dist, env) => ({
1818
input,
19-
external: ['react', 'fs', 'path'].concat(Object.keys(pkg.dependencies)),
19+
external: ['react-dom','react', 'fs', 'path'].concat(Object.keys(pkg.dependencies)),
2020
plugins: commonPlugins
2121
.concat(env ? [
2222
replace({

Diff for: src/configuration.js

+9
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ const configuration = {
55
// Allows using SFC without changes. leading to some components not updated
66
pureSFC: false,
77

8+
// keep render method unpatched, moving sideEffect to componentWillUpdate
9+
pureRender: true,
10+
811
// Allows SFC to be used, enables "intermediate" components used by Relay, should be disabled for Preact
912
allowSFC: true,
1013

14+
// Disable "hot-replacement-render"
15+
disableHotRenderer: false,
16+
17+
// Disable "hot-replacement-render" when injection into react-dom are made
18+
disableHotRendererWhenInjected: false,
19+
1120
// Hook on babel component register.
1221
onComponentRegister: false,
1322

Diff for: src/proxy/createClassProxy.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
181181
instancesCount++
182182
},
183183
)
184+
const UNSAFE_componentWillUpdate = lifeCycleWrapperFactory(
185+
'UNSAFE_componentWillUpdate',
186+
() => ({}),
187+
)
188+
const componentWillUpdate = lifeCycleWrapperFactory(
189+
'componentWillUpdate',
190+
() => ({}),
191+
)
184192
const componentDidUpdate = lifeCycleWrapperFactory(
185193
'componentDidUpdate',
186194
renderOptions.componentDidUpdate,
@@ -225,7 +233,11 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
225233
const defineProxyMethods = (Proxy, Base = {}) => {
226234
defineClassMembers(Proxy, {
227235
...fakeBasePrototype(Base),
228-
render: proxiedRender,
236+
...(proxyConfig.pureRender
237+
? { render: proxiedRender }
238+
: Base.componentWillUpdate
239+
? componentWillUpdate
240+
: UNSAFE_componentWillUpdate),
229241
hotComponentRender,
230242
componentDidMount,
231243
componentDidUpdate,

Diff for: src/reactHotLoader.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-use-before-define */
22
import React from 'react'
3+
import ReactDOM from 'react-dom'
34
import {
45
isCompositeComponent,
56
getComponentDisplayName,
@@ -23,22 +24,27 @@ import configuration from './configuration'
2324
import logger from './logger'
2425

2526
import { preactAdapter } from './adapters/preact'
27+
import { areSwappable } from './reconciler/utils'
28+
import { PROXY_KEY, UNWRAP_PROXY } from './proxy'
2629

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

3033
const updateLazy = (target, type) => {
3134
const ctor = type[lazyConstructor]
3235
if (target[lazyConstructor] !== type[lazyConstructor]) {
33-
ctor()
36+
// ctor()
37+
}
38+
if (!target[lazyConstructor].isPatchedByReactHotLoader) {
39+
target[lazyConstructor] = () =>
40+
ctor().then(m => {
41+
const C = resolveType(m.default)
42+
return {
43+
default: props => <C {...props} />,
44+
}
45+
})
46+
target[lazyConstructor].isPatchedByReactHotLoader = true
3447
}
35-
target[lazyConstructor] = () =>
36-
ctor().then(m => {
37-
const C = resolveType(m.default)
38-
return {
39-
default: props => <C {...props} />,
40-
}
41-
})
4248
}
4349
const updateMemo = (target, { type }) => {
4450
target.type = resolveType(type)
@@ -47,6 +53,28 @@ const updateForward = (target, { render }) => {
4753
target.render = render
4854
}
4955

56+
export const hotComponentCompare = (oldType, newType) => {
57+
if (oldType === newType) {
58+
return true
59+
}
60+
61+
if (isForwardType({ type: oldType }) && isForwardType({ type: newType })) {
62+
return areSwappable(oldType.render, newType.render)
63+
}
64+
65+
if (areSwappable(newType, oldType)) {
66+
const oldProxy = getProxyByType(newType[UNWRAP_PROXY]())
67+
if (oldProxy) {
68+
oldProxy.dereference()
69+
updateProxyById(oldType[PROXY_KEY], newType[UNWRAP_PROXY]())
70+
updateProxyById(newType[PROXY_KEY], oldType[UNWRAP_PROXY]())
71+
}
72+
return true
73+
}
74+
75+
return false
76+
}
77+
5078
const shouldNotPatchComponent = type =>
5179
!isCompositeComponent(type) || isTypeBlacklisted(type) || isProxyType(type)
5280

@@ -142,6 +170,11 @@ const reactHotLoader = {
142170
},
143171

144172
patch(React) {
173+
if (ReactDOM.setHotElementComparator) {
174+
ReactDOM.setHotElementComparator(hotComponentCompare)
175+
configuration.disableHotRenderer =
176+
configuration.disableHotRendererWhenInjected
177+
}
145178
if (!React.createElement.isPatchedByReactHotLoader) {
146179
const originalCreateElement = React.createElement
147180
// Trick React into rendering a proxy so that

Diff for: src/reconciler/hotReplacementRender.js

+10-82
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import levenshtein from 'fast-levenshtein'
21
import { PROXY_IS_MOUNTED, PROXY_KEY, UNWRAP_PROXY } from '../proxy'
32
import {
4-
getIdByType,
53
getProxyByType,
64
isRegisteredComponent,
75
isTypeBlacklisted,
@@ -14,7 +12,6 @@ import {
1412
isContextConsumer,
1513
isContextProvider,
1614
getContextProvider,
17-
isReactClass,
1815
isReactClassInstance,
1916
CONTEXT_CURRENT_VALUE,
2017
isMemoType,
@@ -23,12 +20,8 @@ import {
2320
} from '../internal/reactUtils'
2421
import reactHotLoader from '../reactHotLoader'
2522
import logger from '../logger'
26-
27-
// some `empty` names, React can autoset display name to...
28-
const UNDEFINED_NAMES = {
29-
Unknown: true,
30-
Component: true,
31-
}
23+
import configuration from '../configuration'
24+
import { areSwappable } from './utils'
3225

3326
let renderStack = []
3427

@@ -40,19 +33,12 @@ const stackReport = () => {
4033
const emptyMap = new Map()
4134
const stackContext = () =>
4235
(renderStack[renderStack.length - 1] || {}).context || emptyMap
43-
const areNamesEqual = (a, b) =>
44-
a === b || (UNDEFINED_NAMES[a] && UNDEFINED_NAMES[b])
36+
4537
const shouldUseRenderMethod = fn =>
4638
fn && (isReactClassInstance(fn) || fn.SFC_fake)
4739

48-
const isFunctional = fn => typeof fn === 'function'
49-
const isArray = fn => Array.isArray(fn)
50-
const asArray = a => (isArray(a) ? a : [a])
51-
const getTypeOf = type => {
52-
if (isReactClass(type)) return 'ReactComponent'
53-
if (isFunctional(type)) return 'StatelessFunctional'
54-
return 'Fragment' // ?
55-
}
40+
const getElementType = child =>
41+
child.type[UNWRAP_PROXY] ? child.type[UNWRAP_PROXY]() : child.type
5642

5743
const filterNullArray = a => {
5844
if (!a) return []
@@ -69,69 +55,8 @@ const unflatten = a =>
6955
return acc
7056
}, [])
7157

72-
const getElementType = child =>
73-
child.type[UNWRAP_PROXY] ? child.type[UNWRAP_PROXY]() : child.type
74-
75-
const haveTextSimilarity = (a, b) =>
76-
// equal or slight changed
77-
a === b || levenshtein.get(a, b) < a.length * 0.2
78-
79-
const equalClasses = (a, b) => {
80-
const prototypeA = a.prototype
81-
const prototypeB = Object.getPrototypeOf(b.prototype)
82-
83-
let hits = 0
84-
let misses = 0
85-
let comparisons = 0
86-
Object.getOwnPropertyNames(prototypeA).forEach(key => {
87-
const descriptorA = Object.getOwnPropertyDescriptor(prototypeA, key)
88-
const valueA =
89-
descriptorA && (descriptorA.value || descriptorA.get || descriptorA.set)
90-
const descriptorB = Object.getOwnPropertyDescriptor(prototypeB, key)
91-
const valueB =
92-
descriptorB && (descriptorB.value || descriptorB.get || descriptorB.set)
93-
94-
if (typeof valueA === 'function' && key !== 'constructor') {
95-
comparisons++
96-
if (haveTextSimilarity(String(valueA), String(valueB))) {
97-
hits++
98-
} else {
99-
misses++
100-
if (key === 'render') {
101-
misses++
102-
}
103-
}
104-
}
105-
})
106-
// allow to add or remove one function
107-
return (hits > 0 && misses <= 1) || comparisons === 0
108-
}
109-
110-
export const areSwappable = (a, b) => {
111-
// both are registered components and have the same name
112-
if (getIdByType(b) && getIdByType(a) === getIdByType(b)) {
113-
return true
114-
}
115-
if (getTypeOf(a) !== getTypeOf(b)) {
116-
return false
117-
}
118-
if (isReactClass(a)) {
119-
return (
120-
areNamesEqual(getComponentDisplayName(a), getComponentDisplayName(b)) &&
121-
equalClasses(a, b)
122-
)
123-
}
124-
125-
if (isFunctional(a)) {
126-
const nameA = getComponentDisplayName(a)
127-
return (
128-
(areNamesEqual(nameA, getComponentDisplayName(b)) &&
129-
nameA !== 'Component') ||
130-
haveTextSimilarity(String(a), String(b))
131-
)
132-
}
133-
return false
134-
}
58+
const isArray = fn => Array.isArray(fn)
59+
const asArray = a => (isArray(a) ? a : [a])
13560

13661
const render = component => {
13762
if (!component) {
@@ -481,6 +406,9 @@ export const hotComponentCompare = (oldType, newType) => {
481406
}
482407

483408
export default (instance, stack) => {
409+
if (configuration.disableHotRenderer) {
410+
return
411+
}
484412
try {
485413
// disable reconciler to prevent upcoming components from proxying.
486414
reactHotLoader.disableProxyCreation = true

Diff for: src/reconciler/utils.js

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import levenshtein from 'fast-levenshtein'
2+
import { getIdByType } from './proxies'
3+
import { getComponentDisplayName, isReactClass } from '../internal/reactUtils'
4+
import { UNWRAP_PROXY } from '../proxy'
5+
6+
// some `empty` names, React can autoset display name to...
7+
const UNDEFINED_NAMES = {
8+
Unknown: true,
9+
Component: true,
10+
}
11+
12+
const areNamesEqual = (a, b) =>
13+
a === b || (UNDEFINED_NAMES[a] && UNDEFINED_NAMES[b])
14+
15+
const isFunctional = fn => typeof fn === 'function'
16+
const getTypeOf = type => {
17+
if (isReactClass(type)) return 'ReactComponent'
18+
if (isFunctional(type)) return 'StatelessFunctional'
19+
return 'Fragment' // ?
20+
}
21+
22+
const haveTextSimilarity = (a, b) =>
23+
// equal or slight changed
24+
a === b || levenshtein.get(a, b) < a.length * 0.2
25+
26+
const equalClasses = (a, b) => {
27+
const prototypeA = a.prototype
28+
const prototypeB = Object.getPrototypeOf(b.prototype)
29+
30+
let hits = 0
31+
let misses = 0
32+
let comparisons = 0
33+
Object.getOwnPropertyNames(prototypeA).forEach(key => {
34+
const descriptorA = Object.getOwnPropertyDescriptor(prototypeA, key)
35+
const valueA =
36+
descriptorA && (descriptorA.value || descriptorA.get || descriptorA.set)
37+
const descriptorB = Object.getOwnPropertyDescriptor(prototypeB, key)
38+
const valueB =
39+
descriptorB && (descriptorB.value || descriptorB.get || descriptorB.set)
40+
41+
if (typeof valueA === 'function' && key !== 'constructor') {
42+
comparisons++
43+
if (haveTextSimilarity(String(valueA), String(valueB))) {
44+
hits++
45+
} else {
46+
misses++
47+
if (key === 'render') {
48+
misses++
49+
}
50+
}
51+
}
52+
})
53+
// allow to add or remove one function
54+
return (hits > 0 && misses <= 1) || comparisons === 0
55+
}
56+
57+
export const areSwappable = (a, b) => {
58+
// both are registered components and have the same name
59+
if (getIdByType(b) && getIdByType(a) === getIdByType(b)) {
60+
return true
61+
}
62+
if (getTypeOf(a) !== getTypeOf(b)) {
63+
return false
64+
}
65+
if (isReactClass(a)) {
66+
return (
67+
areNamesEqual(getComponentDisplayName(a), getComponentDisplayName(b)) &&
68+
equalClasses(a, b)
69+
)
70+
}
71+
72+
if (isFunctional(a)) {
73+
const nameA = getComponentDisplayName(a)
74+
return (
75+
(areNamesEqual(nameA, getComponentDisplayName(b)) &&
76+
nameA !== 'Component') ||
77+
haveTextSimilarity(String(a), String(b))
78+
)
79+
}
80+
return false
81+
}

0 commit comments

Comments
 (0)