TsconfigPathsPlugin.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Natsu @xiaoxiaojx
  4. */
  5. "use strict";
  6. const { aliasResolveHandler, compileAliasOptions } = require("./AliasUtils");
  7. const { modulesResolveHandler } = require("./ModulesUtils");
  8. const { readJson } = require("./util/fs");
  9. const { PathType: _PathType, isSubPath, normalize } = require("./util/path");
  10. /** @typedef {import("./Resolver")} Resolver */
  11. /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
  12. /** @typedef {import("./AliasUtils").AliasOption} AliasOption */
  13. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  14. /** @typedef {import("./Resolver").ResolveContext} ResolveContext */
  15. /** @typedef {import("./Resolver").FileSystem} FileSystem */
  16. /** @typedef {import("./Resolver").TsconfigPathsData} TsconfigPathsData */
  17. /** @typedef {import("./Resolver").TsconfigPathsMap} TsconfigPathsMap */
  18. /** @typedef {import("./ResolverFactory").TsconfigOptions} TsconfigOptions */
  19. /**
  20. * @typedef {object} TsconfigCompilerOptions
  21. * @property {string=} baseUrl Base URL for resolving paths
  22. * @property {{ [key: string]: string[] }=} paths TypeScript paths mapping
  23. */
  24. /**
  25. * @typedef {object} TsconfigReference
  26. * @property {string} path Path to the referenced project
  27. */
  28. /**
  29. * @typedef {object} Tsconfig
  30. * @property {TsconfigCompilerOptions=} compilerOptions Compiler options
  31. * @property {string | string[]=} extends Extended configuration paths
  32. * @property {TsconfigReference[]=} references Project references
  33. */
  34. const DEFAULT_CONFIG_FILE = "tsconfig.json";
  35. const READ_JSON_OPTIONS = { stripComments: true };
  36. // Trailing `/*` or `\*` segment of a tsconfig `paths` mapping (e.g.
  37. // `./src/*` → `./src`). Hoisted so we don't allocate a fresh regex per
  38. // path entry on every tsconfig load — and so the same regex object can be
  39. // reused for the matching `test` + `replace` pair below.
  40. const WILDCARD_TAIL_RE = /[/\\]\*$/;
  41. /**
  42. * @param {string} pattern Path pattern
  43. * @returns {number} Length of the prefix
  44. */
  45. function getPrefixLength(pattern) {
  46. const prefixLength = pattern.indexOf("*");
  47. if (prefixLength === -1) {
  48. return pattern.length;
  49. }
  50. return prefixLength;
  51. }
  52. /**
  53. * Sort path patterns.
  54. * If a module name can be matched with multiple patterns then pattern with the longest prefix will be picked.
  55. * @param {string[]} arr Array of path patterns
  56. * @returns {string[]} Array of path patterns sorted by longest prefix
  57. */
  58. function sortByLongestPrefix(arr) {
  59. return [...arr].sort((a, b) => getPrefixLength(b) - getPrefixLength(a));
  60. }
  61. /**
  62. * Merge two tsconfig objects
  63. * @param {Tsconfig | null} base base config
  64. * @param {Tsconfig | null} config config to merge
  65. * @returns {Tsconfig} merged config
  66. */
  67. function mergeTsconfigs(base, config) {
  68. base = base || {};
  69. config = config || {};
  70. return {
  71. ...base,
  72. ...config,
  73. compilerOptions: {
  74. .../** @type {TsconfigCompilerOptions} */ (base.compilerOptions),
  75. .../** @type {TsconfigCompilerOptions} */ (config.compilerOptions),
  76. },
  77. };
  78. }
  79. /**
  80. * Substitute ${configDir} template variable in path
  81. * @param {string} pathValue the path value
  82. * @param {string} configDir the config directory
  83. * @returns {string} the path with substituted template
  84. */
  85. function substituteConfigDir(pathValue, configDir) {
  86. // eslint-disable-next-line no-template-curly-in-string
  87. if (!pathValue.includes("${configDir}")) return pathValue;
  88. return pathValue.replace(/\$\{configDir\}/g, configDir);
  89. }
  90. /**
  91. * Convert tsconfig paths to resolver options
  92. * @param {string} configDir Config file directory
  93. * @param {{ [key: string]: string[] }} paths TypeScript paths mapping
  94. * @param {Resolver} resolver resolver instance
  95. * @param {string=} baseUrl Base URL for resolving paths (relative to configDir)
  96. * @returns {TsconfigPathsData} the resolver options
  97. */
  98. function tsconfigPathsToResolveOptions(configDir, paths, resolver, baseUrl) {
  99. // Calculate absolute base URL
  100. const absoluteBaseUrl = !baseUrl
  101. ? configDir
  102. : resolver.join(configDir, baseUrl);
  103. /** @type {string[]} */
  104. const sortedKeys = sortByLongestPrefix(Object.keys(paths));
  105. /** @type {AliasOption[]} */
  106. const alias = [];
  107. /** @type {string[]} */
  108. const modules = [];
  109. for (const pattern of sortedKeys) {
  110. const mappings = paths[pattern];
  111. // Substitute ${configDir} in path mappings
  112. const absolutePaths = mappings.map((mapping) => {
  113. const substituted = substituteConfigDir(mapping, configDir);
  114. return resolver.join(absoluteBaseUrl, substituted);
  115. });
  116. if (absolutePaths.length > 0) {
  117. if (pattern === "*") {
  118. // Pull `dir/*` entries directly into `modules` with their
  119. // trailing wildcard stripped, skipping anything else. The
  120. // previous `.map(...).filter(Boolean)` form allocated two
  121. // throwaway arrays plus a spread iterator per `*` mapping.
  122. for (let j = 0; j < absolutePaths.length; j++) {
  123. const dir = absolutePaths[j];
  124. if (WILDCARD_TAIL_RE.test(dir)) {
  125. modules.push(dir.replace(WILDCARD_TAIL_RE, ""));
  126. }
  127. }
  128. } else {
  129. alias.push({ name: pattern, alias: absolutePaths });
  130. }
  131. }
  132. }
  133. if (absoluteBaseUrl && !modules.includes(absoluteBaseUrl)) {
  134. modules.push(absoluteBaseUrl);
  135. }
  136. return {
  137. alias: compileAliasOptions(resolver, alias),
  138. modules,
  139. };
  140. }
  141. /**
  142. * Get the base context for the current project
  143. * @param {string} context the context
  144. * @param {Resolver} resolver resolver instance
  145. * @param {string=} baseUrl base URL for resolving paths
  146. * @returns {string} the base context
  147. */
  148. function getAbsoluteBaseUrl(context, resolver, baseUrl) {
  149. return !baseUrl ? context : resolver.join(context, baseUrl);
  150. }
  151. /**
  152. * @param {TsconfigPathsData} main main paths data
  153. * @param {string} mainContext main context
  154. * @param {{ [baseUrl: string]: TsconfigPathsData }} refs references map
  155. * @param {Set<string>} fileDependencies file dependencies
  156. * @returns {TsconfigPathsMap} the tsconfig paths map
  157. */
  158. function buildTsconfigPathsMap(main, mainContext, refs, fileDependencies) {
  159. const allContexts = /** @type {{ [context: string]: TsconfigPathsData }} */ ({
  160. [mainContext]: main,
  161. ...refs,
  162. });
  163. // Precompute the key list once per tsconfig load. `_selectPathsDataForContext`
  164. // runs per resolve and otherwise would call `Object.entries(allContexts)`
  165. // each time, allocating a fresh [key, value][] array.
  166. const contextList = Object.keys(allContexts);
  167. return {
  168. main,
  169. mainContext,
  170. refs,
  171. allContexts,
  172. contextList,
  173. fileDependencies,
  174. };
  175. }
  176. module.exports = class TsconfigPathsPlugin {
  177. /**
  178. * @param {true | string | TsconfigOptions} configFileOrOptions tsconfig file path or options object
  179. */
  180. constructor(configFileOrOptions) {
  181. if (
  182. typeof configFileOrOptions === "object" &&
  183. configFileOrOptions !== null
  184. ) {
  185. // Options object format
  186. const { configFile } = configFileOrOptions;
  187. /** @type {boolean} */
  188. this.isAutoConfigFile = typeof configFile !== "string";
  189. /** @type {string} */
  190. this.configFile = this.isAutoConfigFile
  191. ? DEFAULT_CONFIG_FILE
  192. : /** @type {string} */ (configFile);
  193. /** @type {string[] | "auto"} */
  194. if (Array.isArray(configFileOrOptions.references)) {
  195. /** @type {TsconfigReference[] | "auto"} */
  196. this.references = configFileOrOptions.references.map((ref) => ({
  197. path: ref,
  198. }));
  199. } else if (configFileOrOptions.references === "auto") {
  200. this.references = "auto";
  201. } else {
  202. this.references = [];
  203. }
  204. /** @type {string | undefined} */
  205. this.baseUrl = configFileOrOptions.baseUrl;
  206. } else {
  207. /** @type {boolean} */
  208. this.isAutoConfigFile = configFileOrOptions === true;
  209. /** @type {string} */
  210. this.configFile = this.isAutoConfigFile
  211. ? DEFAULT_CONFIG_FILE
  212. : /** @type {string} */ (configFileOrOptions);
  213. /** @type {TsconfigReference[] | "auto"} */
  214. this.references = [];
  215. /** @type {string | undefined} */
  216. this.baseUrl = undefined;
  217. }
  218. }
  219. /**
  220. * @param {Resolver} resolver the resolver
  221. * @returns {void}
  222. */
  223. apply(resolver) {
  224. const aliasTarget = resolver.ensureHook("internal-resolve");
  225. const moduleTarget = resolver.ensureHook("module");
  226. resolver
  227. .getHook("raw-resolve")
  228. .tapAsync("TsconfigPathsPlugin", (request, resolveContext, callback) => {
  229. this._getTsconfigPathsMap(
  230. resolver,
  231. request,
  232. resolveContext,
  233. (err, tsconfigPathsMap) => {
  234. if (err) return callback(err);
  235. if (!tsconfigPathsMap) return callback();
  236. const selectedData = this._selectPathsDataForContext(
  237. request.path,
  238. tsconfigPathsMap,
  239. );
  240. if (!selectedData) return callback();
  241. aliasResolveHandler(
  242. resolver,
  243. selectedData.alias,
  244. aliasTarget,
  245. request,
  246. resolveContext,
  247. callback,
  248. );
  249. },
  250. );
  251. });
  252. resolver
  253. .getHook("raw-module")
  254. .tapAsync("TsconfigPathsPlugin", (request, resolveContext, callback) => {
  255. this._getTsconfigPathsMap(
  256. resolver,
  257. request,
  258. resolveContext,
  259. (err, tsconfigPathsMap) => {
  260. if (err) return callback(err);
  261. if (!tsconfigPathsMap) return callback();
  262. const selectedData = this._selectPathsDataForContext(
  263. request.path,
  264. tsconfigPathsMap,
  265. );
  266. if (!selectedData) return callback();
  267. modulesResolveHandler(
  268. resolver,
  269. selectedData.modules,
  270. moduleTarget,
  271. request,
  272. resolveContext,
  273. callback,
  274. );
  275. },
  276. );
  277. });
  278. }
  279. /**
  280. * Get TsconfigPathsMap for the request (with caching)
  281. * @param {Resolver} resolver the resolver
  282. * @param {ResolveRequest} request the request
  283. * @param {ResolveContext} resolveContext the resolve context
  284. * @param {(err: Error | null, result?: TsconfigPathsMap | null) => void} callback the callback
  285. * @returns {void}
  286. */
  287. _getTsconfigPathsMap(resolver, request, resolveContext, callback) {
  288. if (typeof request.tsconfigPathsMap !== "undefined") {
  289. const cached = request.tsconfigPathsMap;
  290. if (!cached) return callback(null, null);
  291. if (resolveContext.fileDependencies) {
  292. for (const fileDependency of cached.fileDependencies) {
  293. resolveContext.fileDependencies.add(fileDependency);
  294. }
  295. }
  296. return callback(null, cached);
  297. }
  298. const absTsconfigPath = resolver.join(
  299. request.path || process.cwd(),
  300. this.configFile,
  301. );
  302. this._loadTsconfigPathsMap(resolver, absTsconfigPath, (err, result) => {
  303. if (err) {
  304. request.tsconfigPathsMap = null;
  305. if (
  306. this.isAutoConfigFile &&
  307. /** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT"
  308. ) {
  309. return callback(null, null);
  310. }
  311. return callback(err);
  312. }
  313. const map = /** @type {TsconfigPathsMap} */ (result);
  314. request.tsconfigPathsMap = map;
  315. if (resolveContext.fileDependencies) {
  316. for (const fileDependency of map.fileDependencies) {
  317. resolveContext.fileDependencies.add(fileDependency);
  318. }
  319. }
  320. callback(null, map);
  321. });
  322. }
  323. /**
  324. * Load tsconfig.json and build complete TsconfigPathsMap
  325. * Includes main project paths and all referenced projects
  326. * @param {Resolver} resolver the resolver
  327. * @param {string} absTsconfigPath absolute path to tsconfig.json
  328. * @param {(err: Error | null, result?: TsconfigPathsMap) => void} callback the callback
  329. * @returns {void}
  330. */
  331. _loadTsconfigPathsMap(resolver, absTsconfigPath, callback) {
  332. /** @type {Set<string>} */
  333. const fileDependencies = new Set();
  334. this._loadTsconfig(
  335. resolver,
  336. absTsconfigPath,
  337. fileDependencies,
  338. undefined,
  339. (err, config) => {
  340. if (err) return callback(err);
  341. const cfg = /** @type {Tsconfig} */ (config);
  342. const compilerOptions = cfg.compilerOptions || {};
  343. const mainContext = resolver.dirname(absTsconfigPath);
  344. const baseUrl =
  345. this.baseUrl !== undefined ? this.baseUrl : compilerOptions.baseUrl;
  346. const main = tsconfigPathsToResolveOptions(
  347. mainContext,
  348. compilerOptions.paths || {},
  349. resolver,
  350. baseUrl,
  351. );
  352. /** @type {{ [baseUrl: string]: TsconfigPathsData }} */
  353. const refs = {};
  354. let referencesToUse = null;
  355. if (this.references === "auto") {
  356. referencesToUse = cfg.references;
  357. } else if (Array.isArray(this.references)) {
  358. referencesToUse = this.references;
  359. }
  360. if (!Array.isArray(referencesToUse)) {
  361. return callback(
  362. null,
  363. buildTsconfigPathsMap(main, mainContext, refs, fileDependencies),
  364. );
  365. }
  366. this._loadTsconfigReferences(
  367. resolver,
  368. mainContext,
  369. referencesToUse,
  370. fileDependencies,
  371. refs,
  372. (refErr) => {
  373. if (refErr) return callback(refErr);
  374. callback(
  375. null,
  376. buildTsconfigPathsMap(main, mainContext, refs, fileDependencies),
  377. );
  378. },
  379. );
  380. },
  381. );
  382. }
  383. /**
  384. * Select the correct TsconfigPathsData based on request.path (context-aware)
  385. * Matches the behavior of tsconfig-paths-webpack-plugin
  386. * @param {string | false} requestPath the request path
  387. * @param {TsconfigPathsMap} tsconfigPathsMap the tsconfig paths map
  388. * @returns {TsconfigPathsData | null} the selected paths data
  389. */
  390. _selectPathsDataForContext(requestPath, tsconfigPathsMap) {
  391. const { main, allContexts, contextList } = tsconfigPathsMap;
  392. if (!requestPath) {
  393. return main;
  394. }
  395. let longestMatchContext = null;
  396. let longestMatchLength = 0;
  397. // Iterate the pre-computed key list (the previous
  398. // `Object.entries(allContexts)` form allocated a fresh
  399. // `[key, value][]` per resolve). Defer the `allContexts[context]`
  400. // lookup to after we know the context actually matches — non-matches
  401. // are the common case and don't need the property access.
  402. for (let i = 0; i < contextList.length; i++) {
  403. const context = contextList[i];
  404. if (context === requestPath) {
  405. return allContexts[context];
  406. }
  407. // Cheap integer-compare gate first: a context can only beat the
  408. // current longest match if its own length is strictly greater.
  409. // Skipping `isSubPath` (a `startsWith` + char-code probe) when the
  410. // length already disqualifies the candidate avoids the per-resolve
  411. // scan over every shorter context.
  412. if (
  413. context.length > longestMatchLength &&
  414. isSubPath(context, requestPath)
  415. ) {
  416. longestMatchContext = context;
  417. longestMatchLength = context.length;
  418. }
  419. }
  420. return longestMatchContext === null
  421. ? null
  422. : allContexts[longestMatchContext];
  423. }
  424. /**
  425. * Load tsconfig from extends path
  426. * @param {Resolver} resolver the resolver
  427. * @param {string} configFilePath current config file path
  428. * @param {string} extendedConfigValue extends value
  429. * @param {Set<string>} fileDependencies the file dependencies
  430. * @param {Set<string>} visitedConfigPaths config paths being loaded (for circular extends detection)
  431. * @param {(err: Error | null, result?: Tsconfig) => void} callback callback
  432. * @returns {void}
  433. */
  434. _loadTsconfigFromExtends(
  435. resolver,
  436. configFilePath,
  437. extendedConfigValue,
  438. fileDependencies,
  439. visitedConfigPaths,
  440. callback,
  441. ) {
  442. const { fileSystem } = resolver;
  443. const currentDir = resolver.dirname(configFilePath);
  444. // Substitute ${configDir} in extends path
  445. extendedConfigValue = substituteConfigDir(extendedConfigValue, currentDir);
  446. // Remember the original value before potentially appending .json
  447. const originalExtendedConfigValue = extendedConfigValue;
  448. if (
  449. typeof extendedConfigValue === "string" &&
  450. !extendedConfigValue.includes(".json")
  451. ) {
  452. extendedConfigValue += ".json";
  453. }
  454. const initialExtendedConfigPath = resolver.join(
  455. currentDir,
  456. extendedConfigValue,
  457. );
  458. fileSystem.stat(initialExtendedConfigPath, (existsErr) => {
  459. let extendedConfigPath = initialExtendedConfigPath;
  460. if (existsErr) {
  461. // Handle scoped package extends like "@scope/name" (no sub-path):
  462. // "@scope/name" should resolve to node_modules/@scope/name/tsconfig.json,
  463. // not node_modules/@scope/name.json
  464. // See: test/fixtures/tsconfig-paths/extends-pkg-entry/
  465. if (
  466. typeof originalExtendedConfigValue === "string" &&
  467. originalExtendedConfigValue.startsWith("@") &&
  468. originalExtendedConfigValue.split("/").length === 2
  469. ) {
  470. extendedConfigPath = resolver.join(
  471. currentDir,
  472. normalize(
  473. `node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
  474. ),
  475. );
  476. } else if (extendedConfigValue.includes("/")) {
  477. // Handle package sub-path extends like "react/tsconfig":
  478. // "react/tsconfig" resolves to node_modules/react/tsconfig.json
  479. // See: test/fixtures/tsconfig-paths/extends-npm/
  480. extendedConfigPath = resolver.join(
  481. currentDir,
  482. normalize(`node_modules/${extendedConfigValue}`),
  483. );
  484. } else if (
  485. !originalExtendedConfigValue.startsWith(".") &&
  486. !originalExtendedConfigValue.startsWith("/")
  487. ) {
  488. // Handle unscoped package extends like "my-base-config" (no sub-path):
  489. // "my-base-config" should resolve to node_modules/my-base-config/tsconfig.json
  490. extendedConfigPath = resolver.join(
  491. currentDir,
  492. normalize(
  493. `node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
  494. ),
  495. );
  496. }
  497. }
  498. this._loadTsconfig(
  499. resolver,
  500. extendedConfigPath,
  501. fileDependencies,
  502. visitedConfigPaths,
  503. (err, config) => {
  504. if (err) return callback(err);
  505. const cfg = /** @type {Tsconfig} */ (config);
  506. const compilerOptions = cfg.compilerOptions || {
  507. baseUrl: undefined,
  508. };
  509. if (compilerOptions.baseUrl) {
  510. const extendedConfigDir = resolver.dirname(extendedConfigPath);
  511. compilerOptions.baseUrl = getAbsoluteBaseUrl(
  512. extendedConfigDir,
  513. resolver,
  514. compilerOptions.baseUrl,
  515. );
  516. }
  517. delete cfg.references;
  518. callback(null, cfg);
  519. },
  520. );
  521. });
  522. }
  523. /**
  524. * Load referenced tsconfig projects and store in referenceMatchMap
  525. * Simple implementation matching tsconfig-paths-webpack-plugin:
  526. * Just load each reference and store independently
  527. * @param {Resolver} resolver the resolver
  528. * @param {string} context the context
  529. * @param {TsconfigReference[]} references array of references
  530. * @param {Set<string>} fileDependencies the file dependencies
  531. * @param {{ [baseUrl: string]: TsconfigPathsData }} referenceMatchMap the map to populate
  532. * @param {(err: Error | null) => void} callback callback
  533. * @param {Set<string>=} visitedRefPaths visited reference config paths (for circular reference detection)
  534. * @returns {void}
  535. */
  536. _loadTsconfigReferences(
  537. resolver,
  538. context,
  539. references,
  540. fileDependencies,
  541. referenceMatchMap,
  542. callback,
  543. visitedRefPaths,
  544. ) {
  545. if (references.length === 0) return callback(null);
  546. const visited = visitedRefPaths || new Set();
  547. let pending = references.length;
  548. const finishOne = () => {
  549. if (--pending === 0) callback(null);
  550. };
  551. for (const ref of references) {
  552. const refPath = substituteConfigDir(ref.path, context);
  553. const refConfigPath = resolver.join(
  554. resolver.join(context, refPath),
  555. DEFAULT_CONFIG_FILE,
  556. );
  557. if (visited.has(refConfigPath)) {
  558. finishOne();
  559. continue;
  560. }
  561. visited.add(refConfigPath);
  562. this._loadTsconfig(
  563. resolver,
  564. refConfigPath,
  565. fileDependencies,
  566. undefined,
  567. (err, refConfig) => {
  568. // Failures are swallowed to match tsconfig-paths-webpack-plugin:
  569. // a broken reference must not abort the main project's resolution.
  570. if (err) return finishOne();
  571. const cfg = /** @type {Tsconfig} */ (refConfig);
  572. if (cfg.compilerOptions && cfg.compilerOptions.paths) {
  573. const refContext = resolver.dirname(refConfigPath);
  574. referenceMatchMap[refContext] = tsconfigPathsToResolveOptions(
  575. refContext,
  576. cfg.compilerOptions.paths || {},
  577. resolver,
  578. cfg.compilerOptions.baseUrl,
  579. );
  580. }
  581. if (this.references === "auto" && Array.isArray(cfg.references)) {
  582. this._loadTsconfigReferences(
  583. resolver,
  584. resolver.dirname(refConfigPath),
  585. cfg.references,
  586. fileDependencies,
  587. referenceMatchMap,
  588. finishOne,
  589. visited,
  590. );
  591. } else {
  592. finishOne();
  593. }
  594. },
  595. );
  596. }
  597. }
  598. /**
  599. * Load tsconfig.json with extends support
  600. * @param {Resolver} resolver the resolver
  601. * @param {string} configFilePath absolute path to tsconfig.json
  602. * @param {Set<string>} fileDependencies the file dependencies
  603. * @param {Set<string> | undefined} visitedConfigPaths config paths being loaded (for circular extends detection)
  604. * @param {(err: Error | null, result?: Tsconfig) => void} callback callback
  605. * @returns {void}
  606. */
  607. _loadTsconfig(
  608. resolver,
  609. configFilePath,
  610. fileDependencies,
  611. visitedConfigPaths,
  612. callback,
  613. ) {
  614. const visited = visitedConfigPaths || new Set();
  615. if (visited.has(configFilePath)) {
  616. return callback(null, /** @type {Tsconfig} */ ({}));
  617. }
  618. visited.add(configFilePath);
  619. readJson(
  620. resolver.fileSystem,
  621. configFilePath,
  622. READ_JSON_OPTIONS,
  623. (err, parsed) => {
  624. if (err) return callback(/** @type {Error} */ (err));
  625. const config = /** @type {Tsconfig} */ (parsed);
  626. fileDependencies.add(configFilePath);
  627. const extendedConfig = config.extends;
  628. if (!extendedConfig) return callback(null, config);
  629. if (!Array.isArray(extendedConfig)) {
  630. this._loadTsconfigFromExtends(
  631. resolver,
  632. configFilePath,
  633. extendedConfig,
  634. fileDependencies,
  635. visited,
  636. (extErr, extendedTsconfig) => {
  637. if (extErr) return callback(extErr);
  638. callback(
  639. null,
  640. mergeTsconfigs(
  641. /** @type {Tsconfig} */ (extendedTsconfig),
  642. config,
  643. ),
  644. );
  645. },
  646. );
  647. return;
  648. }
  649. /** @type {Tsconfig} */
  650. let base = {};
  651. let i = 0;
  652. const next = () => {
  653. if (i >= extendedConfig.length) {
  654. return callback(null, mergeTsconfigs(base, config));
  655. }
  656. this._loadTsconfigFromExtends(
  657. resolver,
  658. configFilePath,
  659. extendedConfig[i++],
  660. fileDependencies,
  661. visited,
  662. (extErr, extendedTsconfig) => {
  663. if (extErr) return callback(extErr);
  664. base = mergeTsconfigs(
  665. base,
  666. /** @type {Tsconfig} */ (extendedTsconfig),
  667. );
  668. next();
  669. },
  670. );
  671. };
  672. next();
  673. },
  674. );
  675. }
  676. };