diff --git a/package.json b/package.json index e4ea1c83be801..fb428f35c04b9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-jsx": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.14.5", "@babel/plugin-transform-arrow-functions": "^7.10.4", "@babel/plugin-transform-async-to-generator": "^7.10.4", "@babel/plugin-transform-block-scoped-functions": "^7.10.4", @@ -35,7 +36,6 @@ "@babel/preset-flow": "^7.10.4", "@babel/preset-react": "^7.10.4", "@babel/traverse": "^7.11.0", - "web-streams-polyfill": "^3.1.1", "abort-controller": "^3.0.0", "art": "0.10.1", "babel-eslint": "^10.0.3", @@ -96,6 +96,7 @@ "through2": "^3.0.1", "tmp": "^0.1.0", "typescript": "^3.7.5", + "web-streams-polyfill": "^3.1.1", "webpack": "^4.41.2", "yargs": "^15.3.1" }, diff --git a/packages/react-refresh/src/ReactFreshBabelPlugin.js b/packages/react-refresh/src/ReactFreshBabelPlugin.js index 64013db181a3f..9d1aed0d1d793 100644 --- a/packages/react-refresh/src/ReactFreshBabelPlugin.js +++ b/packages/react-refresh/src/ReactFreshBabelPlugin.js @@ -478,11 +478,16 @@ export default function(babel, opts = {}) { const node = path.node; let programPath; let insertAfterPath; + let modulePrefix = ''; switch (path.parent.type) { case 'Program': insertAfterPath = path; programPath = path.parentPath; break; + case 'TSModuleBlock': + insertAfterPath = path; + programPath = insertAfterPath.parentPath.parentPath; + break; case 'ExportNamedDeclaration': insertAfterPath = path.parentPath; programPath = insertAfterPath.parentPath; @@ -494,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. @@ -512,20 +539,17 @@ export default function(babel, opts = {}) { seenForRegistration.add(node); // Don't mutate the tree above this point. + const innerName = modulePrefix + inferredName; // export function Named() {} // function Named() {} - findInnerComponents( - inferredName, - path, - (persistentID, targetExpr) => { - const handle = createRegistration(programPath, persistentID); - insertAfterPath.insertAfter( - t.expressionStatement( - t.assignmentExpression('=', handle, targetExpr), - ), - ); - }, - ); + findInnerComponents(innerName, path, (persistentID, targetExpr) => { + const handle = createRegistration(programPath, persistentID); + insertAfterPath.insertAfter( + t.expressionStatement( + t.assignmentExpression('=', handle, targetExpr), + ), + ); + }); }, exit(path) { const node = path.node; @@ -679,11 +703,16 @@ export default function(babel, opts = {}) { const node = path.node; let programPath; let insertAfterPath; + let modulePrefix = ''; switch (path.parent.type) { case 'Program': insertAfterPath = path; programPath = path.parentPath; break; + case 'TSModuleBlock': + insertAfterPath = path; + programPath = insertAfterPath.parentPath.parentPath; + break; case 'ExportNamedDeclaration': insertAfterPath = path.parentPath; programPath = insertAfterPath.parentPath; @@ -696,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)) { @@ -710,8 +760,9 @@ export default function(babel, opts = {}) { } const declPath = declPaths[0]; const inferredName = declPath.node.id.name; + const innerName = modulePrefix + inferredName; findInnerComponents( - inferredName, + innerName, declPath, (persistentID, targetExpr, targetPath) => { if (targetPath === null) { diff --git a/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js b/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js index f56f16069dd77..d528d46bb8eaf 100644 --- a/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js +++ b/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js @@ -536,4 +536,29 @@ describe('ReactFreshBabelPlugin', () => { `), ).toMatchSnapshot(); }); + + it('supports typescript namespace syntax', () => { + expect( + transform( + ` + namespace Foo { + export namespace Bar { + export const A = () => {}; + + function B() {}; + export const B1 = B; + } + + export const C = () => {}; + export function D() {}; + + namespace NotExported { + export const E = () => {}; + } + } + `, + {plugins: [['@babel/plugin-syntax-typescript', {isTSX: true}]]}, + ), + ).toMatchSnapshot(); + }); }); diff --git a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js index 09edf1d897288..8a98fe097f1b9 100644 --- a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js +++ b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js @@ -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; @@ -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(() => { @@ -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(` @@ -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