@samitouri / QOS-React-2 / commits / ff9897d23e

[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"