| |
| |
| |
| |
| |
| "use strict"; |
|
|
| const { aliasResolveHandler } = require("./AliasUtils"); |
| const { modulesResolveHandler } = require("./ModulesUtils"); |
| const { readJson } = require("./util/fs"); |
| const { |
| PathType: _PathType, |
| cachedDirname: dirname, |
| cachedJoin: join, |
| isSubPath, |
| normalize, |
| } = require("./util/path"); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
|
|
| const DEFAULT_CONFIG_FILE = "tsconfig.json"; |
|
|
| |
| |
| |
| |
| function getPrefixLength(pattern) { |
| const prefixLength = pattern.indexOf("*"); |
| if (prefixLength === -1) { |
| return pattern.length; |
| } |
| return prefixLength; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function sortByLongestPrefix(arr) { |
| return [...arr].sort((a, b) => getPrefixLength(b) - getPrefixLength(a)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function mergeTsconfigs(base, config) { |
| base = base || {}; |
| config = config || {}; |
|
|
| return { |
| ...base, |
| ...config, |
| compilerOptions: { |
| ... (base.compilerOptions), |
| ... (config.compilerOptions), |
| }, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function substituteConfigDir(pathValue, configDir) { |
| return pathValue.replace(/\$\{configDir\}/g, configDir); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function tsconfigPathsToResolveOptions(configDir, paths, baseUrl) { |
| |
| const absoluteBaseUrl = !baseUrl ? configDir : join(configDir, baseUrl); |
|
|
| |
| const sortedKeys = sortByLongestPrefix(Object.keys(paths)); |
| |
| const alias = []; |
| |
| const modules = []; |
|
|
| for (const pattern of sortedKeys) { |
| const mappings = paths[pattern]; |
| |
| const absolutePaths = mappings.map((mapping) => { |
| const substituted = substituteConfigDir(mapping, configDir); |
| return join(absoluteBaseUrl, substituted); |
| }); |
|
|
| if (absolutePaths.length > 0) { |
| if (pattern === "*") { |
| modules.push( |
| ...absolutePaths |
| .map((dir) => { |
| if (/[/\\]\*$/.test(dir)) { |
| return dir.replace(/[/\\]\*$/, ""); |
| } |
| return ""; |
| }) |
| .filter(Boolean), |
| ); |
| } else { |
| alias.push({ name: pattern, alias: absolutePaths }); |
| } |
| } |
| } |
|
|
| if (absoluteBaseUrl && !modules.includes(absoluteBaseUrl)) { |
| modules.push(absoluteBaseUrl); |
| } |
|
|
| return { |
| alias, |
| modules, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function getAbsoluteBaseUrl(context, baseUrl) { |
| return !baseUrl ? context : join(context, baseUrl); |
| } |
|
|
| module.exports = class TsconfigPathsPlugin { |
| |
| |
| |
| constructor(configFileOrOptions) { |
| if ( |
| typeof configFileOrOptions === "object" && |
| configFileOrOptions !== null |
| ) { |
| |
| this.configFile = configFileOrOptions.configFile || DEFAULT_CONFIG_FILE; |
| |
| if (Array.isArray(configFileOrOptions.references)) { |
| |
| this.references = configFileOrOptions.references.map((ref) => ({ |
| path: ref, |
| })); |
| } else if (configFileOrOptions.references === "auto") { |
| this.references = "auto"; |
| } else { |
| this.references = []; |
| } |
| |
| this.baseUrl = configFileOrOptions.baseUrl; |
| } else { |
| this.configFile = |
| configFileOrOptions === true |
| ? DEFAULT_CONFIG_FILE |
| : (configFileOrOptions); |
| |
| this.references = []; |
| |
| this.baseUrl = undefined; |
| } |
| } |
|
|
| |
| |
| |
| |
| apply(resolver) { |
| const aliasTarget = resolver.ensureHook("internal-resolve"); |
| const moduleTarget = resolver.ensureHook("module"); |
|
|
| resolver |
| .getHook("raw-resolve") |
| .tapAsync( |
| "TsconfigPathsPlugin", |
| async (request, resolveContext, callback) => { |
| try { |
| const tsconfigPathsMap = await this._getTsconfigPathsMap( |
| resolver, |
| request, |
| resolveContext, |
| ); |
|
|
| if (!tsconfigPathsMap) return callback(); |
|
|
| const selectedData = this._selectPathsDataForContext( |
| request.path, |
| tsconfigPathsMap, |
| ); |
|
|
| if (!selectedData) return callback(); |
|
|
| aliasResolveHandler( |
| resolver, |
| selectedData.alias, |
| aliasTarget, |
| request, |
| resolveContext, |
| callback, |
| ); |
| } catch (err) { |
| callback( (err)); |
| } |
| }, |
| ); |
|
|
| resolver |
| .getHook("raw-module") |
| .tapAsync( |
| "TsconfigPathsPlugin", |
| async (request, resolveContext, callback) => { |
| try { |
| const tsconfigPathsMap = await this._getTsconfigPathsMap( |
| resolver, |
| request, |
| resolveContext, |
| ); |
|
|
| if (!tsconfigPathsMap) return callback(); |
|
|
| const selectedData = this._selectPathsDataForContext( |
| request.path, |
| tsconfigPathsMap, |
| ); |
|
|
| if (!selectedData) return callback(); |
|
|
| modulesResolveHandler( |
| resolver, |
| selectedData.modules, |
| moduleTarget, |
| request, |
| resolveContext, |
| callback, |
| ); |
| } catch (err) { |
| callback( (err)); |
| } |
| }, |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async _getTsconfigPathsMap(resolver, request, resolveContext) { |
| if (typeof request.tsconfigPathsMap === "undefined") { |
| try { |
| const absTsconfigPath = join( |
| request.path || process.cwd(), |
| this.configFile, |
| ); |
| const result = await this._loadTsconfigPathsMap( |
| resolver.fileSystem, |
| absTsconfigPath, |
| ); |
|
|
| request.tsconfigPathsMap = result; |
| } catch (err) { |
| request.tsconfigPathsMap = null; |
| throw err; |
| } |
| } |
|
|
| if (!request.tsconfigPathsMap) { |
| return null; |
| } |
|
|
| for (const fileDependency of request.tsconfigPathsMap.fileDependencies) { |
| if (resolveContext.fileDependencies) { |
| resolveContext.fileDependencies.add(fileDependency); |
| } |
| } |
| return request.tsconfigPathsMap; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async _loadTsconfigPathsMap(fileSystem, absTsconfigPath) { |
| |
| const fileDependencies = new Set(); |
| const config = await this._loadTsconfig( |
| fileSystem, |
| absTsconfigPath, |
| fileDependencies, |
| ); |
|
|
| const compilerOptions = config.compilerOptions || {}; |
| const mainContext = dirname(absTsconfigPath); |
|
|
| const baseUrl = |
| this.baseUrl !== undefined ? this.baseUrl : compilerOptions.baseUrl; |
|
|
| const main = tsconfigPathsToResolveOptions( |
| mainContext, |
| compilerOptions.paths || {}, |
| baseUrl, |
| ); |
| |
| const refs = {}; |
|
|
| let referencesToUse = null; |
| if (this.references === "auto") { |
| referencesToUse = config.references; |
| } else if (Array.isArray(this.references)) { |
| referencesToUse = this.references; |
| } |
|
|
| if (Array.isArray(referencesToUse)) { |
| await this._loadTsconfigReferences( |
| fileSystem, |
| mainContext, |
| referencesToUse, |
| fileDependencies, |
| refs, |
| ); |
| } |
|
|
| const allContexts = |
| ({ |
| [mainContext]: main, |
| ...refs, |
| }); |
| return { main, mainContext, refs, allContexts, fileDependencies }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| _selectPathsDataForContext(requestPath, tsconfigPathsMap) { |
| const { main, allContexts } = tsconfigPathsMap; |
| if (!requestPath) { |
| return main; |
| } |
| let longestMatch = null; |
| let longestMatchLength = 0; |
| for (const [context, data] of Object.entries(allContexts)) { |
| if (context === requestPath) { |
| return data; |
| } |
| if ( |
| isSubPath(context, requestPath) && |
| context.length > longestMatchLength |
| ) { |
| longestMatch = data; |
| longestMatchLength = context.length; |
| } |
| } |
| if (longestMatch) { |
| return longestMatch; |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async _loadTsconfigFromExtends( |
| fileSystem, |
| configFilePath, |
| extendedConfigValue, |
| fileDependencies, |
| visitedConfigPaths, |
| ) { |
| const currentDir = dirname(configFilePath); |
|
|
| |
| extendedConfigValue = substituteConfigDir(extendedConfigValue, currentDir); |
|
|
| |
| const originalExtendedConfigValue = extendedConfigValue; |
|
|
| if ( |
| typeof extendedConfigValue === "string" && |
| !extendedConfigValue.includes(".json") |
| ) { |
| extendedConfigValue += ".json"; |
| } |
|
|
| let extendedConfigPath = join(currentDir, extendedConfigValue); |
|
|
| const exists = await new Promise((resolve) => { |
| fileSystem.readFile(extendedConfigPath, (err) => { |
| resolve(!err); |
| }); |
| }); |
| if (!exists) { |
| |
| |
| |
| |
| if ( |
| typeof originalExtendedConfigValue === "string" && |
| originalExtendedConfigValue.startsWith("@") && |
| originalExtendedConfigValue.split("/").length === 2 |
| ) { |
| extendedConfigPath = join( |
| currentDir, |
| normalize( |
| `node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`, |
| ), |
| ); |
| } else if (extendedConfigValue.includes("/")) { |
| |
| |
| |
| extendedConfigPath = join( |
| currentDir, |
| normalize(`node_modules/${extendedConfigValue}`), |
| ); |
| } |
| } |
|
|
| const config = await this._loadTsconfig( |
| fileSystem, |
| extendedConfigPath, |
| fileDependencies, |
| visitedConfigPaths, |
| ); |
| const compilerOptions = config.compilerOptions || { baseUrl: undefined }; |
|
|
| if (compilerOptions.baseUrl) { |
| const extendedConfigDir = dirname(extendedConfigPath); |
| compilerOptions.baseUrl = getAbsoluteBaseUrl( |
| extendedConfigDir, |
| compilerOptions.baseUrl, |
| ); |
| } |
|
|
| delete config.references; |
|
|
| return (config); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async _loadTsconfigReferences( |
| fileSystem, |
| context, |
| references, |
| fileDependencies, |
| referenceMatchMap, |
| ) { |
| await Promise.all( |
| references.map(async (ref) => { |
| const refPath = substituteConfigDir(ref.path, context); |
| const refConfigPath = join(join(context, refPath), DEFAULT_CONFIG_FILE); |
|
|
| try { |
| const refConfig = await this._loadTsconfig( |
| fileSystem, |
| refConfigPath, |
| fileDependencies, |
| ); |
|
|
| if (refConfig.compilerOptions && refConfig.compilerOptions.paths) { |
| const refContext = dirname(refConfigPath); |
|
|
| referenceMatchMap[refContext] = tsconfigPathsToResolveOptions( |
| refContext, |
| refConfig.compilerOptions.paths || {}, |
| refConfig.compilerOptions.baseUrl, |
| ); |
| } |
|
|
| if ( |
| this.references === "auto" && |
| Array.isArray(refConfig.references) |
| ) { |
| await this._loadTsconfigReferences( |
| fileSystem, |
| dirname(refConfigPath), |
| refConfig.references, |
| fileDependencies, |
| referenceMatchMap, |
| ); |
| } |
| } catch (_err) { |
| |
| } |
| }), |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async _loadTsconfig( |
| fileSystem, |
| configFilePath, |
| fileDependencies, |
| visitedConfigPaths = new Set(), |
| ) { |
| if (visitedConfigPaths.has(configFilePath)) { |
| return ({}); |
| } |
| visitedConfigPaths.add(configFilePath); |
| const config = await readJson(fileSystem, configFilePath, { |
| stripComments: true, |
| }); |
| fileDependencies.add(configFilePath); |
|
|
| let result = config; |
|
|
| const extendedConfig = config.extends; |
| if (extendedConfig) { |
| let base; |
|
|
| if (Array.isArray(extendedConfig)) { |
| base = {}; |
| for (const extendedConfigElement of extendedConfig) { |
| const extendedTsconfig = await this._loadTsconfigFromExtends( |
| fileSystem, |
| configFilePath, |
| extendedConfigElement, |
| fileDependencies, |
| visitedConfigPaths, |
| ); |
| base = mergeTsconfigs(base, extendedTsconfig); |
| } |
| } else { |
| base = await this._loadTsconfigFromExtends( |
| fileSystem, |
| configFilePath, |
| extendedConfig, |
| fileDependencies, |
| visitedConfigPaths, |
| ); |
| } |
|
|
| result = (mergeTsconfigs(base, config)); |
| } |
|
|
| return result; |
| } |
| }; |
|
|