entrypoints.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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. /**
  63. * @param {string} a first string
  64. * @param {string} b second string
  65. * @returns {number} compare result
  66. */
  67. function patternKeyCompare(a, b) {
  68. const aPatternIndex = a.indexOf("*");
  69. const bPatternIndex = b.indexOf("*");
  70. const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
  71. const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
  72. if (baseLenA > baseLenB) return -1;
  73. if (baseLenB > baseLenA) return 1;
  74. if (aPatternIndex === -1) return 1;
  75. if (bPatternIndex === -1) return -1;
  76. if (a.length > b.length) return -1;
  77. if (b.length > a.length) return 1;
  78. return 0;
  79. }
  80. /**
  81. * Trying to match request to field
  82. * @param {string} request request
  83. * @param {ExportsField | ImportsField} field exports or import field
  84. * @returns {[MappingValue, string, boolean, boolean, string] | null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
  85. */
  86. function findMatch(request, field) {
  87. if (
  88. Object.prototype.hasOwnProperty.call(field, request) &&
  89. !request.includes("*") &&
  90. !request.endsWith("/")
  91. ) {
  92. const target = /** @type {{ [k: string]: MappingValue }} */ (field)[
  93. request
  94. ];
  95. return [target, "", false, false, request];
  96. }
  97. /** @type {string} */
  98. let bestMatch = "";
  99. /** @type {string | undefined} */
  100. let bestMatchSubpath;
  101. const keys = Object.getOwnPropertyNames(field);
  102. for (let i = 0; i < keys.length; i++) {
  103. const key = keys[i];
  104. const patternIndex = key.indexOf("*");
  105. if (patternIndex !== -1 && request.startsWith(key.slice(0, patternIndex))) {
  106. const patternTrailer = key.slice(patternIndex + 1);
  107. if (
  108. request.length >= key.length &&
  109. request.endsWith(patternTrailer) &&
  110. patternKeyCompare(bestMatch, key) === 1 &&
  111. key.lastIndexOf("*") === patternIndex
  112. ) {
  113. bestMatch = key;
  114. bestMatchSubpath = request.slice(
  115. patternIndex,
  116. request.length - patternTrailer.length,
  117. );
  118. }
  119. }
  120. // For legacy `./foo/`
  121. else if (
  122. key[key.length - 1] === "/" &&
  123. request.startsWith(key) &&
  124. patternKeyCompare(bestMatch, key) === 1
  125. ) {
  126. bestMatch = key;
  127. bestMatchSubpath = request.slice(key.length);
  128. }
  129. }
  130. if (bestMatch === "") return null;
  131. const target =
  132. /** @type {{ [k: string]: MappingValue }} */
  133. (field)[bestMatch];
  134. const isSubpathMapping = bestMatch.endsWith("/");
  135. const isPattern = bestMatch.includes("*");
  136. return [
  137. target,
  138. /** @type {string} */ (bestMatchSubpath),
  139. isSubpathMapping,
  140. isPattern,
  141. bestMatch,
  142. ];
  143. }
  144. /**
  145. * @param {ConditionalMapping | DirectMapping | null} mapping mapping
  146. * @returns {boolean} is conditional mapping
  147. */
  148. function isConditionalMapping(mapping) {
  149. return (
  150. mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
  151. );
  152. }
  153. /**
  154. * @param {ConditionalMapping} conditionalMapping_ conditional mapping
  155. * @param {Set<string>} conditionNames condition names
  156. * @returns {DirectMapping | null} direct mapping if found
  157. */
  158. function conditionalMapping(conditionalMapping_, conditionNames) {
  159. /** @type {[ConditionalMapping, string[], number][]} */
  160. const lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]];
  161. loop: while (lookup.length > 0) {
  162. const [mapping, conditions, j] = lookup[lookup.length - 1];
  163. for (let i = j; i < conditions.length; i++) {
  164. const condition = conditions[i];
  165. if (condition === "default") {
  166. const innerMapping = mapping[condition];
  167. // is nested
  168. if (isConditionalMapping(innerMapping)) {
  169. const conditionalMapping = /** @type {ConditionalMapping} */ (
  170. innerMapping
  171. );
  172. lookup[lookup.length - 1][2] = i + 1;
  173. lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
  174. continue loop;
  175. }
  176. return /** @type {DirectMapping} */ (innerMapping);
  177. }
  178. if (conditionNames.has(condition)) {
  179. const innerMapping = mapping[condition];
  180. // is nested
  181. if (isConditionalMapping(innerMapping)) {
  182. const conditionalMapping = /** @type {ConditionalMapping} */ (
  183. innerMapping
  184. );
  185. lookup[lookup.length - 1][2] = i + 1;
  186. lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
  187. continue loop;
  188. }
  189. return /** @type {DirectMapping} */ (innerMapping);
  190. }
  191. }
  192. lookup.pop();
  193. }
  194. return null;
  195. }
  196. /**
  197. * @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
  198. * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
  199. * @param {boolean} isSubpathMapping true, for subpath mappings
  200. * @param {string} mappingTarget direct export
  201. * @param {(d: string, f: boolean) => void} assert asserting direct value
  202. * @returns {string} mapping result
  203. */
  204. function targetMapping(
  205. remainingRequest,
  206. isPattern,
  207. isSubpathMapping,
  208. mappingTarget,
  209. assert,
  210. ) {
  211. if (remainingRequest === undefined) {
  212. assert(mappingTarget, false);
  213. return mappingTarget;
  214. }
  215. if (isSubpathMapping) {
  216. assert(mappingTarget, true);
  217. return mappingTarget + remainingRequest;
  218. }
  219. assert(mappingTarget, false);
  220. let result = mappingTarget;
  221. if (isPattern) {
  222. result = result.replace(
  223. patternRegEx,
  224. remainingRequest.replace(/\$/g, "$$"),
  225. );
  226. }
  227. return result;
  228. }
  229. /**
  230. * @param {string | undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
  231. * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
  232. * @param {boolean} isSubpathMapping true, for subpath mappings
  233. * @param {DirectMapping | null} mappingTarget direct export
  234. * @param {Set<string>} conditionNames condition names
  235. * @param {(d: string, f: boolean) => void} assert asserting direct value
  236. * @returns {string[]} mapping result
  237. */
  238. function directMapping(
  239. remainingRequest,
  240. isPattern,
  241. isSubpathMapping,
  242. mappingTarget,
  243. conditionNames,
  244. assert,
  245. ) {
  246. if (mappingTarget === null) return [];
  247. if (typeof mappingTarget === "string") {
  248. return [
  249. targetMapping(
  250. remainingRequest,
  251. isPattern,
  252. isSubpathMapping,
  253. mappingTarget,
  254. assert,
  255. ),
  256. ];
  257. }
  258. /** @type {string[]} */
  259. const targets = [];
  260. for (const exp of mappingTarget) {
  261. if (typeof exp === "string") {
  262. targets.push(
  263. targetMapping(
  264. remainingRequest,
  265. isPattern,
  266. isSubpathMapping,
  267. exp,
  268. assert,
  269. ),
  270. );
  271. continue;
  272. }
  273. const mapping = conditionalMapping(exp, conditionNames);
  274. if (!mapping) continue;
  275. const innerExports = directMapping(
  276. remainingRequest,
  277. isPattern,
  278. isSubpathMapping,
  279. mapping,
  280. conditionNames,
  281. assert,
  282. );
  283. for (const innerExport of innerExports) {
  284. targets.push(innerExport);
  285. }
  286. }
  287. return targets;
  288. }
  289. /**
  290. * @param {ExportsField | ImportsField} field root
  291. * @param {(s: string) => string} normalizeRequest Normalize request, for `imports` field it adds `#`, for `exports` field it adds `.` or `./`
  292. * @param {(s: string) => string} assertRequest assertRequest
  293. * @param {(s: string, f: boolean) => void} assertTarget assertTarget
  294. * @returns {FieldProcessor} field processor
  295. */
  296. function createFieldProcessor(
  297. field,
  298. normalizeRequest,
  299. assertRequest,
  300. assertTarget,
  301. ) {
  302. return function fieldProcessor(request, conditionNames) {
  303. request = assertRequest(request);
  304. const match = findMatch(normalizeRequest(request), field);
  305. if (match === null) return [[], null];
  306. const [mapping, remainingRequest, isSubpathMapping, isPattern, usedField] =
  307. match;
  308. /** @type {DirectMapping | null} */
  309. let direct = null;
  310. if (isConditionalMapping(mapping)) {
  311. direct = conditionalMapping(
  312. /** @type {ConditionalMapping} */ (mapping),
  313. conditionNames,
  314. );
  315. // matching not found
  316. if (direct === null) return [[], null];
  317. } else {
  318. direct = /** @type {DirectMapping} */ (mapping);
  319. }
  320. return [
  321. directMapping(
  322. remainingRequest,
  323. isPattern,
  324. isSubpathMapping,
  325. direct,
  326. conditionNames,
  327. assertTarget,
  328. ),
  329. usedField,
  330. ];
  331. };
  332. }
  333. /**
  334. * @param {string} request request
  335. * @returns {string} updated request
  336. */
  337. function assertExportsFieldRequest(request) {
  338. if (request.charCodeAt(0) !== dotCode) {
  339. throw new Error('Request should be relative path and start with "."');
  340. }
  341. if (request.length === 1) return "";
  342. if (request.charCodeAt(1) !== slashCode) {
  343. throw new Error('Request should be relative path and start with "./"');
  344. }
  345. if (request.charCodeAt(request.length - 1) === slashCode) {
  346. throw new Error("Only requesting file allowed");
  347. }
  348. return request.slice(2);
  349. }
  350. /**
  351. * @param {ExportsField} field exports field
  352. * @returns {ExportsField} normalized exports field
  353. */
  354. function buildExportsField(field) {
  355. // handle syntax sugar, if exports field is direct mapping for "."
  356. if (typeof field === "string" || Array.isArray(field)) {
  357. return { ".": field };
  358. }
  359. const keys = Object.keys(field);
  360. for (let i = 0; i < keys.length; i++) {
  361. const key = keys[i];
  362. if (key.charCodeAt(0) !== dotCode) {
  363. // handle syntax sugar, if exports field is conditional mapping for "."
  364. if (i === 0) {
  365. while (i < keys.length) {
  366. const charCode = keys[i].charCodeAt(0);
  367. if (charCode === dotCode || charCode === slashCode) {
  368. throw new Error(
  369. `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
  370. key,
  371. )})`,
  372. );
  373. }
  374. i++;
  375. }
  376. return { ".": field };
  377. }
  378. throw new Error(
  379. `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
  380. key,
  381. )})`,
  382. );
  383. }
  384. if (key.length === 1) {
  385. continue;
  386. }
  387. if (key.charCodeAt(1) !== slashCode) {
  388. throw new Error(
  389. `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
  390. key,
  391. )})`,
  392. );
  393. }
  394. }
  395. return field;
  396. }
  397. /**
  398. * @param {string} exp export target
  399. * @param {boolean} expectFolder is folder expected
  400. */
  401. function assertExportTarget(exp, expectFolder) {
  402. const parsedIdentifier = parseIdentifier(exp);
  403. if (!parsedIdentifier) {
  404. return;
  405. }
  406. const [relativePath] = parsedIdentifier;
  407. const isFolder =
  408. relativePath.charCodeAt(relativePath.length - 1) === slashCode;
  409. if (isFolder !== expectFolder) {
  410. throw new Error(
  411. expectFolder
  412. ? `Expecting folder to folder mapping. ${JSON.stringify(
  413. exp,
  414. )} should end with "/"`
  415. : `Expecting file to file mapping. ${JSON.stringify(
  416. exp,
  417. )} should not end with "/"`,
  418. );
  419. }
  420. }
  421. /**
  422. * @param {ExportsField} exportsField the exports field
  423. * @returns {FieldProcessor} process callback
  424. */
  425. module.exports.processExportsField = function processExportsField(
  426. exportsField,
  427. ) {
  428. return createFieldProcessor(
  429. buildExportsField(exportsField),
  430. (request) => (request.length === 0 ? "." : `./${request}`),
  431. assertExportsFieldRequest,
  432. assertExportTarget,
  433. );
  434. };
  435. /**
  436. * @param {string} request request
  437. * @returns {string} updated request
  438. */
  439. function assertImportsFieldRequest(request) {
  440. if (request.charCodeAt(0) !== hashCode) {
  441. throw new Error('Request should start with "#"');
  442. }
  443. if (request.length === 1) {
  444. throw new Error("Request should have at least 2 characters");
  445. }
  446. // Note: #/ patterns are now allowed per Node.js PR #60864
  447. // https://github.com/nodejs/node/pull/60864
  448. if (request.charCodeAt(request.length - 1) === slashCode) {
  449. throw new Error("Only requesting file allowed");
  450. }
  451. return request.slice(1);
  452. }
  453. /**
  454. * @param {string} imp import target
  455. * @param {boolean} expectFolder is folder expected
  456. */
  457. function assertImportTarget(imp, expectFolder) {
  458. const parsedIdentifier = parseIdentifier(imp);
  459. if (!parsedIdentifier) {
  460. return;
  461. }
  462. const [relativePath] = parsedIdentifier;
  463. const isFolder =
  464. relativePath.charCodeAt(relativePath.length - 1) === slashCode;
  465. if (isFolder !== expectFolder) {
  466. throw new Error(
  467. expectFolder
  468. ? `Expecting folder to folder mapping. ${JSON.stringify(
  469. imp,
  470. )} should end with "/"`
  471. : `Expecting file to file mapping. ${JSON.stringify(
  472. imp,
  473. )} should not end with "/"`,
  474. );
  475. }
  476. }
  477. /**
  478. * @param {ImportsField} importsField the exports field
  479. * @returns {FieldProcessor} process callback
  480. */
  481. module.exports.processImportsField = function processImportsField(
  482. importsField,
  483. ) {
  484. return createFieldProcessor(
  485. importsField,
  486. (request) => `#${request}`,
  487. assertImportsFieldRequest,
  488. assertImportTarget,
  489. );
  490. };