gitea push

This commit is contained in:
2026-05-09 12:19:29 -06:00
parent 06113c95b8
commit 429461e985
1481 changed files with 74306 additions and 52475 deletions
+352 -247
View File
@@ -40,6 +40,14 @@ const { PathType: _PathType, isSubPath, normalize } = require("./util/path");
const DEFAULT_CONFIG_FILE = "tsconfig.json";
const READ_JSON_OPTIONS = { stripComments: true };
// Trailing `/*` or `\*` segment of a tsconfig `paths` mapping (e.g.
// `./src/*` → `./src`). Hoisted so we don't allocate a fresh regex per
// path entry on every tsconfig load — and so the same regex object can be
// reused for the matching `test` + `replace` pair below.
const WILDCARD_TAIL_RE = /[/\\]\*$/;
/**
* @param {string} pattern Path pattern
* @returns {number} Length of the prefix
@@ -89,6 +97,8 @@ function mergeTsconfigs(base, config) {
* @returns {string} the path with substituted template
*/
function substituteConfigDir(pathValue, configDir) {
// eslint-disable-next-line no-template-curly-in-string
if (!pathValue.includes("${configDir}")) return pathValue;
return pathValue.replace(/\$\{configDir\}/g, configDir);
}
@@ -123,16 +133,16 @@ function tsconfigPathsToResolveOptions(configDir, paths, resolver, baseUrl) {
if (absolutePaths.length > 0) {
if (pattern === "*") {
modules.push(
...absolutePaths
.map((dir) => {
if (/[/\\]\*$/.test(dir)) {
return dir.replace(/[/\\]\*$/, "");
}
return "";
})
.filter(Boolean),
);
// Pull `dir/*` entries directly into `modules` with their
// trailing wildcard stripped, skipping anything else. The
// previous `.map(...).filter(Boolean)` form allocated two
// throwaway arrays plus a spread iterator per `*` mapping.
for (let j = 0; j < absolutePaths.length; j++) {
const dir = absolutePaths[j];
if (WILDCARD_TAIL_RE.test(dir)) {
modules.push(dir.replace(WILDCARD_TAIL_RE, ""));
}
}
} else {
alias.push({ name: pattern, alias: absolutePaths });
}
@@ -160,6 +170,32 @@ function getAbsoluteBaseUrl(context, resolver, baseUrl) {
return !baseUrl ? context : resolver.join(context, baseUrl);
}
/**
* @param {TsconfigPathsData} main main paths data
* @param {string} mainContext main context
* @param {{ [baseUrl: string]: TsconfigPathsData }} refs references map
* @param {Set<string>} fileDependencies file dependencies
* @returns {TsconfigPathsMap} the tsconfig paths map
*/
function buildTsconfigPathsMap(main, mainContext, refs, fileDependencies) {
const allContexts = /** @type {{ [context: string]: TsconfigPathsData }} */ ({
[mainContext]: main,
...refs,
});
// Precompute the key list once per tsconfig load. `_selectPathsDataForContext`
// runs per resolve and otherwise would call `Object.entries(allContexts)`
// each time, allocating a fresh [key, value][] array.
const contextList = Object.keys(allContexts);
return {
main,
mainContext,
refs,
allContexts,
contextList,
fileDependencies,
};
}
module.exports = class TsconfigPathsPlugin {
/**
* @param {true | string | TsconfigOptions} configFileOrOptions tsconfig file path or options object
@@ -214,16 +250,13 @@ module.exports = class TsconfigPathsPlugin {
resolver
.getHook("raw-resolve")
.tapAsync(
"TsconfigPathsPlugin",
async (request, resolveContext, callback) => {
try {
const tsconfigPathsMap = await this._getTsconfigPathsMap(
resolver,
request,
resolveContext,
);
.tapAsync("TsconfigPathsPlugin", (request, resolveContext, callback) => {
this._getTsconfigPathsMap(
resolver,
request,
resolveContext,
(err, tsconfigPathsMap) => {
if (err) return callback(err);
if (!tsconfigPathsMap) return callback();
const selectedData = this._selectPathsDataForContext(
@@ -241,24 +274,19 @@ module.exports = class TsconfigPathsPlugin {
resolveContext,
callback,
);
} catch (err) {
callback(/** @type {Error} */ (err));
}
},
);
},
);
});
resolver
.getHook("raw-module")
.tapAsync(
"TsconfigPathsPlugin",
async (request, resolveContext, callback) => {
try {
const tsconfigPathsMap = await this._getTsconfigPathsMap(
resolver,
request,
resolveContext,
);
.tapAsync("TsconfigPathsPlugin", (request, resolveContext, callback) => {
this._getTsconfigPathsMap(
resolver,
request,
resolveContext,
(err, tsconfigPathsMap) => {
if (err) return callback(err);
if (!tsconfigPathsMap) return callback();
const selectedData = this._selectPathsDataForContext(
@@ -276,11 +304,9 @@ module.exports = class TsconfigPathsPlugin {
resolveContext,
callback,
);
} catch (err) {
callback(/** @type {Error} */ (err));
}
},
);
},
);
});
}
/**
@@ -288,43 +314,46 @@ module.exports = class TsconfigPathsPlugin {
* @param {Resolver} resolver the resolver
* @param {ResolveRequest} request the request
* @param {ResolveContext} resolveContext the resolve context
* @returns {Promise<TsconfigPathsMap | null>} the tsconfig paths map or null
* @param {(err: Error | null, result?: TsconfigPathsMap | null) => void} callback the callback
* @returns {void}
*/
async _getTsconfigPathsMap(resolver, request, resolveContext) {
if (typeof request.tsconfigPathsMap === "undefined") {
try {
const absTsconfigPath = resolver.join(
request.path || process.cwd(),
this.configFile,
);
const result = await this._loadTsconfigPathsMap(
resolver,
absTsconfigPath,
);
_getTsconfigPathsMap(resolver, request, resolveContext, callback) {
if (typeof request.tsconfigPathsMap !== "undefined") {
const cached = request.tsconfigPathsMap;
if (!cached) return callback(null, null);
if (resolveContext.fileDependencies) {
for (const fileDependency of cached.fileDependencies) {
resolveContext.fileDependencies.add(fileDependency);
}
}
return callback(null, cached);
}
request.tsconfigPathsMap = result;
} catch (err) {
const absTsconfigPath = resolver.join(
request.path || process.cwd(),
this.configFile,
);
this._loadTsconfigPathsMap(resolver, absTsconfigPath, (err, result) => {
if (err) {
request.tsconfigPathsMap = null;
if (
this.isAutoConfigFile &&
/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT"
) {
return null;
return callback(null, null);
}
throw err;
return callback(err);
}
}
if (!request.tsconfigPathsMap) {
return null;
}
for (const fileDependency of request.tsconfigPathsMap.fileDependencies) {
const map = /** @type {TsconfigPathsMap} */ (result);
request.tsconfigPathsMap = map;
if (resolveContext.fileDependencies) {
resolveContext.fileDependencies.add(fileDependency);
for (const fileDependency of map.fileDependencies) {
resolveContext.fileDependencies.add(fileDependency);
}
}
}
return request.tsconfigPathsMap;
callback(null, map);
});
}
/**
@@ -332,66 +361,67 @@ module.exports = class TsconfigPathsPlugin {
* Includes main project paths and all referenced projects
* @param {Resolver} resolver the resolver
* @param {string} absTsconfigPath absolute path to tsconfig.json
* @returns {Promise<TsconfigPathsMap>} the complete tsconfig paths map
* @param {(err: Error | null, result?: TsconfigPathsMap) => void} callback the callback
* @returns {void}
*/
async _loadTsconfigPathsMap(resolver, absTsconfigPath) {
_loadTsconfigPathsMap(resolver, absTsconfigPath, callback) {
/** @type {Set<string>} */
const fileDependencies = new Set();
const config = await this._loadTsconfig(
this._loadTsconfig(
resolver,
absTsconfigPath,
fileDependencies,
undefined,
(err, config) => {
if (err) return callback(err);
const cfg = /** @type {Tsconfig} */ (config);
const compilerOptions = cfg.compilerOptions || {};
const mainContext = resolver.dirname(absTsconfigPath);
const baseUrl =
this.baseUrl !== undefined ? this.baseUrl : compilerOptions.baseUrl;
const main = tsconfigPathsToResolveOptions(
mainContext,
compilerOptions.paths || {},
resolver,
baseUrl,
);
/** @type {{ [baseUrl: string]: TsconfigPathsData }} */
const refs = {};
let referencesToUse = null;
if (this.references === "auto") {
referencesToUse = cfg.references;
} else if (Array.isArray(this.references)) {
referencesToUse = this.references;
}
if (!Array.isArray(referencesToUse)) {
return callback(
null,
buildTsconfigPathsMap(main, mainContext, refs, fileDependencies),
);
}
this._loadTsconfigReferences(
resolver,
mainContext,
referencesToUse,
fileDependencies,
refs,
(refErr) => {
if (refErr) return callback(refErr);
callback(
null,
buildTsconfigPathsMap(main, mainContext, refs, fileDependencies),
);
},
);
},
);
const compilerOptions = config.compilerOptions || {};
const mainContext = resolver.dirname(absTsconfigPath);
const baseUrl =
this.baseUrl !== undefined ? this.baseUrl : compilerOptions.baseUrl;
const main = tsconfigPathsToResolveOptions(
mainContext,
compilerOptions.paths || {},
resolver,
baseUrl,
);
/** @type {{ [baseUrl: string]: TsconfigPathsData }} */
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(
resolver,
mainContext,
referencesToUse,
fileDependencies,
refs,
);
}
const allContexts =
/** @type {{ [context: string]: TsconfigPathsData }} */ ({
[mainContext]: main,
...refs,
});
// Precompute the key list once per tsconfig load. `_selectPathsDataForContext`
// runs per resolve and otherwise would call `Object.entries(allContexts)`
// each time, allocating a fresh [key, value][] array.
const contextList = Object.keys(allContexts);
return {
main,
mainContext,
refs,
allContexts,
contextList,
fileDependencies,
};
}
/**
@@ -406,29 +436,34 @@ module.exports = class TsconfigPathsPlugin {
if (!requestPath) {
return main;
}
let longestMatch = null;
let longestMatchContext = null;
let longestMatchLength = 0;
// Iterate over the pre-computed key list + indexed access into
// `allContexts` — the previous `Object.entries(allContexts)` form
// allocated a fresh `[key, value][]` array on every resolve.
// Iterate the pre-computed key list (the previous
// `Object.entries(allContexts)` form allocated a fresh
// `[key, value][]` per resolve). Defer the `allContexts[context]`
// lookup to after we know the context actually matches — non-matches
// are the common case and don't need the property access.
for (let i = 0; i < contextList.length; i++) {
const context = contextList[i];
const data = allContexts[context];
if (context === requestPath) {
return data;
return allContexts[context];
}
// Cheap integer-compare gate first: a context can only beat the
// current longest match if its own length is strictly greater.
// Skipping `isSubPath` (a `startsWith` + char-code probe) when the
// length already disqualifies the candidate avoids the per-resolve
// scan over every shorter context.
if (
isSubPath(context, requestPath) &&
context.length > longestMatchLength
context.length > longestMatchLength &&
isSubPath(context, requestPath)
) {
longestMatch = data;
longestMatchContext = context;
longestMatchLength = context.length;
}
}
if (longestMatch) {
return longestMatch;
}
return null;
return longestMatchContext === null
? null
: allContexts[longestMatchContext];
}
/**
@@ -438,14 +473,16 @@ module.exports = class TsconfigPathsPlugin {
* @param {string} extendedConfigValue extends value
* @param {Set<string>} fileDependencies the file dependencies
* @param {Set<string>} visitedConfigPaths config paths being loaded (for circular extends detection)
* @returns {Promise<Tsconfig>} the extended tsconfig
* @param {(err: Error | null, result?: Tsconfig) => void} callback callback
* @returns {void}
*/
async _loadTsconfigFromExtends(
_loadTsconfigFromExtends(
resolver,
configFilePath,
extendedConfigValue,
fileDependencies,
visitedConfigPaths,
callback,
) {
const { fileSystem } = resolver;
const currentDir = resolver.dirname(configFilePath);
@@ -463,60 +500,80 @@ module.exports = class TsconfigPathsPlugin {
extendedConfigValue += ".json";
}
let extendedConfigPath = resolver.join(currentDir, extendedConfigValue);
const exists = await new Promise((resolve) => {
fileSystem.readFile(extendedConfigPath, (err) => {
resolve(!err);
});
});
if (!exists) {
// Handle scoped package extends like "@scope/name" (no sub-path):
// "@scope/name" should resolve to node_modules/@scope/name/tsconfig.json,
// not node_modules/@scope/name.json
// See: test/fixtures/tsconfig-paths/extends-pkg-entry/
if (
typeof originalExtendedConfigValue === "string" &&
originalExtendedConfigValue.startsWith("@") &&
originalExtendedConfigValue.split("/").length === 2
) {
extendedConfigPath = resolver.join(
currentDir,
normalize(
`node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
),
);
} else if (extendedConfigValue.includes("/")) {
// Handle package sub-path extends like "react/tsconfig":
// "react/tsconfig" resolves to node_modules/react/tsconfig.json
// See: test/fixtures/tsconfig-paths/extends-npm/
extendedConfigPath = resolver.join(
currentDir,
normalize(`node_modules/${extendedConfigValue}`),
);
}
}
const config = await this._loadTsconfig(
resolver,
extendedConfigPath,
fileDependencies,
visitedConfigPaths,
const initialExtendedConfigPath = resolver.join(
currentDir,
extendedConfigValue,
);
const compilerOptions = config.compilerOptions || { baseUrl: undefined };
if (compilerOptions.baseUrl) {
const extendedConfigDir = resolver.dirname(extendedConfigPath);
compilerOptions.baseUrl = getAbsoluteBaseUrl(
extendedConfigDir,
fileSystem.stat(initialExtendedConfigPath, (existsErr) => {
let extendedConfigPath = initialExtendedConfigPath;
if (existsErr) {
// Handle scoped package extends like "@scope/name" (no sub-path):
// "@scope/name" should resolve to node_modules/@scope/name/tsconfig.json,
// not node_modules/@scope/name.json
// See: test/fixtures/tsconfig-paths/extends-pkg-entry/
if (
typeof originalExtendedConfigValue === "string" &&
originalExtendedConfigValue.startsWith("@") &&
originalExtendedConfigValue.split("/").length === 2
) {
extendedConfigPath = resolver.join(
currentDir,
normalize(
`node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
),
);
} else if (extendedConfigValue.includes("/")) {
// Handle package sub-path extends like "react/tsconfig":
// "react/tsconfig" resolves to node_modules/react/tsconfig.json
// See: test/fixtures/tsconfig-paths/extends-npm/
extendedConfigPath = resolver.join(
currentDir,
normalize(`node_modules/${extendedConfigValue}`),
);
} else if (
!originalExtendedConfigValue.startsWith(".") &&
!originalExtendedConfigValue.startsWith("/")
) {
// Handle unscoped package extends like "my-base-config" (no sub-path):
// "my-base-config" should resolve to node_modules/my-base-config/tsconfig.json
extendedConfigPath = resolver.join(
currentDir,
normalize(
`node_modules/${originalExtendedConfigValue}/${DEFAULT_CONFIG_FILE}`,
),
);
}
}
this._loadTsconfig(
resolver,
compilerOptions.baseUrl,
extendedConfigPath,
fileDependencies,
visitedConfigPaths,
(err, config) => {
if (err) return callback(err);
const cfg = /** @type {Tsconfig} */ (config);
const compilerOptions = cfg.compilerOptions || {
baseUrl: undefined,
};
if (compilerOptions.baseUrl) {
const extendedConfigDir = resolver.dirname(extendedConfigPath);
compilerOptions.baseUrl = getAbsoluteBaseUrl(
extendedConfigDir,
resolver,
compilerOptions.baseUrl,
);
}
delete cfg.references;
callback(null, cfg);
},
);
}
delete config.references;
return /** @type {Tsconfig} */ (config);
});
}
/**
@@ -528,58 +585,78 @@ module.exports = class TsconfigPathsPlugin {
* @param {TsconfigReference[]} references array of references
* @param {Set<string>} fileDependencies the file dependencies
* @param {{ [baseUrl: string]: TsconfigPathsData }} referenceMatchMap the map to populate
* @returns {Promise<void>}
* @param {(err: Error | null) => void} callback callback
* @param {Set<string>=} visitedRefPaths visited reference config paths (for circular reference detection)
* @returns {void}
*/
async _loadTsconfigReferences(
_loadTsconfigReferences(
resolver,
context,
references,
fileDependencies,
referenceMatchMap,
callback,
visitedRefPaths,
) {
await Promise.all(
references.map(async (ref) => {
const refPath = substituteConfigDir(ref.path, context);
const refConfigPath = resolver.join(
resolver.join(context, refPath),
DEFAULT_CONFIG_FILE,
);
if (references.length === 0) return callback(null);
try {
const refConfig = await this._loadTsconfig(
resolver,
refConfigPath,
fileDependencies,
);
const visited = visitedRefPaths || new Set();
let pending = references.length;
const finishOne = () => {
if (--pending === 0) callback(null);
};
if (refConfig.compilerOptions && refConfig.compilerOptions.paths) {
for (const ref of references) {
const refPath = substituteConfigDir(ref.path, context);
const refConfigPath = resolver.join(
resolver.join(context, refPath),
DEFAULT_CONFIG_FILE,
);
if (visited.has(refConfigPath)) {
finishOne();
continue;
}
visited.add(refConfigPath);
this._loadTsconfig(
resolver,
refConfigPath,
fileDependencies,
undefined,
(err, refConfig) => {
// Failures are swallowed to match tsconfig-paths-webpack-plugin:
// a broken reference must not abort the main project's resolution.
if (err) return finishOne();
const cfg = /** @type {Tsconfig} */ (refConfig);
if (cfg.compilerOptions && cfg.compilerOptions.paths) {
const refContext = resolver.dirname(refConfigPath);
referenceMatchMap[refContext] = tsconfigPathsToResolveOptions(
refContext,
refConfig.compilerOptions.paths || {},
cfg.compilerOptions.paths || {},
resolver,
refConfig.compilerOptions.baseUrl,
cfg.compilerOptions.baseUrl,
);
}
if (
this.references === "auto" &&
Array.isArray(refConfig.references)
) {
await this._loadTsconfigReferences(
if (this.references === "auto" && Array.isArray(cfg.references)) {
this._loadTsconfigReferences(
resolver,
resolver.dirname(refConfigPath),
refConfig.references,
cfg.references,
fileDependencies,
referenceMatchMap,
finishOne,
visited,
);
} else {
finishOne();
}
} catch (_err) {
// continue
}
}),
);
},
);
}
}
/**
@@ -587,55 +664,83 @@ module.exports = class TsconfigPathsPlugin {
* @param {Resolver} resolver the resolver
* @param {string} configFilePath absolute path to tsconfig.json
* @param {Set<string>} fileDependencies the file dependencies
* @param {Set<string>=} visitedConfigPaths config paths being loaded (for circular extends detection)
* @returns {Promise<Tsconfig>} the merged tsconfig
* @param {Set<string> | undefined} visitedConfigPaths config paths being loaded (for circular extends detection)
* @param {(err: Error | null, result?: Tsconfig) => void} callback callback
* @returns {void}
*/
async _loadTsconfig(
_loadTsconfig(
resolver,
configFilePath,
fileDependencies,
visitedConfigPaths = new Set(),
visitedConfigPaths,
callback,
) {
if (visitedConfigPaths.has(configFilePath)) {
return /** @type {Tsconfig} */ ({});
const visited = visitedConfigPaths || new Set();
if (visited.has(configFilePath)) {
return callback(null, /** @type {Tsconfig} */ ({}));
}
visitedConfigPaths.add(configFilePath);
const config = await readJson(resolver.fileSystem, configFilePath, {
stripComments: true,
});
fileDependencies.add(configFilePath);
visited.add(configFilePath);
let result = config;
readJson(
resolver.fileSystem,
configFilePath,
READ_JSON_OPTIONS,
(err, parsed) => {
if (err) return callback(/** @type {Error} */ (err));
const extendedConfig = config.extends;
if (extendedConfig) {
let base;
const config = /** @type {Tsconfig} */ (parsed);
fileDependencies.add(configFilePath);
if (Array.isArray(extendedConfig)) {
base = {};
for (const extendedConfigElement of extendedConfig) {
const extendedTsconfig = await this._loadTsconfigFromExtends(
const extendedConfig = config.extends;
if (!extendedConfig) return callback(null, config);
if (!Array.isArray(extendedConfig)) {
this._loadTsconfigFromExtends(
resolver,
configFilePath,
extendedConfigElement,
extendedConfig,
fileDependencies,
visitedConfigPaths,
visited,
(extErr, extendedTsconfig) => {
if (extErr) return callback(extErr);
callback(
null,
mergeTsconfigs(
/** @type {Tsconfig} */ (extendedTsconfig),
config,
),
);
},
);
base = mergeTsconfigs(base, extendedTsconfig);
return;
}
} else {
base = await this._loadTsconfigFromExtends(
resolver,
configFilePath,
extendedConfig,
fileDependencies,
visitedConfigPaths,
);
}
result = /** @type {Tsconfig} */ (mergeTsconfigs(base, config));
}
return result;
/** @type {Tsconfig} */
let base = {};
let i = 0;
const next = () => {
if (i >= extendedConfig.length) {
return callback(null, mergeTsconfigs(base, config));
}
this._loadTsconfigFromExtends(
resolver,
configFilePath,
extendedConfig[i++],
fileDependencies,
visited,
(extErr, extendedTsconfig) => {
if (extErr) return callback(extErr);
base = mergeTsconfigs(
base,
/** @type {Tsconfig} */ (extendedTsconfig),
);
next();
},
);
};
next();
},
);
}
};