Skip to content

Commit ff9897d

Browse files
irinakkWang Yilin
and
Wang Yilin
authored
[React Refresh] support typescript namespace syntax (#22621)
* [React Refresh] support typescript namespace syntax * [React Refresh] handle nested namespace Co-authored-by: Wang Yilin <[email protected]>
1 parent 0ddd69d commit ff9897d

File tree

6 files changed

+227
-45
lines changed

6 files changed

+227
-45
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
1717
"@babel/plugin-syntax-import-meta": "^7.10.4",
1818
"@babel/plugin-syntax-jsx": "^7.10.4",
19+
"@babel/plugin-syntax-typescript": "^7.14.5",
1920
"@babel/plugin-transform-arrow-functions": "^7.10.4",
2021
"@babel/plugin-transform-async-to-generator": "^7.10.4",
2122
"@babel/plugin-transform-block-scoped-functions": "^7.10.4",
@@ -35,7 +36,6 @@
3536
"@babel/preset-flow": "^7.10.4",
3637
"@babel/preset-react": "^7.10.4",
3738
"@babel/traverse": "^7.11.0",
38-
"web-streams-polyfill": "^3.1.1",
3939
"abort-controller": "^3.0.0",
4040
"art": "0.10.1",
4141
"babel-eslint": "^10.0.3",
@@ -96,6 +96,7 @@
9696
"through2": "^3.0.1",
9797
"tmp": "^0.1.0",
9898
"typescript": "^3.7.5",
99+
"web-streams-polyfill": "^3.1.1",
99100
"webpack": "^4.41.2",
100101
"yargs": "^15.3.1"
101102
},

packages/react-refresh/src/ReactFreshBabelPlugin.js

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -478,11 +478,16 @@ export default function(babel, opts = {}) {
478478
const node = path.node;
479479
let programPath;
480480
let insertAfterPath;
481+
let modulePrefix = '';
481482
switch (path.parent.type) {
482483
case 'Program':
483484
insertAfterPath = path;
484485
programPath = path.parentPath;
485486
break;
487+
case 'TSModuleBlock':
488+
insertAfterPath = path;
489+
programPath = insertAfterPath.parentPath.parentPath;
490+
break;
486491
case 'ExportNamedDeclaration':
487492
insertAfterPath = path.parentPath;
488493
programPath = insertAfterPath.parentPath;
@@ -494,6 +499,28 @@ export default function(babel, opts = {}) {
494499
default:
495500
return;
496501
}
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+
497524
const id = node.id;
498525
if (id === null) {
499526
// We don't currently handle anonymous default exports.
@@ -512,20 +539,17 @@ export default function(babel, opts = {}) {
512539
seenForRegistration.add(node);
513540
// Don't mutate the tree above this point.
514541

542+
const innerName = modulePrefix + inferredName;
515543
// export function Named() {}
516544
// function Named() {}
517-
findInnerComponents(
518-
inferredName,
519-
path,
520-
(persistentID, targetExpr) => {
521-
const handle = createRegistration(programPath, persistentID);
522-
insertAfterPath.insertAfter(
523-
t.expressionStatement(
524-
t.assignmentExpression('=', handle, targetExpr),
525-
),
526-
);
527-
},
528-
);
545+
findInnerComponents(innerName, path, (persistentID, targetExpr) => {
546+
const handle = createRegistration(programPath, persistentID);
547+
insertAfterPath.insertAfter(
548+
t.expressionStatement(
549+
t.assignmentExpression('=', handle, targetExpr),
550+
),
551+
);
552+
});
529553
},
530554
exit(path) {
531555
const node = path.node;
@@ -679,11 +703,16 @@ export default function(babel, opts = {}) {
679703
const node = path.node;
680704
let programPath;
681705
let insertAfterPath;
706+
let modulePrefix = '';
682707
switch (path.parent.type) {
683708
case 'Program':
684709
insertAfterPath = path;
685710
programPath = path.parentPath;
686711
break;
712+
case 'TSModuleBlock':
713+
insertAfterPath = path;
714+
programPath = insertAfterPath.parentPath.parentPath;
715+
break;
687716
case 'ExportNamedDeclaration':
688717
insertAfterPath = path.parentPath;
689718
programPath = insertAfterPath.parentPath;
@@ -696,6 +725,27 @@ export default function(babel, opts = {}) {
696725
return;
697726
}
698727

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+
699749
// Make sure we're not mutating the same tree twice.
700750
// This can happen if another Babel plugin replaces parents.
701751
if (seenForRegistration.has(node)) {
@@ -710,8 +760,9 @@ export default function(babel, opts = {}) {
710760
}
711761
const declPath = declPaths[0];
712762
const inferredName = declPath.node.id.name;
763+
const innerName = modulePrefix + inferredName;
713764
findInnerComponents(
714-
inferredName,
765+
innerName,
715766
declPath,
716767
(persistentID, targetExpr, targetPath) => {
717768
if (targetPath === null) {

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,4 +536,29 @@ describe('ReactFreshBabelPlugin', () => {
536536
`),
537537
).toMatchSnapshot();
538538
});
539+
540+
it('supports typescript namespace syntax', () => {
541+
expect(
542+
transform(
543+
`
544+
namespace Foo {
545+
export namespace Bar {
546+
export const A = () => {};
547+
548+
function B() {};
549+
export const B1 = B;
550+
}
551+
552+
export const C = () => {};
553+
export function D() {};
554+
555+
namespace NotExported {
556+
export const E = () => {};
557+
}
558+
}
559+
`,
560+
{plugins: [['@babel/plugin-syntax-typescript', {isTSX: true}]]},
561+
),
562+
).toMatchSnapshot();
563+
});
539564
});

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,34 @@ $RefreshReg$(_c, "Hello");
618618
$RefreshReg$(_c2, "Bar");
619619
`;
620620

621+
exports[`ReactFreshBabelPlugin supports typescript namespace syntax 1`] = `
622+
namespace Foo {
623+
export namespace Bar {
624+
export const A = () => {};
625+
_c = A;
626+
function B() {}
627+
_c2 = B;
628+
;
629+
export const B1 = B;
630+
}
631+
export const C = () => {};
632+
_c3 = C;
633+
export function D() {}
634+
_c4 = D;
635+
;
636+
namespace NotExported {
637+
export const E = () => {};
638+
}
639+
}
640+
641+
var _c, _c2, _c3, _c4;
642+
643+
$RefreshReg$(_c, "Foo$Bar$A");
644+
$RefreshReg$(_c2, "Foo$Bar$B");
645+
$RefreshReg$(_c3, "Foo$C");
646+
$RefreshReg$(_c4, "Foo$D");
647+
`;
648+
621649
exports[`ReactFreshBabelPlugin uses custom identifiers for $RefreshReg$ and $RefreshSig$ 1`] = `
622650
var _s = import.meta.refreshSig();
623651

0 commit comments

Comments
 (0)