entrypoints.js 14 KB

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