Huáng Jùnliàng 614b486780
Use named imports for babel types (#13685)
* migrate to named babel types imports

* perf: transform babel types import to destructuring

* fix merge errors

* apply plugin to itself
2021-08-18 10:28:40 -04:00

488 lines
13 KiB
TypeScript

import assert from "assert";
import {
booleanLiteral,
callExpression,
cloneNode,
directive,
directiveLiteral,
expressionStatement,
identifier,
isIdentifier,
memberExpression,
stringLiteral,
valueToNode,
variableDeclaration,
variableDeclarator,
} from "@babel/types";
import type * as t from "@babel/types";
import template from "@babel/template";
import { isModule } from "@babel/helper-module-imports";
import rewriteThis from "./rewrite-this";
import rewriteLiveReferences from "./rewrite-live-references";
import normalizeModuleAndLoadMetadata, {
hasExports,
isSideEffectImport,
validateImportInteropOption,
} from "./normalize-and-load-metadata";
import type {
InteropType,
ModuleMetadata,
SourceModuleMetadata,
} from "./normalize-and-load-metadata";
import type { NodePath } from "@babel/traverse";
export { default as getModuleName } from "./get-module-name";
export { hasExports, isSideEffectImport, isModule, rewriteThis };
/**
* Perform all of the generic ES6 module rewriting needed to handle initial
* module processing. This function will rewrite the majority of the given
* program to reference the modules described by the returned metadata,
* and returns a list of statements for use when initializing the module.
*/
export function rewriteModuleStatementsAndPrepareHeader(
path: NodePath<t.Program>,
{
// TODO(Babel 8): Remove this
loose,
exportName,
strict,
allowTopLevelThis,
strictMode,
noInterop,
importInterop = noInterop ? "none" : "babel",
lazy,
esNamespaceOnly,
constantReexports = loose,
enumerableModuleMeta = loose,
noIncompleteNsImportDetection,
}: {
exportName?;
strict;
allowTopLevelThis?;
strictMode;
loose?;
importInterop?: "none" | "babel" | "node";
noInterop?;
lazy?;
esNamespaceOnly?;
constantReexports?;
enumerableModuleMeta?;
noIncompleteNsImportDetection?: boolean;
},
) {
validateImportInteropOption(importInterop);
assert(isModule(path), "Cannot process module statements in a script");
path.node.sourceType = "script";
const meta = normalizeModuleAndLoadMetadata(path, exportName, {
importInterop,
initializeReexports: constantReexports,
lazy,
esNamespaceOnly,
});
if (!allowTopLevelThis) {
rewriteThis(path);
}
rewriteLiveReferences(path, meta);
if (strictMode !== false) {
const hasStrict = path.node.directives.some(directive => {
return directive.value.value === "use strict";
});
if (!hasStrict) {
path.unshiftContainer(
"directives",
directive(directiveLiteral("use strict")),
);
}
}
const headers = [];
if (hasExports(meta) && !strict) {
headers.push(buildESModuleHeader(meta, enumerableModuleMeta));
}
const nameList = buildExportNameListDeclaration(path, meta);
if (nameList) {
meta.exportNameListName = nameList.name;
headers.push(nameList.statement);
}
// Create all of the statically known named exports.
headers.push(
...buildExportInitializationStatements(
path,
meta,
constantReexports,
noIncompleteNsImportDetection,
),
);
return { meta, headers };
}
/**
* Flag a set of statements as hoisted above all else so that module init
* statements all run before user code.
*/
export function ensureStatementsHoisted(statements) {
// Force all of the header fields to be at the top of the file.
statements.forEach(header => {
header._blockHoist = 3;
});
}
/**
* Given an expression for a standard import object, like "require('foo')",
* wrap it in a call to the interop helpers based on the type.
*/
export function wrapInterop(
programPath: NodePath,
expr: t.Expression,
type: InteropType,
): t.CallExpression {
if (type === "none") {
return null;
}
if (type === "node-namespace") {
return callExpression(programPath.hub.addHelper("interopRequireWildcard"), [
expr,
booleanLiteral(true),
]);
} else if (type === "node-default") {
return null;
}
let helper;
if (type === "default") {
helper = "interopRequireDefault";
} else if (type === "namespace") {
helper = "interopRequireWildcard";
} else {
throw new Error(`Unknown interop: ${type}`);
}
return callExpression(programPath.hub.addHelper(helper), [expr]);
}
/**
* Create the runtime initialization statements for a given requested source.
* These will initialize all of the runtime import/export logic that
* can't be handled statically by the statements created by
* buildExportInitializationStatements().
*/
export function buildNamespaceInitStatements(
metadata: ModuleMetadata,
sourceMetadata: SourceModuleMetadata,
constantReexports: boolean = false,
) {
const statements = [];
let srcNamespace: t.Node = identifier(sourceMetadata.name);
if (sourceMetadata.lazy) srcNamespace = callExpression(srcNamespace, []);
for (const localName of sourceMetadata.importsNamespace) {
if (localName === sourceMetadata.name) continue;
// Create and assign binding to namespace object
statements.push(
template.statement`var NAME = SOURCE;`({
NAME: localName,
SOURCE: cloneNode(srcNamespace),
}),
);
}
if (constantReexports) {
statements.push(...buildReexportsFromMeta(metadata, sourceMetadata, true));
}
for (const exportName of sourceMetadata.reexportNamespace) {
// Assign export to namespace object.
statements.push(
(sourceMetadata.lazy
? template.statement`
Object.defineProperty(EXPORTS, "NAME", {
enumerable: true,
get: function() {
return NAMESPACE;
}
});
`
: template.statement`EXPORTS.NAME = NAMESPACE;`)({
EXPORTS: metadata.exportName,
NAME: exportName,
NAMESPACE: cloneNode(srcNamespace),
}),
);
}
if (sourceMetadata.reexportAll) {
const statement = buildNamespaceReexport(
metadata,
cloneNode(srcNamespace),
constantReexports,
);
statement.loc = sourceMetadata.reexportAll.loc;
// Iterate props creating getter for each prop.
statements.push(statement);
}
return statements;
}
const ReexportTemplate = {
constant: template.statement`EXPORTS.EXPORT_NAME = NAMESPACE_IMPORT;`,
constantComputed: template.statement`EXPORTS["EXPORT_NAME"] = NAMESPACE_IMPORT;`,
spec: template`
Object.defineProperty(EXPORTS, "EXPORT_NAME", {
enumerable: true,
get: function() {
return NAMESPACE_IMPORT;
},
});
`,
};
const buildReexportsFromMeta = (
meta: ModuleMetadata,
metadata: SourceModuleMetadata,
constantReexports: boolean,
) => {
const namespace = metadata.lazy
? callExpression(identifier(metadata.name), [])
: identifier(metadata.name);
const { stringSpecifiers } = meta;
return Array.from(metadata.reexports, ([exportName, importName]) => {
let NAMESPACE_IMPORT: t.Expression = cloneNode(namespace);
if (importName === "default" && metadata.interop === "node-default") {
// Nothing, it's ok as-is
} else if (stringSpecifiers.has(importName)) {
NAMESPACE_IMPORT = memberExpression(
NAMESPACE_IMPORT,
stringLiteral(importName),
true,
);
} else {
NAMESPACE_IMPORT = memberExpression(
NAMESPACE_IMPORT,
identifier(importName),
);
}
const astNodes = {
EXPORTS: meta.exportName,
EXPORT_NAME: exportName,
NAMESPACE_IMPORT,
};
if (constantReexports || isIdentifier(NAMESPACE_IMPORT)) {
if (stringSpecifiers.has(exportName)) {
return ReexportTemplate.constantComputed(astNodes);
} else {
return ReexportTemplate.constant(astNodes);
}
} else {
return ReexportTemplate.spec(astNodes);
}
});
};
/**
* Build an "__esModule" header statement setting the property on a given object.
*/
function buildESModuleHeader(
metadata: ModuleMetadata,
enumerableModuleMeta: boolean = false,
) {
return (
enumerableModuleMeta
? template.statement`
EXPORTS.__esModule = true;
`
: template.statement`
Object.defineProperty(EXPORTS, "__esModule", {
value: true,
});
`
)({ EXPORTS: metadata.exportName });
}
/**
* Create a re-export initialization loop for a specific imported namespace.
*/
function buildNamespaceReexport(metadata, namespace, constantReexports) {
return (
constantReexports
? template.statement`
Object.keys(NAMESPACE).forEach(function(key) {
if (key === "default" || key === "__esModule") return;
VERIFY_NAME_LIST;
if (key in EXPORTS && EXPORTS[key] === NAMESPACE[key]) return;
EXPORTS[key] = NAMESPACE[key];
});
`
: // Also skip already assigned bindings if they are strictly equal
// to be somewhat more spec-compliant when a file has multiple
// namespace re-exports that would cause a binding to be exported
// multiple times. However, multiple bindings of the same name that
// export the same primitive value are silently skipped
// (the spec requires an "ambigous bindings" early error here).
template.statement`
Object.keys(NAMESPACE).forEach(function(key) {
if (key === "default" || key === "__esModule") return;
VERIFY_NAME_LIST;
if (key in EXPORTS && EXPORTS[key] === NAMESPACE[key]) return;
Object.defineProperty(EXPORTS, key, {
enumerable: true,
get: function() {
return NAMESPACE[key];
},
});
});
`
)({
NAMESPACE: namespace,
EXPORTS: metadata.exportName,
VERIFY_NAME_LIST: metadata.exportNameListName
? template`
if (Object.prototype.hasOwnProperty.call(EXPORTS_LIST, key)) return;
`({ EXPORTS_LIST: metadata.exportNameListName })
: null,
});
}
/**
* Build a statement declaring a variable that contains all of the exported
* variable names in an object so they can easily be referenced from an
* export * from statement to check for conflicts.
*/
function buildExportNameListDeclaration(
programPath: NodePath,
metadata: ModuleMetadata,
) {
const exportedVars = Object.create(null);
for (const data of metadata.local.values()) {
for (const name of data.names) {
exportedVars[name] = true;
}
}
let hasReexport = false;
for (const data of metadata.source.values()) {
for (const exportName of data.reexports.keys()) {
exportedVars[exportName] = true;
}
for (const exportName of data.reexportNamespace) {
exportedVars[exportName] = true;
}
hasReexport = hasReexport || !!data.reexportAll;
}
if (!hasReexport || Object.keys(exportedVars).length === 0) return null;
const name = programPath.scope.generateUidIdentifier("exportNames");
delete exportedVars.default;
return {
name: name.name,
statement: variableDeclaration("var", [
variableDeclarator(name, valueToNode(exportedVars)),
]),
};
}
/**
* Create a set of statements that will initialize all of the statically-known
* export names with their expected values.
*/
function buildExportInitializationStatements(
programPath: NodePath,
metadata: ModuleMetadata,
constantReexports: boolean = false,
noIncompleteNsImportDetection = false,
) {
const initStatements = [];
const exportNames = [];
for (const [localName, data] of metadata.local) {
if (data.kind === "import") {
// No-open since these are explicitly set with the "reexports" block.
} else if (data.kind === "hoisted") {
initStatements.push(
buildInitStatement(metadata, data.names, identifier(localName)),
);
} else {
exportNames.push(...data.names);
}
}
for (const data of metadata.source.values()) {
if (!constantReexports) {
initStatements.push(...buildReexportsFromMeta(metadata, data, false));
}
for (const exportName of data.reexportNamespace) {
exportNames.push(exportName);
}
}
if (!noIncompleteNsImportDetection) {
initStatements.push(
...chunk(exportNames, 100).map(members => {
return buildInitStatement(
metadata,
members,
programPath.scope.buildUndefinedNode(),
);
}),
);
}
return initStatements;
}
/**
* Given a set of export names, create a set of nested assignments to
* initialize them all to a given expression.
*/
const InitTemplate = {
computed: template.expression`EXPORTS["NAME"] = VALUE`,
default: template.expression`EXPORTS.NAME = VALUE`,
};
function buildInitStatement(metadata: ModuleMetadata, exportNames, initExpr) {
const { stringSpecifiers, exportName: EXPORTS } = metadata;
return expressionStatement(
exportNames.reduce((acc, exportName) => {
const params = {
EXPORTS,
NAME: exportName,
VALUE: acc,
};
if (stringSpecifiers.has(exportName)) {
return InitTemplate.computed(params);
} else {
return InitTemplate.default(params);
}
}, initExpr),
);
}
function chunk(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}