File size: 12,827 Bytes
f119c28 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | /*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const forEachBail = require("./forEachBail");
const { PathType, getType } = require("./util/path");
/** @typedef {import("./Resolver")} Resolver */
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
/** @typedef {import("./Resolver").ResolveContext} ResolveContext */
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
/** @typedef {import("./Resolver").ResolveCallback} ResolveCallback */
/** @typedef {string | string[] | false} Alias */
/** @typedef {{ alias: Alias, name: string, onlyModule?: boolean }} AliasOption */
/**
* @typedef {object} CompiledAliasOption
* @property {string} name original alias name
* @property {string} nameWithSlash name + "/" β precomputed to avoid per-resolve concat
* @property {Alias} alias alias target(s)
* @property {boolean} onlyModule normalized onlyModule flag
* @property {string | null} absolutePath absolute form of `name` (with slash ending), null when not absolute
* @property {string | null} wildcardPrefix substring before the single "*" in `name`, null when no wildcard
* @property {string | null} wildcardSuffix substring after the single "*" in `name`, null when no wildcard
* @property {number} firstCharCode first character code of `name` β used as a cheap screen on the hot path. `-1` indicates "matches any first char" (empty wildcard prefix).
* @property {boolean} arrayAlias true when `alias` is an array β precomputed so the hot path skips `Array.isArray`
*/
/**
* Bucketed view of compiled options used by `aliasResolveHandler` to avoid
* walking the full option list on every resolve. The `all` array preserves
* the legacy linear order (declaration order) for the fallback path. The
* `byFirstChar` map buckets options by the first char code of their `name`
* β each bucket preserves declaration order among its members. The
* `hasAnyFirstChar` flag is true when at least one option matches any
* first char (`firstCharCode === -1`), in which case resolve-time scans
* fall back to `all` to keep declaration-order semantics across buckets.
* The `useBuckets` flag is true only when bucketing would actually help β
* i.e. there are at least 2 distinct first chars AND no empty-prefix
* wildcard. When false, the resolve hot path skips the `Map.get` and
* iterates `all` directly with the per-option first-char-code screen
* (matching the pre-bucketing behavior). This avoids paying for `Map.get`
* on degenerate single-bucket lists like a long chain of aliases that
* all share one first char β the bucket lookup adds overhead without
* narrowing the candidate set, which showed up as a transient-memory
* regression on `pathological-deep-stack`.
* @typedef {object} CompiledAliasOptions
* @property {CompiledAliasOption[]} all declaration-ordered list
* @property {Map<number, CompiledAliasOption[]>} byFirstChar bucketed by first char code
* @property {boolean} hasAnyFirstChar true when an empty-prefix wildcard is present
* @property {boolean} useBuckets true when the bucket fast-path should be used at resolve time
*/
const EMPTY_LIST = /** @type {CompiledAliasOption[]} */ ([]);
const EMPTY_COMPILED_OPTIONS = /** @type {CompiledAliasOptions} */ ({
all: EMPTY_LIST,
byFirstChar: new Map(),
hasAnyFirstChar: false,
useBuckets: false,
});
/**
* Precompute per-option strings used on every resolve so the hot path in
* `aliasResolveHandler` does no string concatenation / split work per entry.
* Called once per plugin apply β the returned structure is stable for the
* lifetime of the resolver.
*
* Beyond the per-option precompute step, this also partitions the list into
* a `byFirstChar` map so that, when no "empty-prefix" wildcards are
* present, the resolve-time scan only walks options whose `name` starts
* with the same char as the current request. For large alias lists (300+
* entries) this turns an O(N) screen into O(K) where K is the bucket size
* for the request's first char.
* @param {Resolver} resolver resolver
* @param {AliasOption[]} options options
* @returns {CompiledAliasOptions} compiled options
*/
function compileAliasOptions(resolver, options) {
if (options.length === 0) return EMPTY_COMPILED_OPTIONS;
const all = /** @type {CompiledAliasOption[]} */ (
Array.from({ length: options.length })
);
/** @type {Map<number, CompiledAliasOption[]>} */
const byFirstChar = new Map();
let hasAnyFirstChar = false;
for (let i = 0; i < options.length; i++) {
const item = options[i];
const { name } = item;
let absolutePath = null;
const type = getType(name);
if (type === PathType.AbsolutePosix || type === PathType.AbsoluteWin) {
absolutePath = resolver.join(name, "_").slice(0, -1);
}
const firstStar = name.indexOf("*");
let wildcardPrefix = null;
let wildcardSuffix = null;
if (firstStar !== -1 && !name.includes("*", firstStar + 1)) {
wildcardPrefix = name.slice(0, firstStar);
wildcardSuffix = name.slice(firstStar + 1);
}
// firstCharCode: used by `aliasResolveHandler` to quickly skip aliases
// whose name can't possibly match the current innerRequest. For a plain
// alias (no wildcard) the first char of the name is also the first char
// of `nameWithSlash` and of `absolutePath` (since the latter is derived
// from name via `resolver.join(name, "_")`, which only appends). For a
// wildcard with a non-empty prefix, the first char of that prefix is
// also the first char of name. Only the `name === "*"` case (empty
// wildcard prefix) can match arbitrary first chars β encode that as -1.
let firstCharCode;
if (wildcardPrefix !== null && wildcardPrefix.length === 0) {
firstCharCode = -1;
} else {
firstCharCode = name.length > 0 ? name.charCodeAt(0) : -1;
}
const compiled = {
name,
nameWithSlash: `${name}/`,
alias: item.alias,
onlyModule: Boolean(item.onlyModule),
absolutePath,
wildcardPrefix,
wildcardSuffix,
firstCharCode,
arrayAlias: Array.isArray(item.alias),
};
all[i] = compiled;
if (firstCharCode === -1) {
hasAnyFirstChar = true;
} else {
let bucket = byFirstChar.get(firstCharCode);
if (bucket === undefined) {
bucket = [];
byFirstChar.set(firstCharCode, bucket);
}
bucket.push(compiled);
}
}
// Only enable the bucket fast-path when it would actually help. With
// a single bucket (all aliases share one first char, e.g. a chain of
// `chain-0 -> chain-1 -> β¦` rewrites), the resolve-time `Map.get`
// does no discrimination β every request lands in that one bucket
// or in nothing β and the lookup is overhead compared to walking
// `all` with the per-option first-char-code screen. Requiring 2+
// distinct first chars matches the cases where bucketing has
// measurable benefit (huge-alias-* / large-alias-list / stack-churn).
const useBuckets = !hasAnyFirstChar && byFirstChar.size >= 2;
return { all, byFirstChar, hasAnyFirstChar, useBuckets };
}
/** @typedef {(err?: null | Error, result?: null | ResolveRequest) => void} InnerCallback */
/**
* @param {Resolver} resolver resolver
* @param {CompiledAliasOptions} options compiled options
* @param {ResolveStepHook} target target
* @param {ResolveRequest} request request
* @param {ResolveContext} resolveContext resolve context
* @param {InnerCallback} callback callback
* @returns {void}
*/
function aliasResolveHandler(
resolver,
options,
target,
request,
resolveContext,
callback,
) {
if (options.all.length === 0) return callback();
const innerRequest = request.request || request.path;
if (!innerRequest) return callback();
// Precompute values used in the inner scan loop so we don't recompute
// them per option. This is meaningful when `options` has hundreds of
// entries (e.g. monorepos with generated alias lists) β see the
// `huge-alias-list` / `huge-alias-miss` benchmarks.
const innerFirstCharCode = innerRequest.charCodeAt(0);
const hasRequestString = Boolean(request.request);
// Dispatch through the first-char-code bucket when it actually
// narrows the candidate set (`useBuckets` requires 2+ distinct
// first chars and no empty-prefix wildcard). When the field has
// only one first-char bucket β e.g. a long chain of `chain-N`
// aliases that all start with the same char β every request lands
// in that one bucket or nothing, so `Map.get` is overhead vs. just
// walking `all` with the per-option char-code screen. Walking
// `all` also matches the pre-bucketing behavior and keeps the
// `pathological-deep-stack` allocation profile flat.
let scan;
if (options.useBuckets) {
const bucket = options.byFirstChar.get(innerFirstCharCode);
if (bucket === undefined) return callback();
scan = bucket;
} else {
scan = options.all;
}
forEachBail(
scan,
(item, callback) => {
// Char-code screen left in for the fallback (`options.all`) path
// where the bucket dispatch above wasn't usable. In the bucket
// path this is always true and folds into a no-op.
const { firstCharCode } = item;
if (firstCharCode !== -1 && firstCharCode !== innerFirstCharCode) {
return callback();
}
/** @type {boolean} */
let shouldStop = false;
// For absolute-name aliases, accept the normalized
// `absolutePath` form as well as the raw `nameWithSlash`.
// `nameWithSlash` unconditionally appends `/`, so a raw
// 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. 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)) ||
(absolutePath !== null && innerRequest.startsWith(absolutePath))));
const matchWildcard = !item.onlyModule && item.wildcardPrefix !== null;
if (matchRequest || matchWildcard) {
/**
* @param {Alias} alias alias
* @param {(err?: null | Error, result?: null | ResolveRequest) => void} callback callback
* @returns {void}
*/
const resolveWithAlias = (alias, callback) => {
if (alias === false) {
/** @type {ResolveRequest} */
const ignoreObj = {
...request,
path: false,
};
if (typeof resolveContext.yield === "function") {
resolveContext.yield(ignoreObj);
return callback(null, null);
}
return callback(null, ignoreObj);
}
let newRequestStr;
if (
matchWildcard &&
innerRequest.startsWith(
/** @type {string} */ (item.wildcardPrefix),
) &&
innerRequest.endsWith(/** @type {string} */ (item.wildcardSuffix))
) {
const match = innerRequest.slice(
/** @type {string} */ (item.wildcardPrefix).length,
innerRequest.length -
/** @type {string} */ (item.wildcardSuffix).length,
);
newRequestStr = alias.toString().replace("*", match);
}
if (
matchRequest &&
innerRequest !== alias &&
!innerRequest.startsWith(`${alias}/`)
) {
/** @type {string} */
const remainingRequest = innerRequest.slice(item.name.length);
newRequestStr = alias + remainingRequest;
}
if (newRequestStr !== undefined) {
shouldStop = true;
/** @type {ResolveRequest} */
const obj = {
...request,
request: newRequestStr,
fullySpecified: false,
};
return resolver.doResolve(
target,
obj,
`aliased with mapping '${item.name}': '${alias}' to '${newRequestStr}'`,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
return callback();
},
);
}
return callback();
};
/**
* @param {(null | Error)=} err error
* @param {(null | ResolveRequest)=} result result
* @returns {void}
*/
const stoppingCallback = (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
// Don't allow other aliasing or raw request
if (shouldStop) return callback(null, null);
return callback();
};
if (item.arrayAlias) {
return forEachBail(
/** @type {string[]} */ (item.alias),
resolveWithAlias,
stoppingCallback,
);
}
return resolveWithAlias(item.alias, stoppingCallback);
}
return callback();
},
callback,
);
}
module.exports.aliasResolveHandler = aliasResolveHandler;
module.exports.compileAliasOptions = compileAliasOptions;
|