gitea push
This commit is contained in:
+6
-11
@@ -55,18 +55,13 @@ module.exports = class AliasFieldPlugin {
|
||||
descriptionFileData,
|
||||
this.field,
|
||||
);
|
||||
if (raw === null || typeof raw !== "object") {
|
||||
this._fieldDataCache.set(descriptionFileData, NO_FIELD_OBJECT);
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(
|
||||
`Field '${this.field}' doesn't contain a valid alias configuration`,
|
||||
);
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
fieldData = /** @type {{ [k: string]: JsonPrimitive }} */ (raw);
|
||||
fieldData =
|
||||
raw === null || typeof raw !== "object"
|
||||
? NO_FIELD_OBJECT
|
||||
: /** @type {{ [k: string]: JsonPrimitive }} */ (raw);
|
||||
this._fieldDataCache.set(descriptionFileData, fieldData);
|
||||
} else if (fieldData === NO_FIELD_OBJECT) {
|
||||
}
|
||||
if (fieldData === NO_FIELD_OBJECT) {
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(
|
||||
`Field '${this.field}' doesn't contain a valid alias configuration`,
|
||||
|
||||
+6
-9
@@ -138,18 +138,15 @@ function aliasResolveHandler(
|
||||
// windows request with native backslashes
|
||||
// (e.g. `C:\\abs\\foo\\baz` against `name: "C:\\abs\\foo"`)
|
||||
// otherwise fails `startsWith("C:\\abs\\foo/")` and is
|
||||
// silently skipped. The `!hasRequestString` branch already
|
||||
// uses `absolutePath`; mirroring it here closes the gap
|
||||
// without changing any existing matches.
|
||||
// silently skipped. Mirroring the `absolutePath` check in
|
||||
// both branches closes the gap without changing any
|
||||
// existing matches.
|
||||
const { absolutePath } = item;
|
||||
const matchRequest =
|
||||
innerRequest === item.name ||
|
||||
(!item.onlyModule &&
|
||||
(hasRequestString
|
||||
? innerRequest.startsWith(item.nameWithSlash) ||
|
||||
(item.absolutePath !== null &&
|
||||
innerRequest.startsWith(item.absolutePath))
|
||||
: item.absolutePath !== null &&
|
||||
innerRequest.startsWith(item.absolutePath)));
|
||||
((hasRequestString && innerRequest.startsWith(item.nameWithSlash)) ||
|
||||
(absolutePath !== null && innerRequest.startsWith(absolutePath))));
|
||||
|
||||
const matchWildcard = !item.onlyModule && item.wildcardPrefix !== null;
|
||||
|
||||
|
||||
+3
-6
@@ -11,6 +11,8 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
|
||||
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
|
||||
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
|
||||
|
||||
const BACKSLASH_G = /\\/g;
|
||||
|
||||
module.exports = class DescriptionFilePlugin {
|
||||
/**
|
||||
* @param {string | ResolveStepHook} source source
|
||||
@@ -66,15 +68,10 @@ module.exports = class DescriptionFilePlugin {
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
// Normalize the relative path to use POSIX separators. On
|
||||
// POSIX most paths never contain a backslash, so skip the
|
||||
// regex replace when possible — `includes` is one pass,
|
||||
// `replace` with a global regex allocates a lastIndex
|
||||
// state machine.
|
||||
const rawRelative = path.slice(result.directory.length);
|
||||
const relativePath = `.${
|
||||
rawRelative.includes("\\")
|
||||
? rawRelative.replace(/\\/g, "/")
|
||||
? rawRelative.replace(BACKSLASH_G, "/")
|
||||
: rawRelative
|
||||
}`;
|
||||
/** @type {ResolveRequest} */
|
||||
|
||||
+112
-102
@@ -83,112 +83,122 @@ function loadDescriptionFile(
|
||||
resolveContext,
|
||||
callback,
|
||||
) {
|
||||
(function findDescriptionFile() {
|
||||
// Hoist the per-filename iterator and the per-level done callback out
|
||||
// of `findDescriptionFile`. They both close over `directory`, which we
|
||||
// reassign as we walk up the tree, so the same closures keep working
|
||||
// across every level — the previous implementation re-allocated both
|
||||
// arrows on every recursion step, which adds up on deep walks (multiple
|
||||
// `DescriptionFilePlugin` taps per resolve, each climbing several
|
||||
// directories looking for `package.json`).
|
||||
/**
|
||||
* @param {string} filename filename
|
||||
* @param {(err?: null | Error, result?: null | Result) => void} iterCallback callback
|
||||
* @returns {void}
|
||||
*/
|
||||
const iterFilename = (filename, iterCallback) => {
|
||||
const descriptionFilePath = resolver.join(directory, filename);
|
||||
|
||||
/**
|
||||
* @param {(null | Error)=} err error
|
||||
* @param {JsonObject=} resolvedContent content
|
||||
* @returns {void}
|
||||
*/
|
||||
function onJson(err, resolvedContent) {
|
||||
if (err) {
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(
|
||||
`${descriptionFilePath} (directory description file): ${err}`,
|
||||
);
|
||||
} else {
|
||||
err.message = `${descriptionFilePath} (directory description file): ${err}`;
|
||||
}
|
||||
return iterCallback(err);
|
||||
}
|
||||
iterCallback(null, {
|
||||
content: /** @type {JsonObject} */ (resolvedContent),
|
||||
directory,
|
||||
path: descriptionFilePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolver.fileSystem.readJson) {
|
||||
resolver.fileSystem.readJson(descriptionFilePath, (err, content) => {
|
||||
if (err) {
|
||||
if (
|
||||
typeof (/** @type {NodeJS.ErrnoException} */ (err).code) !==
|
||||
"undefined"
|
||||
) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(descriptionFilePath);
|
||||
}
|
||||
return iterCallback();
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
resolveContext.fileDependencies.add(descriptionFilePath);
|
||||
}
|
||||
return onJson(err);
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
resolveContext.fileDependencies.add(descriptionFilePath);
|
||||
}
|
||||
onJson(null, content);
|
||||
});
|
||||
} else {
|
||||
resolver.fileSystem.readFile(descriptionFilePath, (err, content) => {
|
||||
if (err) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(descriptionFilePath);
|
||||
}
|
||||
return iterCallback();
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
resolveContext.fileDependencies.add(descriptionFilePath);
|
||||
}
|
||||
|
||||
/** @type {JsonObject | undefined} */
|
||||
let json;
|
||||
|
||||
if (content) {
|
||||
try {
|
||||
json = JSON.parse(content.toString());
|
||||
} catch (/** @type {unknown} */ err_) {
|
||||
return onJson(/** @type {Error} */ (err_));
|
||||
}
|
||||
} else {
|
||||
return onJson(new Error("No content in file"));
|
||||
}
|
||||
|
||||
onJson(null, json);
|
||||
});
|
||||
}
|
||||
};
|
||||
// Forward-declared so the helpers below can reference each other
|
||||
// without falling foul of `no-use-before-define`.
|
||||
/** @type {() => void} */
|
||||
let findDescriptionFile;
|
||||
/**
|
||||
* @param {(null | Error)=} err error
|
||||
* @param {(null | Result)=} result result
|
||||
* @returns {void}
|
||||
*/
|
||||
const onLevelDone = (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result) return callback(null, result);
|
||||
const dir = cdUp(directory);
|
||||
if (!dir) {
|
||||
return callback();
|
||||
}
|
||||
directory = dir;
|
||||
return findDescriptionFile();
|
||||
};
|
||||
findDescriptionFile = () => {
|
||||
if (oldInfo && oldInfo.directory === directory) {
|
||||
// We already have info for this directory and can reuse it
|
||||
return callback(null, oldInfo);
|
||||
}
|
||||
forEachBail(
|
||||
filenames,
|
||||
/**
|
||||
* @param {string} filename filename
|
||||
* @param {(err?: null | Error, result?: null | Result) => void} callback callback
|
||||
* @returns {void}
|
||||
*/
|
||||
(filename, callback) => {
|
||||
const descriptionFilePath = resolver.join(directory, filename);
|
||||
|
||||
/**
|
||||
* @param {(null | Error)=} err error
|
||||
* @param {JsonObject=} resolvedContent content
|
||||
* @returns {void}
|
||||
*/
|
||||
function onJson(err, resolvedContent) {
|
||||
if (err) {
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(
|
||||
`${descriptionFilePath} (directory description file): ${err}`,
|
||||
);
|
||||
} else {
|
||||
err.message = `${descriptionFilePath} (directory description file): ${err}`;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, {
|
||||
content: /** @type {JsonObject} */ (resolvedContent),
|
||||
directory,
|
||||
path: descriptionFilePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolver.fileSystem.readJson) {
|
||||
resolver.fileSystem.readJson(descriptionFilePath, (err, content) => {
|
||||
if (err) {
|
||||
if (
|
||||
typeof (/** @type {NodeJS.ErrnoException} */ (err).code) !==
|
||||
"undefined"
|
||||
) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(descriptionFilePath);
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
resolveContext.fileDependencies.add(descriptionFilePath);
|
||||
}
|
||||
return onJson(err);
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
resolveContext.fileDependencies.add(descriptionFilePath);
|
||||
}
|
||||
onJson(null, content);
|
||||
});
|
||||
} else {
|
||||
resolver.fileSystem.readFile(descriptionFilePath, (err, content) => {
|
||||
if (err) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(descriptionFilePath);
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
resolveContext.fileDependencies.add(descriptionFilePath);
|
||||
}
|
||||
|
||||
/** @type {JsonObject | undefined} */
|
||||
let json;
|
||||
|
||||
if (content) {
|
||||
try {
|
||||
json = JSON.parse(content.toString());
|
||||
} catch (/** @type {unknown} */ err_) {
|
||||
return onJson(/** @type {Error} */ (err_));
|
||||
}
|
||||
} else {
|
||||
return onJson(new Error("No content in file"));
|
||||
}
|
||||
|
||||
onJson(null, json);
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {(null | Error)=} err error
|
||||
* @param {(null | Result)=} result result
|
||||
* @returns {void}
|
||||
*/
|
||||
(err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result) return callback(null, result);
|
||||
const dir = cdUp(directory);
|
||||
if (!dir) {
|
||||
return callback();
|
||||
}
|
||||
directory = dir;
|
||||
return findDescriptionFile();
|
||||
},
|
||||
);
|
||||
})();
|
||||
forEachBail(filenames, iterFilename, onLevelDone);
|
||||
};
|
||||
findDescriptionFile();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+11
-11
@@ -33,21 +33,21 @@ module.exports = class DirectoryExistsPlugin {
|
||||
const directory = request.path;
|
||||
if (!directory) return callback();
|
||||
fs.stat(directory, (err, stat) => {
|
||||
if (err || !stat) {
|
||||
// Combine the two miss branches: a stat failure and a
|
||||
// "not a directory" result share the same handling — record
|
||||
// the path on `missingDependencies`, log the right reason,
|
||||
// then bail. The error-message ternary picks the wording
|
||||
// that matched the failing condition.
|
||||
if (err || !stat || !stat.isDirectory()) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(directory);
|
||||
}
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(`${directory} doesn't exist`);
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(directory);
|
||||
}
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(`${directory} is not a directory`);
|
||||
resolveContext.log(
|
||||
err || !stat
|
||||
? `${directory} doesn't exist`
|
||||
: `${directory} is not a directory`,
|
||||
);
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
|
||||
+2
-2
@@ -151,7 +151,7 @@ module.exports = class ExportsFieldPlugin {
|
||||
|
||||
const [relativePath, query, fragment] = parsedIdentifier;
|
||||
|
||||
if (relativePath.length === 0 || !relativePath.startsWith("./")) {
|
||||
if (!relativePath.startsWith("./")) {
|
||||
if (paths.length === i) {
|
||||
return callback(
|
||||
new Error(
|
||||
@@ -165,7 +165,7 @@ module.exports = class ExportsFieldPlugin {
|
||||
|
||||
const withoutDotSlash = relativePath.slice(2);
|
||||
if (
|
||||
invalidSegmentRegEx.exec(withoutDotSlash) !== null &&
|
||||
invalidSegmentRegEx.test(withoutDotSlash) &&
|
||||
deprecatedInvalidSegmentRegEx.test(withoutDotSlash)
|
||||
) {
|
||||
if (paths.length === i) {
|
||||
|
||||
+12
-8
@@ -31,18 +31,22 @@ module.exports = class FileExistsPlugin {
|
||||
const file = request.path;
|
||||
if (!file) return callback();
|
||||
fs.stat(file, (err, stat) => {
|
||||
if (err || !stat) {
|
||||
// Combine the two miss branches: a stat failure and a
|
||||
// "not a file" result share the same handling — record the
|
||||
// path on `missingDependencies`, log the right reason, then
|
||||
// bail. The error-message ternary picks the wording that
|
||||
// matched the failing condition.
|
||||
if (err || !stat || !stat.isFile()) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(file);
|
||||
}
|
||||
if (resolveContext.log) resolveContext.log(`${file} doesn't exist`);
|
||||
return callback();
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
if (resolveContext.missingDependencies) {
|
||||
resolveContext.missingDependencies.add(file);
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log(
|
||||
err || !stat
|
||||
? `${file} doesn't exist`
|
||||
: `${file} is not a file`,
|
||||
);
|
||||
}
|
||||
if (resolveContext.log) resolveContext.log(`${file} is not a file`);
|
||||
return callback();
|
||||
}
|
||||
if (resolveContext.fileDependencies) {
|
||||
|
||||
+8
-9
@@ -9,10 +9,7 @@ const DescriptionFileUtils = require("./DescriptionFileUtils");
|
||||
const forEachBail = require("./forEachBail");
|
||||
const { processImportsField } = require("./util/entrypoints");
|
||||
const { parseIdentifier } = require("./util/identifier");
|
||||
const {
|
||||
deprecatedInvalidSegmentRegEx,
|
||||
invalidSegmentRegEx,
|
||||
} = require("./util/path");
|
||||
const { invalidSegmentRegEx } = require("./util/path");
|
||||
|
||||
/** @typedef {import("./Resolver")} Resolver */
|
||||
/** @typedef {import("./Resolver").JsonObject} JsonObject */
|
||||
@@ -67,8 +64,13 @@ module.exports = class ImportsFieldPlugin {
|
||||
}
|
||||
|
||||
const { descriptionFileData } = request;
|
||||
// Skip the concat when there's nothing to append — the common
|
||||
// case has empty query/fragment, so this avoids an allocation
|
||||
// per resolve. Mirrors the pattern in ExportsFieldPlugin.
|
||||
const remainingRequest =
|
||||
request.request + request.query + request.fragment;
|
||||
request.query || request.fragment
|
||||
? request.request + request.query + request.fragment
|
||||
: request.request;
|
||||
|
||||
/** @type {string[]} */
|
||||
let paths;
|
||||
@@ -152,10 +154,7 @@ module.exports = class ImportsFieldPlugin {
|
||||
// should be relative
|
||||
case dotCode: {
|
||||
const withoutDotSlash = path_.slice(2);
|
||||
if (
|
||||
invalidSegmentRegEx.exec(withoutDotSlash) !== null &&
|
||||
deprecatedInvalidSegmentRegEx.test(withoutDotSlash) !== null
|
||||
) {
|
||||
if (invalidSegmentRegEx.test(withoutDotSlash)) {
|
||||
if (paths.length === i) {
|
||||
return callback(
|
||||
new Error(
|
||||
|
||||
+64
-17
@@ -89,6 +89,8 @@ const _withResolvers =
|
||||
/** @type {WeakMap<FileSystem, PathCacheFunctions>} */
|
||||
const _pathCacheByFs = new WeakMap();
|
||||
|
||||
const HASH_ESCAPE_RE = /#/g;
|
||||
|
||||
/** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
|
||||
|
||||
/**
|
||||
@@ -450,6 +452,16 @@ class StackEntry {
|
||||
/**
|
||||
* Walk the linked list looking for an entry with the same request shape.
|
||||
* Set-compatible: callers that used `stack.has(entry)` keep working.
|
||||
*
|
||||
* NOTE: kept monomorphic on purpose. An earlier draft accepted a string
|
||||
* query too (so pre-5.21 plugins keeping their own `Set<string>` of
|
||||
* seen entries could probe the live stack with the formatted form),
|
||||
* but adding the second shape regressed `doResolve`'s heap profile by
|
||||
* ~1 MiB / 200 resolves on stack-churn — V8 keeps a polymorphic
|
||||
* call-site state for `parent.has(stackEntry)` once `has` has two
|
||||
* argument shapes. Plugins that need string membership can reach for
|
||||
* `[...stack].find(e => e.includes(formattedString))` via the
|
||||
* `String`-method proxies on `StackEntry` instead.
|
||||
* @param {StackEntry} query entry to look for
|
||||
* @returns {boolean} whether the stack already contains an equivalent entry
|
||||
*/
|
||||
@@ -493,7 +505,15 @@ class StackEntry {
|
||||
* `Set` that was populated in insertion order would iterate. Pre-seeded
|
||||
* legacy `Set<string>` entries come first so error-message output stays
|
||||
* ordered oldest-to-newest.
|
||||
* @returns {IterableIterator<StackEntry | string>} iterator
|
||||
*
|
||||
* Yields each entry as its formatted `toString()` form. Plugins written
|
||||
* against the pre-5.21 `Set<string>` shape — e.g.
|
||||
* `[...resolveContext.stack].find(a => a.includes("module:"))` — keep
|
||||
* working unchanged because each yielded value is a plain string with
|
||||
* all of `String.prototype` available natively. Resolves that never
|
||||
* iterate the stack pay nothing; iteration costs one `toString()`
|
||||
* allocation per stack frame.
|
||||
* @returns {IterableIterator<string>} iterator
|
||||
*/
|
||||
*[Symbol.iterator]() {
|
||||
if (this.preSeeded !== undefined) {
|
||||
@@ -507,12 +527,14 @@ class StackEntry {
|
||||
entries.push(node);
|
||||
node = node.parent;
|
||||
}
|
||||
for (let i = entries.length - 1; i >= 0; i--) yield entries[i];
|
||||
for (let i = entries.length - 1; i >= 0; i--) yield entries[i].toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable form used in recursion error messages and logs.
|
||||
* Matches the historical string format so existing log parsers stay valid.
|
||||
* Human-readable form used in recursion error messages, logs, and the
|
||||
* iterator above. Not memoized: caching would require an extra slot on
|
||||
* every `StackEntry`, which costs heap even on resolves that never look
|
||||
* at the formatted form.
|
||||
* @returns {string} formatted entry
|
||||
*/
|
||||
toString() {
|
||||
@@ -887,16 +909,27 @@ class Resolver {
|
||||
* @param {ResolveRequest} result result
|
||||
* @returns {void}
|
||||
*/
|
||||
const finishResolved = (result) =>
|
||||
callback(
|
||||
const finishResolved = (result) => {
|
||||
const resultPath = result.path;
|
||||
if (resultPath === false) return callback(null, false, result);
|
||||
const escapedPath = resultPath.includes("#")
|
||||
? resultPath.replace(HASH_ESCAPE_RE, "\0#")
|
||||
: resultPath;
|
||||
const resultQuery = result.query;
|
||||
let escapedQuery;
|
||||
if (resultQuery) {
|
||||
escapedQuery = resultQuery.includes("#")
|
||||
? resultQuery.replace(HASH_ESCAPE_RE, "\0#")
|
||||
: resultQuery;
|
||||
} else {
|
||||
escapedQuery = "";
|
||||
}
|
||||
return callback(
|
||||
null,
|
||||
result.path === false
|
||||
? false
|
||||
: `${result.path.replace(/#/g, "\0#")}${
|
||||
result.query ? result.query.replace(/#/g, "\0#") : ""
|
||||
}${result.fragment || ""}`,
|
||||
`${escapedPath}${escapedQuery}${result.fragment || ""}`,
|
||||
result,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string[]} log logs
|
||||
@@ -1051,9 +1084,7 @@ class Resolver {
|
||||
* @type {Error & { recursion?: boolean }}
|
||||
*/
|
||||
const recursionError = new Error(
|
||||
`Recursion in resolving\nStack:\n ${[...stackEntry]
|
||||
.map((entry) => entry.toString())
|
||||
.join("\n ")}`,
|
||||
`Recursion in resolving\nStack:\n ${[...stackEntry].join("\n ")}`,
|
||||
);
|
||||
recursionError.recursion = true;
|
||||
if (resolveContext.log) {
|
||||
@@ -1106,9 +1137,25 @@ class Resolver {
|
||||
[part.request, part.query, part.fragment] = parsedIdentifier;
|
||||
|
||||
if (part.request.length > 0) {
|
||||
part.internal = this.isPrivate(identifier);
|
||||
part.module = this.isModule(part.request);
|
||||
part.directory = this.isDirectory(part.request);
|
||||
// `getType` looks at the prefix of its input and the prefix is
|
||||
// identical between `identifier` and `part.request` in every
|
||||
// non-`\0`-escape case (slicing off `?query` / `#fragment` doesn't
|
||||
// touch the head). `parseIdentifier`'s common fast path returns
|
||||
// the same `identifier` reference as `parsedIdentifier[0]`, so a
|
||||
// pointer-equality check detects the case where we can compute
|
||||
// `getType` once and use it for both `module` and `internal`. The
|
||||
// `\0#…` escape path produces a fresh `part.request` and falls
|
||||
// through to the second `getType(identifier)` call to preserve
|
||||
// the original `internal` flag.
|
||||
const requestType = getType(part.request);
|
||||
part.module = requestType === PathType.Normal;
|
||||
part.internal =
|
||||
identifier === part.request
|
||||
? requestType === PathType.Internal
|
||||
: getType(identifier) === PathType.Internal;
|
||||
// `isDirectory` is just `endsWith("/")` — inline so `parse()`
|
||||
// doesn't pay for the extra method dispatch on every resolve.
|
||||
part.directory = part.request.endsWith("/");
|
||||
if (part.directory) {
|
||||
part.request = part.request.slice(0, -1);
|
||||
}
|
||||
|
||||
+352
-247
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+48
-61
@@ -78,6 +78,7 @@ const slashCode = "/".charCodeAt(0);
|
||||
const dotCode = ".".charCodeAt(0);
|
||||
const hashCode = "#".charCodeAt(0);
|
||||
const patternRegEx = /\*/g;
|
||||
const DOLLAR_ESCAPE_RE = /\$/g;
|
||||
|
||||
/** @typedef {Record<string, MappingValue>} RecordMapping */
|
||||
|
||||
@@ -139,7 +140,12 @@ function getFieldKeyInfos(field) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const patternIndex = key.indexOf("*");
|
||||
const lastStar = patternIndex === -1 ? -1 : key.lastIndexOf("*");
|
||||
// `isValidPattern` is true when the key has at most one `*`. Searching
|
||||
// from `patternIndex + 1` stops as soon as a second `*` is found, so
|
||||
// we avoid the full-string scan that `lastIndexOf` would do — and the
|
||||
// single-star common case finishes in one pass.
|
||||
const isValidPattern =
|
||||
patternIndex === -1 || !key.includes("*", patternIndex + 1);
|
||||
const keyLen = key.length;
|
||||
const endsWithSlash =
|
||||
keyLen > 0 && key.charCodeAt(keyLen - 1) === slashCode;
|
||||
@@ -151,7 +157,7 @@ function getFieldKeyInfos(field) {
|
||||
isLegacySubpath: patternIndex === -1 && endsWithSlash,
|
||||
isPattern: patternIndex !== -1,
|
||||
isSubpathMapping: endsWithSlash,
|
||||
isValidPattern: patternIndex === -1 || lastStar === patternIndex,
|
||||
isValidPattern,
|
||||
};
|
||||
}
|
||||
_fieldKeyInfoCache.set(fieldKey, infos);
|
||||
@@ -286,16 +292,17 @@ function computeFindMatch(request, field) {
|
||||
function findMatch(request, field) {
|
||||
const fieldKey = /** @type {RecordMapping} */ (field);
|
||||
let perRequest = _findMatchCache.get(fieldKey);
|
||||
if (perRequest !== undefined) {
|
||||
const cached = perRequest.get(request);
|
||||
if (cached !== undefined) return cached;
|
||||
// `null` is a valid cached value (= "no match"), so a `get(...)`
|
||||
// that returns undefined could either mean "not cached yet" or
|
||||
// "cached null". Do the explicit `has` only in the undefined case.
|
||||
if (perRequest.has(request)) return null;
|
||||
} else {
|
||||
if (perRequest === undefined) {
|
||||
perRequest = new Map();
|
||||
_findMatchCache.set(fieldKey, perRequest);
|
||||
} else {
|
||||
// `computeFindMatch` only ever returns `MatchTuple | null` — never
|
||||
// `undefined` — and `Map.set(k, null)` then `Map.get(k)` returns
|
||||
// `null`, not `undefined`. So `get(...) === undefined` already
|
||||
// unambiguously means "not cached yet"; one Map lookup is enough,
|
||||
// no follow-up `has` needed to disambiguate "cached null".
|
||||
const cached = perRequest.get(request);
|
||||
if (cached !== undefined) return cached;
|
||||
}
|
||||
|
||||
const result = computeFindMatch(request, field);
|
||||
@@ -303,16 +310,6 @@ function findMatch(request, field) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConditionalMapping | DirectMapping | null} mapping mapping
|
||||
* @returns {boolean} is conditional mapping
|
||||
*/
|
||||
function isConditionalMapping(mapping) {
|
||||
return (
|
||||
mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentinel stored in the conditional-mapping cache for inputs whose walk
|
||||
* returns `null` ("no condition matched"). Using a non-null marker lets the
|
||||
@@ -352,35 +349,22 @@ function computeConditionalMapping(conditionalMapping_, conditionNames) {
|
||||
const lookup = [[conditionalMapping_, cachedKeys(conditionalMapping_), 0]];
|
||||
|
||||
loop: while (lookup.length > 0) {
|
||||
const [mapping, conditions, j] = lookup[lookup.length - 1];
|
||||
const top = lookup[lookup.length - 1];
|
||||
const [mapping, conditions, j] = top;
|
||||
|
||||
for (let i = j; i < conditions.length; i++) {
|
||||
const condition = conditions[i];
|
||||
|
||||
if (condition === "default") {
|
||||
if (condition === "default" || conditionNames.has(condition)) {
|
||||
const innerMapping = mapping[condition];
|
||||
// is nested
|
||||
if (isConditionalMapping(innerMapping)) {
|
||||
const conditionalMapping = /** @type {ConditionalMapping} */ (
|
||||
innerMapping
|
||||
);
|
||||
lookup[lookup.length - 1][2] = i + 1;
|
||||
lookup.push([conditionalMapping, cachedKeys(conditionalMapping), 0]);
|
||||
continue loop;
|
||||
}
|
||||
|
||||
return /** @type {DirectMapping} */ (innerMapping);
|
||||
}
|
||||
|
||||
if (conditionNames.has(condition)) {
|
||||
const innerMapping = mapping[condition];
|
||||
// is nested
|
||||
if (isConditionalMapping(innerMapping)) {
|
||||
const conditionalMapping = /** @type {ConditionalMapping} */ (
|
||||
innerMapping
|
||||
);
|
||||
lookup[lookup.length - 1][2] = i + 1;
|
||||
lookup.push([conditionalMapping, cachedKeys(conditionalMapping), 0]);
|
||||
if (
|
||||
innerMapping !== null &&
|
||||
typeof innerMapping === "object" &&
|
||||
!Array.isArray(innerMapping)
|
||||
) {
|
||||
const nested = /** @type {ConditionalMapping} */ (innerMapping);
|
||||
top[2] = i + 1;
|
||||
lookup.push([nested, cachedKeys(nested), 0]);
|
||||
continue loop;
|
||||
}
|
||||
|
||||
@@ -449,10 +433,10 @@ function targetMapping(
|
||||
let result = mappingTarget;
|
||||
|
||||
if (isPattern) {
|
||||
result = result.replace(
|
||||
patternRegEx,
|
||||
remainingRequest.replace(/\$/g, "$$"),
|
||||
);
|
||||
const escapedRemainder = remainingRequest.includes("$")
|
||||
? remainingRequest.replace(DOLLAR_ESCAPE_RE, "$$")
|
||||
: remainingRequest;
|
||||
result = result.replace(patternRegEx, escapedRemainder);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -492,7 +476,8 @@ function directMapping(
|
||||
/** @type {string[]} */
|
||||
const targets = [];
|
||||
|
||||
for (const exp of mappingTarget) {
|
||||
for (let i = 0, len = mappingTarget.length; i < len; i++) {
|
||||
const exp = mappingTarget[i];
|
||||
if (typeof exp === "string") {
|
||||
targets.push(
|
||||
targetMapping(
|
||||
@@ -516,14 +501,17 @@ function directMapping(
|
||||
conditionNames,
|
||||
assert,
|
||||
);
|
||||
for (const innerExport of innerExports) {
|
||||
targets.push(innerExport);
|
||||
for (let j = 0, innerLen = innerExports.length; j < innerLen; j++) {
|
||||
targets.push(innerExports[j]);
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** @type {[string[], null]} */
|
||||
const EMPTY_NO_MATCH = /** @type {[string[], null]} */ ([[], null]);
|
||||
|
||||
/**
|
||||
* @param {ExportsField | ImportsField} field root
|
||||
* @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
|
||||
@@ -538,26 +526,25 @@ function createFieldProcessor(
|
||||
assertTarget,
|
||||
) {
|
||||
return function fieldProcessor(request, conditionNames) {
|
||||
request = assertRequest(request);
|
||||
const match = findMatch(normalizeRequest(assertRequest(request)), field);
|
||||
|
||||
const match = findMatch(normalizeRequest(request), field);
|
||||
|
||||
if (match === null) return [[], null];
|
||||
if (match === null) return EMPTY_NO_MATCH;
|
||||
|
||||
const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
|
||||
match;
|
||||
|
||||
/** @type {DirectMapping | null} */
|
||||
let direct = null;
|
||||
|
||||
if (isConditionalMapping(mapping)) {
|
||||
let direct;
|
||||
if (
|
||||
mapping !== null &&
|
||||
typeof mapping === "object" &&
|
||||
!Array.isArray(mapping)
|
||||
) {
|
||||
direct = conditionalMapping(
|
||||
/** @type {ConditionalMapping} */ (mapping),
|
||||
conditionNames,
|
||||
);
|
||||
|
||||
// matching not found
|
||||
if (direct === null) return [[], null];
|
||||
if (direct === null) return EMPTY_NO_MATCH;
|
||||
} else {
|
||||
direct = /** @type {DirectMapping} */ (mapping);
|
||||
}
|
||||
|
||||
+42
-34
@@ -19,49 +19,57 @@ const stripJsonComments = require("./strip-json-comments");
|
||||
const _stripCommentsCache = new WeakMap();
|
||||
|
||||
/**
|
||||
* Read and parse JSON file (supports JSONC with comments)
|
||||
* @template T
|
||||
* Read and parse JSON file (supports JSONC with comments).
|
||||
* Callback-based so a synchronous `fileSystem` stays synchronous all the
|
||||
* way through — Promise wrapping would defer resolution by a Promise tick
|
||||
* and break `resolveSync` when `tsconfig` is used together with
|
||||
* `useSyncFileSystemCalls: true`.
|
||||
* @param {FileSystem} fileSystem the file system
|
||||
* @param {string} jsonFilePath absolute path to JSON file
|
||||
* @param {ReadJsonOptions} options Options
|
||||
* @returns {Promise<T>} parsed JSON content
|
||||
* @param {(err: NodeJS.ErrnoException | Error | null, content?: JsonObject) => void} callback callback
|
||||
* @returns {void}
|
||||
*/
|
||||
async function readJson(fileSystem, jsonFilePath, options = {}) {
|
||||
function readJson(fileSystem, jsonFilePath, options, callback) {
|
||||
const { stripComments = false } = options;
|
||||
const { readJson } = fileSystem;
|
||||
if (readJson && !stripComments) {
|
||||
return new Promise((resolve, reject) => {
|
||||
readJson(jsonFilePath, (err, content) => {
|
||||
if (err) return reject(err);
|
||||
resolve(/** @type {T} */ (content));
|
||||
});
|
||||
const { readJson: fsReadJson } = fileSystem;
|
||||
if (fsReadJson && !stripComments) {
|
||||
fsReadJson(jsonFilePath, (err, content) => {
|
||||
if (err) return callback(err);
|
||||
callback(null, /** @type {JsonObject} */ (content));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const buf = await new Promise((resolve, reject) => {
|
||||
fileSystem.readFile(jsonFilePath, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
fileSystem.readFile(jsonFilePath, (err, data) => {
|
||||
if (err) return callback(err);
|
||||
const buf = /** @type {Buffer} */ (data);
|
||||
|
||||
if (stripComments) {
|
||||
const cached = _stripCommentsCache.get(buf);
|
||||
if (cached !== undefined) return callback(null, cached);
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
const jsonText = buf.toString();
|
||||
const jsonWithoutComments = stripComments
|
||||
? stripJsonComments(jsonText, {
|
||||
trailingCommas: true,
|
||||
whitespace: true,
|
||||
})
|
||||
: jsonText;
|
||||
result = JSON.parse(jsonWithoutComments);
|
||||
} catch (parseErr) {
|
||||
return callback(/** @type {Error} */ (parseErr));
|
||||
}
|
||||
|
||||
if (stripComments) {
|
||||
_stripCommentsCache.set(buf, result);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
if (stripComments) {
|
||||
const cached = _stripCommentsCache.get(buf);
|
||||
if (cached !== undefined) return /** @type {T} */ (cached);
|
||||
}
|
||||
|
||||
const jsonText = /** @type {string} */ (buf.toString());
|
||||
// Strip comments to support JSONC (e.g., tsconfig.json with comments)
|
||||
const jsonWithoutComments = stripComments
|
||||
? stripJsonComments(jsonText, { trailingCommas: true, whitespace: true })
|
||||
: jsonText;
|
||||
const result = JSON.parse(jsonWithoutComments);
|
||||
|
||||
if (stripComments) {
|
||||
_stripCommentsCache.set(buf, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports.readJson = readJson;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "enhanced-resolve",
|
||||
"version": "5.21.0",
|
||||
"version": "5.21.2",
|
||||
"description": "Offers a async require.resolve function. It's highly configurable.",
|
||||
"homepage": "http://github.com/webpack/enhanced-resolve",
|
||||
"repository": {
|
||||
|
||||
+13
-2
@@ -1691,6 +1691,15 @@ declare abstract class StackEntry {
|
||||
/**
|
||||
* Walk the linked list looking for an entry with the same request shape.
|
||||
* Set-compatible: callers that used `stack.has(entry)` keep working.
|
||||
* NOTE: kept monomorphic on purpose. An earlier draft accepted a string
|
||||
* query too (so pre-5.21 plugins keeping their own `Set<string>` of
|
||||
* seen entries could probe the live stack with the formatted form),
|
||||
* but adding the second shape regressed `doResolve`'s heap profile by
|
||||
* ~1 MiB / 200 resolves on stack-churn — V8 keeps a polymorphic
|
||||
* call-site state for `parent.has(stackEntry)` once `has` has two
|
||||
* argument shapes. Plugins that need string membership can reach for
|
||||
* `[...stack].find(e => e.includes(formattedString))` via the
|
||||
* `String`-method proxies on `StackEntry` instead.
|
||||
*/
|
||||
has(query: StackEntry): boolean;
|
||||
|
||||
@@ -1700,8 +1709,10 @@ declare abstract class StackEntry {
|
||||
get size(): number;
|
||||
|
||||
/**
|
||||
* Human-readable form used in recursion error messages and logs.
|
||||
* Matches the historical string format so existing log parsers stay valid.
|
||||
* Human-readable form used in recursion error messages, logs, and the
|
||||
* iterator above. Not memoized: caching would require an extra slot on
|
||||
* every `StackEntry`, which costs heap even on resolves that never look
|
||||
* at the formatted form.
|
||||
*/
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user