Няма описание

CJSImportProcessor.js 15KB

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