entrypoints.js 14 KB

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