Skip to content

Commit

Permalink
Fix React Refresh boundary export changes (#5442)
Browse files Browse the repository at this point in the history
* Fix React Refresh boundaries by invalidating

* Don't show "Aw Snap" for Sandpack

* Explicitly return something in evaluateModule

* Also send firstLoad for `start` message

* Reset exports for module hot reloading too
  • Loading branch information
CompuIves authored Feb 3, 2021
1 parent 6658899 commit 3bd6cc1
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 47 deletions.
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
"react-modal": "^3.6.1",
"react-motion": "^0.5.0",
"react-outside-click-handler": "^1.2.3",
"react-refresh": "^0.7.2",
"react-refresh": "0.9.0",
"react-router-dom": "^5.2.0",
"react-show": "^3.0.4",
"react-split-pane": "^0.1.87",
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/sandbox/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ async function compile({
}
}

dispatch({ type: 'start' });
dispatch({ type: 'start', firstLoad });
metrics.measure('compilation');

const startTime = Date.now();
Expand Down Expand Up @@ -639,7 +639,8 @@ async function compile({
if (
firstLoad &&
localStorage.getItem('running') &&
Date.now() - +localStorage.getItem('running') > 8000
Date.now() - +localStorage.getItem('running') > 8000 &&
!process.env.SANDPACK
) {
localStorage.removeItem('running');
showRunOnClick();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const reactPreset = babelConfig => {
dependencies['react-dom'] &&
isMinimalReactVersion(dependencies['react-dom'], '16.9.0')
) {
return { ...dependencies, 'react-refresh': '0.8.1' };
return { ...dependencies, 'react-refresh': '0.9.0' };
}

return dependencies;
Expand Down
121 changes: 90 additions & 31 deletions packages/app/src/sandbox/eval/transpilers/react/refresh-transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,47 @@ function isReactRefreshBoundary(moduleExports) {
return hasExports && areAllExportsComponents;
};
// When this signature changes, it's unsafe to stop at this refresh boundary.
function getRefreshBoundarySignature(moduleExports) {
const signature = [];
signature.push(Refresh.getFamilyByType(moduleExports));
if (moduleExports == null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
// (This is important for legacy environments.)
return signature;
}
for (const key in moduleExports) {
if (key === '__esModule') {
continue;
}
const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
if (desc && desc.get) {
continue;
}
const exportValue = moduleExports[key];
signature.push(key);
signature.push(Refresh.getFamilyByType(exportValue));
}
return signature;
};
function shouldInvalidateReactRefreshBoundary(
prevExports,
nextExports,
) {
const prevSignature = getRefreshBoundarySignature(prevExports);
const nextSignature = getRefreshBoundarySignature(nextExports);
if (prevSignature.length !== nextSignature.length) {
return true;
}
for (let i = 0; i < nextSignature.length; i++) {
if (prevSignature[i] !== nextSignature[i]) {
return true;
}
}
return false;
};
var registerExportsForReactRefresh = (moduleExports, moduleID) => {
Refresh.register(moduleExports, moduleID + ' %exports%');
if (moduleExports == null || typeof moduleExports !== 'object') {
Expand All @@ -83,46 +124,64 @@ var registerExportsForReactRefresh = (moduleExports, moduleID) => {
module.exports = {
enqueueUpdate: debounce(enqueueUpdate, 30),
isReactRefreshBoundary,
registerExportsForReactRefresh
registerExportsForReactRefresh,
shouldInvalidateReactRefreshBoundary
};
`.trim();

/**
var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');
window.$RefreshReg$ = (type, id) => {
// Note module.id is webpack-specific, this may vary in other bundlers
const fullId = module.id + ' ' + id;
RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
try {
${sourceCode}
} finally {
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
}
const _csbRefreshUtils = require('${HELPER_PATH}');
if (_csbRefreshUtils.isReactRefreshBoundary && _csbRefreshUtils.isReactRefreshBoundary(module.exports)) {
_csbRefreshUtils.registerExportsForReactRefresh(module.exports, module.id)
module.hot.accept();
_csbRefreshUtils.enqueueUpdate();
}
`
*/
// const getWrapperCode = (sourceCode: string) =>
// `
// var prevRefreshReg = window.$RefreshReg$;
// var prevRefreshSig = window.$RefreshSig$;
// var RefreshRuntime = require('react-refresh/runtime');

// window.$RefreshReg$ = (type, id) => {
// // Note module.id is webpack-specific, this may vary in other bundlers
// const fullId = module.id + ' ' + id;
// RefreshRuntime.register(type, fullId);
// }
// window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

// try {
// ${sourceCode}
// } finally {
// window.$RefreshReg$ = prevRefreshReg;
// window.$RefreshSig$ = prevRefreshSig;
// }

// const _csbRefreshUtils = require('${HELPER_PATH}');

// const isHotUpdate = !!module.hot.data;
// const prevExports = isHotUpdate ? module.hot.data.prevExports : null;
// if (_csbRefreshUtils.isReactRefreshBoundary) {
// if (_csbRefreshUtils.isReactRefreshBoundary(module.exports)) {
// _csbRefreshUtils.registerExportsForReactRefresh(module.exports, module.id)
// const currentExports = { ...module.exports };

// module.hot.dispose(function hotDisposeCallback(data) {
// data.prevExports = currentExports;
// });

// if (isHotUpdate && _csbRefreshUtils.shouldInvalidateReactRefreshBoundary(prevExports, currentExports)) {
// module.hot.invalidate();
// } else {
// module.hot.accept();
// }

// _csbRefreshUtils.enqueueUpdate();
// } else if (isHotUpdate && _csbRefreshUtils.isReactRefreshBoundary(prevExports)) {
// module.hot.invalidate();
// }
// }
// `;

/**
* This is the compressed version of the code in the comment above. We compress the code
* to a single line so we don't mess with the source mapping when showing errors.
*/
const getWrapperCode = (sourceCode: string) =>
`var prevRefreshReg=window.$RefreshReg$,prevRefreshSig=window.$RefreshSig$,RefreshRuntime=require("react-refresh/runtime");window.$RefreshReg$=((e,r)=>{const s=module.id+" "+r;RefreshRuntime.register(e,s)}),window.$RefreshSig$=RefreshRuntime.createSignatureFunctionForTransform;try{${sourceCode}
}finally{window.$RefreshReg$=prevRefreshReg,window.$RefreshSig$=prevRefreshSig}const _csbRefreshUtils=require("${HELPER_PATH}");_csbRefreshUtils.isReactRefreshBoundary&&_csbRefreshUtils.isReactRefreshBoundary(module.exports)&&(_csbRefreshUtils.registerExportsForReactRefresh(module.exports,module.id),module.hot.accept(),_csbRefreshUtils.enqueueUpdate());`.trim();
`var prevRefreshReg=window.$RefreshReg$,prevRefreshSig=window.$RefreshSig$,RefreshRuntime=require("react-refresh/runtime");window.$RefreshReg$=(e,r)=>{const s=module.id+" "+r;RefreshRuntime.register(e,s)},window.$RefreshSig$=RefreshRuntime.createSignatureFunctionForTransform;try{${sourceCode}
}finally{window.$RefreshReg$=prevRefreshReg,window.$RefreshSig$=prevRefreshSig}const _csbRefreshUtils=require("${HELPER_PATH}"),isHotUpdate=!!module.hot.data,prevExports=isHotUpdate?module.hot.data.prevExports:null;if(_csbRefreshUtils.isReactRefreshBoundary)if(_csbRefreshUtils.isReactRefreshBoundary(module.exports)){_csbRefreshUtils.registerExportsForReactRefresh(module.exports,module.id);const e={...module.exports};module.hot.dispose((function(r){r.prevExports=e})),isHotUpdate&&_csbRefreshUtils.shouldInvalidateReactRefreshBoundary(prevExports,e)?module.hot.invalidate():module.hot.accept(),_csbRefreshUtils.enqueueUpdate()}else isHotUpdate&&_csbRefreshUtils.isReactRefreshBoundary(prevExports)&&module.hot.invalidate();`;

class RefreshTranspiler extends Transpiler {
constructor() {
Expand Down
22 changes: 21 additions & 1 deletion packages/sandpack-core/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export default class Manager implements IEvaluator {
evaluateModule(
module: Module,
{ force = false, globals }: { force?: boolean; globals?: object } = {}
) {
): any {
if (this.hardReload && !this.isFirstLoad) {
// Do a hard reload
document.location.reload();
Expand All @@ -395,6 +395,26 @@ export default class Manager implements IEvaluator {
{ force, globals }
);

if (this.webpackHMR) {
// Check if any module has been invalidated, because in that case we need to
// restart evaluation.

const invalidatedModules = this.getTranspiledModules().filter(t => {
if (t.hmrConfig?.invalidated) {
t.compilation = null;
t.hmrConfig = null;

return true;
}

return false;
});

if (invalidatedModules.length > 0) {
return this.evaluateModule(module, { force, globals });
}
}

this.setHmrStatus('idle');

return exports;
Expand Down
12 changes: 11 additions & 1 deletion packages/sandpack-core/src/transpiled-module/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default class HMR {
type?: 'accept' | 'decline';
dirty: boolean = false;
selfAccepted: boolean = false;
invalidated = false;

callDisposeHandler() {
if (this.disposeHandler) {
Expand Down Expand Up @@ -36,7 +37,7 @@ export default class HMR {
}
}

setType(type: 'accept' | 'decline') {
setType(type: 'accept' | 'decline' | undefined) {
this.type = type;
}

Expand All @@ -62,4 +63,13 @@ export default class HMR {

return !this.isHot() && isEntry;
}

/**
* Setting the module to invalidated means that we MUST evaluate it again, which means
* that we throw away its compilation and hmrConfig, and we're going to force a second evaluation
* once this has been run.
*/
setInvalidated(invalidated: boolean) {
this.invalidated = invalidated;
}
}
26 changes: 20 additions & 6 deletions packages/sandpack-core/src/transpiled-module/transpiled-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import hashsum from 'hash-sum';

import * as pathUtils from '@codesandbox/common/lib/utils/path';

import { measure, endMeasure } from '@codesandbox/common/lib/utils/metrics';
import { Module } from '../types/module';
import { SourceMap } from '../transpiler/utils/get-source-map';
import ModuleError from './errors/module-error';
Expand All @@ -23,7 +24,6 @@ import Manager, { HMRStatus } from '../manager';
import HMR from './hmr';
import { splitQueryFromPath } from './utils/query-path';
import delay from '../utils/delay';
import { measure, endMeasure } from '@codesandbox/common/lib/utils/metrics';

declare const BrowserFS: any;

Expand Down Expand Up @@ -144,7 +144,8 @@ export type Compilation = {
accept: (() => void) | ((arg: string | string[], cb: () => void) => void);
decline: (path: string | Array<string>) => void;
dispose: (cb: () => void) => void;
data: Object;
invalidate: () => void;
data: Object | undefined;
status: () => HMRStatus;
addStatusHandler: (cb: (status: HMRStatus) => void) => void;
removeStatusHandler: (cb: (status: HMRStatus) => void) => void;
Expand Down Expand Up @@ -273,13 +274,13 @@ export class TranspiledModule {
}

resetCompilation() {
if (this.compilation) {
this.compilation = null;
}

if (this.hmrConfig && this.hmrConfig.isHot()) {
this.hmrConfig.setDirty(true);
} else {
if (this.compilation) {
this.compilation = null;
}

Array.from(this.initiators)
.filter(t => t.compilation)
.forEach(dep => {
Expand Down Expand Up @@ -932,6 +933,19 @@ export class TranspiledModule {

this.hmrConfig.setDisposeHandler(cb);
},
invalidate: () => {
this.hmrConfig = this.hmrConfig || new HMR();

// We have to bubble up, so reset compilation of parents
Array.from(this.initiators)
.filter(t => t.compilation)
.forEach(dep => {
dep.resetCompilation();
});

this.hmrConfig.setInvalidated(true);
},

data: hotData,
status: () => manager.hmrStatus,
addStatusHandler: manager.addStatusHandler,
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26751,10 +26751,10 @@ react-redux@^7.0.2:
prop-types "^15.7.2"
react-is "^16.8.6"

react-refresh@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.7.2.tgz#f30978d21eb8cac6e2f2fde056a7d04f6844dd50"
integrity sha512-u5l7fhAJXecWUJzVxzMRU2Zvw8m4QmDNHlTrT5uo3KBlYBhmChd7syAakBoay1yIiVhx/8Fi7a6v6kQZfsw81Q==
react-refresh@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==

react-router-dom@^4.2.2:
version "4.3.1"
Expand Down

0 comments on commit 3bd6cc1

Please sign in to comment.