Sin descripción

ESMImportTransformer.js 12KB

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