Skip to content

Commit 003b251

Browse files
author
Wang Yilin
committed
[React Refresh] handle nested namespace
1 parent 9e86f63 commit 003b251

File tree

4 files changed

+164
-48
lines changed

4 files changed

+164
-48
lines changed

packages/react-refresh/src/ReactFreshBabelPlugin.js

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -484,16 +484,13 @@ export default function(babel, opts = {}) {
484484
insertAfterPath = path;
485485
programPath = path.parentPath;
486486
break;
487+
case 'TSModuleBlock':
488+
insertAfterPath = path;
489+
programPath = insertAfterPath.parentPath.parentPath;
490+
break;
487491
case 'ExportNamedDeclaration':
488492
insertAfterPath = path.parentPath;
489493
programPath = insertAfterPath.parentPath;
490-
while (programPath.type !== 'Program') {
491-
if (programPath.type === 'TSModuleDeclaration') {
492-
// ExportNamedDeclaration can be nested in TSModules in typescript
493-
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
494-
}
495-
programPath = programPath.parentPath;
496-
}
497494
break;
498495
case 'ExportDefaultDeclaration':
499496
insertAfterPath = path.parentPath;
@@ -502,6 +499,28 @@ export default function(babel, opts = {}) {
502499
default:
503500
return;
504501
}
502+
503+
// These types can be nested in typescript namespace
504+
// We need to find the export chain
505+
// Or return if it stays local
506+
if (
507+
path.parent.type === 'TSModuleBlock' ||
508+
path.parent.type === 'ExportNamedDeclaration'
509+
) {
510+
while (programPath.type !== 'Program') {
511+
if (programPath.type === 'TSModuleDeclaration') {
512+
if (
513+
programPath.parentPath.type !== 'Program' &&
514+
programPath.parentPath.type !== 'ExportNamedDeclaration'
515+
) {
516+
return;
517+
}
518+
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
519+
}
520+
programPath = programPath.parentPath;
521+
}
522+
}
523+
505524
const id = node.id;
506525
if (id === null) {
507526
// We don't currently handle anonymous default exports.
@@ -690,16 +709,13 @@ export default function(babel, opts = {}) {
690709
insertAfterPath = path;
691710
programPath = path.parentPath;
692711
break;
712+
case 'TSModuleBlock':
713+
insertAfterPath = path;
714+
programPath = insertAfterPath.parentPath.parentPath;
715+
break;
693716
case 'ExportNamedDeclaration':
694717
insertAfterPath = path.parentPath;
695718
programPath = insertAfterPath.parentPath;
696-
while (programPath.type !== 'Program') {
697-
if (programPath.type === 'TSModuleDeclaration') {
698-
// ExportNamedDeclaration can be nested in TSModules in typescript
699-
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
700-
}
701-
programPath = programPath.parentPath;
702-
}
703719
break;
704720
case 'ExportDefaultDeclaration':
705721
insertAfterPath = path.parentPath;
@@ -709,6 +725,27 @@ export default function(babel, opts = {}) {
709725
return;
710726
}
711727

728+
// These types can be nested in typescript namespace
729+
// We need to find the export chain
730+
// Or return if it stays local
731+
if (
732+
path.parent.type === 'TSModuleBlock' ||
733+
path.parent.type === 'ExportNamedDeclaration'
734+
) {
735+
while (programPath.type !== 'Program') {
736+
if (programPath.type === 'TSModuleDeclaration') {
737+
if (
738+
programPath.parentPath.type !== 'Program' &&
739+
programPath.parentPath.type !== 'ExportNamedDeclaration'
740+
) {
741+
return;
742+
}
743+
modulePrefix = programPath.node.id.name + '$' + modulePrefix;
744+
}
745+
programPath = programPath.parentPath;
746+
}
747+
}
748+
712749
// Make sure we're not mutating the same tree twice.
713750
// This can happen if another Babel plugin replaces parents.
714751
if (seenForRegistration.has(node)) {

packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,17 @@ describe('ReactFreshBabelPlugin', () => {
544544
namespace Foo {
545545
export namespace Bar {
546546
export const A = () => {};
547-
export function B() {};
547+
548+
function B() {};
549+
export const B1 = B;
548550
}
549551
550552
export const C = () => {};
553+
export function D() {};
554+
555+
namespace NotExported {
556+
export const E = () => {};
557+
}
551558
}
552559
`,
553560
{plugins: [['@babel/plugin-syntax-typescript', {isTSX: true}]]},

packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let act;
1818

1919
const babel = require('@babel/core');
2020
const freshPlugin = require('react-refresh/babel');
21+
const ts = require('typescript');
2122

2223
describe('ReactFreshIntegration', () => {
2324
let container;
@@ -46,42 +47,72 @@ describe('ReactFreshIntegration', () => {
4647
}
4748
});
4849

50+
function executeCommon(source, compileDestructuring) {
51+
const compiled = babel.transform(source, {
52+
babelrc: false,
53+
presets: ['@babel/react'],
54+
plugins: [
55+
[freshPlugin, {skipEnvCheck: true}],
56+
'@babel/plugin-transform-modules-commonjs',
57+
compileDestructuring && '@babel/plugin-transform-destructuring',
58+
].filter(Boolean),
59+
}).code;
60+
return executeCompiled(compiled);
61+
}
62+
63+
function executeCompiled(compiled) {
64+
exportsObj = {};
65+
// eslint-disable-next-line no-new-func
66+
new Function(
67+
'global',
68+
'React',
69+
'exports',
70+
'$RefreshReg$',
71+
'$RefreshSig$',
72+
compiled,
73+
)(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
74+
// Module systems will register exports as a fallback.
75+
// This is useful for cases when e.g. a class is exported,
76+
// and we don't want to propagate the update beyond this module.
77+
$RefreshReg$(exportsObj.default, 'exports.default');
78+
return exportsObj.default;
79+
}
80+
81+
function $RefreshReg$(type, id) {
82+
ReactFreshRuntime.register(type, id);
83+
}
84+
85+
function $RefreshSig$() {
86+
return ReactFreshRuntime.createSignatureFunctionForTransform();
87+
}
88+
4989
describe('with compiled destructuring', () => {
50-
runTests(true);
90+
runTests(executeCommon, testCommon);
5191
});
5292

5393
describe('without compiled destructuring', () => {
54-
runTests(false);
94+
runTests(executeCommon, testCommon);
5595
});
5696

57-
function runTests(compileDestructuring) {
58-
function execute(source) {
59-
const compiled = babel.transform(source, {
97+
describe('with typescript syntax', () => {
98+
runTests(function(source) {
99+
const typescriptSource = babel.transform(source, {
60100
babelrc: false,
101+
configFile: false,
61102
presets: ['@babel/react'],
62103
plugins: [
63104
[freshPlugin, {skipEnvCheck: true}],
64-
'@babel/plugin-transform-modules-commonjs',
65-
compileDestructuring && '@babel/plugin-transform-destructuring',
66-
].filter(Boolean),
105+
['@babel/plugin-syntax-typescript', {isTSX: true}],
106+
],
67107
}).code;
68-
exportsObj = {};
69-
// eslint-disable-next-line no-new-func
70-
new Function(
71-
'global',
72-
'React',
73-
'exports',
74-
'$RefreshReg$',
75-
'$RefreshSig$',
76-
compiled,
77-
)(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
78-
// Module systems will register exports as a fallback.
79-
// This is useful for cases when e.g. a class is exported,
80-
// and we don't want to propagate the update beyond this module.
81-
$RefreshReg$(exportsObj.default, 'exports.default');
82-
return exportsObj.default;
83-
}
108+
const compiled = ts.transpileModule(typescriptSource, {
109+
module: ts.ModuleKind.CommonJS,
110+
}).outputText;
111+
return executeCompiled(compiled);
112+
}, testTypescript);
113+
});
84114

115+
function runTests(execute, test) {
85116
function render(source) {
86117
const Component = execute(source);
87118
act(() => {
@@ -127,14 +158,10 @@ describe('ReactFreshIntegration', () => {
127158
expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
128159
}
129160

130-
function $RefreshReg$(type, id) {
131-
ReactFreshRuntime.register(type, id);
132-
}
133-
134-
function $RefreshSig$() {
135-
return ReactFreshRuntime.createSignatureFunctionForTransform();
136-
}
161+
test(render, patch);
162+
}
137163

164+
function testCommon(render, patch) {
138165
it('reloads function declarations', () => {
139166
if (__DEV__) {
140167
render(`
@@ -1947,4 +1974,41 @@ describe('ReactFreshIntegration', () => {
19471974
});
19481975
});
19491976
}
1977+
1978+
function testTypescript(render, patch) {
1979+
it('reloads component exported in typescript namespace', () => {
1980+
if (__DEV__) {
1981+
render(`
1982+
namespace Foo {
1983+
export namespace Bar {
1984+
export const Child = ({prop}) => {
1985+
return <h1>{prop}1</h1>
1986+
};
1987+
}
1988+
}
1989+
1990+
export default function Parent() {
1991+
return <Foo.Bar.Child prop={'A'} />;
1992+
}
1993+
`);
1994+
const el = container.firstChild;
1995+
expect(el.textContent).toBe('A1');
1996+
patch(`
1997+
namespace Foo {
1998+
export namespace Bar {
1999+
export const Child = ({prop}) => {
2000+
return <h1>{prop}2</h1>
2001+
};
2002+
}
2003+
}
2004+
2005+
export default function Parent() {
2006+
return <Foo.Bar.Child prop={'B'} />;
2007+
}
2008+
`);
2009+
expect(container.firstChild).toBe(el);
2010+
expect(el.textContent).toBe('B2');
2011+
}
2012+
});
2013+
}
19502014
});

packages/react-refresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,19 +623,27 @@ namespace Foo {
623623
export namespace Bar {
624624
export const A = () => {};
625625
_c = A;
626-
export function B() {}
626+
function B() {}
627627
_c2 = B;
628628
;
629+
export const B1 = B;
629630
}
630631
export const C = () => {};
631632
_c3 = C;
633+
export function D() {}
634+
_c4 = D;
635+
;
636+
namespace NotExported {
637+
export const E = () => {};
638+
}
632639
}
633640
634-
var _c, _c2, _c3;
641+
var _c, _c2, _c3, _c4;
635642
636643
$RefreshReg$(_c, "Foo$Bar$A");
637644
$RefreshReg$(_c2, "Foo$Bar$B");
638645
$RefreshReg$(_c3, "Foo$C");
646+
$RefreshReg$(_c4, "Foo$D");
639647
`;
640648

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

0 commit comments

Comments
 (0)