[ts] Insert export {} when necessary to imply ESM (#13314)
This commit is contained in:
parent
f0b9b25a23
commit
b4c798e754
@ -21,8 +21,12 @@ function isInType(path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARSED_PARAMS = new WeakSet();
|
|
||||||
const GLOBAL_TYPES = new WeakMap();
|
const GLOBAL_TYPES = new WeakMap();
|
||||||
|
// Track programs which contain imports/exports of values, so that we can include
|
||||||
|
// empty exports for programs that do not, but were parsed as modules. This allows
|
||||||
|
// tools to infer unamibiguously that results are ESM.
|
||||||
|
const NEEDS_EXPLICIT_ESM = new WeakMap();
|
||||||
|
const PARSED_PARAMS = new WeakSet();
|
||||||
|
|
||||||
function isGlobalType(path, name) {
|
function isGlobalType(path, name) {
|
||||||
const program = path.find(path => path.isProgram()).node;
|
const program = path.find(path => path.isProgram()).node;
|
||||||
@ -175,118 +179,143 @@ export default declare((api, opts) => {
|
|||||||
Identifier: visitPattern,
|
Identifier: visitPattern,
|
||||||
RestElement: visitPattern,
|
RestElement: visitPattern,
|
||||||
|
|
||||||
Program(path, state) {
|
Program: {
|
||||||
const { file } = state;
|
enter(path, state) {
|
||||||
let fileJsxPragma = null;
|
const { file } = state;
|
||||||
let fileJsxPragmaFrag = null;
|
let fileJsxPragma = null;
|
||||||
|
let fileJsxPragmaFrag = null;
|
||||||
|
|
||||||
if (!GLOBAL_TYPES.has(path.node)) {
|
if (!GLOBAL_TYPES.has(path.node)) {
|
||||||
GLOBAL_TYPES.set(path.node, new Set());
|
GLOBAL_TYPES.set(path.node, new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.ast.comments) {
|
if (file.ast.comments) {
|
||||||
for (const comment of file.ast.comments) {
|
for (const comment of file.ast.comments) {
|
||||||
const jsxMatches = JSX_PRAGMA_REGEX.exec(comment.value);
|
const jsxMatches = JSX_PRAGMA_REGEX.exec(comment.value);
|
||||||
if (jsxMatches) {
|
if (jsxMatches) {
|
||||||
if (jsxMatches[1]) {
|
if (jsxMatches[1]) {
|
||||||
// isFragment
|
// isFragment
|
||||||
fileJsxPragmaFrag = jsxMatches[2];
|
fileJsxPragmaFrag = jsxMatches[2];
|
||||||
} else {
|
} else {
|
||||||
fileJsxPragma = jsxMatches[2];
|
fileJsxPragma = jsxMatches[2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let pragmaImportName = fileJsxPragma || jsxPragma;
|
let pragmaImportName = fileJsxPragma || jsxPragma;
|
||||||
if (pragmaImportName) {
|
if (pragmaImportName) {
|
||||||
[pragmaImportName] = pragmaImportName.split(".");
|
[pragmaImportName] = pragmaImportName.split(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
let pragmaFragImportName = fileJsxPragmaFrag || jsxPragmaFrag;
|
let pragmaFragImportName = fileJsxPragmaFrag || jsxPragmaFrag;
|
||||||
if (pragmaFragImportName) {
|
if (pragmaFragImportName) {
|
||||||
[pragmaFragImportName] = pragmaFragImportName.split(".");
|
[pragmaFragImportName] = pragmaFragImportName.split(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove type imports
|
// remove type imports
|
||||||
for (let stmt of path.get("body")) {
|
for (let stmt of path.get("body")) {
|
||||||
if (stmt.isImportDeclaration()) {
|
if (stmt.isImportDeclaration()) {
|
||||||
if (stmt.node.importKind === "type") {
|
if (!NEEDS_EXPLICIT_ESM.has(state.file.ast.program)) {
|
||||||
stmt.remove();
|
NEEDS_EXPLICIT_ESM.set(state.file.ast.program, true);
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If onlyRemoveTypeImports is `true`, only remove type-only imports
|
if (stmt.node.importKind === "type") {
|
||||||
// and exports introduced in TypeScript 3.8.
|
stmt.remove();
|
||||||
if (!onlyRemoveTypeImports) {
|
|
||||||
// Note: this will allow both `import { } from "m"` and `import "m";`.
|
|
||||||
// In TypeScript, the former would be elided.
|
|
||||||
if (stmt.node.specifiers.length === 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let allElided = true;
|
// If onlyRemoveTypeImports is `true`, only remove type-only imports
|
||||||
const importsToRemove: NodePath<t.Node>[] = [];
|
// and exports introduced in TypeScript 3.8.
|
||||||
|
if (onlyRemoveTypeImports) {
|
||||||
for (const specifier of stmt.node.specifiers) {
|
NEEDS_EXPLICIT_ESM.set(path.node, false);
|
||||||
const binding = stmt.scope.getBinding(specifier.local.name);
|
|
||||||
|
|
||||||
// The binding may not exist if the import node was explicitly
|
|
||||||
// injected by another plugin. Currently core does not do a good job
|
|
||||||
// of keeping scope bindings synchronized with the AST. For now we
|
|
||||||
// just bail if there is no binding, since chances are good that if
|
|
||||||
// the import statement was injected then it wasn't a typescript type
|
|
||||||
// import anyway.
|
|
||||||
if (
|
|
||||||
binding &&
|
|
||||||
isImportTypeOnly({
|
|
||||||
binding,
|
|
||||||
programPath: path,
|
|
||||||
pragmaImportName,
|
|
||||||
pragmaFragImportName,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
importsToRemove.push(binding.path);
|
|
||||||
} else {
|
|
||||||
allElided = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allElided) {
|
|
||||||
stmt.remove();
|
|
||||||
} else {
|
} else {
|
||||||
for (const importPath of importsToRemove) {
|
// Note: this will allow both `import { } from "m"` and `import "m";`.
|
||||||
importPath.remove();
|
// In TypeScript, the former would be elided.
|
||||||
|
if (stmt.node.specifiers.length === 0) {
|
||||||
|
NEEDS_EXPLICIT_ESM.set(path.node, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allElided = true;
|
||||||
|
const importsToRemove: NodePath<t.Node>[] = [];
|
||||||
|
|
||||||
|
for (const specifier of stmt.node.specifiers) {
|
||||||
|
const binding = stmt.scope.getBinding(specifier.local.name);
|
||||||
|
|
||||||
|
// The binding may not exist if the import node was explicitly
|
||||||
|
// injected by another plugin. Currently core does not do a good job
|
||||||
|
// of keeping scope bindings synchronized with the AST. For now we
|
||||||
|
// just bail if there is no binding, since chances are good that if
|
||||||
|
// the import statement was injected then it wasn't a typescript type
|
||||||
|
// import anyway.
|
||||||
|
if (
|
||||||
|
binding &&
|
||||||
|
isImportTypeOnly({
|
||||||
|
binding,
|
||||||
|
programPath: path,
|
||||||
|
pragmaImportName,
|
||||||
|
pragmaFragImportName,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
importsToRemove.push(binding.path);
|
||||||
|
} else {
|
||||||
|
allElided = false;
|
||||||
|
NEEDS_EXPLICIT_ESM.set(path.node, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allElided) {
|
||||||
|
stmt.remove();
|
||||||
|
} else {
|
||||||
|
for (const importPath of importsToRemove) {
|
||||||
|
importPath.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
if (stmt.isExportDeclaration()) {
|
||||||
}
|
stmt = stmt.get("declaration");
|
||||||
|
|
||||||
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() ||
|
if (stmt.isVariableDeclaration({ declare: true })) {
|
||||||
stmt.isTSDeclareFunction() ||
|
for (const name of Object.keys(stmt.getBindingIdentifiers())) {
|
||||||
stmt.isTSInterfaceDeclaration() ||
|
registerGlobalType(path.scope, name);
|
||||||
stmt.isClassDeclaration({ declare: true }) ||
|
}
|
||||||
stmt.isTSEnumDeclaration({ declare: true }) ||
|
} else if (
|
||||||
(stmt.isTSModuleDeclaration({ declare: true }) &&
|
stmt.isTSTypeAliasDeclaration() ||
|
||||||
stmt.get("id").isIdentifier())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exit(path) {
|
||||||
|
if (
|
||||||
|
path.node.sourceType === "module" &&
|
||||||
|
NEEDS_EXPLICIT_ESM.get(path.node)
|
||||||
) {
|
) {
|
||||||
registerGlobalType(path.scope, stmt.node.id.name);
|
// If there are no remaining value exports, this file can no longer
|
||||||
|
// be inferred to be ESM. Leave behind an empty export declaration
|
||||||
|
// so it can be.
|
||||||
|
path.pushContainer("body", t.exportNamedDeclaration());
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ExportNamedDeclaration(path) {
|
ExportNamedDeclaration(path, state) {
|
||||||
|
if (!NEEDS_EXPLICIT_ESM.has(state.file.ast.program)) {
|
||||||
|
NEEDS_EXPLICIT_ESM.set(state.file.ast.program, true);
|
||||||
|
}
|
||||||
|
|
||||||
if (path.node.exportKind === "type") {
|
if (path.node.exportKind === "type") {
|
||||||
path.remove();
|
path.remove();
|
||||||
return;
|
return;
|
||||||
@ -307,7 +336,10 @@ export default declare((api, opts) => {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
path.remove();
|
path.remove();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NEEDS_EXPLICIT_ESM.set(state.file.ast.program, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
ExportSpecifier(path) {
|
ExportSpecifier(path) {
|
||||||
@ -317,14 +349,22 @@ export default declare((api, opts) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ExportDefaultDeclaration(path) {
|
ExportDefaultDeclaration(path, state) {
|
||||||
|
if (!NEEDS_EXPLICIT_ESM.has(state.file.ast.program)) {
|
||||||
|
NEEDS_EXPLICIT_ESM.set(state.file.ast.program, true);
|
||||||
|
}
|
||||||
|
|
||||||
// remove whole declaration if it's exporting a TS type
|
// remove whole declaration if it's exporting a TS type
|
||||||
if (
|
if (
|
||||||
t.isIdentifier(path.node.declaration) &&
|
t.isIdentifier(path.node.declaration) &&
|
||||||
isGlobalType(path, path.node.declaration.name)
|
isGlobalType(path, path.node.declaration.name)
|
||||||
) {
|
) {
|
||||||
path.remove();
|
path.remove();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NEEDS_EXPLICIT_ESM.set(state.file.ast.program, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
TSDeclareFunction(path) {
|
TSDeclareFunction(path) {
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
; // Otherwise-empty file
|
; // Otherwise-empty file
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
; // Otherwise-empty file
|
; // Otherwise-empty file
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
;
|
;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
;
|
;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
const x = 0;
|
const x = 0;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
const x = 0;
|
const x = 0;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
const x = 0;
|
const x = 0;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
const x = 0;
|
const x = 0;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
const x = 0;
|
const x = 0;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -5,3 +5,4 @@ var Enum;
|
|||||||
})(Enum || (Enum = {}));
|
})(Enum || (Enum = {}));
|
||||||
|
|
||||||
;
|
;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
// TODO: This should not be removed
|
// TODO: This should not be removed
|
||||||
;
|
;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
;
|
;
|
||||||
|
export {};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
const obj = {
|
const obj = {
|
||||||
A: 'foo'
|
A: 'foo'
|
||||||
};
|
};
|
||||||
|
export {};
|
||||||
|
|||||||
@ -37,3 +37,5 @@ for (let s of strings) {
|
|||||||
console.log(`"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`);
|
console.log(`"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user