Skip to content

Commit

Permalink
[React Refresh] handle nested namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
Wang Yilin committed Oct 28, 2021
1 parent 9e86f63 commit 003b251
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 48 deletions.
65 changes: 51 additions & 14 deletions packages/react-refresh/src/ReactFreshBabelPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,16 +484,13 @@ export default function(babel, opts = {}) {
insertAfterPath = path;
programPath = path.parentPath;
break;
case 'TSModuleBlock':
insertAfterPath = path;
programPath = insertAfterPath.parentPath.parentPath;
break;
case 'ExportNamedDeclaration':
insertAfterPath = path.parentPath;
programPath = insertAfterPath.parentPath;
while (programPath.type !== 'Program') {
if (programPath.type === 'TSModuleDeclaration') {
// ExportNamedDeclaration can be nested in TSModules in typescript
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
}
programPath = programPath.parentPath;
}
break;
case 'ExportDefaultDeclaration':
insertAfterPath = path.parentPath;
Expand All @@ -502,6 +499,28 @@ export default function(babel, opts = {}) {
default:
return;
}

// These types can be nested in typescript namespace
// We need to find the export chain
// Or return if it stays local
if (
path.parent.type === 'TSModuleBlock' ||
path.parent.type === 'ExportNamedDeclaration'
) {
while (programPath.type !== 'Program') {
if (programPath.type === 'TSModuleDeclaration') {
if (
programPath.parentPath.type !== 'Program' &&
programPath.parentPath.type !== 'ExportNamedDeclaration'
) {
return;
}
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
}
programPath = programPath.parentPath;
}
}

const id = node.id;
if (id === null) {
// We don't currently handle anonymous default exports.
Expand Down Expand Up @@ -690,16 +709,13 @@ export default function(babel, opts = {}) {
insertAfterPath = path;
programPath = path.parentPath;
break;
case 'TSModuleBlock':
insertAfterPath = path;
programPath = insertAfterPath.parentPath.parentPath;
break;
case 'ExportNamedDeclaration':
insertAfterPath = path.parentPath;
programPath = insertAfterPath.parentPath;
while (programPath.type !== 'Program') {
if (programPath.type === 'TSModuleDeclaration') {
// ExportNamedDeclaration can be nested in TSModules in typescript
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
}
programPath = programPath.parentPath;
}
break;
case 'ExportDefaultDeclaration':
insertAfterPath = path.parentPath;
Expand All @@ -709,6 +725,27 @@ export default function(babel, opts = {}) {
return;
}

// These types can be nested in typescript namespace
// We need to find the export chain
// Or return if it stays local
if (
path.parent.type === 'TSModuleBlock' ||
path.parent.type === 'ExportNamedDeclaration'
) {
while (programPath.type !== 'Program') {
if (programPath.type === 'TSModuleDeclaration') {
if (
programPath.parentPath.type !== 'Program' &&
programPath.parentPath.type !== 'ExportNamedDeclaration'
) {
return;
}
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
}
programPath = programPath.parentPath;
}
}

// Make sure we're not mutating the same tree twice.
// This can happen if another Babel plugin replaces parents.
if (seenForRegistration.has(node)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,10 +544,17 @@ describe('ReactFreshBabelPlugin', () => {
namespace Foo {
export namespace Bar {
export const A = () => {};
export function B() {};
function B() {};
export const B1 = B;
}
export const C = () => {};
export function D() {};
namespace NotExported {
export const E = () => {};
}
}
`,
{plugins: [['@babel/plugin-syntax-typescript', {isTSX: true}]]},
Expand Down
126 changes: 95 additions & 31 deletions packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let act;

const babel = require('@babel/core');
const freshPlugin = require('react-refresh/babel');
const ts = require('typescript');

describe('ReactFreshIntegration', () => {
let container;
Expand Down Expand Up @@ -46,42 +47,72 @@ describe('ReactFreshIntegration', () => {
}
});

function executeCommon(source, compileDestructuring) {
const compiled = babel.transform(source, {
babelrc: false,
presets: ['@babel/react'],
plugins: [
[freshPlugin, {skipEnvCheck: true}],
'@babel/plugin-transform-modules-commonjs',
compileDestructuring && '@babel/plugin-transform-destructuring',
].filter(Boolean),
}).code;
return executeCompiled(compiled);
}

function executeCompiled(compiled) {
exportsObj = {};
// eslint-disable-next-line no-new-func
new Function(
'global',
'React',
'exports',
'$RefreshReg$',
'$RefreshSig$',
compiled,
)(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
// Module systems will register exports as a fallback.
// This is useful for cases when e.g. a class is exported,
// and we don't want to propagate the update beyond this module.
$RefreshReg$(exportsObj.default, 'exports.default');
return exportsObj.default;
}

function $RefreshReg$(type, id) {
ReactFreshRuntime.register(type, id);
}

function $RefreshSig$() {
return ReactFreshRuntime.createSignatureFunctionForTransform();
}

describe('with compiled destructuring', () => {
runTests(true);
runTests(executeCommon, testCommon);
});

describe('without compiled destructuring', () => {
runTests(false);
runTests(executeCommon, testCommon);
});

function runTests(compileDestructuring) {
function execute(source) {
const compiled = babel.transform(source, {
describe('with typescript syntax', () => {
runTests(function(source) {
const typescriptSource = babel.transform(source, {
babelrc: false,
configFile: false,
presets: ['@babel/react'],
plugins: [
[freshPlugin, {skipEnvCheck: true}],
'@babel/plugin-transform-modules-commonjs',
compileDestructuring && '@babel/plugin-transform-destructuring',
].filter(Boolean),
['@babel/plugin-syntax-typescript', {isTSX: true}],
],
}).code;
exportsObj = {};
// eslint-disable-next-line no-new-func
new Function(
'global',
'React',
'exports',
'$RefreshReg$',
'$RefreshSig$',
compiled,
)(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
// Module systems will register exports as a fallback.
// This is useful for cases when e.g. a class is exported,
// and we don't want to propagate the update beyond this module.
$RefreshReg$(exportsObj.default, 'exports.default');
return exportsObj.default;
}
const compiled = ts.transpileModule(typescriptSource, {
module: ts.ModuleKind.CommonJS,
}).outputText;
return executeCompiled(compiled);
}, testTypescript);
});

function runTests(execute, test) {
function render(source) {
const Component = execute(source);
act(() => {
Expand Down Expand Up @@ -127,14 +158,10 @@ describe('ReactFreshIntegration', () => {
expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
}

function $RefreshReg$(type, id) {
ReactFreshRuntime.register(type, id);
}

function $RefreshSig$() {
return ReactFreshRuntime.createSignatureFunctionForTransform();
}
test(render, patch);
}

function testCommon(render, patch) {
it('reloads function declarations', () => {
if (__DEV__) {
render(`
Expand Down Expand Up @@ -1947,4 +1974,41 @@ describe('ReactFreshIntegration', () => {
});
});
}

function testTypescript(render, patch) {
it('reloads component exported in typescript namespace', () => {
if (__DEV__) {
render(`
namespace Foo {
export namespace Bar {
export const Child = ({prop}) => {
return <h1>{prop}1</h1>
};
}
}
export default function Parent() {
return <Foo.Bar.Child prop={'A'} />;
}
`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
namespace Foo {
export namespace Bar {
export const Child = ({prop}) => {
return <h1>{prop}2</h1>
};
}
}
export default function Parent() {
return <Foo.Bar.Child prop={'B'} />;
}
`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}
});
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -623,19 +623,27 @@ namespace Foo {
export namespace Bar {
export const A = () => {};
_c = A;
export function B() {}
function B() {}
_c2 = B;
;
export const B1 = B;
}
export const C = () => {};
_c3 = C;
export function D() {}
_c4 = D;
;
namespace NotExported {
export const E = () => {};
}
}
var _c, _c2, _c3;
var _c, _c2, _c3, _c4;
$RefreshReg$(_c, "Foo$Bar$A");
$RefreshReg$(_c2, "Foo$Bar$B");
$RefreshReg$(_c3, "Foo$C");
$RefreshReg$(_c4, "Foo$D");
`;

exports[`ReactFreshBabelPlugin uses custom identifiers for $RefreshReg$ and $RefreshSig$ 1`] = `
Expand Down

0 comments on commit 003b251

Please sign in to comment.