entrypoints.js 15 KB

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