[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
This commit is contained in:
Dan Abramov
2019-05-21 21:54:10 +01:00
committed by GitHub
parent 7265fe8827
commit aa7d541fb3
13 changed files with 255 additions and 107 deletions
+7
View File
@@ -0,0 +1,7 @@
# react-fresh
This is an experimental package for hot reloading.
**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.**
**Use it at your own risk.**
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const ReactFreshBabelPlugin = require('./src/ReactFreshBabelPlugin');
// This is hacky but makes it work with both Rollup and Jest.
module.exports = ReactFreshBabelPlugin.default || ReactFreshBabelPlugin;
+7
View File
@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-fresh-babel.production.min.js');
} else {
module.exports = require('./cjs/react-fresh-babel.development.js');
}
+7
View File
@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-fresh-runtime.production.min.js');
} else {
module.exports = require('./cjs/react-fresh-runtime.development.js');
}
+30
View File
@@ -0,0 +1,30 @@
{
"name": "react-fresh",
"private": true,
"description": "React is a JavaScript library for building user interfaces.",
"keywords": [
"react"
],
"version": "0.1.0",
"homepage": "https://reactjs.org/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",
"files": [
"LICENSE",
"README.md",
"babel.js",
"runtime.js",
"build-info.json",
"cjs/",
"umd/"
],
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react"
},
"engines": {
"node": ">=0.10.0"
}
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const ReactFreshRuntime = require('./src/ReactFreshRuntime');
// This is hacky but makes it work with both Rollup and Jest.
module.exports = ReactFreshRuntime.default || ReactFreshRuntime;
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
// TODO
export default function(babel) {
return {
visitor: {},
};
}
+103
View File
@@ -0,0 +1,103 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {
Family,
HotUpdate,
} from 'react-reconciler/src/ReactFiberHotReloading';
import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
// We never remove these associations.
// It's OK to reference families, but use WeakMap/Set for types.
const allFamiliesByID: Map<string, Family> = new Map();
const allTypes: WeakSet<any> = new WeakSet();
const allSignaturesByType: WeakMap<any, string> = new WeakMap();
// This WeakMap is read by React, so we only put families
// that have actually been edited here. This keeps checks fast.
const familiesByType: WeakMap<any, Family> = new WeakMap();
// This is cleared on every prepareUpdate() call.
// It is an array of [Family, NextType] tuples.
let pendingUpdates: Array<[Family, any]> = [];
export function prepareUpdate(): HotUpdate {
const staleFamilies = new Set();
const updatedFamilies = new Set();
const updates = pendingUpdates;
pendingUpdates = [];
updates.forEach(([family, nextType]) => {
// Now that we got a real edit, we can create associations
// that will be read by the React reconciler.
const prevType = family.current;
familiesByType.set(prevType, family);
familiesByType.set(nextType, family);
family.current = nextType;
// Determine whether this should be a re-render or a re-mount.
const prevSignature = allSignaturesByType.get(prevType);
const nextSignature = allSignaturesByType.get(nextType);
if (prevSignature !== nextSignature) {
staleFamilies.add(family);
} else {
updatedFamilies.add(family);
}
});
return {
familiesByType,
updatedFamilies,
staleFamilies,
};
}
export function register(type: any, id: string): void {
if (type === null) {
return;
}
if (typeof type !== 'function' && typeof type !== 'object') {
return;
}
// This can happen in an edge case, e.g. if we register
// return value of a HOC but it returns a cached component.
// Ignore anything but the first registration for each type.
if (allTypes.has(type)) {
return;
}
allTypes.add(type);
// Create family or remember to update it.
// None of this bookkeeping affects reconciliation
// until the first prepareUpdate() call above.
let family = allFamiliesByID.get(id);
if (family === undefined) {
family = {current: type};
allFamiliesByID.set(id, family);
} else {
pendingUpdates.push([family, type]);
}
// Visit inner types because we might not have registered them.
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE:
register(type.render, id + '$render');
break;
case REACT_MEMO_TYPE:
register(type.type, id + '$type');
break;
}
}
}
export function setSignature(type: any, signature: string): void {
allSignaturesByType.set(type, signature);
}
@@ -13,21 +13,16 @@
let React;
let ReactDOM;
let ReactFreshRuntime;
let Scheduler;
let act;
let lastRoot;
describe('ReactFresh', () => {
let container;
let familiesByID;
let familiesByType;
let newFamilies;
let updatedFamilies;
let performHotReload;
let signaturesByType;
let scheduleHotUpdate;
beforeEach(() => {
let scheduleHotUpdate;
let lastRoot;
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: injected => {
@@ -42,24 +37,11 @@ describe('ReactFresh', () => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactFreshRuntime = require('react-fresh/runtime');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;
container = document.createElement('div');
document.body.appendChild(container);
familiesByID = new Map();
familiesByType = new WeakMap();
if (__DEV__) {
performHotReload = function(staleFamilies) {
scheduleHotUpdate({
root: lastRoot,
familiesByType,
updatedFamilies,
staleFamilies,
});
};
}
});
afterEach(() => {
@@ -67,26 +49,12 @@ describe('ReactFresh', () => {
});
function prepare(version) {
newFamilies = new Set();
updatedFamilies = new Set();
signaturesByType = new Map();
const Component = version();
// Fill in the signatures.
for (let family of newFamilies) {
const latestSignature = signaturesByType.get(family.currentType) || null;
family.currentSignature = latestSignature;
}
newFamilies = null;
updatedFamilies = null;
signaturesByType = null;
return Component;
}
function render(version, props) {
const Component = prepare(version);
const Component = version();
act(() => {
ReactDOM.render(<Component {...props} />, container);
});
@@ -94,74 +62,18 @@ describe('ReactFresh', () => {
}
function patch(version) {
// Will be filled in by __register__ calls in user code.
newFamilies = new Set();
updatedFamilies = new Set();
signaturesByType = new Map();
const Component = version();
// Fill in the signatures.
for (let family of newFamilies) {
const latestSignature = signaturesByType.get(family.currentType) || null;
family.currentSignature = latestSignature;
}
// Now that all registration and signatures are collected,
// find which registrations changed their signatures since last time.
const staleFamilies = new Set();
for (let family of updatedFamilies) {
const latestSignature = signaturesByType.get(family.currentType) || null;
if (family.currentSignature !== latestSignature) {
family.currentSignature = latestSignature;
staleFamilies.add(family);
}
}
performHotReload(staleFamilies);
newFamilies = null;
updatedFamilies = null;
signaturesByType = null;
const hotUpdate = ReactFreshRuntime.prepareUpdate();
scheduleHotUpdate(lastRoot, hotUpdate);
return Component;
}
function __register__(type, id) {
if (familiesByType.has(type)) {
return;
}
let family = familiesByID.get(id);
let isNew = false;
if (family === undefined) {
isNew = true;
family = {currentType: type, currentSignature: null};
familiesByID.set(id, family);
}
const prevType = family.currentType;
if (isNew) {
// The first time a type is registered, we don't need
// any special reconciliation logic. So we won't add it to the map.
// Instead, this will happen the firt time it is edited.
newFamilies.add(family);
} else {
family.currentType = type;
// Point both previous and next types to this family.
familiesByType.set(prevType, family);
familiesByType.set(type, family);
updatedFamilies.add(family);
}
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case Symbol.for('react.forward_ref'):
__register__(type.render, id + '$render');
break;
case Symbol.for('react.memo'):
__register__(type.type, id + '$type');
break;
}
}
ReactFreshRuntime.register(type, id);
}
function __signature__(type, signature) {
signaturesByType.set(type, signature);
function __signature__(type, id) {
ReactFreshRuntime.setSignature(type, id);
}
it('can preserve state for compatible types', () => {
@@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
let babel = require('babel-core');
let freshPlugin = require('react-fresh/babel');
function transform(input, options = {}) {
return babel.transform(input, {
plugins: [[freshPlugin]],
}).code;
}
describe('ReactFreshBabelPlugin', () => {
it('hello world', () => {
expect(transform(`hello()`)).toMatchSnapshot();
});
});
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReactFreshBabelPlugin hello world 1`] = `"hello();"`;
+7 -9
View File
@@ -29,14 +29,12 @@ import {
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
type Family = {|
currentType: any,
currentSignature: null | string,
export type Family = {|
current: any,
|};
type HotUpdate = {|
export type HotUpdate = {|
familiesByType: WeakMap<any, Family>,
root: FiberRoot,
staleFamilies: Set<Family>,
updatedFamilies: Set<Family>,
|};
@@ -56,7 +54,7 @@ export function resolveFunctionForHotReloading(type: any): any {
return type;
}
// Use the latest known implementation.
return family.currentType;
return family.current;
} else {
return type;
}
@@ -94,7 +92,7 @@ export function resolveForwardRefForHotReloading(type: any): any {
return type;
}
// Use the latest known implementation.
return family.currentType;
return family.current;
} else {
return type;
}
@@ -194,12 +192,12 @@ export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
}
}
export function scheduleHotUpdate(hotUpdate: HotUpdate): void {
export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void {
if (__DEV__) {
// TODO: warn if its identity changes over time?
familiesByType = hotUpdate.familiesByType;
const {root, staleFamilies, updatedFamilies} = hotUpdate;
const {staleFamilies, updatedFamilies} = hotUpdate;
flushPassiveEffects();
flushSync(() => {
scheduleFibersWithFamiliesRecursively(
+17
View File
@@ -453,6 +453,23 @@ const bundles = [
global: 'ESLintPluginReactHooks',
externals: [],
},
/******* React Fresh *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: ISOMORPHIC,
entry: 'react-fresh/babel',
global: 'ReactFreshBabelPlugin',
externals: [],
},
{
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: ISOMORPHIC,
entry: 'react-fresh/runtime',
global: 'ReactFreshRuntime',
externals: [],
},
{
bundleTypes: [
FB_WWW_DEV,