entrypoints.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. /** @typedef {string|(string|ConditionalMapping)[]} DirectMapping */
  7. /** @typedef {{[k: string]: MappingValue}} ConditionalMapping */
  8. /** @typedef {ConditionalMapping|DirectMapping|null} MappingValue */
  9. /** @typedef {Record<string, MappingValue>|ConditionalMapping|DirectMapping} ExportsField */
  10. /** @typedef {Record<string, MappingValue>} ImportsField */
  11. /**
  12. * @typedef {Object} PathTreeNode
  13. * @property {Map<string, PathTreeNode>|null} children
  14. * @property {MappingValue} folder
  15. * @property {Map<string, MappingValue>|null} wildcards
  16. * @property {Map<string, MappingValue>} files
  17. */
  18. /**
  19. * Processing exports/imports field
  20. * @callback FieldProcessor
  21. * @param {string} request request
  22. * @param {Set<string>} conditionNames condition names
  23. * @returns {string[]} resolved paths
  24. */
  25. /*
  26. Example exports field:
  27. {
  28. ".": "./main.js",
  29. "./feature": {
  30. "browser": "./feature-browser.js",
  31. "default": "./feature.js"
  32. }
  33. }
  34. Terminology:
  35. Enhanced-resolve name keys ("." and "./feature") as exports field keys.
  36. If value is string or string[], mapping is called as a direct mapping
  37. and value called as a direct export.
  38. If value is key-value object, mapping is called as a conditional mapping
  39. and value called as a conditional export.
  40. Key in conditional mapping is called condition name.
  41. Conditional mapping nested in another conditional mapping is called nested mapping.
  42. ----------
  43. Example imports field:
  44. {
  45. "#a": "./main.js",
  46. "#moment": {
  47. "browser": "./moment/index.js",
  48. "default": "moment"
  49. },
  50. "#moment/": {
  51. "browser": "./moment/",
  52. "default": "moment/"
  53. }
  54. }
  55. Terminology:
  56. Enhanced-resolve name keys ("#a" and "#moment/", "#moment") as imports field keys.
  57. If value is string or string[], mapping is called as a direct mapping
  58. and value called as a direct export.
  59. If value is key-value object, mapping is called as a conditional mapping
  60. and value called as a conditional export.
  61. Key in conditional mapping is called condition name.
  62. Conditional mapping nested in another conditional mapping is called nested mapping.
  63. */
  64. const slashCode = "/".charCodeAt(0);
  65. const dotCode = ".".charCodeAt(0);
  66. const hashCode = "#".charCodeAt(0);
  67. /**
  68. * @param {ExportsField} exportsField the exports field
  69. * @returns {FieldProcessor} process callback
  70. */
  71. module.exports.processExportsField = function processExportsField(
  72. exportsField
  73. ) {
  74. return createFieldProcessor(
  75. buildExportsFieldPathTree(exportsField),
  76. assertExportsFieldRequest,
  77. assertExportTarget
  78. );
  79. };
  80. /**
  81. * @param {ImportsField} importsField the exports field
  82. * @returns {FieldProcessor} process callback
  83. */
  84. module.exports.processImportsField = function processImportsField(
  85. importsField
  86. ) {
  87. return createFieldProcessor(
  88. buildImportsFieldPathTree(importsField),
  89. assertImportsFieldRequest,
  90. assertImportTarget
  91. );
  92. };
  93. /**
  94. * @param {PathTreeNode} treeRoot root
  95. * @param {(s: string) => string} assertRequest assertRequest
  96. * @param {(s: string, f: boolean) => void} assertTarget assertTarget
  97. * @returns {FieldProcessor} field processor
  98. */
  99. function createFieldProcessor(treeRoot, assertRequest, assertTarget) {
  100. return function fieldProcessor(request, conditionNames) {
  101. request = assertRequest(request);
  102. const match = findMatch(request, treeRoot);
  103. if (match === null) return [];
  104. const [mapping, remainRequestIndex] = match;
  105. /** @type {DirectMapping|null} */
  106. let direct = null;
  107. if (isConditionalMapping(mapping)) {
  108. direct = conditionalMapping(
  109. /** @type {ConditionalMapping} */ (mapping),
  110. conditionNames
  111. );
  112. // matching not found
  113. if (direct === null) return [];
  114. } else {
  115. direct = /** @type {DirectMapping} */ (mapping);
  116. }
  117. const remainingRequest =
  118. remainRequestIndex === request.length + 1
  119. ? undefined
  120. : remainRequestIndex < 0
  121. ? request.slice(-remainRequestIndex - 1)
  122. : request.slice(remainRequestIndex);
  123. return directMapping(
  124. remainingRequest,
  125. remainRequestIndex < 0,
  126. direct,
  127. conditionNames,
  128. assertTarget
  129. );
  130. };
  131. }
  132. /**
  133. * @param {string} request request
  134. * @returns {string} updated request
  135. */
  136. function assertExportsFieldRequest(request) {
  137. if (request.charCodeAt(0) !== dotCode) {
  138. throw new Error('Request should be relative path and start with "."');
  139. }
  140. if (request.length === 1) return "";
  141. if (request.charCodeAt(1) !== slashCode) {
  142. throw new Error('Request should be relative path and start with "./"');
  143. }
  144. if (request.charCodeAt(request.length - 1) === slashCode) {
  145. throw new Error("Only requesting file allowed");
  146. }
  147. return request.slice(2);
  148. }
  149. /**
  150. * @param {string} request request
  151. * @returns {string} updated request
  152. */
  153. function assertImportsFieldRequest(request) {
  154. if (request.charCodeAt(0) !== hashCode) {
  155. throw new Error('Request should start with "#"');
  156. }
  157. if (request.length === 1) {
  158. throw new Error("Request should have at least 2 characters");
  159. }
  160. if (request.charCodeAt(1) === slashCode) {
  161. throw new Error('Request should not start with "#/"');
  162. }
  163. if (request.charCodeAt(request.length - 1) === slashCode) {
  164. throw new Error("Only requesting file allowed");
  165. }
  166. return request.slice(1);
  167. }
  168. /**
  169. * @param {string} exp export target
  170. * @param {boolean} expectFolder is folder expected
  171. */
  172. function assertExportTarget(exp, expectFolder) {
  173. if (
  174. exp.charCodeAt(0) === slashCode ||
  175. (exp.charCodeAt(0) === dotCode && exp.charCodeAt(1) !== slashCode)
  176. ) {
  177. throw new Error(
  178. `Export should be relative path and start with "./", got ${JSON.stringify(
  179. exp
  180. )}.`
  181. );
  182. }
  183. const isFolder = exp.charCodeAt(exp.length - 1) === slashCode;
  184. if (isFolder !== expectFolder) {
  185. throw new Error(
  186. expectFolder
  187. ? `Expecting folder to folder mapping. ${JSON.stringify(
  188. exp
  189. )} should end with "/"`
  190. : `Expecting file to file mapping. ${JSON.stringify(
  191. exp
  192. )} should not end with "/"`
  193. );
  194. }
  195. }
  196. /**
  197. * @param {string} imp import target
  198. * @param {boolean} expectFolder is folder expected
  199. */
  200. function assertImportTarget(imp, expectFolder) {
  201. const isFolder = imp.charCodeAt(imp.length - 1) === slashCode;
  202. if (isFolder !== expectFolder) {
  203. throw new Error(
  204. expectFolder
  205. ? `Expecting folder to folder mapping. ${JSON.stringify(
  206. imp
  207. )} should end with "/"`
  208. : `Expecting file to file mapping. ${JSON.stringify(
  209. imp
  210. )} should not end with "/"`
  211. );
  212. }
  213. }
  214. /**
  215. * Trying to match request to field
  216. * @param {string} request request
  217. * @param {PathTreeNode} treeRoot path tree root
  218. * @returns {[MappingValue, number]|null} match or null, number is negative and one less when it's a folder mapping, number is request.length + 1 for direct mappings
  219. */
  220. function findMatch(request, treeRoot) {
  221. if (request.length === 0) {
  222. const value = treeRoot.files.get("");
  223. return value ? [value, 1] : null;
  224. }
  225. if (
  226. treeRoot.children === null &&
  227. treeRoot.folder === null &&
  228. treeRoot.wildcards === null
  229. ) {
  230. const value = treeRoot.files.get(request);
  231. return value ? [value, request.length + 1] : null;
  232. }
  233. let node = treeRoot;
  234. let lastNonSlashIndex = 0;
  235. let slashIndex = request.indexOf("/", 0);
  236. /** @type {[MappingValue, number]|null} */
  237. let lastFolderMatch = null;
  238. const applyFolderMapping = () => {
  239. const folderMapping = node.folder;
  240. if (folderMapping) {
  241. if (lastFolderMatch) {
  242. lastFolderMatch[0] = folderMapping;
  243. lastFolderMatch[1] = -lastNonSlashIndex - 1;
  244. } else {
  245. lastFolderMatch = [folderMapping, -lastNonSlashIndex - 1];
  246. }
  247. }
  248. };
  249. const applyWildcardMappings = (wildcardMappings, remainingRequest) => {
  250. if (wildcardMappings) {
  251. for (const [key, target] of wildcardMappings) {
  252. if (remainingRequest.startsWith(key)) {
  253. if (!lastFolderMatch) {
  254. lastFolderMatch = [target, lastNonSlashIndex + key.length];
  255. } else if (lastFolderMatch[1] < lastNonSlashIndex + key.length) {
  256. lastFolderMatch[0] = target;
  257. lastFolderMatch[1] = lastNonSlashIndex + key.length;
  258. }
  259. }
  260. }
  261. }
  262. };
  263. while (slashIndex !== -1) {
  264. applyFolderMapping();
  265. const wildcardMappings = node.wildcards;
  266. if (!wildcardMappings && node.children === null) return lastFolderMatch;
  267. const folder = request.slice(lastNonSlashIndex, slashIndex);
  268. applyWildcardMappings(wildcardMappings, folder);
  269. if (node.children === null) return lastFolderMatch;
  270. const newNode = node.children.get(folder);
  271. if (!newNode) {
  272. return lastFolderMatch;
  273. }
  274. node = newNode;
  275. lastNonSlashIndex = slashIndex + 1;
  276. slashIndex = request.indexOf("/", lastNonSlashIndex);
  277. }
  278. const remainingRequest =
  279. lastNonSlashIndex > 0 ? request.slice(lastNonSlashIndex) : request;
  280. const value = node.files.get(remainingRequest);
  281. if (value) {
  282. return [value, request.length + 1];
  283. }
  284. applyFolderMapping();
  285. applyWildcardMappings(node.wildcards, remainingRequest);
  286. return lastFolderMatch;
  287. }
  288. /**
  289. * @param {ConditionalMapping|DirectMapping|null} mapping mapping
  290. * @returns {boolean} is conditional mapping
  291. */
  292. function isConditionalMapping(mapping) {
  293. return (
  294. mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
  295. );
  296. }
  297. /**
  298. * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
  299. * @param {boolean} subpathMapping true, for subpath mappings
  300. * @param {DirectMapping|null} mappingTarget direct export
  301. * @param {Set<string>} conditionNames condition names
  302. * @param {(d: string, f: boolean) => void} assert asserting direct value
  303. * @returns {string[]} mapping result
  304. */
  305. function directMapping(
  306. remainingRequest,
  307. subpathMapping,
  308. mappingTarget,
  309. conditionNames,
  310. assert
  311. ) {
  312. if (mappingTarget === null) return [];
  313. if (typeof mappingTarget === "string") {
  314. return [
  315. targetMapping(remainingRequest, subpathMapping, mappingTarget, assert)
  316. ];
  317. }
  318. const targets = [];
  319. for (const exp of mappingTarget) {
  320. if (typeof exp === "string") {
  321. targets.push(
  322. targetMapping(remainingRequest, subpathMapping, exp, assert)
  323. );
  324. continue;
  325. }
  326. const mapping = conditionalMapping(exp, conditionNames);
  327. if (!mapping) continue;
  328. const innerExports = directMapping(
  329. remainingRequest,
  330. subpathMapping,
  331. mapping,
  332. conditionNames,
  333. assert
  334. );
  335. for (const innerExport of innerExports) {
  336. targets.push(innerExport);
  337. }
  338. }
  339. return targets;
  340. }
  341. /**
  342. * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
  343. * @param {boolean} subpathMapping true, for subpath mappings
  344. * @param {string} mappingTarget direct export
  345. * @param {(d: string, f: boolean) => void} assert asserting direct value
  346. * @returns {string} mapping result
  347. */
  348. function targetMapping(
  349. remainingRequest,
  350. subpathMapping,
  351. mappingTarget,
  352. assert
  353. ) {
  354. if (remainingRequest === undefined) {
  355. assert(mappingTarget, false);
  356. return mappingTarget;
  357. }
  358. if (subpathMapping) {
  359. assert(mappingTarget, true);
  360. return mappingTarget + remainingRequest;
  361. }
  362. assert(mappingTarget, false);
  363. return mappingTarget.replace(/\*/g, remainingRequest.replace(/\$/g, "$$"));
  364. }
  365. /**
  366. * @param {ConditionalMapping} conditionalMapping_ conditional mapping
  367. * @param {Set<string>} conditionNames condition names
  368. * @returns {DirectMapping|null} direct mapping if found
  369. */
  370. function conditionalMapping(conditionalMapping_, conditionNames) {
  371. /** @type {[ConditionalMapping, string[], number][]} */
  372. let lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]];
  373. loop: while (lookup.length > 0) {
  374. const [mapping, conditions, j] = lookup[lookup.length - 1];
  375. const last = conditions.length - 1;
  376. for (let i = j; i < conditions.length; i++) {
  377. const condition = conditions[i];
  378. // assert default. Could be last only
  379. if (i !== last) {
  380. if (condition === "default") {
  381. throw new Error("Default condition should be last one");
  382. }
  383. } else if (condition === "default") {
  384. const innerMapping = mapping[condition];
  385. // is nested
  386. if (isConditionalMapping(innerMapping)) {
  387. const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping);
  388. lookup[lookup.length - 1][2] = i + 1;
  389. lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
  390. continue loop;
  391. }
  392. return /** @type {DirectMapping} */ (innerMapping);
  393. }
  394. if (conditionNames.has(condition)) {
  395. const innerMapping = mapping[condition];
  396. // is nested
  397. if (isConditionalMapping(innerMapping)) {
  398. const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping);
  399. lookup[lookup.length - 1][2] = i + 1;
  400. lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
  401. continue loop;
  402. }
  403. return /** @type {DirectMapping} */ (innerMapping);
  404. }
  405. }
  406. lookup.pop();
  407. }
  408. return null;
  409. }
  410. /**
  411. * Internal helper to create path tree node
  412. * to ensure that each node gets the same hidden class
  413. * @returns {PathTreeNode} node
  414. */
  415. function createNode() {
  416. return {
  417. children: null,
  418. folder: null,
  419. wildcards: null,
  420. files: new Map()
  421. };
  422. }
  423. /**
  424. * Internal helper for building path tree
  425. * @param {PathTreeNode} root root
  426. * @param {string} path path
  427. * @param {MappingValue} target target
  428. */
  429. function walkPath(root, path, target) {
  430. if (path.length === 0) {
  431. root.folder = target;
  432. return;
  433. }
  434. let node = root;
  435. // Typical path tree can looks like
  436. // root
  437. // - files: ["a.js", "b.js"]
  438. // - children:
  439. // node1:
  440. // - files: ["a.js", "b.js"]
  441. let lastNonSlashIndex = 0;
  442. let slashIndex = path.indexOf("/", 0);
  443. while (slashIndex !== -1) {
  444. const folder = path.slice(lastNonSlashIndex, slashIndex);
  445. let newNode;
  446. if (node.children === null) {
  447. newNode = createNode();
  448. node.children = new Map();
  449. node.children.set(folder, newNode);
  450. } else {
  451. newNode = node.children.get(folder);
  452. if (!newNode) {
  453. newNode = createNode();
  454. node.children.set(folder, newNode);
  455. }
  456. }
  457. node = newNode;
  458. lastNonSlashIndex = slashIndex + 1;
  459. slashIndex = path.indexOf("/", lastNonSlashIndex);
  460. }
  461. if (lastNonSlashIndex >= path.length) {
  462. node.folder = target;
  463. } else {
  464. const file = lastNonSlashIndex > 0 ? path.slice(lastNonSlashIndex) : path;
  465. if (file.endsWith("*")) {
  466. if (node.wildcards === null) node.wildcards = new Map();
  467. node.wildcards.set(file.slice(0, -1), target);
  468. } else {
  469. node.files.set(file, target);
  470. }
  471. }
  472. }
  473. /**
  474. * @param {ExportsField} field exports field
  475. * @returns {PathTreeNode} tree root
  476. */
  477. function buildExportsFieldPathTree(field) {
  478. const root = createNode();
  479. // handle syntax sugar, if exports field is direct mapping for "."
  480. if (typeof field === "string") {
  481. root.files.set("", field);
  482. return root;
  483. } else if (Array.isArray(field)) {
  484. root.files.set("", field.slice());
  485. return root;
  486. }
  487. const keys = Object.keys(field);
  488. for (let i = 0; i < keys.length; i++) {
  489. const key = keys[i];
  490. if (key.charCodeAt(0) !== dotCode) {
  491. // handle syntax sugar, if exports field is conditional mapping for "."
  492. if (i === 0) {
  493. while (i < keys.length) {
  494. const charCode = keys[i].charCodeAt(0);
  495. if (charCode === dotCode || charCode === slashCode) {
  496. throw new Error(
  497. `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
  498. key
  499. )})`
  500. );
  501. }
  502. i++;
  503. }
  504. root.files.set("", field);
  505. return root;
  506. }
  507. throw new Error(
  508. `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
  509. key
  510. )})`
  511. );
  512. }
  513. if (key.length === 1) {
  514. root.files.set("", field[key]);
  515. continue;
  516. }
  517. if (key.charCodeAt(1) !== slashCode) {
  518. throw new Error(
  519. `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
  520. key
  521. )})`
  522. );
  523. }
  524. walkPath(root, key.slice(2), field[key]);
  525. }
  526. return root;
  527. }
  528. /**
  529. * @param {ImportsField} field imports field
  530. * @returns {PathTreeNode} root
  531. */
  532. function buildImportsFieldPathTree(field) {
  533. const root = createNode();
  534. const keys = Object.keys(field);
  535. for (let i = 0; i < keys.length; i++) {
  536. const key = keys[i];
  537. if (key.charCodeAt(0) !== hashCode) {
  538. throw new Error(
  539. `Imports field key should start with "#" (key: ${JSON.stringify(key)})`
  540. );
  541. }
  542. if (key.length === 1) {
  543. throw new Error(
  544. `Imports field key should have at least 2 characters (key: ${JSON.stringify(
  545. key
  546. )})`
  547. );
  548. }
  549. if (key.charCodeAt(1) === slashCode) {
  550. throw new Error(
  551. `Imports field key should not start with "#/" (key: ${JSON.stringify(
  552. key
  553. )})`
  554. );
  555. }
  556. walkPath(root, key.slice(1), field[key]);
  557. }
  558. return root;
  559. }