diff --git a/packages/babel-plugin-transform-typescript/src/index.js b/packages/babel-plugin-transform-typescript/src/index.js index 8498da7afa..c89e2dbe81 100644 --- a/packages/babel-plugin-transform-typescript/src/index.js +++ b/packages/babel-plugin-transform-typescript/src/index.js @@ -19,6 +19,29 @@ function isInType(path) { } const PARSED_PARAMS = new WeakSet(); +const GLOBAL_TYPES = new WeakMap(); + +function isGlobalType(path, name) { + const program = path.find(path => path.isProgram()).node; + if (path.scope.hasOwnBinding(name)) return false; + if (GLOBAL_TYPES.get(program).has(name)) return true; + + console.warn( + `The exported identifier "${name}" is not declared in Babel's scope tracker\n` + + `as a JavaScript value binding, and "@babel/plugin-transform-typescript"\n` + + `never encountered it as a TypeScript type declaration.\n` + + `It will be treated as a JavaScript value.\n\n` + + `This problem is likely caused by another plugin injecting\n` + + `"${name}" without registering it in the scope tracker. If you are the author\n` + + ` of that plugin, please use "scope.registerDeclaration(declarationPath)".`, + ); + + return false; +} + +function registerGlobalType(programScope, name) { + GLOBAL_TYPES.get(programScope.path.node).add(name); +} export default declare( (api, { jsxPragma = "React", allowNamespaces = false }) => { @@ -40,6 +63,10 @@ export default declare( const { file } = state; let fileJsxPragma = null; + if (!GLOBAL_TYPES.has(path.node)) { + GLOBAL_TYPES.set(path.node, new Set()); + } + if (file.ast.comments) { for (const comment of (file.ast.comments: Array)) { const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value); @@ -50,7 +77,7 @@ export default declare( } // remove type imports - for (const stmt of path.get("body")) { + for (let stmt of path.get("body")) { if (t.isImportDeclaration(stmt)) { // Note: this will allow both `import { } from "m"` and `import "m";`. // In TypeScript, the former would be elided. @@ -91,6 +118,28 @@ export default declare( importPath.remove(); } } + + continue; + } + + if (stmt.isExportDeclaration()) { + stmt = stmt.get("declaration"); + } + + if (stmt.isVariableDeclaration({ declare: true })) { + for (const name of Object.keys(stmt.getBindingIdentifiers())) { + registerGlobalType(path.scope, name); + } + } else if ( + stmt.isTSTypeAliasDeclaration() || + stmt.isTSDeclareFunction() || + stmt.isTSInterfaceDeclaration() || + stmt.isClassDeclaration({ declare: true }) || + stmt.isTSEnumDeclaration({ declare: true }) || + (stmt.isTSModuleDeclaration({ declare: true }) && + stmt.get("id").isIdentifier()) + ) { + registerGlobalType(path.scope, stmt.node.id.name); } } }, @@ -100,8 +149,8 @@ export default declare( if ( !path.node.source && path.node.specifiers.length > 0 && - !path.node.specifiers.find(exportSpecifier => - path.scope.hasOwnBinding(exportSpecifier.local.name), + path.node.specifiers.every(({ local }) => + isGlobalType(path, local.name), ) ) { path.remove(); @@ -110,10 +159,7 @@ export default declare( ExportSpecifier(path) { // remove type exports - if ( - !path.parent.source && - !path.scope.hasOwnBinding(path.node.local.name) - ) { + if (!path.parent.source && isGlobalType(path, path.node.local.name)) { path.remove(); } }, @@ -122,7 +168,7 @@ export default declare( // remove whole declaration if it's exporting a TS type if ( t.isIdentifier(path.node.declaration) && - !path.scope.hasOwnBinding(path.node.declaration.name) + isGlobalType(path, path.node.declaration.name) ) { path.remove(); } @@ -137,7 +183,9 @@ export default declare( }, VariableDeclaration(path) { - if (path.node.declare) path.remove(); + if (path.node.declare) { + path.remove(); + } }, VariableDeclarator({ node }) { diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/input.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/input.mjs new file mode 100644 index 0000000000..350b30f5eb --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/input.mjs @@ -0,0 +1 @@ +export default 2; \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/options.json new file mode 100644 index 0000000000..713ccc312f --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["./plugin.js", "transform-typescript"] +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/output.mjs new file mode 100644 index 0000000000..6b7714ba49 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/output.mjs @@ -0,0 +1,3 @@ +const foo = 2; +export default foo; +["The exported identifier \"foo\" is not declared in Babel's scope tracker\nas a JavaScript value binding, and \"@babel/plugin-transform-typescript\"\nnever encountered it as a TypeScript type declaration.\nIt will be treated as a JavaScript value.\n\nThis problem is likely caused by another plugin injecting\n\"foo\" without registering it in the scope tracker. If you are the author\n of that plugin, please use \"scope.registerDeclaration(declarationPath)\"."]; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/plugin.js b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/plugin.js new file mode 100644 index 0000000000..ce5dbd4fd0 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/regression/10162/plugin.js @@ -0,0 +1,30 @@ +"use strict"; + +module.exports = function({ template, types: t }) { + const warnings = []; + let consoleWarn; + + return { + pre() { + consoleWarn = console.warn; + console.warn = msg => warnings.push(msg); + }, + + post({ path }) { + console.warn = consoleWarn; + + const stmt = t.expressionStatement(t.valueToNode(warnings)); + path.pushContainer("body", stmt); + }, + + visitor: { + ExportDefaultDeclaration(path) { + path.insertBefore(template.statement.ast` + const foo = ${path.node.declaration}; + `); + + path.node.declaration = t.identifier("foo"); + }, + }, + }; +};