416 lines
14 KiB
Plaintext
416 lines
14 KiB
Plaintext
|
|
|
|
|
|
import {ContextualKeyword} from "../parser/tokenizer/keywords";
|
|
import {TokenType as tt} from "../parser/tokenizer/types";
|
|
|
|
import elideImportEquals from "../util/elideImportEquals";
|
|
import getDeclarationInfo, {
|
|
|
|
EMPTY_DECLARATION_INFO,
|
|
} from "../util/getDeclarationInfo";
|
|
import getImportExportSpecifierInfo from "../util/getImportExportSpecifierInfo";
|
|
import {getNonTypeIdentifiers} from "../util/getNonTypeIdentifiers";
|
|
import isExportFrom from "../util/isExportFrom";
|
|
import {removeMaybeImportAttributes} from "../util/removeMaybeImportAttributes";
|
|
import shouldElideDefaultExport from "../util/shouldElideDefaultExport";
|
|
|
|
import Transformer from "./Transformer";
|
|
|
|
/**
|
|
* Class for editing import statements when we are keeping the code as ESM. We still need to remove
|
|
* type-only imports in TypeScript and Flow.
|
|
*/
|
|
export default class ESMImportTransformer extends Transformer {
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
tokens,
|
|
nameManager,
|
|
helperManager,
|
|
reactHotLoaderTransformer,
|
|
isTypeScriptTransformEnabled,
|
|
isFlowTransformEnabled,
|
|
keepUnusedImports,
|
|
options,
|
|
) {
|
|
super();this.tokens = tokens;this.nameManager = nameManager;this.helperManager = helperManager;this.reactHotLoaderTransformer = reactHotLoaderTransformer;this.isTypeScriptTransformEnabled = isTypeScriptTransformEnabled;this.isFlowTransformEnabled = isFlowTransformEnabled;this.keepUnusedImports = keepUnusedImports;;
|
|
this.nonTypeIdentifiers =
|
|
isTypeScriptTransformEnabled && !keepUnusedImports
|
|
? getNonTypeIdentifiers(tokens, options)
|
|
: new Set();
|
|
this.declarationInfo =
|
|
isTypeScriptTransformEnabled && !keepUnusedImports
|
|
? getDeclarationInfo(tokens)
|
|
: EMPTY_DECLARATION_INFO;
|
|
this.injectCreateRequireForImportRequire = Boolean(options.injectCreateRequireForImportRequire);
|
|
}
|
|
|
|
process() {
|
|
// TypeScript `import foo = require('foo');` should always just be translated to plain require.
|
|
if (this.tokens.matches3(tt._import, tt.name, tt.eq)) {
|
|
return this.processImportEquals();
|
|
}
|
|
if (
|
|
this.tokens.matches4(tt._import, tt.name, tt.name, tt.eq) &&
|
|
this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._type)
|
|
) {
|
|
// import type T = require('T')
|
|
this.tokens.removeInitialToken();
|
|
// This construct is always exactly 8 tokens long, so remove the 7 remaining tokens.
|
|
for (let i = 0; i < 7; i++) {
|
|
this.tokens.removeToken();
|
|
}
|
|
return true;
|
|
}
|
|
if (this.tokens.matches2(tt._export, tt.eq)) {
|
|
this.tokens.replaceToken("module.exports");
|
|
return true;
|
|
}
|
|
if (
|
|
this.tokens.matches5(tt._export, tt._import, tt.name, tt.name, tt.eq) &&
|
|
this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 2, ContextualKeyword._type)
|
|
) {
|
|
// export import type T = require('T')
|
|
this.tokens.removeInitialToken();
|
|
// This construct is always exactly 9 tokens long, so remove the 8 remaining tokens.
|
|
for (let i = 0; i < 8; i++) {
|
|
this.tokens.removeToken();
|
|
}
|
|
return true;
|
|
}
|
|
if (this.tokens.matches1(tt._import)) {
|
|
return this.processImport();
|
|
}
|
|
if (this.tokens.matches2(tt._export, tt._default)) {
|
|
return this.processExportDefault();
|
|
}
|
|
if (this.tokens.matches2(tt._export, tt.braceL)) {
|
|
return this.processNamedExports();
|
|
}
|
|
if (
|
|
this.tokens.matches2(tt._export, tt.name) &&
|
|
this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._type)
|
|
) {
|
|
// export type {a};
|
|
// export type {a as b};
|
|
// export type {a} from './b';
|
|
// export type * from './b';
|
|
// export type * as ns from './b';
|
|
this.tokens.removeInitialToken();
|
|
this.tokens.removeToken();
|
|
if (this.tokens.matches1(tt.braceL)) {
|
|
while (!this.tokens.matches1(tt.braceR)) {
|
|
this.tokens.removeToken();
|
|
}
|
|
this.tokens.removeToken();
|
|
} else {
|
|
// *
|
|
this.tokens.removeToken();
|
|
if (this.tokens.matches1(tt._as)) {
|
|
// as
|
|
this.tokens.removeToken();
|
|
// ns
|
|
this.tokens.removeToken();
|
|
}
|
|
}
|
|
// Remove type re-export `... } from './T'`
|
|
if (
|
|
this.tokens.matchesContextual(ContextualKeyword._from) &&
|
|
this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.string)
|
|
) {
|
|
this.tokens.removeToken();
|
|
this.tokens.removeToken();
|
|
removeMaybeImportAttributes(this.tokens);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
processImportEquals() {
|
|
const importName = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1);
|
|
if (this.shouldAutomaticallyElideImportedName(importName)) {
|
|
// If this name is only used as a type, elide the whole import.
|
|
elideImportEquals(this.tokens);
|
|
} else if (this.injectCreateRequireForImportRequire) {
|
|
// We're using require in an environment (Node ESM) that doesn't provide
|
|
// it as a global, so generate a helper to import it.
|
|
// import -> const
|
|
this.tokens.replaceToken("const");
|
|
// Foo
|
|
this.tokens.copyToken();
|
|
// =
|
|
this.tokens.copyToken();
|
|
// require
|
|
this.tokens.replaceToken(this.helperManager.getHelperName("require"));
|
|
} else {
|
|
// Otherwise, just switch `import` to `const`.
|
|
this.tokens.replaceToken("const");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
processImport() {
|
|
if (this.tokens.matches2(tt._import, tt.parenL)) {
|
|
// Dynamic imports don't need to be transformed.
|
|
return false;
|
|
}
|
|
|
|
const snapshot = this.tokens.snapshot();
|
|
const allImportsRemoved = this.removeImportTypeBindings();
|
|
if (allImportsRemoved) {
|
|
this.tokens.restoreToSnapshot(snapshot);
|
|
while (!this.tokens.matches1(tt.string)) {
|
|
this.tokens.removeToken();
|
|
}
|
|
this.tokens.removeToken();
|
|
removeMaybeImportAttributes(this.tokens);
|
|
if (this.tokens.matches1(tt.semi)) {
|
|
this.tokens.removeToken();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove type bindings from this import, leaving the rest of the import intact.
|
|
*
|
|
* Return true if this import was ONLY types, and thus is eligible for removal. This will bail out
|
|
* of the replacement operation, so we can return early here.
|
|
*/
|
|
removeImportTypeBindings() {
|
|
this.tokens.copyExpectedToken(tt._import);
|
|
if (
|
|
this.tokens.matchesContextual(ContextualKeyword._type) &&
|
|
!this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.comma) &&
|
|
!this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._from)
|
|
) {
|
|
// This is an "import type" statement, so exit early.
|
|
return true;
|
|
}
|
|
|
|
if (this.tokens.matches1(tt.string)) {
|
|
// This is a bare import, so we should proceed with the import.
|
|
this.tokens.copyToken();
|
|
return false;
|
|
}
|
|
|
|
// Skip the "module" token in import reflection.
|
|
if (
|
|
this.tokens.matchesContextual(ContextualKeyword._module) &&
|
|
this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 2, ContextualKeyword._from)
|
|
) {
|
|
this.tokens.copyToken();
|
|
}
|
|
|
|
let foundNonTypeImport = false;
|
|
let foundAnyNamedImport = false;
|
|
let needsComma = false;
|
|
|
|
// Handle default import.
|
|
if (this.tokens.matches1(tt.name)) {
|
|
if (this.shouldAutomaticallyElideImportedName(this.tokens.identifierName())) {
|
|
this.tokens.removeToken();
|
|
if (this.tokens.matches1(tt.comma)) {
|
|
this.tokens.removeToken();
|
|
}
|
|
} else {
|
|
foundNonTypeImport = true;
|
|
this.tokens.copyToken();
|
|
if (this.tokens.matches1(tt.comma)) {
|
|
// We're in a statement like:
|
|
// import A, * as B from './A';
|
|
// or
|
|
// import A, {foo} from './A';
|
|
// where the `A` is being kept. The comma should be removed if an only
|
|
// if the next part of the import statement is elided, but that's hard
|
|
// to determine at this point in the code. Instead, always remove it
|
|
// and set a flag to add it back if necessary.
|
|
needsComma = true;
|
|
this.tokens.removeToken();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.tokens.matches1(tt.star)) {
|
|
if (this.shouldAutomaticallyElideImportedName(this.tokens.identifierNameAtRelativeIndex(2))) {
|
|
this.tokens.removeToken();
|
|
this.tokens.removeToken();
|
|
this.tokens.removeToken();
|
|
} else {
|
|
if (needsComma) {
|
|
this.tokens.appendCode(",");
|
|
}
|
|
foundNonTypeImport = true;
|
|
this.tokens.copyExpectedToken(tt.star);
|
|
this.tokens.copyExpectedToken(tt.name);
|
|
this.tokens.copyExpectedToken(tt.name);
|
|
}
|
|
} else if (this.tokens.matches1(tt.braceL)) {
|
|
if (needsComma) {
|
|
this.tokens.appendCode(",");
|
|
}
|
|
this.tokens.copyToken();
|
|
while (!this.tokens.matches1(tt.braceR)) {
|
|
foundAnyNamedImport = true;
|
|
const specifierInfo = getImportExportSpecifierInfo(this.tokens);
|
|
if (
|
|
specifierInfo.isType ||
|
|
this.shouldAutomaticallyElideImportedName(specifierInfo.rightName)
|
|
) {
|
|
while (this.tokens.currentIndex() < specifierInfo.endIndex) {
|
|
this.tokens.removeToken();
|
|
}
|
|
if (this.tokens.matches1(tt.comma)) {
|
|
this.tokens.removeToken();
|
|
}
|
|
} else {
|
|
foundNonTypeImport = true;
|
|
while (this.tokens.currentIndex() < specifierInfo.endIndex) {
|
|
this.tokens.copyToken();
|
|
}
|
|
if (this.tokens.matches1(tt.comma)) {
|
|
this.tokens.copyToken();
|
|
}
|
|
}
|
|
}
|
|
this.tokens.copyExpectedToken(tt.braceR);
|
|
}
|
|
|
|
if (this.keepUnusedImports) {
|
|
return false;
|
|
}
|
|
if (this.isTypeScriptTransformEnabled) {
|
|
return !foundNonTypeImport;
|
|
} else if (this.isFlowTransformEnabled) {
|
|
// In Flow, unlike TS, `import {} from 'foo';` preserves the import.
|
|
return foundAnyNamedImport && !foundNonTypeImport;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
shouldAutomaticallyElideImportedName(name) {
|
|
return (
|
|
this.isTypeScriptTransformEnabled &&
|
|
!this.keepUnusedImports &&
|
|
!this.nonTypeIdentifiers.has(name)
|
|
);
|
|
}
|
|
|
|
processExportDefault() {
|
|
if (
|
|
shouldElideDefaultExport(
|
|
this.isTypeScriptTransformEnabled,
|
|
this.keepUnusedImports,
|
|
this.tokens,
|
|
this.declarationInfo,
|
|
)
|
|
) {
|
|
// If the exported value is just an identifier and should be elided by TypeScript
|
|
// rules, then remove it entirely. It will always have the form `export default e`,
|
|
// where `e` is an identifier.
|
|
this.tokens.removeInitialToken();
|
|
this.tokens.removeToken();
|
|
this.tokens.removeToken();
|
|
return true;
|
|
}
|
|
|
|
const alreadyHasName =
|
|
this.tokens.matches4(tt._export, tt._default, tt._function, tt.name) ||
|
|
// export default async function
|
|
(this.tokens.matches5(tt._export, tt._default, tt.name, tt._function, tt.name) &&
|
|
this.tokens.matchesContextualAtIndex(
|
|
this.tokens.currentIndex() + 2,
|
|
ContextualKeyword._async,
|
|
)) ||
|
|
this.tokens.matches4(tt._export, tt._default, tt._class, tt.name) ||
|
|
this.tokens.matches5(tt._export, tt._default, tt._abstract, tt._class, tt.name);
|
|
|
|
if (!alreadyHasName && this.reactHotLoaderTransformer) {
|
|
// This is a plain "export default E" statement and we need to assign E to a variable.
|
|
// Change "export default E" to "let _default; export default _default = E"
|
|
const defaultVarName = this.nameManager.claimFreeName("_default");
|
|
this.tokens.replaceToken(`let ${defaultVarName}; export`);
|
|
this.tokens.copyToken();
|
|
this.tokens.appendCode(` ${defaultVarName} =`);
|
|
this.reactHotLoaderTransformer.setExtractedDefaultExportName(defaultVarName);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle a statement with one of these forms:
|
|
* export {a, type b};
|
|
* export {c, type d} from 'foo';
|
|
*
|
|
* In both cases, any explicit type exports should be removed. In the first
|
|
* case, we also need to handle implicit export elision for names declared as
|
|
* types. In the second case, we must NOT do implicit named export elision,
|
|
* but we must remove the runtime import if all exports are type exports.
|
|
*/
|
|
processNamedExports() {
|
|
if (!this.isTypeScriptTransformEnabled) {
|
|
return false;
|
|
}
|
|
this.tokens.copyExpectedToken(tt._export);
|
|
this.tokens.copyExpectedToken(tt.braceL);
|
|
|
|
const isReExport = isExportFrom(this.tokens);
|
|
let foundNonTypeExport = false;
|
|
while (!this.tokens.matches1(tt.braceR)) {
|
|
const specifierInfo = getImportExportSpecifierInfo(this.tokens);
|
|
if (
|
|
specifierInfo.isType ||
|
|
(!isReExport && this.shouldElideExportedName(specifierInfo.leftName))
|
|
) {
|
|
// Type export, so remove all tokens, including any comma.
|
|
while (this.tokens.currentIndex() < specifierInfo.endIndex) {
|
|
this.tokens.removeToken();
|
|
}
|
|
if (this.tokens.matches1(tt.comma)) {
|
|
this.tokens.removeToken();
|
|
}
|
|
} else {
|
|
// Non-type export, so copy all tokens, including any comma.
|
|
foundNonTypeExport = true;
|
|
while (this.tokens.currentIndex() < specifierInfo.endIndex) {
|
|
this.tokens.copyToken();
|
|
}
|
|
if (this.tokens.matches1(tt.comma)) {
|
|
this.tokens.copyToken();
|
|
}
|
|
}
|
|
}
|
|
this.tokens.copyExpectedToken(tt.braceR);
|
|
|
|
if (!this.keepUnusedImports && isReExport && !foundNonTypeExport) {
|
|
// This is a type-only re-export, so skip evaluating the other module. Technically this
|
|
// leaves the statement as `export {}`, but that's ok since that's a no-op.
|
|
this.tokens.removeToken();
|
|
this.tokens.removeToken();
|
|
removeMaybeImportAttributes(this.tokens);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* ESM elides all imports with the rule that we only elide if we see that it's
|
|
* a type and never see it as a value. This is in contrast to CJS, which
|
|
* elides imports that are completely unknown.
|
|
*/
|
|
shouldElideExportedName(name) {
|
|
return (
|
|
this.isTypeScriptTransformEnabled &&
|
|
!this.keepUnusedImports &&
|
|
this.declarationInfo.typeDeclarations.has(name) &&
|
|
!this.declarationInfo.valueDeclarations.has(name)
|
|
);
|
|
}
|
|
}
|