entrypoints.js 21 KB

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