Skip to content

Commit

Permalink
[Fresh] Set up infra for runtime and Babel plugin (facebook#15698)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gaearon committed Jun 19, 2019
1 parent 7b4c2f7 commit 39a3f2f
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 107 deletions.
7 changes: 7 additions & 0 deletions packages/react-fresh/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions packages/react-fresh/babel.js
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions packages/react-fresh/npm/babel.js
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions packages/react-fresh/npm/runtime.js
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions packages/react-fresh/package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions packages/react-fresh/runtime.js
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions packages/react-fresh/src/ReactFreshBabelPlugin.js
Original file line number Diff line number Diff line change
@@ -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 changes: 103 additions & 0 deletions packages/react-fresh/src/ReactFreshRuntime.js
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -42,126 +37,43 @@ 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(() => {
document.body.removeChild(container);
});

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);
});
return Component;
}

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', () => {
Expand Down
23 changes: 23 additions & 0 deletions packages/react-fresh/src/__tests__/ReactFreshBabelPlugin-test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ReactFreshBabelPlugin hello world 1`] = `"hello();"`;

0 comments on commit 39a3f2f

Please sign in to comment.