mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[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>
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 <h1>{prop}1</h1>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Parent() {
|
||||
return <Foo.Bar.Child prop={'A'} />;
|
||||
}
|
||||
`);
|
||||
const el = container.firstChild;
|
||||
expect(el.textContent).toBe('A1');
|
||||
patch(`
|
||||
namespace Foo {
|
||||
export namespace Bar {
|
||||
export const Child = ({prop}) => {
|
||||
return <h1>{prop}2</h1>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Parent() {
|
||||
return <Foo.Bar.Child prop={'B'} />;
|
||||
}
|
||||
`);
|
||||
expect(container.firstChild).toBe(el);
|
||||
expect(el.textContent).toBe('B2');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -618,6 +618,34 @@ $RefreshReg$(_c, "Hello");
|
||||
$RefreshReg$(_c2, "Bar");
|
||||
`;
|
||||
|
||||
exports[`ReactFreshBabelPlugin supports typescript namespace syntax 1`] = `
|
||||
namespace Foo {
|
||||
export namespace Bar {
|
||||
export const A = () => {};
|
||||
_c = A;
|
||||
function B() {}
|
||||
_c2 = B;
|
||||
;
|
||||
export const B1 = B;
|
||||
}
|
||||
export const C = () => {};
|
||||
_c3 = C;
|
||||
export function D() {}
|
||||
_c4 = D;
|
||||
;
|
||||
namespace NotExported {
|
||||
export const E = () => {};
|
||||
}
|
||||
}
|
||||
|
||||
var _c, _c2, _c3, _c4;
|
||||
|
||||
$RefreshReg$(_c, "Foo$Bar$A");
|
||||
$RefreshReg$(_c2, "Foo$Bar$B");
|
||||
$RefreshReg$(_c3, "Foo$C");
|
||||
$RefreshReg$(_c4, "Foo$D");
|
||||
`;
|
||||
|
||||
exports[`ReactFreshBabelPlugin uses custom identifiers for $RefreshReg$ and $RefreshSig$ 1`] = `
|
||||
var _s = import.meta.refreshSig();
|
||||
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@@ -405,6 +405,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af"
|
||||
integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==
|
||||
|
||||
"@babel/helper-plugin-utils@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
|
||||
integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
|
||||
|
||||
"@babel/helper-regex@^7.10.4":
|
||||
version "7.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0"
|
||||
@@ -847,6 +852,13 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/plugin-syntax-typescript@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716"
|
||||
integrity sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/plugin-transform-arrow-functions@^7.0.0":
|
||||
version "7.8.3"
|
||||
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:
|
||||
|
||||
"eslint-plugin-react-internal@link:./scripts/eslint-rules":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
eslint-plugin-react@^6.7.1:
|
||||
version "6.10.3"
|
||||
|
||||
Reference in New Issue
Block a user