[Fresh] Set up infra for runtime and Babel plugin (#15698)
* Add a stub for React Fresh Babel plugin package * Move ReactFresh-test into ReactFresh top level directory * Add a stub for React Fresh Runtime entry point * Extract Fresh runtime from tests into its entry point
Dan Abramov committed
May 21, 2019 at 21:54 UTC
6d4f85b611cd7bac40db44854bab06776ce30c96
13 files changed
+255
-107
packages/react-fresh/README.md
new
+7
@@ -0,0 +1,7 @@
1
+# react-fresh
2
+
3
+This is an experimental package for hot reloading.
4
+
5
+**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
6
+
7
+**Use it at your own risk.**
packages/react-fresh/babel.js
new
+13
@@ -0,0 +1,13 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+'use strict';
9
+
10
+const ReactFreshBabelPlugin = require('./src/ReactFreshBabelPlugin');
11
+
12
+// This is hacky but makes it work with both Rollup and Jest.
13
+module.exports = ReactFreshBabelPlugin.default || ReactFreshBabelPlugin;
packages/react-fresh/npm/babel.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-fresh-babel.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-fresh-babel.development.js');
7
+}
packages/react-fresh/npm/runtime.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-fresh-runtime.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-fresh-runtime.development.js');
7
+}
packages/react-fresh/package.json
new
+30
@@ -0,0 +1,30 @@
1
+{
2
+ "name": "react-fresh",
3
+ "private": true,
4
+ "description": "React is a JavaScript library for building user interfaces.",
5
+ "keywords": [
6
+ "react"
7
+ ],
8
+ "version": "0.1.0",
9
+ "homepage": "https://reactjs.org/",
10
+ "bugs": "https://github.com/facebook/react/issues",
11
+ "license": "MIT",
12
+ "files": [
13
+ "LICENSE",
14
+ "README.md",
15
+ "babel.js",
16
+ "runtime.js",
17
+ "build-info.json",
18
+ "cjs/",
19
+ "umd/"
20
+ ],
21
+ "main": "index.js",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/facebook/react.git",
25
+ "directory": "packages/react"
26
+ },
27
+ "engines": {
28
+ "node": ">=0.10.0"
29
+ }
30
+}
packages/react-fresh/runtime.js
new
+13
@@ -0,0 +1,13 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+'use strict';
9
+
10
+const ReactFreshRuntime = require('./src/ReactFreshRuntime');
11
+
12
+// This is hacky but makes it work with both Rollup and Jest.
13
+module.exports = ReactFreshRuntime.default || ReactFreshRuntime;
packages/react-fresh/src/ReactFreshBabelPlugin.js
new
+15
@@ -0,0 +1,15 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+'use strict';
9
+
10
+// TODO
11
+export default function(babel) {
12
+ return {
13
+ visitor: {},
14
+ };
15
+}
packages/react-fresh/src/ReactFreshRuntime.js
new
+103
@@ -0,0 +1,103 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ Family,
12
+ HotUpdate,
13
+} from 'react-reconciler/src/ReactFiberHotReloading';
14
+
15
+import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
16
+
17
+// We never remove these associations.
18
+// It's OK to reference families, but use WeakMap/Set for types.
19
+const allFamiliesByID: Map<string, Family> = new Map();
20
+const allTypes: WeakSet<any> = new WeakSet();
21
+const allSignaturesByType: WeakMap<any, string> = new WeakMap();
22
+// This WeakMap is read by React, so we only put families
23
+// that have actually been edited here. This keeps checks fast.
24
+const familiesByType: WeakMap<any, Family> = new WeakMap();
25
+
26
+// This is cleared on every prepareUpdate() call.
27
+// It is an array of [Family, NextType] tuples.
28
+let pendingUpdates: Array<[Family, any]> = [];
29
+
30
+export function prepareUpdate(): HotUpdate {
31
+ const staleFamilies = new Set();
32
+ const updatedFamilies = new Set();
33
+
34
+ const updates = pendingUpdates;
35
+ pendingUpdates = [];
36
+ updates.forEach(([family, nextType]) => {
37
+ // Now that we got a real edit, we can create associations
38
+ // that will be read by the React reconciler.
39
+ const prevType = family.current;
40
+ familiesByType.set(prevType, family);
41
+ familiesByType.set(nextType, family);
42
+ family.current = nextType;
43
+
44
+ // Determine whether this should be a re-render or a re-mount.
45
+ const prevSignature = allSignaturesByType.get(prevType);
46
+ const nextSignature = allSignaturesByType.get(nextType);
47
+ if (prevSignature !== nextSignature) {
48
+ staleFamilies.add(family);
49
+ } else {
50
+ updatedFamilies.add(family);
51
+ }
52
+ });
53
+
54
+ return {
55
+ familiesByType,
56
+ updatedFamilies,
57
+ staleFamilies,
58
+ };
59
+}
60
+
61
+export function register(type: any, id: string): void {
62
+ if (type === null) {
63
+ return;
64
+ }
65
+ if (typeof type !== 'function' && typeof type !== 'object') {
66
+ return;
67
+ }
68
+
69
+ // This can happen in an edge case, e.g. if we register
70
+ // return value of a HOC but it returns a cached component.
71
+ // Ignore anything but the first registration for each type.
72
+ if (allTypes.has(type)) {
73
+ return;
74
+ }
75
+ allTypes.add(type);
76
+
77
+ // Create family or remember to update it.
78
+ // None of this bookkeeping affects reconciliation
79
+ // until the first prepareUpdate() call above.
80
+ let family = allFamiliesByID.get(id);
81
+ if (family === undefined) {
82
+ family = {current: type};
83
+ allFamiliesByID.set(id, family);
84
+ } else {
85
+ pendingUpdates.push([family, type]);
86
+ }
87
+
88
+ // Visit inner types because we might not have registered them.
89
+ if (typeof type === 'object' && type !== null) {
90
+ switch (type.$$typeof) {
91
+ case REACT_FORWARD_REF_TYPE:
92
+ register(type.render, id + '$render');
93
+ break;
94
+ case REACT_MEMO_TYPE:
95
+ register(type.type, id + '$type');
96
+ break;
97
+ }
98
+ }
99
+}
100
+
101
+export function setSignature(type: any, signature: string): void {
102
+ allSignaturesByType.set(type, signature);
103
+}
packages/react-fresh/src/__tests__/ReactFresh-test.js
renamed
+10
-98
@@ -13,21 +13,16 @@
13
14
let React;
15
let ReactDOM;
16
+let ReactFreshRuntime;
17
let Scheduler;
18
let act;
19
+let lastRoot;
20
21
describe('ReactFresh', () => {
22
let container;
21
- let familiesByID;
22
- let familiesByType;
23
- let newFamilies;
24
- let updatedFamilies;
25
- let performHotReload;
26
- let signaturesByType;
23
+ let scheduleHotUpdate;
24
25
beforeEach(() => {
29
- let scheduleHotUpdate;
30
- let lastRoot;
26
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
27
supportsFiber: true,
28
inject: injected => {
@@ -42,24 +37,11 @@ describe('ReactFresh', () => {
37
jest.resetModules();
38
React = require('react');
39
ReactDOM = require('react-dom');
40
+ ReactFreshRuntime = require('react-fresh/runtime');
41
Scheduler = require('scheduler');
42
act = require('react-dom/test-utils').act;
43
container = document.createElement('div');
44
document.body.appendChild(container);
49
-
50
- familiesByID = new Map();
51
- familiesByType = new WeakMap();
52
-
53
- if (__DEV__) {
54
- performHotReload = function(staleFamilies) {
55
- scheduleHotUpdate({
56
- root: lastRoot,
57
- familiesByType,
58
- updatedFamilies,
59
- staleFamilies,
60
- });
61
- };
62
- }
45
});
46
47
afterEach(() => {
@@ -67,26 +49,12 @@ describe('ReactFresh', () => {
49
});
50
51
function prepare(version) {
70
- newFamilies = new Set();
71
- updatedFamilies = new Set();
72
- signaturesByType = new Map();
52
const Component = version();
74
-
75
- // Fill in the signatures.
76
- for (let family of newFamilies) {
77
- const latestSignature = signaturesByType.get(family.currentType) || null;
78
- family.currentSignature = latestSignature;
79
- }
80
-
81
- newFamilies = null;
82
- updatedFamilies = null;
83
- signaturesByType = null;
84
-
53
return Component;
54
}
55
56
function render(version, props) {
89
- const Component = prepare(version);
57
+ const Component = version();
58
act(() => {
59
ReactDOM.render(<Component {...props} />, container);
60
});
@@ -94,74 +62,18 @@ describe('ReactFresh', () => {
62
}
63
64
function patch(version) {
97
- // Will be filled in by __register__ calls in user code.
98
- newFamilies = new Set();
99
- updatedFamilies = new Set();
100
- signaturesByType = new Map();
65
const Component = version();
102
-
103
- // Fill in the signatures.
104
- for (let family of newFamilies) {
105
- const latestSignature = signaturesByType.get(family.currentType) || null;
106
- family.currentSignature = latestSignature;
107
- }
108
- // Now that all registration and signatures are collected,
109
- // find which registrations changed their signatures since last time.
110
- const staleFamilies = new Set();
111
- for (let family of updatedFamilies) {
112
- const latestSignature = signaturesByType.get(family.currentType) || null;
113
- if (family.currentSignature !== latestSignature) {
114
- family.currentSignature = latestSignature;
115
- staleFamilies.add(family);
116
- }
117
- }
118
-
119
- performHotReload(staleFamilies);
120
- newFamilies = null;
121
- updatedFamilies = null;
122
- signaturesByType = null;
66
+ const hotUpdate = ReactFreshRuntime.prepareUpdate();
67
+ scheduleHotUpdate(lastRoot, hotUpdate);
68
return Component;
69
}
70
71
function __register__(type, id) {
127
- if (familiesByType.has(type)) {
128
- return;
129
- }
130
- let family = familiesByID.get(id);
131
- let isNew = false;
132
- if (family === undefined) {
133
- isNew = true;
134
- family = {currentType: type, currentSignature: null};
135
- familiesByID.set(id, family);
136
- }
137
- const prevType = family.currentType;
138
- if (isNew) {
139
- // The first time a type is registered, we don't need
140
- // any special reconciliation logic. So we won't add it to the map.
141
- // Instead, this will happen the firt time it is edited.
142
- newFamilies.add(family);
143
- } else {
144
- family.currentType = type;
145
- // Point both previous and next types to this family.
146
- familiesByType.set(prevType, family);
147
- familiesByType.set(type, family);
148
- updatedFamilies.add(family);
149
- }
150
-
151
- if (typeof type === 'object' && type !== null) {
152
- switch (type.$$typeof) {
153
- case Symbol.for('react.forward_ref'):
154
- __register__(type.render, id + '$render');
155
- break;
156
- case Symbol.for('react.memo'):
157
- __register__(type.type, id + '$type');
158
- break;
159
- }
160
- }
72
+ ReactFreshRuntime.register(type, id);
73
}
74
163
- function __signature__(type, signature) {
164
- signaturesByType.set(type, signature);
75
+ function __signature__(type, id) {
76
+ ReactFreshRuntime.setSignature(type, id);
77
}
78
79
it('can preserve state for compatible types', () => {
packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
new
+23
@@ -0,0 +1,23 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+'use strict';
9
+
10
+let babel = require('babel-core');
11
+let freshPlugin = require('react-fresh/babel');
12
+
13
+function transform(input, options = {}) {
14
+ return babel.transform(input, {
15
+ plugins: [[freshPlugin]],
16
+ }).code;
17
+}
18
+
19
+describe('ReactFreshBabelPlugin', () => {
20
+ it('hello world', () => {
21
+ expect(transform(`hello()`)).toMatchSnapshot();
22
+ });
23
+});
packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
new
+3
@@ -0,0 +1,3 @@
1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`ReactFreshBabelPlugin hello world 1`] = `"hello();"`;
packages/react-reconciler/src/ReactFiberHotReloading.js
+7
-9
@@ -29,14 +29,12 @@ import {
29
REACT_LAZY_TYPE,
30
} from 'shared/ReactSymbols';
31
32
-type Family = {|
33
- currentType: any,
34
- currentSignature: null | string,
32
+export type Family = {|
33
+ current: any,
34
|};
35
37
-type HotUpdate = {|
36
+export type HotUpdate = {|
37
familiesByType: WeakMap<any, Family>,
39
- root: FiberRoot,
38
staleFamilies: Set<Family>,
39
updatedFamilies: Set<Family>,
40
|};
@@ -56,7 +54,7 @@ export function resolveFunctionForHotReloading(type: any): any {
54
return type;
55
}
56
// Use the latest known implementation.
59
- return family.currentType;
57
+ return family.current;
58
} else {
59
return type;
60
}
@@ -94,7 +92,7 @@ export function resolveForwardRefForHotReloading(type: any): any {
92
return type;
93
}
94
// Use the latest known implementation.
97
- return family.currentType;
95
+ return family.current;
96
} else {
97
return type;
98
}
@@ -194,12 +192,12 @@ export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
192
}
193
}
194
197
-export function scheduleHotUpdate(hotUpdate: HotUpdate): void {
195
+export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void {
196
if (__DEV__) {
197
// TODO: warn if its identity changes over time?
198
familiesByType = hotUpdate.familiesByType;
199
202
- const {root, staleFamilies, updatedFamilies} = hotUpdate;
200
+ const {staleFamilies, updatedFamilies} = hotUpdate;
201
flushPassiveEffects();
202
flushSync(() => {
203
scheduleFibersWithFamiliesRecursively(
scripts/rollup/bundles.js
+17
@@ -453,6 +453,23 @@ const bundles = [
453
global: 'ESLintPluginReactHooks',
454
externals: [],
455
},
456
+
457
+ /******* React Fresh *******/
458
+ {
459
+ bundleTypes: [NODE_DEV, NODE_PROD],
460
+ moduleType: ISOMORPHIC,
461
+ entry: 'react-fresh/babel',
462
+ global: 'ReactFreshBabelPlugin',
463
+ externals: [],
464
+ },
465
+ {
466
+ bundleTypes: [NODE_DEV, NODE_PROD],
467
+ moduleType: ISOMORPHIC,
468
+ entry: 'react-fresh/runtime',
469
+ global: 'ReactFreshRuntime',
470
+ externals: [],
471
+ },
472
+
473
{
474
bundleTypes: [
475
FB_WWW_DEV,