123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- 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)
- );
- }
- }
|