entrypoints.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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. function patternKeyCompare(a, b) {
  212. const aPatternIndex = a.indexOf("*");
  213. const bPatternIndex = b.indexOf("*");
  214. const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
  215. const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
  216. if (baseLenA > baseLenB) return -1;
  217. if (baseLenB > baseLenA) return 1;
  218. if (aPatternIndex === -1) return 1;
  219. if (bPatternIndex === -1) return -1;
  220. if (a.length > b.length) return -1;
  221. if (b.length > a.length) return 1;
  222. return 0;
  223. }
  224. /**
  225. * Trying to match request to field
  226. * @param {string} request request
  227. * @param {ExportsField | ImportsField} field exports or import field
  228. * @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
  229. */
  230. function findMatch(request, field) {
  231. if (
  232. Object.prototype.hasOwnProperty.call(field, request) &&
  233. !request.includes("*") &&
  234. !request.endsWith("/")
  235. ) {
  236. const target = field[request];
  237. return [target, "", false, false];
  238. }
  239. let bestMatch = "";
  240. let bestMatchSubpath;
  241. const keys = Object.getOwnPropertyNames(field);
  242. for (let i = 0; i < keys.length; i++) {
  243. const key = keys[i];
  244. const patternIndex = key.indexOf("*");
  245. if (patternIndex !== -1 && request.startsWith(key.slice(0, patternIndex))) {
  246. const patternTrailer = key.slice(patternIndex + 1);
  247. if (
  248. request.length >= key.length &&
  249. request.endsWith(patternTrailer) &&
  250. patternKeyCompare(bestMatch, key) === 1 &&
  251. key.lastIndexOf("*") === patternIndex
  252. ) {
  253. bestMatch = key;
  254. bestMatchSubpath = request.slice(
  255. patternIndex,
  256. request.length - patternTrailer.length
  257. );
  258. }
  259. }
  260. // For legacy `./foo/`
  261. else if (
  262. key[key.length - 1] === "/" &&
  263. request.startsWith(key) &&
  264. patternKeyCompare(bestMatch, key) === 1
  265. ) {
  266. bestMatch = key;
  267. bestMatchSubpath = request.slice(key.length);
  268. }
  269. }
  270. if (bestMatch === "") return null;
  271. const target = field[bestMatch];
  272. const isSubpathMapping = bestMatch.endsWith("/");
  273. const isPattern = bestMatch.includes("*");
  274. return [
  275. target,
  276. /** @type {string} */ (bestMatchSubpath),
  277. isSubpathMapping,
  278. isPattern
  279. ];
  280. }
  281. /**
  282. * @param {ConditionalMapping|DirectMapping|null} mapping mapping
  283. * @returns {boolean} is conditional mapping
  284. */
  285. function isConditionalMapping(mapping) {
  286. return (
  287. mapping !== null && typeof mapping === "object" && !Array.isArray(mapping)
  288. );
  289. }
  290. /**
  291. * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
  292. * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
  293. * @param {boolean} isSubpathMapping true, for subpath mappings
  294. * @param {DirectMapping|null} mappingTarget direct export
  295. * @param {Set<string>} conditionNames condition names
  296. * @param {(d: string, f: boolean) => void} assert asserting direct value
  297. * @returns {string[]} mapping result
  298. */
  299. function directMapping(
  300. remainingRequest,
  301. isPattern,
  302. isSubpathMapping,
  303. mappingTarget,
  304. conditionNames,
  305. assert
  306. ) {
  307. if (mappingTarget === null) return [];
  308. if (typeof mappingTarget === "string") {
  309. return [
  310. targetMapping(
  311. remainingRequest,
  312. isPattern,
  313. isSubpathMapping,
  314. mappingTarget,
  315. assert
  316. )
  317. ];
  318. }
  319. const targets = [];
  320. for (const exp of mappingTarget) {
  321. if (typeof exp === "string") {
  322. targets.push(
  323. targetMapping(
  324. remainingRequest,
  325. isPattern,
  326. isSubpathMapping,
  327. exp,
  328. assert
  329. )
  330. );
  331. continue;
  332. }
  333. const mapping = conditionalMapping(exp, conditionNames);
  334. if (!mapping) continue;
  335. const innerExports = directMapping(
  336. remainingRequest,
  337. isPattern,
  338. isSubpathMapping,
  339. mapping,
  340. conditionNames,
  341. assert
  342. );
  343. for (const innerExport of innerExports) {
  344. targets.push(innerExport);
  345. }
  346. }
  347. return targets;
  348. }
  349. /**
  350. * @param {string|undefined} remainingRequest remaining request when folder mapping, undefined for file mappings
  351. * @param {boolean} isPattern true, if mapping is a pattern (contains "*")
  352. * @param {boolean} isSubpathMapping true, for subpath mappings
  353. * @param {string} mappingTarget direct export
  354. * @param {(d: string, f: boolean) => void} assert asserting direct value
  355. * @returns {string} mapping result
  356. */
  357. function targetMapping(
  358. remainingRequest,
  359. isPattern,
  360. isSubpathMapping,
  361. mappingTarget,
  362. assert
  363. ) {
  364. if (remainingRequest === undefined) {
  365. assert(mappingTarget, false);
  366. return mappingTarget;
  367. }
  368. if (isSubpathMapping) {
  369. assert(mappingTarget, true);
  370. return mappingTarget + remainingRequest;
  371. }
  372. assert(mappingTarget, false);
  373. let result = mappingTarget;
  374. if (isPattern) {
  375. result = result.replace(
  376. patternRegEx,
  377. remainingRequest.replace(/\$/g, "$$")
  378. );
  379. }
  380. return result;
  381. }
  382. /**
  383. * @param {ConditionalMapping} conditionalMapping_ conditional mapping
  384. * @param {Set<string>} conditionNames condition names
  385. * @returns {DirectMapping|null} direct mapping if found
  386. */
  387. function conditionalMapping(conditionalMapping_, conditionNames) {
  388. /** @type {[ConditionalMapping, string[], number][]} */
  389. let lookup = [[conditionalMapping_, Object.keys(conditionalMapping_), 0]];
  390. loop: while (lookup.length > 0) {
  391. const [mapping, conditions, j] = lookup[lookup.length - 1];
  392. const last = conditions.length - 1;
  393. for (let i = j; i < conditions.length; i++) {
  394. const condition = conditions[i];
  395. // assert default. Could be last only
  396. if (i !== last) {
  397. if (condition === "default") {
  398. throw new Error("Default condition should be last one");
  399. }
  400. } else if (condition === "default") {
  401. const innerMapping = mapping[condition];
  402. // is nested
  403. if (isConditionalMapping(innerMapping)) {
  404. const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping);
  405. lookup[lookup.length - 1][2] = i + 1;
  406. lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
  407. continue loop;
  408. }
  409. return /** @type {DirectMapping} */ (innerMapping);
  410. }
  411. if (conditionNames.has(condition)) {
  412. const innerMapping = mapping[condition];
  413. // is nested
  414. if (isConditionalMapping(innerMapping)) {
  415. const conditionalMapping = /** @type {ConditionalMapping} */ (innerMapping);
  416. lookup[lookup.length - 1][2] = i + 1;
  417. lookup.push([conditionalMapping, Object.keys(conditionalMapping), 0]);
  418. continue loop;
  419. }
  420. return /** @type {DirectMapping} */ (innerMapping);
  421. }
  422. }
  423. lookup.pop();
  424. }
  425. return null;
  426. }
  427. /**
  428. * @param {ExportsField} field exports field
  429. * @returns {ExportsField} normalized exports field
  430. */
  431. function buildExportsField(field) {
  432. // handle syntax sugar, if exports field is direct mapping for "."
  433. if (typeof field === "string" || Array.isArray(field)) {
  434. return { ".": field };
  435. }
  436. const keys = Object.keys(field);
  437. for (let i = 0; i < keys.length; i++) {
  438. const key = keys[i];
  439. if (key.charCodeAt(0) !== dotCode) {
  440. // handle syntax sugar, if exports field is conditional mapping for "."
  441. if (i === 0) {
  442. while (i < keys.length) {
  443. const charCode = keys[i].charCodeAt(0);
  444. if (charCode === dotCode || charCode === slashCode) {
  445. throw new Error(
  446. `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
  447. key
  448. )})`
  449. );
  450. }
  451. i++;
  452. }
  453. return { ".": field };
  454. }
  455. throw new Error(
  456. `Exports field key should be relative path and start with "." (key: ${JSON.stringify(
  457. key
  458. )})`
  459. );
  460. }
  461. if (key.length === 1) {
  462. continue;
  463. }
  464. if (key.charCodeAt(1) !== slashCode) {
  465. throw new Error(
  466. `Exports field key should be relative path and start with "./" (key: ${JSON.stringify(
  467. key
  468. )})`
  469. );
  470. }
  471. }
  472. return field;
  473. }
  474. /**
  475. * @param {ImportsField} field imports field
  476. * @returns {ImportsField} normalized imports field
  477. */
  478. function buildImportsField(field) {
  479. const keys = Object.keys(field);
  480. for (let i = 0; i < keys.length; i++) {
  481. const key = keys[i];
  482. if (key.charCodeAt(0) !== hashCode) {
  483. throw new Error(
  484. `Imports field key should start with "#" (key: ${JSON.stringify(key)})`
  485. );
  486. }
  487. if (key.length === 1) {
  488. throw new Error(
  489. `Imports field key should have at least 2 characters (key: ${JSON.stringify(
  490. key
  491. )})`
  492. );
  493. }
  494. if (key.charCodeAt(1) === slashCode) {
  495. throw new Error(
  496. `Imports field key should not start with "#/" (key: ${JSON.stringify(
  497. key
  498. )})`
  499. );
  500. }
  501. }
  502. return field;
  503. }