[React Refresh] support typescript namespace syntax (#22621)
* [React Refresh] support typescript namespace syntax * [React Refresh] handle nested namespace Co-authored-by: Wang Yilin <wang_yil@worksap.co.jp>
irinakk committed
Nov 10, 2021 at 04:22 UTC
ff9897d23ec713d4e0c802386dffe4024916749e
6 files changed
+227
-45
package.json
+2
-1
@@ -16,6 +16,7 @@
16
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
17
"@babel/plugin-syntax-import-meta": "^7.10.4",
18
"@babel/plugin-syntax-jsx": "^7.10.4",
19
+ "@babel/plugin-syntax-typescript": "^7.14.5",
20
"@babel/plugin-transform-arrow-functions": "^7.10.4",
21
"@babel/plugin-transform-async-to-generator": "^7.10.4",
22
"@babel/plugin-transform-block-scoped-functions": "^7.10.4",
@@ -35,7 +36,6 @@
36
"@babel/preset-flow": "^7.10.4",
37
"@babel/preset-react": "^7.10.4",
38
"@babel/traverse": "^7.11.0",
38
- "web-streams-polyfill": "^3.1.1",
39
"abort-controller": "^3.0.0",
40
"art": "0.10.1",
41
"babel-eslint": "^10.0.3",
@@ -96,6 +96,7 @@
96
"through2": "^3.0.1",
97
"tmp": "^0.1.0",
98
"typescript": "^3.7.5",
99
+ "web-streams-polyfill": "^3.1.1",
100
"webpack": "^4.41.2",
101
"yargs": "^15.3.1"
102
},
packages/react-refresh/src/ReactFreshBabelPlugin.js
+64
-13
@@ -478,11 +478,16 @@ export default function(babel, opts = {}) {
478
const node = path.node;
479
let programPath;
480
let insertAfterPath;
481
+ let modulePrefix = '';
482
switch (path.parent.type) {
483
case 'Program':
484
insertAfterPath = path;
485
programPath = path.parentPath;
486
break;
487
+ case 'TSModuleBlock':
488
+ insertAfterPath = path;
489
+ programPath = insertAfterPath.parentPath.parentPath;
490
+ break;
491
case 'ExportNamedDeclaration':
492
insertAfterPath = path.parentPath;
493
programPath = insertAfterPath.parentPath;
@@ -494,6 +499,28 @@ export default function(babel, opts = {}) {
499
default:
500
return;
501
}
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
+
524
const id = node.id;
525
if (id === null) {
526
// We don't currently handle anonymous default exports.
@@ -512,20 +539,17 @@ export default function(babel, opts = {}) {
539
seenForRegistration.add(node);
540
// Don't mutate the tree above this point.
541
542
+ const innerName = modulePrefix + inferredName;
543
// export function Named() {}
544
// 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
+ });
553
},
554
exit(path) {
555
const node = path.node;
@@ -679,11 +703,16 @@ export default function(babel, opts = {}) {
703
const node = path.node;
704
let programPath;
705
let insertAfterPath;
706
+ let modulePrefix = '';
707
switch (path.parent.type) {
708
case 'Program':
709
insertAfterPath = path;
710
programPath = path.parentPath;
711
break;
712
+ case 'TSModuleBlock':
713
+ insertAfterPath = path;
714
+ programPath = insertAfterPath.parentPath.parentPath;
715
+ break;
716
case 'ExportNamedDeclaration':
717
insertAfterPath = path.parentPath;
718
programPath = insertAfterPath.parentPath;
@@ -696,6 +725,27 @@ export default function(babel, opts = {}) {
725
return;
726
}
727
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
+
749
// Make sure we're not mutating the same tree twice.
750
// This can happen if another Babel plugin replaces parents.
751
if (seenForRegistration.has(node)) {
@@ -710,8 +760,9 @@ export default function(babel, opts = {}) {
760
}
761
const declPath = declPaths[0];
762
const inferredName = declPath.node.id.name;
763
+ const innerName = modulePrefix + inferredName;
764
findInnerComponents(
714
- inferredName,
765
+ innerName,
766
declPath,
767
(persistentID, targetExpr, targetPath) => {
768
if (targetPath === null) {
packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js
+25
@@ -536,4 +536,29 @@ describe('ReactFreshBabelPlugin', () => {
536
`),
537
).toMatchSnapshot();
538
});
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
+ });
564
});
packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+95
-31
@@ -18,6 +18,7 @@ let act;
18
19
const babel = require('@babel/core');
20
const freshPlugin = require('react-refresh/babel');
21
+const ts = require('typescript');
22
23
describe('ReactFreshIntegration', () => {
24
let container;
@@ -46,42 +47,72 @@ describe('ReactFreshIntegration', () => {
47
}
48
});
49
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
+
89
describe('with compiled destructuring', () => {
50
- runTests(true);
90
+ runTests(executeCommon, testCommon);
91
});
92
93
describe('without compiled destructuring', () => {
54
- runTests(false);
94
+ runTests(executeCommon, testCommon);
95
});
96
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, {
100
babelrc: false,
101
+ configFile: false,
102
presets: ['@babel/react'],
103
plugins: [
104
[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
+ ],
107
}).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
+ });
114
115
+ function runTests(execute, test) {
116
function render(source) {
117
const Component = execute(source);
118
act(() => {
@@ -127,14 +158,10 @@ describe('ReactFreshIntegration', () => {
158
expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
159
}
160
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
+ }
163
164
+ function testCommon(render, patch) {
165
it('reloads function declarations', () => {
166
if (__DEV__) {
167
render(`
@@ -1947,4 +1974,41 @@ describe('ReactFreshIntegration', () => {
1974
});
1975
});
1976
}
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
+ }
2014
});
packages/react-refresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
+28
@@ -618,6 +618,34 @@ $RefreshReg$(_c, "Hello");
618
$RefreshReg$(_c2, "Bar");
619
`;
620
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
+
649
exports[`ReactFreshBabelPlugin uses custom identifiers for $RefreshReg$ and $RefreshSig$ 1`] = `
650
var _s = import.meta.refreshSig();
651
yarn.lock
+13
@@ -405,6 +405,11 @@
405
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af"
406
integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==
407
408
+"@babel/helper-plugin-utils@^7.14.5":
409
+ version "7.14.5"
410
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
411
+ integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
412
+
413
"@babel/helper-regex@^7.10.4":
414
version "7.10.5"
415
resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
@@ -847,6 +852,13 @@
852
dependencies:
853
"@babel/helper-plugin-utils" "^7.12.13"
854
855
+"@babel/plugin-syntax-typescript@^7.14.5":
856
+ version "7.14.5"
857
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716"
858
+ integrity sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q==
859
+ dependencies:
860
+ "@babel/helper-plugin-utils" "^7.14.5"
861
+
862
"@babel/plugin-transform-arrow-functions@^7.0.0":
863
version "7.8.3"
864
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
@@ -6442,6 +6454,7 @@ eslint-plugin-no-unsanitized@3.1.2:
6454
6455
"eslint-plugin-react-internal@link:./scripts/eslint-rules":
6456
version "0.0.0"
6457
+ uid ""
6458
6459
eslint-plugin-react@^6.7.1:
6460
version "6.10.3"