source: frontend/node_modules/enhanced-resolve/lib/TsconfigPathsPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 26.0 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Natsu @xiaoxiaojx
4*/
5
6"use strict";
7
8const { aliasResolveHandler, compileAliasOptions } = require("./AliasUtils");
9const { modulesResolveHandler } = require("./ModulesUtils");
10const { readJson } = require("./util/fs");
11const { PathType: _PathType, isSubPath, normalize } = require("./util/path");
12
13/** @typedef {import("./Resolver")} Resolver */
14/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
15/** @typedef {import("./AliasUtils").AliasOption} AliasOption */
16/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
17/** @typedef {import("./Resolver").ResolveContext} ResolveContext */
18/** @typedef {import("./Resolver").FileSystem} FileSystem */
19/** @typedef {import("./Resolver").TsconfigPathsData} TsconfigPathsData */
20/** @typedef {import("./Resolver").TsconfigPathsMap} TsconfigPathsMap */
21/** @typedef {import("./ResolverFactory").TsconfigOptions} TsconfigOptions */
22
23// Sentinel stored in `_contextSelectionCache` for `requestPath`s whose
24// scan returned `null` ("no context matched"). Using a non-null marker
25// lets the cache-hit path be a single `Map.get()` — we distinguish
26// "cached null" from "not cached yet" without a second `has` lookup.
27const NULL_CONTEXT = Symbol("NULL_CONTEXT");
28
29/**
30 * Per-`TsconfigPathsMap` memoization of `_selectPathsDataForContext`.
31 *
32 * Real-world builds resolve hundreds of requests per source file (every
33 * import in the file), and webpack-style resolvers walk the same
34 * `requestPath` (= source-file directory) for each one. Without this
35 * cache every resolve re-scans the full `contextList` even though the
36 * answer is invariant for a given `(map, requestPath)` pair.
37 *
38 * The outer key is the `TsconfigPathsMap` itself — rebuilt on every
39 * tsconfig change — so a `WeakMap` lets the inner map be collected
40 * automatically once the map goes away. The inner Map is keyed by
41 * `requestPath` (string); a `Symbol` sentinel stands in for "no
42 * matching context" so `Map.get` alone distinguishes the three states
43 * (cached data / cached null / not cached).
44 * @type {WeakMap<TsconfigPathsMap, Map<string, TsconfigPathsData | typeof NULL_CONTEXT>>}
45 */
46const _contextSelectionCache = new WeakMap();
47
48/**
49 * @typedef {object} TsconfigCompilerOptions
50 * @property {string=} baseUrl Base URL for resolving paths
51 * @property {{ [key: string]: string[] }=} paths TypeScript paths mapping
52 */
53
54/**
55 * @typedef {object} TsconfigReference
56 * @property {string} path Path to the referenced project
57 */
58
59/**
60 * @typedef {object} Tsconfig
61 * @property {TsconfigCompilerOptions=} compilerOptions Compiler options
62 * @property {string | string[]=} extends Extended configuration paths
63 * @property {TsconfigReference[]=} references Project references
64 */
65
66const DEFAULT_CONFIG_FILE = "tsconfig.json";
67
68const READ_JSON_OPTIONS = { stripComments: true };
69
70// Trailing `/*` or `\*` segment of a tsconfig `paths` mapping (e.g.
71// `./src/*` → `./src`). Hoisted so we don't allocate a fresh regex per
72// path entry on every tsconfig load — and so the same regex object can be
73// reused for the matching `test` + `replace` pair below.
74const WILDCARD_TAIL_RE = /[/\\]\*$/;
75
76/**
77 * @param {string} pattern Path pattern
78 * @returns {number} Length of the prefix
79 */
80function getPrefixLength(pattern) {
81 const prefixLength = pattern.indexOf("*");
82 if (prefixLength === -1) {
83 return pattern.length;
84 }
85 return prefixLength;
86}
87
88/**
89 * Sort path patterns.
90 * If a module name can be matched with multiple patterns then pattern with the longest prefix will be picked.
91 * @param {string[]} arr Array of path patterns
92 * @returns {string[]} Array of path patterns sorted by longest prefix
93 */
94function sortByLongestPrefix(arr) {
95 return [...arr].sort((a, b) => getPrefixLength(b) - getPrefixLength(a));
96}
97
98/**
99 * Merge two tsconfig objects
100 * @param {Tsconfig | null} base base config
101 * @param {Tsconfig | null} config config to merge
102 * @returns {Tsconfig} merged config
103 */
104function mergeTsconfigs(base, config) {
105 base = base || {};
106 config = config || {};
107
108 return {
109 ...base,
110 ...config,
111 compilerOptions: {
112 .../** @type {TsconfigCompilerOptions} */ (base.compilerOptions),
113 .../** @type {TsconfigCompilerOptions} */ (config.compilerOptions),
114 },
115 };
116}
117
118/**
119 * Substitute ${configDir} template variable in path
120 * @param {string} pathValue the path value
121 * @param {string} configDir the config directory
122 * @returns {string} the path with substituted template
123 */
124function substituteConfigDir(pathValue, configDir) {
125 // eslint-disable-next-line no-template-curly-in-string
126 if (!pathValue.includes("${configDir}")) return pathValue;
127 return pathValue.replace(/\$\{configDir\}/g, configDir);
128}
129
130/**
131 * Convert tsconfig paths to resolver options
132 * @param {string} configDir Config file directory
133 * @param {{ [key: string]: string[] }} paths TypeScript paths mapping
134 * @param {Resolver} resolver resolver instance
135 * @param {string=} baseUrl Base URL for resolving paths (relative to configDir)
136 * @returns {TsconfigPathsData} the resolver options
137 */
138function tsconfigPathsToResolveOptions(configDir, paths, resolver, baseUrl) {
139 // Calculate absolute base URL
140 const absoluteBaseUrl = !baseUrl
141 ? configDir
142 : resolver.join(configDir, baseUrl);
143
144 /** @type {string[]} */
145 const sortedKeys = sortByLongestPrefix(Object.keys(paths));
146 /** @type {AliasOption[]} */
147 const alias = [];
148 /** @type {string[]} */
149 const modules = [];
150
151 for (const pattern of sortedKeys) {
152 const mappings = paths[pattern];
153 // Substitute ${configDir} in path mappings
154 const absolutePaths = mappings.map((mapping) => {
155 const substituted = substituteConfigDir(mapping, configDir);
156 return resolver.join(absoluteBaseUrl, substituted);
157 });
158
159 if (absolutePaths.length > 0) {
160 if (pattern === "*") {
161 // Pull `dir/*` entries directly into `modules` with their
162 // trailing wildcard stripped, skipping anything else. The
163 // previous `.map(...).filter(Boolean)` form allocated two
164 // throwaway arrays plus a spread iterator per `*` mapping.
165 for (let j = 0; j < absolutePaths.length; j++) {
166 const dir = absolutePaths[j];
167 if (WILDCARD_TAIL_RE.test(dir)) {
168 modules.push(dir.replace(WILDCARD_TAIL_RE, ""));
169 }
170 }
171 } else {
172 alias.push({ name: pattern, alias: absolutePaths });
173 }
174 }
175 }
176
177 if (baseUrl && absoluteBaseUrl && !modules.includes(absoluteBaseUrl)) {
178 modules.push(absoluteBaseUrl);
179 }
180
181 return {
182 alias: compileAliasOptions(resolver, alias),
183 modules,
184 };
185}
186
187/**
188 * Get the base context for the current project
189 * @param {string} context the context
190 * @param {Resolver} resolver resolver instance
191 * @param {string=} baseUrl base URL for resolving paths
192 * @returns {string} the base context
193 */
194function getAbsoluteBaseUrl(context, resolver, baseUrl) {
195 return !baseUrl ? context : resolver.join(context, baseUrl);
196}
197
198/**
199 * @param {TsconfigPathsData} main main paths data
200 * @param {string} mainContext main context
201 * @param {{ [baseUrl: string]: TsconfigPathsData }} refs references map
202 * @param {Set<string>} fileDependencies file dependencies
203 * @returns {TsconfigPathsMap} the tsconfig paths map
204 */
205function buildTsconfigPathsMap(main, mainContext, refs, fileDependencies) {
206 const allContexts = /** @type {{ [context: string]: TsconfigPathsData }} */ ({
207 [mainContext]: main,
208 ...refs,
209 });
210 // Precompute the key list once per tsconfig load. `_selectPathsDataForContext`
211 // runs per resolve and otherwise would call `Object.entries(allContexts)`
212 // each time, allocating a fresh [key, value][] array.
213 const contextList = Object.keys(allContexts);
214 return {
215 main,
216 mainContext,
217 refs,
218 allContexts,
219 contextList,
220 fileDependencies,
221 };
222}
223
224module.exports = class TsconfigPathsPlugin {
225 /**
226 * @param {true | string | TsconfigOptions} configFileOrOptions tsconfig file path or options object
227 */
228 constructor(configFileOrOptions) {
229 if (
230 typeof configFileOrOptions === "object" &&
231 configFileOrOptions !== null
232 ) {
233 // Options object format
234 const { configFile } = configFileOrOptions;
235 /** @type {boolean} */
236 this.isAutoConfigFile = typeof configFile !== "string";
237 /** @type {string} */
238 this.configFile = this.isAutoConfigFile
239 ? DEFAULT_CONFIG_FILE
240 : /** @type {string} */ (configFile);
241 /** @type {string[] | "auto"} */
242 if (Array.isArray(configFileOrOptions.references)) {
243 /** @type {TsconfigReference[] | "auto"} */
244 this.references = configFileOrOptions.references.map((ref) => ({
245 path: ref,
246 }));
247 } else if (configFileOrOptions.references === "auto") {
248 this.references = "auto";
249 } else {
250 this.references = [];
251 }
252 /** @type {string | undefined} */
253 this.baseUrl = configFileOrOptions.baseUrl;
254 } else {
255 /** @type {boolean} */
256 this.isAutoConfigFile = configFileOrOptions === true;
257 /** @type {string} */
258 this.configFile = this.isAutoConfigFile
259 ? DEFAULT_CONFIG_FILE
260 : /** @type {string} */ (configFileOrOptions);
261 /** @type {TsconfigReference[] | "auto"} */
262 this.references = [];
263 /** @type {string | undefined} */
264 this.baseUrl = undefined;
265 }
266 }
267
268 /**
269 * @param {Resolver} resolver the resolver
270 * @returns {void}
271 */
272 apply(resolver) {
273 const aliasTarget = resolver.ensureHook("internal-resolve");
274 const moduleTarget = resolver.ensureHook("module");
275
276 resolver
277 .getHook("raw-resolve")
278 .tapAsync("TsconfigPathsPlugin", (request, resolveContext, callback) => {
279 this._getTsconfigPathsMap(
280 resolver,
281 request,
282 resolveContext,
283 (err, tsconfigPathsMap) => {
284 if (err) return callback(err);
285 if (!tsconfigPathsMap) return callback();
286
287 const selectedData = this._selectPathsDataForContext(
288 request.path,
289 tsconfigPathsMap,
290 );
291
292 if (!selectedData) return callback();
293
294 aliasResolveHandler(
295 resolver,
296 selectedData.alias,
297 aliasTarget,
298 request,
299 resolveContext,
300 (err, result) => {
301 if (err) return callback(err);
302 if (result) return callback(null, result);
303 // https://github.com/webpack/webpack/issues/20944
304 // Unlike resolve.alias, tsconfig paths should fall through
305 // when a pattern matches but the mapped path does not exist
306 // (matching TypeScript's native resolution behavior).
307 return callback();
308 },
309 );
310 },
311 );
312 });
313
314 resolver
315 .getHook("raw-module")
316 .tapAsync("TsconfigPathsPlugin", (request, resolveContext, callback) => {
317 this._getTsconfigPathsMap(
318 resolver,
319 request,
320 resolveContext,
321 (err, tsconfigPathsMap) => {
322 if (err) return callback(err);
323 if (!tsconfigPathsMap) return callback();
324
325 const selectedData = this._selectPathsDataForContext(
326 request.path,
327 tsconfigPathsMap,
328 );
329
330 if (!selectedData) return callback();
331
332 modulesResolveHandler(
333 resolver,
334 selectedData.modules,
335 moduleTarget,
336 request,
337 resolveContext,
338 callback,
339 );
340 },
341 );
342 });
343 }
344
345 /**
346 * Get TsconfigPathsMap for the request (with caching)
347 * @param {Resolver} resolver the resolver
348 * @param {ResolveRequest} request the request
349 * @param {ResolveContext} resolveContext the resolve context
350 * @param {(err: Error | null, result?: TsconfigPathsMap | null) => void} callback the callback
351 * @returns {void}
352 */
353 _getTsconfigPathsMap(resolver, request, resolveContext, callback) {
354 if (typeof request.tsconfigPathsMap !== "undefined") {
355 const cached = request.tsconfigPathsMap;
356 if (!cached) return callback(null, null);
357 if (resolveContext.fileDependencies) {
358 for (const fileDependency of cached.fileDependencies) {
359 resolveContext.fileDependencies.add(fileDependency);
360 }
361 }
362 return callback(null, cached);
363 }
364
365 if (this.isAutoConfigFile) {
366 this._findTsconfigUpward(
367 resolver,
368 request.path || process.cwd(),
369 (err, result) => {
370 if (err) {
371 request.tsconfigPathsMap = null;
372 return callback(err);
373 }
374 if (!result) {
375 request.tsconfigPathsMap = null;
376 return callback(null, null);
377 }
378 const map = /** @type {TsconfigPathsMap} */ (result);
379 request.tsconfigPathsMap = map;
380 if (resolveContext.fileDependencies) {
381 for (const fileDependency of map.fileDependencies) {
382 resolveContext.fileDependencies.add(fileDependency);
383 }
384 }
385 callback(null, map);
386 },
387 );
388 return;
389 }
390
391 const absTsconfigPath = resolver.join(
392 request.path || process.cwd(),
393 this.configFile,
394 );
395 this._loadTsconfigPathsMap(resolver, absTsconfigPath, (err, result) => {
396 if (err) {
397 request.tsconfigPathsMap = null;
398 return callback(err);
399 }
400
401 const map = /** @type {TsconfigPathsMap} */ (result);
402 request.tsconfigPathsMap = map;
403 if (resolveContext.fileDependencies) {
404 for (const fileDependency of map.fileDependencies) {
405 resolveContext.fileDependencies.add(fileDependency);
406 }
407 }
408 callback(null, map);
409 });
410 }
411
412 /**
413 * Walk up from startDir to the filesystem root looking for tsconfig.json.
414 * Like TypeScript's own `findConfigFile` / `forEachAncestorDirectory`.
415 * @param {Resolver} resolver the resolver
416 * @param {string} startDir the directory to start searching from
417 * @param {(err: Error | null, result?: TsconfigPathsMap | null) => void} callback the callback
418 * @returns {void}
419 */
420 _findTsconfigUpward(resolver, startDir, callback) {
421 const { fileSystem } = resolver;
422 const configFileName = this.configFile;
423
424 /**
425 * @param {string} dir current directory
426 */
427 const check = (dir) => {
428 const candidate = resolver.join(dir, configFileName);
429 fileSystem.stat(candidate, (statErr) => {
430 if (!statErr) {
431 // Found — load it
432 this._loadTsconfigPathsMap(resolver, candidate, (loadErr, result) => {
433 if (loadErr) return callback(loadErr);
434 callback(null, result);
435 });
436 return;
437 }
438 // Not found — move to parent
439 const parentDir = resolver.dirname(dir);
440 if (parentDir === dir) {
441 // Reached filesystem root, no tsconfig.json found
442 return callback(null, null);
443 }
444 check(parentDir);
445 });
446 };
447
448 check(startDir);
449 }
450
451 /**
452 * Load tsconfig.json and build complete TsconfigPathsMap
453 * Includes main project paths and all referenced projects
454 * @param {Resolver} resolver the resolver
455 * @param {string} absTsconfigPath absolute path to tsconfig.json
456 * @param {(err: Error | null, result?: TsconfigPathsMap) => void} callback the callback
457 * @returns {void}
458 */
459 _loadTsconfigPathsMap(resolver, absTsconfigPath, callback) {
460 /** @type {Set<string>} */
461 const fileDependencies = new Set();
462
463 this._loadTsconfig(
464 resolver,
465 absTsconfigPath,
466 fileDependencies,
467 undefined,
468 (err, config) => {
469 if (err) return callback(err);
470
471 const cfg = /** @type {Tsconfig} */ (config);
472 const compilerOptions = cfg.compilerOptions || {};
473 const mainContext = resolver.dirname(absTsconfigPath);
474
475 const baseUrl =
476 this.baseUrl !== undefined ? this.baseUrl : compilerOptions.baseUrl;
477
478 const main = tsconfigPathsToResolveOptions(
479 mainContext,
480 compilerOptions.paths || {},
481 resolver,
482 baseUrl,
483 );
484 /** @type {{ [baseUrl: string]: TsconfigPathsData }} */
485 const refs = {};
486
487 let referencesToUse = null;
488 if (this.references === "auto") {
489 referencesToUse = cfg.references;
490 } else if (Array.isArray(this.references)) {
491 referencesToUse = this.references;
492 }
493
494 if (!Array.isArray(referencesToUse)) {
495 return callback(
496 null,
497 buildTsconfigPathsMap(main, mainContext, refs, fileDependencies),
498 );
499 }
500
501 this._loadTsconfigReferences(
502 resolver,
503 mainContext,
504 referencesToUse,
505 fileDependencies,
506 refs,
507 (refErr) => {
508 if (refErr) return callback(refErr);
509 callback(
510 null,
511 buildTsconfigPathsMap(main, mainContext, refs, fileDependencies),
512 );
513 },
514 );
515 },
516 );
517 }
518
519 /**
520 * Select the correct TsconfigPathsData based on request.path (context-aware)
521 * Matches the behavior of tsconfig-paths-webpack-plugin
522 * @param {string | false} requestPath the request path
523 * @param {TsconfigPathsMap} tsconfigPathsMap the tsconfig paths map
524 * @returns {TsconfigPathsData | null} the selected paths data
525 */
526 _selectPathsDataForContext(requestPath, tsconfigPathsMap) {
527 const { main, allContexts, contextList } = tsconfigPathsMap;
528 if (!requestPath) {
529 return main;
530 }
531 // Single-context tsconfigs (no project references) hit the loop
532 // below at most once; in that case the cache lookup costs more
533 // than the loop itself. Only memoize when there are 2+ contexts
534 // — that's the monorepo / project-references shape where the
535 // scan actually walks multiple entries per resolve and the
536 // `(map, requestPath)` answer can be reused.
537 /** @type {Map<string, TsconfigPathsData | typeof NULL_CONTEXT> | undefined} */
538 let perMap;
539 if (contextList.length >= 2) {
540 perMap = _contextSelectionCache.get(tsconfigPathsMap);
541 if (perMap !== undefined) {
542 const cached = perMap.get(requestPath);
543 if (cached !== undefined) {
544 return cached === NULL_CONTEXT
545 ? null
546 : /** @type {TsconfigPathsData} */ (cached);
547 }
548 } else {
549 perMap = new Map();
550 _contextSelectionCache.set(tsconfigPathsMap, perMap);
551 }
552 }
553 let longestMatchContext = null;
554 let longestMatchLength = 0;
555 // Iterate the pre-computed key list (the previous
556 // `Object.entries(allContexts)` form allocated a fresh
557 // `[key, value][]` per resolve). Defer the `allContexts[context]`
558 // lookup to after we know the context actually matches — non-matches
559 // are the common case and don't need the property access.
560 for (let i = 0; i < contextList.length; i++) {
561 const context = contextList[i];
562 if (context === requestPath) {
563 const exact = allContexts[context];
564 if (perMap !== undefined) perMap.set(requestPath, exact);
565 return exact;
566 }
567 // Cheap integer-compare gate first: a context can only beat the
568 // current longest match if its own length is strictly greater.
569 // Skipping `isSubPath` (a `startsWith` + char-code probe) when the
570 // length already disqualifies the candidate avoids the per-resolve
571 // scan over every shorter context.
572 if (
573 context.length > longestMatchLength &&
574 isSubPath(context, requestPath)
575 ) {
576 longestMatchContext = context;
577 longestMatchLength = context.length;
578 }
579 }
580 const result =
581 longestMatchContext === null ? null : allContexts[longestMatchContext];
582 if (perMap !== undefined) {
583 perMap.set(requestPath, result === null ? NULL_CONTEXT : result);
584 }
585 return result;
586 }
587
588 /**
589 * Load tsconfig from extends path
590 * @param {Resolver} resolver the resolver
591 * @param {string} configFilePath current config file path
592 * @param {string} extendedConfigValue extends value
593 * @param {Set<string>} fileDependencies the file dependencies
594 * @param {Set<string>} visitedConfigPaths config paths being loaded (for circular extends detection)
595 * @param {(err: Error | null, result?: Tsconfig) => void} callback callback
596 * @returns {void}
597 */
598 _loadTsconfigFromExtends(
599 resolver,
600 configFilePath,
601 extendedConfigValue,
602 fileDependencies,
603 visitedConfigPaths,
604 callback,
605 ) {
606 const { fileSystem } = resolver;
607 const currentDir = resolver.dirname(configFilePath);
608
609 // Substitute ${configDir} in extends path
610 extendedConfigValue = substituteConfigDir(extendedConfigValue, currentDir);
611
612 // Remember the original value before potentially appending .json
613 const originalExtendedConfigValue = extendedConfigValue;
614
615 if (
616 typeof extendedConfigValue === "string" &&
617 !extendedConfigValue.includes(".json")
618 ) {
619 extendedConfigValue += ".json";
620 }
621
622 const initialExtendedConfigPath = resolver.join(
623 currentDir,
624 extendedConfigValue,
625 );
626
627 fileSystem.stat(initialExtendedConfigPath, (existsErr) => {
628 let extendedConfigPath = initialExtendedConfigPath;
629 if (existsErr) {
630 // Handle scoped package extends like "@scope/name" (no sub-path):
631 // "@scope/name" should resolve to node_modules/@scope/name/tsconfig.json,
632 // not node_modules/@scope/name.json
633 // See: test/fixtures/tsconfig-paths/extends-pkg-entry/
634 if (
635 typeof originalExtendedConfigValue === "string" &&
636 originalExtendedConfigValue.startsWith("@") &&
637 originalExtendedConfigValue.split("/").length === 2
638 ) {
639 extendedConfigPath = resolver.join(
640 currentDir,
641 normalize(
642 `node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
643 ),
644 );
645 } else if (extendedConfigValue.includes("/")) {
646 // Handle package sub-path extends like "react/tsconfig":
647 // "react/tsconfig" resolves to node_modules/react/tsconfig.json
648 // See: test/fixtures/tsconfig-paths/extends-npm/
649 extendedConfigPath = resolver.join(
650 currentDir,
651 normalize(`node_modules/${extendedConfigValue}`),
652 );
653 } else if (
654 !originalExtendedConfigValue.startsWith(".") &&
655 !originalExtendedConfigValue.startsWith("/")
656 ) {
657 // Handle unscoped package extends like "my-base-config" (no sub-path):
658 // "my-base-config" should resolve to node_modules/my-base-config/tsconfig.json
659 extendedConfigPath = resolver.join(
660 currentDir,
661 normalize(
662 `node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
663 ),
664 );
665 }
666 }
667
668 this._loadTsconfig(
669 resolver,
670 extendedConfigPath,
671 fileDependencies,
672 visitedConfigPaths,
673 (err, config) => {
674 if (err) return callback(err);
675
676 const cfg = /** @type {Tsconfig} */ (config);
677 const compilerOptions = cfg.compilerOptions || {
678 baseUrl: undefined,
679 };
680
681 if (compilerOptions.baseUrl) {
682 const extendedConfigDir = resolver.dirname(extendedConfigPath);
683 compilerOptions.baseUrl = getAbsoluteBaseUrl(
684 extendedConfigDir,
685 resolver,
686 compilerOptions.baseUrl,
687 );
688 }
689
690 delete cfg.references;
691
692 callback(null, cfg);
693 },
694 );
695 });
696 }
697
698 /**
699 * Load referenced tsconfig projects and store in referenceMatchMap
700 * Simple implementation matching tsconfig-paths-webpack-plugin:
701 * Just load each reference and store independently
702 * @param {Resolver} resolver the resolver
703 * @param {string} context the context
704 * @param {TsconfigReference[]} references array of references
705 * @param {Set<string>} fileDependencies the file dependencies
706 * @param {{ [baseUrl: string]: TsconfigPathsData }} referenceMatchMap the map to populate
707 * @param {(err: Error | null) => void} callback callback
708 * @param {Set<string>=} visitedRefPaths visited reference config paths (for circular reference detection)
709 * @returns {void}
710 */
711 _loadTsconfigReferences(
712 resolver,
713 context,
714 references,
715 fileDependencies,
716 referenceMatchMap,
717 callback,
718 visitedRefPaths,
719 ) {
720 if (references.length === 0) return callback(null);
721
722 const visited = visitedRefPaths || new Set();
723 let pending = references.length;
724 const finishOne = () => {
725 if (--pending === 0) callback(null);
726 };
727
728 for (const ref of references) {
729 const refPath = substituteConfigDir(ref.path, context);
730 const refConfigPath = resolver.join(
731 resolver.join(context, refPath),
732 DEFAULT_CONFIG_FILE,
733 );
734
735 if (visited.has(refConfigPath)) {
736 finishOne();
737 continue;
738 }
739 visited.add(refConfigPath);
740
741 this._loadTsconfig(
742 resolver,
743 refConfigPath,
744 fileDependencies,
745 undefined,
746 (err, refConfig) => {
747 // Failures are swallowed to match tsconfig-paths-webpack-plugin:
748 // a broken reference must not abort the main project's resolution.
749 if (err) return finishOne();
750
751 const cfg = /** @type {Tsconfig} */ (refConfig);
752 if (cfg.compilerOptions && cfg.compilerOptions.paths) {
753 const refContext = resolver.dirname(refConfigPath);
754
755 referenceMatchMap[refContext] = tsconfigPathsToResolveOptions(
756 refContext,
757 cfg.compilerOptions.paths || {},
758 resolver,
759 cfg.compilerOptions.baseUrl,
760 );
761 }
762
763 if (this.references === "auto" && Array.isArray(cfg.references)) {
764 this._loadTsconfigReferences(
765 resolver,
766 resolver.dirname(refConfigPath),
767 cfg.references,
768 fileDependencies,
769 referenceMatchMap,
770 finishOne,
771 visited,
772 );
773 } else {
774 finishOne();
775 }
776 },
777 );
778 }
779 }
780
781 /**
782 * Load tsconfig.json with extends support
783 * @param {Resolver} resolver the resolver
784 * @param {string} configFilePath absolute path to tsconfig.json
785 * @param {Set<string>} fileDependencies the file dependencies
786 * @param {Set<string> | undefined} visitedConfigPaths config paths being loaded (for circular extends detection)
787 * @param {(err: Error | null, result?: Tsconfig) => void} callback callback
788 * @returns {void}
789 */
790 _loadTsconfig(
791 resolver,
792 configFilePath,
793 fileDependencies,
794 visitedConfigPaths,
795 callback,
796 ) {
797 const visited = visitedConfigPaths || new Set();
798
799 if (visited.has(configFilePath)) {
800 return callback(null, /** @type {Tsconfig} */ ({}));
801 }
802 visited.add(configFilePath);
803
804 readJson(
805 resolver.fileSystem,
806 configFilePath,
807 READ_JSON_OPTIONS,
808 (err, parsed) => {
809 if (err) return callback(/** @type {Error} */ (err));
810
811 const config = /** @type {Tsconfig} */ (parsed);
812 fileDependencies.add(configFilePath);
813
814 const extendedConfig = config.extends;
815 if (!extendedConfig) return callback(null, config);
816
817 if (!Array.isArray(extendedConfig)) {
818 this._loadTsconfigFromExtends(
819 resolver,
820 configFilePath,
821 extendedConfig,
822 fileDependencies,
823 visited,
824 (extErr, extendedTsconfig) => {
825 if (extErr) return callback(extErr);
826 callback(
827 null,
828 mergeTsconfigs(
829 /** @type {Tsconfig} */ (extendedTsconfig),
830 config,
831 ),
832 );
833 },
834 );
835 return;
836 }
837
838 /** @type {Tsconfig} */
839 let base = {};
840 let i = 0;
841 const next = () => {
842 if (i >= extendedConfig.length) {
843 return callback(null, mergeTsconfigs(base, config));
844 }
845 this._loadTsconfigFromExtends(
846 resolver,
847 configFilePath,
848 extendedConfig[i++],
849 fileDependencies,
850 visited,
851 (extErr, extendedTsconfig) => {
852 if (extErr) return callback(extErr);
853 base = mergeTsconfigs(
854 base,
855 /** @type {Tsconfig} */ (extendedTsconfig),
856 );
857 next();
858 },
859 );
860 };
861 next();
862 },
863 );
864 }
865};
Note: See TracBrowser for help on using the repository browser.