"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _keywords = require('../parser/tokenizer/keywords'); var _types = require('../parser/tokenizer/types'); var _elideImportEquals = require('../util/elideImportEquals'); var _elideImportEquals2 = _interopRequireDefault(_elideImportEquals); var _getDeclarationInfo = require('../util/getDeclarationInfo'); var _getDeclarationInfo2 = _interopRequireDefault(_getDeclarationInfo); var _getImportExportSpecifierInfo = require('../util/getImportExportSpecifierInfo'); var _getImportExportSpecifierInfo2 = _interopRequireDefault(_getImportExportSpecifierInfo); var _getNonTypeIdentifiers = require('../util/getNonTypeIdentifiers'); var _isExportFrom = require('../util/isExportFrom'); var _isExportFrom2 = _interopRequireDefault(_isExportFrom); var _removeMaybeImportAttributes = require('../util/removeMaybeImportAttributes'); var _shouldElideDefaultExport = require('../util/shouldElideDefaultExport'); var _shouldElideDefaultExport2 = _interopRequireDefault(_shouldElideDefaultExport); var _Transformer = require('./Transformer'); var _Transformer2 = _interopRequireDefault(_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. */ class ESMImportTransformer extends _Transformer2.default { 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.getNonTypeIdentifiers.call(void 0, tokens, options) : new Set(); this.declarationInfo = isTypeScriptTransformEnabled && !keepUnusedImports ? _getDeclarationInfo2.default.call(void 0, tokens) : _getDeclarationInfo.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(_types.TokenType._import, _types.TokenType.name, _types.TokenType.eq)) { return this.processImportEquals(); } if ( this.tokens.matches4(_types.TokenType._import, _types.TokenType.name, _types.TokenType.name, _types.TokenType.eq) && this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, _keywords.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(_types.TokenType._export, _types.TokenType.eq)) { this.tokens.replaceToken("module.exports"); return true; } if ( this.tokens.matches5(_types.TokenType._export, _types.TokenType._import, _types.TokenType.name, _types.TokenType.name, _types.TokenType.eq) && this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 2, _keywords.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(_types.TokenType._import)) { return this.processImport(); } if (this.tokens.matches2(_types.TokenType._export, _types.TokenType._default)) { return this.processExportDefault(); } if (this.tokens.matches2(_types.TokenType._export, _types.TokenType.braceL)) { return this.processNamedExports(); } if ( this.tokens.matches2(_types.TokenType._export, _types.TokenType.name) && this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, _keywords.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(_types.TokenType.braceL)) { while (!this.tokens.matches1(_types.TokenType.braceR)) { this.tokens.removeToken(); } this.tokens.removeToken(); } else { // * this.tokens.removeToken(); if (this.tokens.matches1(_types.TokenType._as)) { // as this.tokens.removeToken(); // ns this.tokens.removeToken(); } } // Remove type re-export `... } from './T'` if ( this.tokens.matchesContextual(_keywords.ContextualKeyword._from) && this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, _types.TokenType.string) ) { this.tokens.removeToken(); this.tokens.removeToken(); _removeMaybeImportAttributes.removeMaybeImportAttributes.call(void 0, 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. _elideImportEquals2.default.call(void 0, 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(_types.TokenType._import, _types.TokenType.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(_types.TokenType.string)) { this.tokens.removeToken(); } this.tokens.removeToken(); _removeMaybeImportAttributes.removeMaybeImportAttributes.call(void 0, this.tokens); if (this.tokens.matches1(_types.TokenType.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(_types.TokenType._import); if ( this.tokens.matchesContextual(_keywords.ContextualKeyword._type) && !this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, _types.TokenType.comma) && !this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, _keywords.ContextualKeyword._from) ) { // This is an "import type" statement, so exit early. return true; } if (this.tokens.matches1(_types.TokenType.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(_keywords.ContextualKeyword._module) && this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 2, _keywords.ContextualKeyword._from) ) { this.tokens.copyToken(); } let foundNonTypeImport = false; let foundAnyNamedImport = false; let needsComma = false; // Handle default import. if (this.tokens.matches1(_types.TokenType.name)) { if (this.shouldAutomaticallyElideImportedName(this.tokens.identifierName())) { this.tokens.removeToken(); if (this.tokens.matches1(_types.TokenType.comma)) { this.tokens.removeToken(); } } else { foundNonTypeImport = true; this.tokens.copyToken(); if (this.tokens.matches1(_types.TokenType.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(_types.TokenType.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(_types.TokenType.star); this.tokens.copyExpectedToken(_types.TokenType.name); this.tokens.copyExpectedToken(_types.TokenType.name); } } else if (this.tokens.matches1(_types.TokenType.braceL)) { if (needsComma) { this.tokens.appendCode(","); } this.tokens.copyToken(); while (!this.tokens.matches1(_types.TokenType.braceR)) { foundAnyNamedImport = true; const specifierInfo = _getImportExportSpecifierInfo2.default.call(void 0, this.tokens); if ( specifierInfo.isType || this.shouldAutomaticallyElideImportedName(specifierInfo.rightName) ) { while (this.tokens.currentIndex() < specifierInfo.endIndex) { this.tokens.removeToken(); } if (this.tokens.matches1(_types.TokenType.comma)) { this.tokens.removeToken(); } } else { foundNonTypeImport = true; while (this.tokens.currentIndex() < specifierInfo.endIndex) { this.tokens.copyToken(); } if (this.tokens.matches1(_types.TokenType.comma)) { this.tokens.copyToken(); } } } this.tokens.copyExpectedToken(_types.TokenType.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 ( _shouldElideDefaultExport2.default.call(void 0, 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(_types.TokenType._export, _types.TokenType._default, _types.TokenType._function, _types.TokenType.name) || // export default async function (this.tokens.matches5(_types.TokenType._export, _types.TokenType._default, _types.TokenType.name, _types.TokenType._function, _types.TokenType.name) && this.tokens.matchesContextualAtIndex( this.tokens.currentIndex() + 2, _keywords.ContextualKeyword._async, )) || this.tokens.matches4(_types.TokenType._export, _types.TokenType._default, _types.TokenType._class, _types.TokenType.name) || this.tokens.matches5(_types.TokenType._export, _types.TokenType._default, _types.TokenType._abstract, _types.TokenType._class, _types.TokenType.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(_types.TokenType._export); this.tokens.copyExpectedToken(_types.TokenType.braceL); const isReExport = _isExportFrom2.default.call(void 0, this.tokens); let foundNonTypeExport = false; while (!this.tokens.matches1(_types.TokenType.braceR)) { const specifierInfo = _getImportExportSpecifierInfo2.default.call(void 0, 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(_types.TokenType.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(_types.TokenType.comma)) { this.tokens.copyToken(); } } } this.tokens.copyExpectedToken(_types.TokenType.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.removeMaybeImportAttributes.call(void 0, 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) ); } } exports.default = ESMImportTransformer;