CJSImportProcessor.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import {isDeclaration} from "./parser/tokenizer";
  2. import {ContextualKeyword} from "./parser/tokenizer/keywords";
  3. import {TokenType as tt} from "./parser/tokenizer/types";
  4. import getImportExportSpecifierInfo from "./util/getImportExportSpecifierInfo";
  5. import {getNonTypeIdentifiers} from "./util/getNonTypeIdentifiers";
  6. /**
  7. * Class responsible for preprocessing and bookkeeping import and export declarations within the
  8. * file.
  9. *
  10. * TypeScript uses a simpler mechanism that does not use functions like interopRequireDefault and
  11. * interopRequireWildcard, so we also allow that mode for compatibility.
  12. */
  13. export default class CJSImportProcessor {
  14. __init() {this.nonTypeIdentifiers = new Set()}
  15. __init2() {this.importInfoByPath = new Map()}
  16. __init3() {this.importsToReplace = new Map()}
  17. __init4() {this.identifierReplacements = new Map()}
  18. __init5() {this.exportBindingsByLocalName = new Map()}
  19. constructor(
  20. nameManager,
  21. tokens,
  22. enableLegacyTypeScriptModuleInterop,
  23. options,
  24. isTypeScriptTransformEnabled,
  25. keepUnusedImports,
  26. helperManager,
  27. ) {;this.nameManager = nameManager;this.tokens = tokens;this.enableLegacyTypeScriptModuleInterop = enableLegacyTypeScriptModuleInterop;this.options = options;this.isTypeScriptTransformEnabled = isTypeScriptTransformEnabled;this.keepUnusedImports = keepUnusedImports;this.helperManager = helperManager;CJSImportProcessor.prototype.__init.call(this);CJSImportProcessor.prototype.__init2.call(this);CJSImportProcessor.prototype.__init3.call(this);CJSImportProcessor.prototype.__init4.call(this);CJSImportProcessor.prototype.__init5.call(this);}
  28. preprocessTokens() {
  29. for (let i = 0; i < this.tokens.tokens.length; i++) {
  30. if (
  31. this.tokens.matches1AtIndex(i, tt._import) &&
  32. !this.tokens.matches3AtIndex(i, tt._import, tt.name, tt.eq)
  33. ) {
  34. this.preprocessImportAtIndex(i);
  35. }
  36. if (
  37. this.tokens.matches1AtIndex(i, tt._export) &&
  38. !this.tokens.matches2AtIndex(i, tt._export, tt.eq)
  39. ) {
  40. this.preprocessExportAtIndex(i);
  41. }
  42. }
  43. this.generateImportReplacements();
  44. }
  45. /**
  46. * In TypeScript, import statements that only import types should be removed.
  47. * This includes `import {} from 'foo';`, but not `import 'foo';`.
  48. */
  49. pruneTypeOnlyImports() {
  50. this.nonTypeIdentifiers = getNonTypeIdentifiers(this.tokens, this.options);
  51. for (const [path, importInfo] of this.importInfoByPath.entries()) {
  52. if (
  53. importInfo.hasBareImport ||
  54. importInfo.hasStarExport ||
  55. importInfo.exportStarNames.length > 0 ||
  56. importInfo.namedExports.length > 0
  57. ) {
  58. continue;
  59. }
  60. const names = [
  61. ...importInfo.defaultNames,
  62. ...importInfo.wildcardNames,
  63. ...importInfo.namedImports.map(({localName}) => localName),
  64. ];
  65. if (names.every((name) => this.shouldAutomaticallyElideImportedName(name))) {
  66. this.importsToReplace.set(path, "");
  67. }
  68. }
  69. }
  70. shouldAutomaticallyElideImportedName(name) {
  71. return (
  72. this.isTypeScriptTransformEnabled &&
  73. !this.keepUnusedImports &&
  74. !this.nonTypeIdentifiers.has(name)
  75. );
  76. }
  77. generateImportReplacements() {
  78. for (const [path, importInfo] of this.importInfoByPath.entries()) {
  79. const {
  80. defaultNames,
  81. wildcardNames,
  82. namedImports,
  83. namedExports,
  84. exportStarNames,
  85. hasStarExport,
  86. } = importInfo;
  87. if (
  88. defaultNames.length === 0 &&
  89. wildcardNames.length === 0 &&
  90. namedImports.length === 0 &&
  91. namedExports.length === 0 &&
  92. exportStarNames.length === 0 &&
  93. !hasStarExport
  94. ) {
  95. // Import is never used, so don't even assign a name.
  96. this.importsToReplace.set(path, `require('${path}');`);
  97. continue;
  98. }
  99. const primaryImportName = this.getFreeIdentifierForPath(path);
  100. let secondaryImportName;
  101. if (this.enableLegacyTypeScriptModuleInterop) {
  102. secondaryImportName = primaryImportName;
  103. } else {
  104. secondaryImportName =
  105. wildcardNames.length > 0 ? wildcardNames[0] : this.getFreeIdentifierForPath(path);
  106. }
  107. let requireCode = `var ${primaryImportName} = require('${path}');`;
  108. if (wildcardNames.length > 0) {
  109. for (const wildcardName of wildcardNames) {
  110. const moduleExpr = this.enableLegacyTypeScriptModuleInterop
  111. ? primaryImportName
  112. : `${this.helperManager.getHelperName("interopRequireWildcard")}(${primaryImportName})`;
  113. requireCode += ` var ${wildcardName} = ${moduleExpr};`;
  114. }
  115. } else if (exportStarNames.length > 0 && secondaryImportName !== primaryImportName) {
  116. requireCode += ` var ${secondaryImportName} = ${this.helperManager.getHelperName(
  117. "interopRequireWildcard",
  118. )}(${primaryImportName});`;
  119. } else if (defaultNames.length > 0 && secondaryImportName !== primaryImportName) {
  120. requireCode += ` var ${secondaryImportName} = ${this.helperManager.getHelperName(
  121. "interopRequireDefault",
  122. )}(${primaryImportName});`;
  123. }
  124. for (const {importedName, localName} of namedExports) {
  125. requireCode += ` ${this.helperManager.getHelperName(
  126. "createNamedExportFrom",
  127. )}(${primaryImportName}, '${localName}', '${importedName}');`;
  128. }
  129. for (const exportStarName of exportStarNames) {
  130. requireCode += ` exports.${exportStarName} = ${secondaryImportName};`;
  131. }
  132. if (hasStarExport) {
  133. requireCode += ` ${this.helperManager.getHelperName(
  134. "createStarExport",
  135. )}(${primaryImportName});`;
  136. }
  137. this.importsToReplace.set(path, requireCode);
  138. for (const defaultName of defaultNames) {
  139. this.identifierReplacements.set(defaultName, `${secondaryImportName}.default`);
  140. }
  141. for (const {importedName, localName} of namedImports) {
  142. this.identifierReplacements.set(localName, `${primaryImportName}.${importedName}`);
  143. }
  144. }
  145. }
  146. getFreeIdentifierForPath(path) {
  147. const components = path.split("/");
  148. const lastComponent = components[components.length - 1];
  149. const baseName = lastComponent.replace(/\W/g, "");
  150. return this.nameManager.claimFreeName(`_${baseName}`);
  151. }
  152. preprocessImportAtIndex(index) {
  153. const defaultNames = [];
  154. const wildcardNames = [];
  155. const namedImports = [];
  156. index++;
  157. if (
  158. (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._type) ||
  159. this.tokens.matches1AtIndex(index, tt._typeof)) &&
  160. !this.tokens.matches1AtIndex(index + 1, tt.comma) &&
  161. !this.tokens.matchesContextualAtIndex(index + 1, ContextualKeyword._from)
  162. ) {
  163. // import type declaration, so no need to process anything.
  164. return;
  165. }
  166. if (this.tokens.matches1AtIndex(index, tt.parenL)) {
  167. // Dynamic import, so nothing to do
  168. return;
  169. }
  170. if (this.tokens.matches1AtIndex(index, tt.name)) {
  171. defaultNames.push(this.tokens.identifierNameAtIndex(index));
  172. index++;
  173. if (this.tokens.matches1AtIndex(index, tt.comma)) {
  174. index++;
  175. }
  176. }
  177. if (this.tokens.matches1AtIndex(index, tt.star)) {
  178. // * as
  179. index += 2;
  180. wildcardNames.push(this.tokens.identifierNameAtIndex(index));
  181. index++;
  182. }
  183. if (this.tokens.matches1AtIndex(index, tt.braceL)) {
  184. const result = this.getNamedImports(index + 1);
  185. index = result.newIndex;
  186. for (const namedImport of result.namedImports) {
  187. // Treat {default as X} as a default import to ensure usage of require interop helper
  188. if (namedImport.importedName === "default") {
  189. defaultNames.push(namedImport.localName);
  190. } else {
  191. namedImports.push(namedImport);
  192. }
  193. }
  194. }
  195. if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) {
  196. index++;
  197. }
  198. if (!this.tokens.matches1AtIndex(index, tt.string)) {
  199. throw new Error("Expected string token at the end of import statement.");
  200. }
  201. const path = this.tokens.stringValueAtIndex(index);
  202. const importInfo = this.getImportInfo(path);
  203. importInfo.defaultNames.push(...defaultNames);
  204. importInfo.wildcardNames.push(...wildcardNames);
  205. importInfo.namedImports.push(...namedImports);
  206. if (defaultNames.length === 0 && wildcardNames.length === 0 && namedImports.length === 0) {
  207. importInfo.hasBareImport = true;
  208. }
  209. }
  210. preprocessExportAtIndex(index) {
  211. if (
  212. this.tokens.matches2AtIndex(index, tt._export, tt._var) ||
  213. this.tokens.matches2AtIndex(index, tt._export, tt._let) ||
  214. this.tokens.matches2AtIndex(index, tt._export, tt._const)
  215. ) {
  216. this.preprocessVarExportAtIndex(index);
  217. } else if (
  218. this.tokens.matches2AtIndex(index, tt._export, tt._function) ||
  219. this.tokens.matches2AtIndex(index, tt._export, tt._class)
  220. ) {
  221. const exportName = this.tokens.identifierNameAtIndex(index + 2);
  222. this.addExportBinding(exportName, exportName);
  223. } else if (this.tokens.matches3AtIndex(index, tt._export, tt.name, tt._function)) {
  224. const exportName = this.tokens.identifierNameAtIndex(index + 3);
  225. this.addExportBinding(exportName, exportName);
  226. } else if (this.tokens.matches2AtIndex(index, tt._export, tt.braceL)) {
  227. this.preprocessNamedExportAtIndex(index);
  228. } else if (this.tokens.matches2AtIndex(index, tt._export, tt.star)) {
  229. this.preprocessExportStarAtIndex(index);
  230. }
  231. }
  232. preprocessVarExportAtIndex(index) {
  233. let depth = 0;
  234. // Handle cases like `export let {x} = y;`, starting at the open-brace in that case.
  235. for (let i = index + 2; ; i++) {
  236. if (
  237. this.tokens.matches1AtIndex(i, tt.braceL) ||
  238. this.tokens.matches1AtIndex(i, tt.dollarBraceL) ||
  239. this.tokens.matches1AtIndex(i, tt.bracketL)
  240. ) {
  241. depth++;
  242. } else if (
  243. this.tokens.matches1AtIndex(i, tt.braceR) ||
  244. this.tokens.matches1AtIndex(i, tt.bracketR)
  245. ) {
  246. depth--;
  247. } else if (depth === 0 && !this.tokens.matches1AtIndex(i, tt.name)) {
  248. break;
  249. } else if (this.tokens.matches1AtIndex(1, tt.eq)) {
  250. const endIndex = this.tokens.currentToken().rhsEndIndex;
  251. if (endIndex == null) {
  252. throw new Error("Expected = token with an end index.");
  253. }
  254. i = endIndex - 1;
  255. } else {
  256. const token = this.tokens.tokens[i];
  257. if (isDeclaration(token)) {
  258. const exportName = this.tokens.identifierNameAtIndex(i);
  259. this.identifierReplacements.set(exportName, `exports.${exportName}`);
  260. }
  261. }
  262. }
  263. }
  264. /**
  265. * Walk this export statement just in case it's an export...from statement.
  266. * If it is, combine it into the import info for that path. Otherwise, just
  267. * bail out; it'll be handled later.
  268. */
  269. preprocessNamedExportAtIndex(index) {
  270. // export {
  271. index += 2;
  272. const {newIndex, namedImports} = this.getNamedImports(index);
  273. index = newIndex;
  274. if (this.tokens.matchesContextualAtIndex(index, ContextualKeyword._from)) {
  275. index++;
  276. } else {
  277. // Reinterpret "a as b" to be local/exported rather than imported/local.
  278. for (const {importedName: localName, localName: exportedName} of namedImports) {
  279. this.addExportBinding(localName, exportedName);
  280. }
  281. return;
  282. }
  283. if (!this.tokens.matches1AtIndex(index, tt.string)) {
  284. throw new Error("Expected string token at the end of import statement.");
  285. }
  286. const path = this.tokens.stringValueAtIndex(index);
  287. const importInfo = this.getImportInfo(path);
  288. importInfo.namedExports.push(...namedImports);
  289. }
  290. preprocessExportStarAtIndex(index) {
  291. let exportedName = null;
  292. if (this.tokens.matches3AtIndex(index, tt._export, tt.star, tt._as)) {
  293. // export * as
  294. index += 3;
  295. exportedName = this.tokens.identifierNameAtIndex(index);
  296. // foo from
  297. index += 2;
  298. } else {
  299. // export * from
  300. index += 3;
  301. }
  302. if (!this.tokens.matches1AtIndex(index, tt.string)) {
  303. throw new Error("Expected string token at the end of star export statement.");
  304. }
  305. const path = this.tokens.stringValueAtIndex(index);
  306. const importInfo = this.getImportInfo(path);
  307. if (exportedName !== null) {
  308. importInfo.exportStarNames.push(exportedName);
  309. } else {
  310. importInfo.hasStarExport = true;
  311. }
  312. }
  313. getNamedImports(index) {
  314. const namedImports = [];
  315. while (true) {
  316. if (this.tokens.matches1AtIndex(index, tt.braceR)) {
  317. index++;
  318. break;
  319. }
  320. const specifierInfo = getImportExportSpecifierInfo(this.tokens, index);
  321. index = specifierInfo.endIndex;
  322. if (!specifierInfo.isType) {
  323. namedImports.push({
  324. importedName: specifierInfo.leftName,
  325. localName: specifierInfo.rightName,
  326. });
  327. }
  328. if (this.tokens.matches2AtIndex(index, tt.comma, tt.braceR)) {
  329. index += 2;
  330. break;
  331. } else if (this.tokens.matches1AtIndex(index, tt.braceR)) {
  332. index++;
  333. break;
  334. } else if (this.tokens.matches1AtIndex(index, tt.comma)) {
  335. index++;
  336. } else {
  337. throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[index])}`);
  338. }
  339. }
  340. return {newIndex: index, namedImports};
  341. }
  342. /**
  343. * Get a mutable import info object for this path, creating one if it doesn't
  344. * exist yet.
  345. */
  346. getImportInfo(path) {
  347. const existingInfo = this.importInfoByPath.get(path);
  348. if (existingInfo) {
  349. return existingInfo;
  350. }
  351. const newInfo = {
  352. defaultNames: [],
  353. wildcardNames: [],
  354. namedImports: [],
  355. namedExports: [],
  356. hasBareImport: false,
  357. exportStarNames: [],
  358. hasStarExport: false,
  359. };
  360. this.importInfoByPath.set(path, newInfo);
  361. return newInfo;
  362. }
  363. addExportBinding(localName, exportedName) {
  364. if (!this.exportBindingsByLocalName.has(localName)) {
  365. this.exportBindingsByLocalName.set(localName, []);
  366. }
  367. this.exportBindingsByLocalName.get(localName).push(exportedName);
  368. }
  369. /**
  370. * Return the code to use for the import for this path, or the empty string if
  371. * the code has already been "claimed" by a previous import.
  372. */
  373. claimImportCode(importPath) {
  374. const result = this.importsToReplace.get(importPath);
  375. this.importsToReplace.set(importPath, "");
  376. return result || "";
  377. }
  378. getIdentifierReplacement(identifierName) {
  379. return this.identifierReplacements.get(identifierName) || null;
  380. }
  381. /**
  382. * Return a string like `exports.foo = exports.bar`.
  383. */
  384. resolveExportBinding(assignedName) {
  385. const exportedNames = this.exportBindingsByLocalName.get(assignedName);
  386. if (!exportedNames || exportedNames.length === 0) {
  387. return null;
  388. }
  389. return exportedNames.map((exportedName) => `exports.${exportedName}`).join(" = ");
  390. }
  391. /**
  392. * Return all imported/exported names where we might be interested in whether usages of those
  393. * names are shadowed.
  394. */
  395. getGlobalNames() {
  396. return new Set([
  397. ...this.identifierReplacements.keys(),
  398. ...this.exportBindingsByLocalName.keys(),
  399. ]);
  400. }
  401. }