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

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

Fix frontend appearance

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