ESMImportTransformer.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import {ContextualKeyword} from "../parser/tokenizer/keywords";
  2. import {TokenType as tt} from "../parser/tokenizer/types";
  3. import elideImportEquals from "../util/elideImportEquals";
  4. import getDeclarationInfo, {
  5. EMPTY_DECLARATION_INFO,
  6. } from "../util/getDeclarationInfo";
  7. import getImportExportSpecifierInfo from "../util/getImportExportSpecifierInfo";
  8. import {getNonTypeIdentifiers} from "../util/getNonTypeIdentifiers";
  9. import isExportFrom from "../util/isExportFrom";
  10. import {removeMaybeImportAttributes} from "../util/removeMaybeImportAttributes";
  11. import shouldElideDefaultExport from "../util/shouldElideDefaultExport";
  12. import Transformer from "./Transformer";
  13. /**
  14. * Class for editing import statements when we are keeping the code as ESM. We still need to remove
  15. * type-only imports in TypeScript and Flow.
  16. */
  17. export default class ESMImportTransformer extends Transformer {
  18. constructor(
  19. tokens,
  20. nameManager,
  21. helperManager,
  22. reactHotLoaderTransformer,
  23. isTypeScriptTransformEnabled,
  24. isFlowTransformEnabled,
  25. keepUnusedImports,
  26. options,
  27. ) {
  28. super();this.tokens = tokens;this.nameManager = nameManager;this.helperManager = helperManager;this.reactHotLoaderTransformer = reactHotLoaderTransformer;this.isTypeScriptTransformEnabled = isTypeScriptTransformEnabled;this.isFlowTransformEnabled = isFlowTransformEnabled;this.keepUnusedImports = keepUnusedImports;;
  29. this.nonTypeIdentifiers =
  30. isTypeScriptTransformEnabled && !keepUnusedImports
  31. ? getNonTypeIdentifiers(tokens, options)
  32. : new Set();
  33. this.declarationInfo =
  34. isTypeScriptTransformEnabled && !keepUnusedImports
  35. ? getDeclarationInfo(tokens)
  36. : EMPTY_DECLARATION_INFO;
  37. this.injectCreateRequireForImportRequire = Boolean(options.injectCreateRequireForImportRequire);
  38. }
  39. process() {
  40. // TypeScript `import foo = require('foo');` should always just be translated to plain require.
  41. if (this.tokens.matches3(tt._import, tt.name, tt.eq)) {
  42. return this.processImportEquals();
  43. }
  44. if (
  45. this.tokens.matches4(tt._import, tt.name, tt.name, tt.eq) &&
  46. this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._type)
  47. ) {
  48. // import type T = require('T')
  49. this.tokens.removeInitialToken();
  50. // This construct is always exactly 8 tokens long, so remove the 7 remaining tokens.
  51. for (let i = 0; i < 7; i++) {
  52. this.tokens.removeToken();
  53. }
  54. return true;
  55. }
  56. if (this.tokens.matches2(tt._export, tt.eq)) {
  57. this.tokens.replaceToken("module.exports");
  58. return true;
  59. }
  60. if (
  61. this.tokens.matches5(tt._export, tt._import, tt.name, tt.name, tt.eq) &&
  62. this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 2, ContextualKeyword._type)
  63. ) {
  64. // export import type T = require('T')
  65. this.tokens.removeInitialToken();
  66. // This construct is always exactly 9 tokens long, so remove the 8 remaining tokens.
  67. for (let i = 0; i < 8; i++) {
  68. this.tokens.removeToken();
  69. }
  70. return true;
  71. }
  72. if (this.tokens.matches1(tt._import)) {
  73. return this.processImport();
  74. }
  75. if (this.tokens.matches2(tt._export, tt._default)) {
  76. return this.processExportDefault();
  77. }
  78. if (this.tokens.matches2(tt._export, tt.braceL)) {
  79. return this.processNamedExports();
  80. }
  81. if (
  82. this.tokens.matches2(tt._export, tt.name) &&
  83. this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._type)
  84. ) {
  85. // export type {a};
  86. // export type {a as b};
  87. // export type {a} from './b';
  88. // export type * from './b';
  89. // export type * as ns from './b';
  90. this.tokens.removeInitialToken();
  91. this.tokens.removeToken();
  92. if (this.tokens.matches1(tt.braceL)) {
  93. while (!this.tokens.matches1(tt.braceR)) {
  94. this.tokens.removeToken();
  95. }
  96. this.tokens.removeToken();
  97. } else {
  98. // *
  99. this.tokens.removeToken();
  100. if (this.tokens.matches1(tt._as)) {
  101. // as
  102. this.tokens.removeToken();
  103. // ns
  104. this.tokens.removeToken();
  105. }
  106. }
  107. // Remove type re-export `... } from './T'`
  108. if (
  109. this.tokens.matchesContextual(ContextualKeyword._from) &&
  110. this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.string)
  111. ) {
  112. this.tokens.removeToken();
  113. this.tokens.removeToken();
  114. removeMaybeImportAttributes(this.tokens);
  115. }
  116. return true;
  117. }
  118. return false;
  119. }
  120. processImportEquals() {
  121. const importName = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1);
  122. if (this.shouldAutomaticallyElideImportedName(importName)) {
  123. // If this name is only used as a type, elide the whole import.
  124. elideImportEquals(this.tokens);
  125. } else if (this.injectCreateRequireForImportRequire) {
  126. // We're using require in an environment (Node ESM) that doesn't provide
  127. // it as a global, so generate a helper to import it.
  128. // import -> const
  129. this.tokens.replaceToken("const");
  130. // Foo
  131. this.tokens.copyToken();
  132. // =
  133. this.tokens.copyToken();
  134. // require
  135. this.tokens.replaceToken(this.helperManager.getHelperName("require"));
  136. } else {
  137. // Otherwise, just switch `import` to `const`.
  138. this.tokens.replaceToken("const");
  139. }
  140. return true;
  141. }
  142. processImport() {
  143. if (this.tokens.matches2(tt._import, tt.parenL)) {
  144. // Dynamic imports don't need to be transformed.
  145. return false;
  146. }
  147. const snapshot = this.tokens.snapshot();
  148. const allImportsRemoved = this.removeImportTypeBindings();
  149. if (allImportsRemoved) {
  150. this.tokens.restoreToSnapshot(snapshot);
  151. while (!this.tokens.matches1(tt.string)) {
  152. this.tokens.removeToken();
  153. }
  154. this.tokens.removeToken();
  155. removeMaybeImportAttributes(this.tokens);
  156. if (this.tokens.matches1(tt.semi)) {
  157. this.tokens.removeToken();
  158. }
  159. }
  160. return true;
  161. }
  162. /**
  163. * Remove type bindings from this import, leaving the rest of the import intact.
  164. *
  165. * Return true if this import was ONLY types, and thus is eligible for removal. This will bail out
  166. * of the replacement operation, so we can return early here.
  167. */
  168. removeImportTypeBindings() {
  169. this.tokens.copyExpectedToken(tt._import);
  170. if (
  171. this.tokens.matchesContextual(ContextualKeyword._type) &&
  172. !this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.comma) &&
  173. !this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._from)
  174. ) {
  175. // This is an "import type" statement, so exit early.
  176. return true;
  177. }
  178. if (this.tokens.matches1(tt.string)) {
  179. // This is a bare import, so we should proceed with the import.
  180. this.tokens.copyToken();
  181. return false;
  182. }
  183. // Skip the "module" token in import reflection.
  184. if (
  185. this.tokens.matchesContextual(ContextualKeyword._module) &&
  186. this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 2, ContextualKeyword._from)
  187. ) {
  188. this.tokens.copyToken();
  189. }
  190. let foundNonTypeImport = false;
  191. let foundAnyNamedImport = false;
  192. let needsComma = false;
  193. // Handle default import.
  194. if (this.tokens.matches1(tt.name)) {
  195. if (this.shouldAutomaticallyElideImportedName(this.tokens.identifierName())) {
  196. this.tokens.removeToken();
  197. if (this.tokens.matches1(tt.comma)) {
  198. this.tokens.removeToken();
  199. }
  200. } else {
  201. foundNonTypeImport = true;
  202. this.tokens.copyToken();
  203. if (this.tokens.matches1(tt.comma)) {
  204. // We're in a statement like:
  205. // import A, * as B from './A';
  206. // or
  207. // import A, {foo} from './A';
  208. // where the `A` is being kept. The comma should be removed if an only
  209. // if the next part of the import statement is elided, but that's hard
  210. // to determine at this point in the code. Instead, always remove it
  211. // and set a flag to add it back if necessary.
  212. needsComma = true;
  213. this.tokens.removeToken();
  214. }
  215. }
  216. }
  217. if (this.tokens.matches1(tt.star)) {
  218. if (this.shouldAutomaticallyElideImportedName(this.tokens.identifierNameAtRelativeIndex(2))) {
  219. this.tokens.removeToken();
  220. this.tokens.removeToken();
  221. this.tokens.removeToken();
  222. } else {
  223. if (needsComma) {
  224. this.tokens.appendCode(",");
  225. }
  226. foundNonTypeImport = true;
  227. this.tokens.copyExpectedToken(tt.star);
  228. this.tokens.copyExpectedToken(tt.name);
  229. this.tokens.copyExpectedToken(tt.name);
  230. }
  231. } else if (this.tokens.matches1(tt.braceL)) {
  232. if (needsComma) {
  233. this.tokens.appendCode(",");
  234. }
  235. this.tokens.copyToken();
  236. while (!this.tokens.matches1(tt.braceR)) {
  237. foundAnyNamedImport = true;
  238. const specifierInfo = getImportExportSpecifierInfo(this.tokens);
  239. if (
  240. specifierInfo.isType ||
  241. this.shouldAutomaticallyElideImportedName(specifierInfo.rightName)
  242. ) {
  243. while (this.tokens.currentIndex() < specifierInfo.endIndex) {
  244. this.tokens.removeToken();
  245. }
  246. if (this.tokens.matches1(tt.comma)) {
  247. this.tokens.removeToken();
  248. }
  249. } else {
  250. foundNonTypeImport = true;
  251. while (this.tokens.currentIndex() < specifierInfo.endIndex) {
  252. this.tokens.copyToken();
  253. }
  254. if (this.tokens.matches1(tt.comma)) {
  255. this.tokens.copyToken();
  256. }
  257. }
  258. }
  259. this.tokens.copyExpectedToken(tt.braceR);
  260. }
  261. if (this.keepUnusedImports) {
  262. return false;
  263. }
  264. if (this.isTypeScriptTransformEnabled) {
  265. return !foundNonTypeImport;
  266. } else if (this.isFlowTransformEnabled) {
  267. // In Flow, unlike TS, `import {} from 'foo';` preserves the import.
  268. return foundAnyNamedImport && !foundNonTypeImport;
  269. } else {
  270. return false;
  271. }
  272. }
  273. shouldAutomaticallyElideImportedName(name) {
  274. return (
  275. this.isTypeScriptTransformEnabled &&
  276. !this.keepUnusedImports &&
  277. !this.nonTypeIdentifiers.has(name)
  278. );
  279. }
  280. processExportDefault() {
  281. if (
  282. shouldElideDefaultExport(
  283. this.isTypeScriptTransformEnabled,
  284. this.keepUnusedImports,
  285. this.tokens,
  286. this.declarationInfo,
  287. )
  288. ) {
  289. // If the exported value is just an identifier and should be elided by TypeScript
  290. // rules, then remove it entirely. It will always have the form `export default e`,
  291. // where `e` is an identifier.
  292. this.tokens.removeInitialToken();
  293. this.tokens.removeToken();
  294. this.tokens.removeToken();
  295. return true;
  296. }
  297. const alreadyHasName =
  298. this.tokens.matches4(tt._export, tt._default, tt._function, tt.name) ||
  299. // export default async function
  300. (this.tokens.matches5(tt._export, tt._default, tt.name, tt._function, tt.name) &&
  301. this.tokens.matchesContextualAtIndex(
  302. this.tokens.currentIndex() + 2,
  303. ContextualKeyword._async,
  304. )) ||
  305. this.tokens.matches4(tt._export, tt._default, tt._class, tt.name) ||
  306. this.tokens.matches5(tt._export, tt._default, tt._abstract, tt._class, tt.name);
  307. if (!alreadyHasName && this.reactHotLoaderTransformer) {
  308. // This is a plain "export default E" statement and we need to assign E to a variable.
  309. // Change "export default E" to "let _default; export default _default = E"
  310. const defaultVarName = this.nameManager.claimFreeName("_default");
  311. this.tokens.replaceToken(`let ${defaultVarName}; export`);
  312. this.tokens.copyToken();
  313. this.tokens.appendCode(` ${defaultVarName} =`);
  314. this.reactHotLoaderTransformer.setExtractedDefaultExportName(defaultVarName);
  315. return true;
  316. }
  317. return false;
  318. }
  319. /**
  320. * Handle a statement with one of these forms:
  321. * export {a, type b};
  322. * export {c, type d} from 'foo';
  323. *
  324. * In both cases, any explicit type exports should be removed. In the first
  325. * case, we also need to handle implicit export elision for names declared as
  326. * types. In the second case, we must NOT do implicit named export elision,
  327. * but we must remove the runtime import if all exports are type exports.
  328. */
  329. processNamedExports() {
  330. if (!this.isTypeScriptTransformEnabled) {
  331. return false;
  332. }
  333. this.tokens.copyExpectedToken(tt._export);
  334. this.tokens.copyExpectedToken(tt.braceL);
  335. const isReExport = isExportFrom(this.tokens);
  336. let foundNonTypeExport = false;
  337. while (!this.tokens.matches1(tt.braceR)) {
  338. const specifierInfo = getImportExportSpecifierInfo(this.tokens);
  339. if (
  340. specifierInfo.isType ||
  341. (!isReExport && this.shouldElideExportedName(specifierInfo.leftName))
  342. ) {
  343. // Type export, so remove all tokens, including any comma.
  344. while (this.tokens.currentIndex() < specifierInfo.endIndex) {
  345. this.tokens.removeToken();
  346. }
  347. if (this.tokens.matches1(tt.comma)) {
  348. this.tokens.removeToken();
  349. }
  350. } else {
  351. // Non-type export, so copy all tokens, including any comma.
  352. foundNonTypeExport = true;
  353. while (this.tokens.currentIndex() < specifierInfo.endIndex) {
  354. this.tokens.copyToken();
  355. }
  356. if (this.tokens.matches1(tt.comma)) {
  357. this.tokens.copyToken();
  358. }
  359. }
  360. }
  361. this.tokens.copyExpectedToken(tt.braceR);
  362. if (!this.keepUnusedImports && isReExport && !foundNonTypeExport) {
  363. // This is a type-only re-export, so skip evaluating the other module. Technically this
  364. // leaves the statement as `export {}`, but that's ok since that's a no-op.
  365. this.tokens.removeToken();
  366. this.tokens.removeToken();
  367. removeMaybeImportAttributes(this.tokens);
  368. }
  369. return true;
  370. }
  371. /**
  372. * ESM elides all imports with the rule that we only elide if we see that it's
  373. * a type and never see it as a value. This is in contrast to CJS, which
  374. * elides imports that are completely unknown.
  375. */
  376. shouldElideExportedName(name) {
  377. return (
  378. this.isTypeScriptTransformEnabled &&
  379. !this.keepUnusedImports &&
  380. this.declarationInfo.typeDeclarations.has(name) &&
  381. !this.declarationInfo.valueDeclarations.has(name)
  382. );
  383. }
  384. }