* migrate to named babel types imports * perf: transform babel types import to destructuring * fix merge errors * apply plugin to itself
488 lines
13 KiB
TypeScript
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;
|
|
}
|