AliasUtils.js 13 KB

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