ESMImportTransformer.js 16 KB

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