devsite/node_modules/sucrase/dist/transformers/ESMImportTransformer.js
2024-07-07 18:49:38 -07:00

416 lines
16 KiB
JavaScript

"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;