| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Ivan Kopeykin @vankop
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const { parseIdentifier } = require("./identifier");
|
|---|
| 9 |
|
|---|
| 10 | /** @typedef {string | (string | ConditionalMapping)[]} DirectMapping */
|
|---|
| 11 | /** @typedef {{ [k: string]: MappingValue }} ConditionalMapping */
|
|---|
| 12 | /** @typedef {ConditionalMapping | DirectMapping | null} MappingValue */
|
|---|
| 13 | /** @typedef {Record<string, MappingValue> | ConditionalMapping | DirectMapping} ExportsField */
|
|---|
| 14 | /** @typedef {Record<string, MappingValue>} ImportsField */
|
|---|
| 15 |
|
|---|
| 16 | /**
|
|---|
| 17 | * Processing exports/imports field
|
|---|
| 18 | * @callback FieldProcessor
|
|---|
| 19 | * @param {string} request request
|
|---|
| 20 | * @param {Set<string>} conditionNames condition names
|
|---|
| 21 | * @returns {[string[], string | null]} resolved paths with used field
|
|---|
| 22 | */
|
|---|
| 23 |
|
|---|
| 24 | /*
|
|---|
| 25 | Example exports field:
|
|---|
| 26 | {
|
|---|
| 27 | ".": "./main.js",
|
|---|
| 28 | "./feature": {
|
|---|
| 29 | "browser": "./feature-browser.js",
|
|---|
| 30 | "default": "./feature.js"
|
|---|
| 31 | }
|
|---|
| 32 | }
|
|---|
| 33 | Terminology:
|
|---|
| 34 |
|
|---|
| 35 | Enhanced-resolve name keys ("." and "./feature") as exports field keys.
|
|---|
| 36 |
|
|---|
| 37 | If value is string or string[], mapping is called as a direct mapping
|
|---|
| 38 | and value called as a direct export.
|
|---|
| 39 |
|
|---|
| 40 | If value is key-value object, mapping is called as a conditional mapping
|
|---|
| 41 | and value called as a conditional export.
|
|---|
| 42 |
|
|---|
| 43 | Key in conditional mapping is called condition name.
|
|---|
| 44 |
|
|---|
| 45 | Conditional mapping nested in another conditional mapping is called nested mapping.
|
|---|
| 46 |
|
|---|
| 47 | ----------
|
|---|
| 48 |
|
|---|
| 49 | Example imports field:
|
|---|
| 50 | {
|
|---|
| 51 | "#a": "./main.js",
|
|---|
| 52 | "#moment": {
|
|---|
| 53 | "browser": "./moment/index.js",
|
|---|
| 54 | "default": "moment"
|
|---|
| 55 | },
|
|---|
| 56 | "#moment/": {
|
|---|
| 57 | "browser": "./moment/",
|
|---|
| 58 | "default": "moment/"
|
|---|
| 59 | }
|
|---|
| 60 | }
|
|---|
| 61 | Terminology:
|
|---|
| 62 |
|
|---|
| 63 | Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
|
|---|
| 64 |
|
|---|
| 65 | If value is string or string[], mapping is called as a direct mapping
|
|---|
| 66 | and value called as a direct export.
|
|---|
| 67 |
|
|---|
| 68 | If value is key-value object, mapping is called as a conditional mapping
|
|---|
| 69 | and value called as a conditional export.
|
|---|
| 70 |
|
|---|
| 71 | Key in conditional mapping is called condition name.
|
|---|
| 72 |
|
|---|
| 73 | Conditional mapping nested in another conditional mapping is called nested mapping.
|
|---|
| 74 |
|
|---|
| 75 | */
|
|---|
| 76 |
|
|---|
| 77 | const slashCode = "/".charCodeAt(0);
|
|---|
| 78 | const dotCode = ".".charCodeAt(0);
|
|---|
| 79 | const hashCode = "#".charCodeAt(0);
|
|---|
| 80 | const patternRegEx = /\*/g;
|
|---|
| 81 | const DOLLAR_ESCAPE_RE = /\$/g;
|
|---|
| 82 |
|
|---|
| 83 | /** @typedef {Record<string, MappingValue>} RecordMapping */
|
|---|
| 84 |
|
|---|
| 85 | /**
|
|---|
| 86 | * Cached `Object.keys()` for objects whose shape does not change after the
|
|---|
| 87 | * first observation — i.e. parsed `package.json` fields and the nested
|
|---|
| 88 | * conditional mappings inside them. `Object.keys` allocates a fresh array
|
|---|
| 89 | * on every call; since `findMatch` / `conditionalMapping` run on every
|
|---|
| 90 | * bare-specifier resolve, the allocation adds up quickly.
|
|---|
| 91 | * @type {WeakMap<RecordMapping, string[]>}
|
|---|
| 92 | */
|
|---|
| 93 | const _keysCache = new WeakMap();
|
|---|
| 94 |
|
|---|
| 95 | /**
|
|---|
| 96 | * @param {RecordMapping} obj object to read keys from
|
|---|
| 97 | * @returns {string[]} cached keys array (DO NOT mutate)
|
|---|
| 98 | */
|
|---|
| 99 | function cachedKeys(obj) {
|
|---|
| 100 | let keys = _keysCache.get(obj);
|
|---|
| 101 | if (keys === undefined) {
|
|---|
| 102 | keys = Object.keys(obj);
|
|---|
| 103 | _keysCache.set(obj, keys);
|
|---|
| 104 | }
|
|---|
| 105 | return keys;
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | /**
|
|---|
| 109 | * Per-key precomputed info used by `findMatch`. Equivalent to what the
|
|---|
| 110 | * previous implementation recomputed inline on every resolve.
|
|---|
| 111 | * @typedef {object} FieldKeyInfo
|
|---|
| 112 | * @property {string} key the original key
|
|---|
| 113 | * @property {number} patternIndex position of the single "*" in the key, or -1 when absent
|
|---|
| 114 | * @property {string} wildcardPrefix substring before "*" (empty when patternIndex === -1)
|
|---|
| 115 | * @property {string} wildcardSuffix substring after "*" (empty when patternIndex === -1)
|
|---|
| 116 | * @property {boolean} isLegacySubpath true when key is a legacy `./foo/`-style folder key with no "*"
|
|---|
| 117 | * @property {boolean} isPattern true when key contains "*"
|
|---|
| 118 | * @property {boolean} isSubpathMapping true when key ends with "/"
|
|---|
| 119 | * @property {boolean} isValidPattern true when key has at most one "*"
|
|---|
| 120 | */
|
|---|
| 121 |
|
|---|
| 122 | /**
|
|---|
| 123 | * Cached per-field key metadata, keyed by the exports/imports field
|
|---|
| 124 | * object. Computed lazily on first `findMatch` call and reused forever.
|
|---|
| 125 | * Safe because `package.json` fields are immutable JSON values.
|
|---|
| 126 | * @type {WeakMap<RecordMapping, FieldKeyInfo[]>}
|
|---|
| 127 | */
|
|---|
| 128 | const _fieldKeyInfoCache = new WeakMap();
|
|---|
| 129 |
|
|---|
| 130 | /**
|
|---|
| 131 | * @param {ExportsField | ImportsField} field field object
|
|---|
| 132 | * @returns {FieldKeyInfo[]} precomputed per-key info
|
|---|
| 133 | */
|
|---|
| 134 | function getFieldKeyInfos(field) {
|
|---|
| 135 | const fieldKey = /** @type {RecordMapping} */ (field);
|
|---|
| 136 | let infos = _fieldKeyInfoCache.get(fieldKey);
|
|---|
| 137 | if (infos !== undefined) return infos;
|
|---|
| 138 | const keys = Object.getOwnPropertyNames(field);
|
|---|
| 139 | infos = Array.from({ length: keys.length });
|
|---|
| 140 | for (let i = 0; i < keys.length; i++) {
|
|---|
| 141 | const key = keys[i];
|
|---|
| 142 | const patternIndex = key.indexOf("*");
|
|---|
| 143 | // `isValidPattern` is true when the key has at most one `*`. Searching
|
|---|
| 144 | // from `patternIndex + 1` stops as soon as a second `*` is found, so
|
|---|
| 145 | // we avoid the full-string scan that `lastIndexOf` would do — and the
|
|---|
| 146 | // single-star common case finishes in one pass.
|
|---|
| 147 | const isValidPattern =
|
|---|
| 148 | patternIndex === -1 || !key.includes("*", patternIndex + 1);
|
|---|
| 149 | const keyLen = key.length;
|
|---|
| 150 | const endsWithSlash =
|
|---|
| 151 | keyLen > 0 && key.charCodeAt(keyLen - 1) === slashCode;
|
|---|
| 152 | infos[i] = {
|
|---|
| 153 | key,
|
|---|
| 154 | patternIndex,
|
|---|
| 155 | wildcardPrefix: patternIndex === -1 ? "" : key.slice(0, patternIndex),
|
|---|
| 156 | wildcardSuffix: patternIndex === -1 ? "" : key.slice(patternIndex + 1),
|
|---|
| 157 | isLegacySubpath: patternIndex === -1 && endsWithSlash,
|
|---|
| 158 | isPattern: patternIndex !== -1,
|
|---|
| 159 | isSubpathMapping: endsWithSlash,
|
|---|
| 160 | isValidPattern,
|
|---|
| 161 | };
|
|---|
| 162 | }
|
|---|
| 163 | _fieldKeyInfoCache.set(fieldKey, infos);
|
|---|
| 164 | return infos;
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | /**
|
|---|
| 168 | * @param {string} a first string
|
|---|
| 169 | * @param {string} b second string
|
|---|
| 170 | * @returns {number} compare result
|
|---|
| 171 | */
|
|---|
| 172 | function patternKeyCompare(a, b) {
|
|---|
| 173 | const aPatternIndex = a.indexOf("*");
|
|---|
| 174 | const bPatternIndex = b.indexOf("*");
|
|---|
| 175 | const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
|
|---|
| 176 | const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
|
|---|
| 177 |
|
|---|
| 178 | if (baseLenA > baseLenB) return -1;
|
|---|
| 179 | if (baseLenB > baseLenA) return 1;
|
|---|
| 180 | if (aPatternIndex === -1) return 1;
|
|---|
| 181 | if (bPatternIndex === -1) return -1;
|
|---|
| 182 | if (a.length > b.length) return -1;
|
|---|
| 183 | if (b.length > a.length) return 1;
|
|---|
| 184 |
|
|---|
| 185 | return 0;
|
|---|
| 186 | }
|
|---|
| 187 |
|
|---|
| 188 | /** @typedef {[MappingValue, string, boolean, boolean, string] | null} MatchTuple */
|
|---|
| 189 |
|
|---|
| 190 | /**
|
|---|
| 191 | * Per-field memoization of `findMatch(request, field)`. For a given field
|
|---|
| 192 | * the result depends only on the `request` string (it does NOT depend on
|
|---|
| 193 | * `conditionNames` — that's applied separately by `conditionalMapping`),
|
|---|
| 194 | * so we can cache the tuple keyed by request.
|
|---|
| 195 | *
|
|---|
| 196 | * Typical build traffic runs the same request through the resolver
|
|---|
| 197 | * repeatedly (same import re-resolved from different source files, module
|
|---|
| 198 | * graph traversals that revisit a package, etc.), and every one of those
|
|---|
| 199 | * hits walks the same key list and allocates the same tuple. Caching the
|
|---|
| 200 | * tuple turns the second-and-onward call into a single Map lookup.
|
|---|
| 201 | *
|
|---|
| 202 | * Keyed on the field object via a module-level `WeakMap`, so the cache
|
|---|
| 203 | * is freed automatically when the owning description file is GC'd.
|
|---|
| 204 | * @type {WeakMap<RecordMapping, Map<string, MatchTuple>>}
|
|---|
| 205 | */
|
|---|
| 206 | const _findMatchCache = new WeakMap();
|
|---|
| 207 |
|
|---|
| 208 | /**
|
|---|
| 209 | * @param {string} request request
|
|---|
| 210 | * @param {ExportsField | ImportsField} field exports or import field
|
|---|
| 211 | * @returns {MatchTuple} match result (uncached)
|
|---|
| 212 | */
|
|---|
| 213 | function computeFindMatch(request, field) {
|
|---|
| 214 | const requestLen = request.length;
|
|---|
| 215 | const requestEndsWithSlash =
|
|---|
| 216 | requestLen > 0 && request.charCodeAt(requestLen - 1) === slashCode;
|
|---|
| 217 | const requestHasStar = request.includes("*");
|
|---|
| 218 |
|
|---|
| 219 | if (
|
|---|
| 220 | !requestHasStar &&
|
|---|
| 221 | !requestEndsWithSlash &&
|
|---|
| 222 | Object.prototype.hasOwnProperty.call(field, request)
|
|---|
| 223 | ) {
|
|---|
| 224 | const target = /** @type {{ [k: string]: MappingValue }} */ (field)[
|
|---|
| 225 | request
|
|---|
| 226 | ];
|
|---|
| 227 |
|
|---|
| 228 | return [target, "", false, false, request];
|
|---|
| 229 | }
|
|---|
| 230 |
|
|---|
| 231 | /** @type {string} */
|
|---|
| 232 | let bestMatch = "";
|
|---|
| 233 | /** @type {FieldKeyInfo | null} */
|
|---|
| 234 | let bestMatchInfo = null;
|
|---|
| 235 | /** @type {string | undefined} */
|
|---|
| 236 | let bestMatchSubpath;
|
|---|
| 237 |
|
|---|
| 238 | const infos = getFieldKeyInfos(field);
|
|---|
| 239 |
|
|---|
| 240 | for (let i = 0; i < infos.length; i++) {
|
|---|
| 241 | const info = infos[i];
|
|---|
| 242 | const { key, patternIndex } = info;
|
|---|
| 243 |
|
|---|
| 244 | if (patternIndex !== -1) {
|
|---|
| 245 | if (
|
|---|
| 246 | !info.isValidPattern ||
|
|---|
| 247 | !request.startsWith(info.wildcardPrefix) ||
|
|---|
| 248 | requestLen < key.length ||
|
|---|
| 249 | !request.endsWith(info.wildcardSuffix) ||
|
|---|
| 250 | patternKeyCompare(bestMatch, key) !== 1
|
|---|
| 251 | ) {
|
|---|
| 252 | continue;
|
|---|
| 253 | }
|
|---|
| 254 | bestMatch = key;
|
|---|
| 255 | bestMatchInfo = info;
|
|---|
| 256 | bestMatchSubpath = request.slice(
|
|---|
| 257 | patternIndex,
|
|---|
| 258 | requestLen - info.wildcardSuffix.length,
|
|---|
| 259 | );
|
|---|
| 260 | } else if (
|
|---|
| 261 | info.isLegacySubpath &&
|
|---|
| 262 | request.startsWith(key) &&
|
|---|
| 263 | patternKeyCompare(bestMatch, key) === 1
|
|---|
| 264 | ) {
|
|---|
| 265 | bestMatch = key;
|
|---|
| 266 | bestMatchInfo = info;
|
|---|
| 267 | bestMatchSubpath = request.slice(key.length);
|
|---|
| 268 | }
|
|---|
| 269 | }
|
|---|
| 270 |
|
|---|
| 271 | if (bestMatch === "") return null;
|
|---|
| 272 |
|
|---|
| 273 | const target =
|
|---|
| 274 | /** @type {{ [k: string]: MappingValue }} */
|
|---|
| 275 | (field)[bestMatch];
|
|---|
| 276 |
|
|---|
| 277 | return [
|
|---|
| 278 | target,
|
|---|
| 279 | /** @type {string} */ (bestMatchSubpath),
|
|---|
| 280 | /** @type {FieldKeyInfo} */ (bestMatchInfo).isSubpathMapping,
|
|---|
| 281 | /** @type {FieldKeyInfo} */ (bestMatchInfo).isPattern,
|
|---|
| 282 | bestMatch,
|
|---|
| 283 | ];
|
|---|
| 284 | }
|
|---|
| 285 |
|
|---|
| 286 | /**
|
|---|
| 287 | * Trying to match request to field
|
|---|
| 288 | * @param {string} request request
|
|---|
| 289 | * @param {ExportsField | ImportsField} field exports or import field
|
|---|
| 290 | * @returns {MatchTuple} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
|
|---|
| 291 | */
|
|---|
| 292 | function findMatch(request, field) {
|
|---|
| 293 | const fieldKey = /** @type {RecordMapping} */ (field);
|
|---|
| 294 | let perRequest = _findMatchCache.get(fieldKey);
|
|---|
| 295 | if (perRequest === undefined) {
|
|---|
| 296 | perRequest = new Map();
|
|---|
| 297 | _findMatchCache.set(fieldKey, perRequest);
|
|---|
| 298 | } else {
|
|---|
| 299 | // `computeFindMatch` only ever returns `MatchTuple | null` — never
|
|---|
| 300 | // `undefined` — and `Map.set(k, null)` then `Map.get(k)` returns
|
|---|
| 301 | // `null`, not `undefined`. So `get(...) === undefined` already
|
|---|
| 302 | // unambiguously means "not cached yet"; one Map lookup is enough,
|
|---|
| 303 | // no follow-up `has` needed to disambiguate "cached null".
|
|---|
| 304 | const cached = perRequest.get(request);
|
|---|
| 305 | if (cached !== undefined) return cached;
|
|---|
| 306 | }
|
|---|
| 307 |
|
|---|
| 308 | const result = computeFindMatch(request, field);
|
|---|
| 309 | perRequest.set(request, result);
|
|---|
| 310 | return result;
|
|---|
| 311 | }
|
|---|
| 312 |
|
|---|
| 313 | /**
|
|---|
| 314 | * Sentinel stored in the conditional-mapping cache for inputs whose walk
|
|---|
| 315 | * returns `null` ("no condition matched"). Using a non-null marker lets the
|
|---|
| 316 | * cache-hit path be a single `WeakMap.get()` — we distinguish
|
|---|
| 317 | * "cached null" from "not cached yet" without a second `has` call.
|
|---|
| 318 | */
|
|---|
| 319 | const NULL_RESULT = Symbol("NULL_RESULT");
|
|---|
| 320 |
|
|---|
| 321 | /**
|
|---|
| 322 | * Memoization of `conditionalMapping(mapping, conditionNames)`. The result
|
|---|
| 323 | * depends only on the mapping object (immutable — owned by a parsed
|
|---|
| 324 | * `package.json`) and the `conditionNames` Set (owned by the resolver's
|
|---|
| 325 | * options and stable for its lifetime), so it is safe to cache per (mapping,
|
|---|
| 326 | * conditionNames) pair.
|
|---|
| 327 | *
|
|---|
| 328 | * A conditional `exports` entry that appears inside a `directMapping` array
|
|---|
| 329 | * (the common `"browser": [...fallback list...]` shape, plus nested
|
|---|
| 330 | * conditions) gets walked on every resolve that traverses the parent entry.
|
|---|
| 331 | * Without this cache each of those walks re-reads `Object.keys` on the
|
|---|
| 332 | * mapping and re-visits every condition until one matches, even though the
|
|---|
| 333 | * inputs are identical.
|
|---|
| 334 | *
|
|---|
| 335 | * Outer key is the conditional mapping itself; inner key is the condition
|
|---|
| 336 | * Set. Both are object references, so WeakMap-of-WeakMap lets both levels
|
|---|
| 337 | * be collected automatically when the description file or resolver go away.
|
|---|
| 338 | * @type {WeakMap<ConditionalMapping, WeakMap<Set<string>, DirectMapping | typeof NULL_RESULT>>}
|
|---|
| 339 | */
|
|---|
| 340 | const _conditionalMappingCache = new WeakMap();
|
|---|
| 341 |
|
|---|
| 342 | /**
|
|---|
| 343 | * @param {ConditionalMapping} conditionalMapping_ conditional mapping
|
|---|
| 344 | * @param {Set<string>} conditionNames condition names
|
|---|
| 345 | * @returns {DirectMapping | null} direct mapping if found (uncached)
|
|---|
| 346 | */
|
|---|
| 347 | function computeConditionalMapping(conditionalMapping_, conditionNames) {
|
|---|
| 348 | /** @type {[ConditionalMapping, string[], number][]} */
|
|---|
| 349 | const lookup = [[conditionalMapping_, cachedKeys(conditionalMapping_), 0]];
|
|---|
| 350 |
|
|---|
| 351 | loop: while (lookup.length > 0) {
|
|---|
| 352 | const top = lookup[lookup.length - 1];
|
|---|
| 353 | const [mapping, conditions, j] = top;
|
|---|
| 354 |
|
|---|
| 355 | for (let i = j; i < conditions.length; i++) {
|
|---|
| 356 | const condition = conditions[i];
|
|---|
| 357 |
|
|---|
| 358 | if (condition === "default" || conditionNames.has(condition)) {
|
|---|
| 359 | const innerMapping = mapping[condition];
|
|---|
| 360 | if (
|
|---|
| 361 | innerMapping !== null &&
|
|---|
| 362 | typeof innerMapping === "object" &&
|
|---|
| 363 | !Array.isArray(innerMapping)
|
|---|
| 364 | ) {
|
|---|
| 365 | const nested = /** @type {ConditionalMapping} */ (innerMapping);
|
|---|
| 366 | top[2] = i + 1;
|
|---|
| 367 | lookup.push([nested, cachedKeys(nested), 0]);
|
|---|
| 368 | continue loop;
|
|---|
| 369 | }
|
|---|
| 370 |
|
|---|
| 371 | return /** @type {DirectMapping} */ (innerMapping);
|
|---|
| 372 | }
|
|---|
| 373 | }
|
|---|
| 374 |
|
|---|
| 375 | lookup.pop();
|
|---|
| 376 | }
|
|---|
| 377 |
|
|---|
| 378 | return null;
|
|---|
| 379 | }
|
|---|
| 380 |
|
|---|
| 381 | /**
|
|---|
| 382 | * @param {ConditionalMapping} conditionalMapping_ conditional mapping
|
|---|
| 383 | * @param {Set<string>} conditionNames condition names
|
|---|
| 384 | * @returns {DirectMapping | null} direct mapping if found
|
|---|
| 385 | */
|
|---|
| 386 | function conditionalMapping(conditionalMapping_, conditionNames) {
|
|---|
| 387 | let perSet = _conditionalMappingCache.get(conditionalMapping_);
|
|---|
| 388 | if (perSet !== undefined) {
|
|---|
| 389 | const cached = perSet.get(conditionNames);
|
|---|
| 390 | if (cached !== undefined) {
|
|---|
| 391 | return cached === NULL_RESULT
|
|---|
| 392 | ? null
|
|---|
| 393 | : /** @type {DirectMapping} */ (cached);
|
|---|
| 394 | }
|
|---|
| 395 | } else {
|
|---|
| 396 | perSet = new WeakMap();
|
|---|
| 397 | _conditionalMappingCache.set(conditionalMapping_, perSet);
|
|---|
| 398 | }
|
|---|
| 399 | const result = computeConditionalMapping(conditionalMapping_, conditionNames);
|
|---|
| 400 | perSet.set(conditionNames, result === null ? NULL_RESULT : result);
|
|---|
| 401 | return result;
|
|---|
| 402 | }
|
|---|
| 403 |
|
|---|
| 404 | /**
|
|---|
| 405 | * @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
|
|---|
| 406 | * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
|
|---|
| 407 | * @param {boolean} isSubpathMapping true, for subpath mappings
|
|---|
| 408 | * @param {string} mappingTarget direct export
|
|---|
| 409 | * @param {(d: string, f: boolean) => void} assert asserting direct value
|
|---|
| 410 | * @returns {string} mapping result
|
|---|
| 411 | */
|
|---|
| 412 | function targetMapping(
|
|---|
| 413 | remainingRequest,
|
|---|
| 414 | isPattern,
|
|---|
| 415 | isSubpathMapping,
|
|---|
| 416 | mappingTarget,
|
|---|
| 417 | assert,
|
|---|
| 418 | ) {
|
|---|
| 419 | if (remainingRequest === undefined) {
|
|---|
| 420 | assert(mappingTarget, false);
|
|---|
| 421 |
|
|---|
| 422 | return mappingTarget;
|
|---|
| 423 | }
|
|---|
| 424 |
|
|---|
| 425 | if (isSubpathMapping) {
|
|---|
| 426 | assert(mappingTarget, true);
|
|---|
| 427 |
|
|---|
| 428 | return mappingTarget + remainingRequest;
|
|---|
| 429 | }
|
|---|
| 430 |
|
|---|
| 431 | assert(mappingTarget, false);
|
|---|
| 432 |
|
|---|
| 433 | let result = mappingTarget;
|
|---|
| 434 |
|
|---|
| 435 | if (isPattern) {
|
|---|
| 436 | const escapedRemainder = remainingRequest.includes("$")
|
|---|
| 437 | ? remainingRequest.replace(DOLLAR_ESCAPE_RE, "$$")
|
|---|
| 438 | : remainingRequest;
|
|---|
| 439 | result = result.replace(patternRegEx, escapedRemainder);
|
|---|
| 440 | }
|
|---|
| 441 |
|
|---|
| 442 | return result;
|
|---|
| 443 | }
|
|---|
| 444 |
|
|---|
| 445 | /**
|
|---|
| 446 | * @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
|
|---|
| 447 | * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
|
|---|
| 448 | * @param {boolean} isSubpathMapping true, for subpath mappings
|
|---|
| 449 | * @param {DirectMapping | null} mappingTarget direct export
|
|---|
| 450 | * @param {Set<string>} conditionNames condition names
|
|---|
| 451 | * @param {(d: string, f: boolean) => void} assert asserting direct value
|
|---|
| 452 | * @returns {string[]} mapping result
|
|---|
| 453 | */
|
|---|
| 454 | function directMapping(
|
|---|
| 455 | remainingRequest,
|
|---|
| 456 | isPattern,
|
|---|
| 457 | isSubpathMapping,
|
|---|
| 458 | mappingTarget,
|
|---|
| 459 | conditionNames,
|
|---|
| 460 | assert,
|
|---|
| 461 | ) {
|
|---|
| 462 | if (mappingTarget === null) return [];
|
|---|
| 463 |
|
|---|
| 464 | if (typeof mappingTarget === "string") {
|
|---|
| 465 | return [
|
|---|
| 466 | targetMapping(
|
|---|
| 467 | remainingRequest,
|
|---|
| 468 | isPattern,
|
|---|
| 469 | isSubpathMapping,
|
|---|
| 470 | mappingTarget,
|
|---|
| 471 | assert,
|
|---|
| 472 | ),
|
|---|
| 473 | ];
|
|---|
| 474 | }
|
|---|
| 475 |
|
|---|
| 476 | /** @type {string[]} */
|
|---|
| 477 | const targets = [];
|
|---|
| 478 |
|
|---|
| 479 | for (let i = 0, len = mappingTarget.length; i < len; i++) {
|
|---|
| 480 | const exp = mappingTarget[i];
|
|---|
| 481 | if (typeof exp === "string") {
|
|---|
| 482 | targets.push(
|
|---|
| 483 | targetMapping(
|
|---|
| 484 | remainingRequest,
|
|---|
| 485 | isPattern,
|
|---|
| 486 | isSubpathMapping,
|
|---|
| 487 | exp,
|
|---|
| 488 | assert,
|
|---|
| 489 | ),
|
|---|
| 490 | );
|
|---|
| 491 | continue;
|
|---|
| 492 | }
|
|---|
| 493 |
|
|---|
| 494 | const mapping = conditionalMapping(exp, conditionNames);
|
|---|
| 495 | if (!mapping) continue;
|
|---|
| 496 | const innerExports = directMapping(
|
|---|
| 497 | remainingRequest,
|
|---|
| 498 | isPattern,
|
|---|
| 499 | isSubpathMapping,
|
|---|
| 500 | mapping,
|
|---|
| 501 | conditionNames,
|
|---|
| 502 | assert,
|
|---|
| 503 | );
|
|---|
| 504 | for (let j = 0, innerLen = innerExports.length; j < innerLen; j++) {
|
|---|
| 505 | targets.push(innerExports[j]);
|
|---|
| 506 | }
|
|---|
| 507 | }
|
|---|
| 508 |
|
|---|
| 509 | return targets;
|
|---|
| 510 | }
|
|---|
| 511 |
|
|---|
| 512 | /** @type {[string[], null]} */
|
|---|
| 513 | const EMPTY_NO_MATCH = /** @type {[string[], null]} */ ([[], null]);
|
|---|
| 514 |
|
|---|
| 515 | /**
|
|---|
| 516 | * @param {ExportsField | ImportsField} field root
|
|---|
| 517 | * @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
|
|---|
| 518 | * @param {(s: string) => string} assertRequest assertRequest
|
|---|
| 519 | * @param {(s: string, f: boolean) => void} assertTarget assertTarget
|
|---|
| 520 | * @returns {FieldProcessor} field processor
|
|---|
| 521 | */
|
|---|
| 522 | function createFieldProcessor(
|
|---|
| 523 | field,
|
|---|
| 524 | normalizeRequest,
|
|---|
| 525 | assertRequest,
|
|---|
| 526 | assertTarget,
|
|---|
| 527 | ) {
|
|---|
| 528 | return function fieldProcessor(request, conditionNames) {
|
|---|
| 529 | const match = findMatch(normalizeRequest(assertRequest(request)), field);
|
|---|
| 530 |
|
|---|
| 531 | if (match === null) return EMPTY_NO_MATCH;
|
|---|
| 532 |
|
|---|
| 533 | const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
|
|---|
| 534 | match;
|
|---|
| 535 |
|
|---|
| 536 | /** @type {DirectMapping | null} */
|
|---|
| 537 | let direct;
|
|---|
| 538 | if (
|
|---|
| 539 | mapping !== null &&
|
|---|
| 540 | typeof mapping === "object" &&
|
|---|
| 541 | !Array.isArray(mapping)
|
|---|
| 542 | ) {
|
|---|
| 543 | direct = conditionalMapping(
|
|---|
| 544 | /** @type {ConditionalMapping} */ (mapping),
|
|---|
| 545 | conditionNames,
|
|---|
| 546 | );
|
|---|
| 547 | if (direct === null) return EMPTY_NO_MATCH;
|
|---|
| 548 | } else {
|
|---|
| 549 | direct = /** @type {DirectMapping} */ (mapping);
|
|---|
| 550 | }
|
|---|
| 551 |
|
|---|
| 552 | return [
|
|---|
| 553 | directMapping(
|
|---|
| 554 | remainingRequest,
|
|---|
| 555 | isPattern,
|
|---|
| 556 | isSubpathMapping,
|
|---|
| 557 | direct,
|
|---|
| 558 | conditionNames,
|
|---|
| 559 | assertTarget,
|
|---|
| 560 | ),
|
|---|
| 561 | usedField,
|
|---|
| 562 | ];
|
|---|
| 563 | };
|
|---|
| 564 | }
|
|---|
| 565 |
|
|---|
| 566 | /**
|
|---|
| 567 | * @param {string} request request
|
|---|
| 568 | * @returns {string} updated request
|
|---|
| 569 | */
|
|---|
| 570 | function assertExportsFieldRequest(request) {
|
|---|
| 571 | if (request.charCodeAt(0) !== dotCode) {
|
|---|
| 572 | throw new Error('Request should be relative path and start with "."');
|
|---|
| 573 | }
|
|---|
| 574 | if (request.length === 1) return "";
|
|---|
| 575 | if (request.charCodeAt(1) !== slashCode) {
|
|---|
| 576 | throw new Error('Request should be relative path and start with "./"');
|
|---|
| 577 | }
|
|---|
| 578 | if (request.charCodeAt(request.length - 1) === slashCode) {
|
|---|
| 579 | throw new Error("Only requesting file allowed");
|
|---|
| 580 | }
|
|---|
| 581 |
|
|---|
| 582 | return request.slice(2);
|
|---|
| 583 | }
|
|---|
| 584 |
|
|---|
| 585 | /**
|
|---|
| 586 | * @param {ExportsField} field exports field
|
|---|
| 587 | * @returns {ExportsField} normalized exports field
|
|---|
| 588 | */
|
|---|
| 589 | function buildExportsField(field) {
|
|---|
| 590 | // handle syntax sugar, if exports field is direct mapping for "."
|
|---|
| 591 | if (typeof field === "string" || Array.isArray(field)) {
|
|---|
| 592 | return { ".": field };
|
|---|
| 593 | }
|
|---|
| 594 |
|
|---|
| 595 | const keys = Object.keys(field);
|
|---|
| 596 |
|
|---|
| 597 | for (let i = 0; i < keys.length; i++) {
|
|---|
| 598 | const key = keys[i];
|
|---|
| 599 |
|
|---|
| 600 | if (key.charCodeAt(0) !== dotCode) {
|
|---|
| 601 | // handle syntax sugar, if exports field is conditional mapping for "."
|
|---|
| 602 | if (i === 0) {
|
|---|
| 603 | while (i < keys.length) {
|
|---|
| 604 | const charCode = keys[i].charCodeAt(0);
|
|---|
| 605 | if (charCode === dotCode || charCode === slashCode) {
|
|---|
| 606 | throw new Error(
|
|---|
| 607 | `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
|
|---|
| 608 | key,
|
|---|
| 609 | )})`,
|
|---|
| 610 | );
|
|---|
| 611 | }
|
|---|
| 612 | i++;
|
|---|
| 613 | }
|
|---|
| 614 |
|
|---|
| 615 | return { ".": field };
|
|---|
| 616 | }
|
|---|
| 617 |
|
|---|
| 618 | throw new Error(
|
|---|
| 619 | `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
|
|---|
| 620 | key,
|
|---|
| 621 | )})`,
|
|---|
| 622 | );
|
|---|
| 623 | }
|
|---|
| 624 |
|
|---|
| 625 | if (key.length === 1) {
|
|---|
| 626 | continue;
|
|---|
| 627 | }
|
|---|
| 628 |
|
|---|
| 629 | if (key.charCodeAt(1) !== slashCode) {
|
|---|
| 630 | throw new Error(
|
|---|
| 631 | `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
|
|---|
| 632 | key,
|
|---|
| 633 | )})`,
|
|---|
| 634 | );
|
|---|
| 635 | }
|
|---|
| 636 | }
|
|---|
| 637 |
|
|---|
| 638 | return field;
|
|---|
| 639 | }
|
|---|
| 640 |
|
|---|
| 641 | /**
|
|---|
| 642 | * @param {string} exp export target
|
|---|
| 643 | * @param {boolean} expectFolder is folder expected
|
|---|
| 644 | */
|
|---|
| 645 | function assertExportTarget(exp, expectFolder) {
|
|---|
| 646 | const parsedIdentifier = parseIdentifier(exp);
|
|---|
| 647 |
|
|---|
| 648 | if (!parsedIdentifier) {
|
|---|
| 649 | return;
|
|---|
| 650 | }
|
|---|
| 651 |
|
|---|
| 652 | const [relativePath] = parsedIdentifier;
|
|---|
| 653 | const isFolder =
|
|---|
| 654 | relativePath.charCodeAt(relativePath.length - 1) === slashCode;
|
|---|
| 655 |
|
|---|
| 656 | if (isFolder !== expectFolder) {
|
|---|
| 657 | throw new Error(
|
|---|
| 658 | expectFolder
|
|---|
| 659 | ? `Expecting folder to folder mapping. ${JSON.stringify(
|
|---|
| 660 | exp,
|
|---|
| 661 | )} should end with "/"`
|
|---|
| 662 | : `Expecting file to file mapping. ${JSON.stringify(
|
|---|
| 663 | exp,
|
|---|
| 664 | )} should not end with "/"`,
|
|---|
| 665 | );
|
|---|
| 666 | }
|
|---|
| 667 | }
|
|---|
| 668 |
|
|---|
| 669 | /**
|
|---|
| 670 | * @param {ExportsField} exportsField the exports field
|
|---|
| 671 | * @returns {FieldProcessor} process callback
|
|---|
| 672 | */
|
|---|
| 673 | module.exports.processExportsField = function processExportsField(
|
|---|
| 674 | exportsField,
|
|---|
| 675 | ) {
|
|---|
| 676 | return createFieldProcessor(
|
|---|
| 677 | buildExportsField(exportsField),
|
|---|
| 678 | (request) => (request.length === 0 ? "." : `./${request}`),
|
|---|
| 679 | assertExportsFieldRequest,
|
|---|
| 680 | assertExportTarget,
|
|---|
| 681 | );
|
|---|
| 682 | };
|
|---|
| 683 |
|
|---|
| 684 | /**
|
|---|
| 685 | * @param {string} request request
|
|---|
| 686 | * @returns {string} updated request
|
|---|
| 687 | */
|
|---|
| 688 | function assertImportsFieldRequest(request) {
|
|---|
| 689 | if (request.charCodeAt(0) !== hashCode) {
|
|---|
| 690 | throw new Error('Request should start with "#"');
|
|---|
| 691 | }
|
|---|
| 692 | if (request.length === 1) {
|
|---|
| 693 | throw new Error("Request should have at least 2 characters");
|
|---|
| 694 | }
|
|---|
| 695 | // Note: #/ patterns are now allowed per Node.js PR #60864
|
|---|
| 696 | // https://github.com/nodejs/node/pull/60864
|
|---|
| 697 | if (request.charCodeAt(request.length - 1) === slashCode) {
|
|---|
| 698 | throw new Error("Only requesting file allowed");
|
|---|
| 699 | }
|
|---|
| 700 |
|
|---|
| 701 | return request.slice(1);
|
|---|
| 702 | }
|
|---|
| 703 |
|
|---|
| 704 | /**
|
|---|
| 705 | * @param {string} imp import target
|
|---|
| 706 | * @param {boolean} expectFolder is folder expected
|
|---|
| 707 | */
|
|---|
| 708 | function assertImportTarget(imp, expectFolder) {
|
|---|
| 709 | const parsedIdentifier = parseIdentifier(imp);
|
|---|
| 710 |
|
|---|
| 711 | if (!parsedIdentifier) {
|
|---|
| 712 | return;
|
|---|
| 713 | }
|
|---|
| 714 |
|
|---|
| 715 | const [relativePath] = parsedIdentifier;
|
|---|
| 716 | const isFolder =
|
|---|
| 717 | relativePath.charCodeAt(relativePath.length - 1) === slashCode;
|
|---|
| 718 |
|
|---|
| 719 | if (isFolder !== expectFolder) {
|
|---|
| 720 | throw new Error(
|
|---|
| 721 | expectFolder
|
|---|
| 722 | ? `Expecting folder to folder mapping. ${JSON.stringify(
|
|---|
| 723 | imp,
|
|---|
| 724 | )} should end with "/"`
|
|---|
| 725 | : `Expecting file to file mapping. ${JSON.stringify(
|
|---|
| 726 | imp,
|
|---|
| 727 | )} should not end with "/"`,
|
|---|
| 728 | );
|
|---|
| 729 | }
|
|---|
| 730 | }
|
|---|
| 731 |
|
|---|
| 732 | /**
|
|---|
| 733 | * @param {ImportsField} importsField the exports field
|
|---|
| 734 | * @returns {FieldProcessor} process callback
|
|---|
| 735 | */
|
|---|
| 736 | module.exports.processImportsField = function processImportsField(
|
|---|
| 737 | importsField,
|
|---|
| 738 | ) {
|
|---|
| 739 | return createFieldProcessor(
|
|---|
| 740 | importsField,
|
|---|
| 741 | (request) => `#${request}`,
|
|---|
| 742 | assertImportsFieldRequest,
|
|---|
| 743 | assertImportTarget,
|
|---|
| 744 | );
|
|---|
| 745 | };
|
|---|