ExportsFieldPlugin.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. const DescriptionFileUtils = require("./DescriptionFileUtils");
  7. const forEachBail = require("./forEachBail");
  8. const { processExportsField } = require("./util/entrypoints");
  9. const { parseIdentifier } = require("./util/identifier");
  10. const { checkImportsExportsFieldTarget } = require("./util/path");
  11. /** @typedef {import("./Resolver")} Resolver */
  12. /** @typedef {import("./Resolver").JsonObject} JsonObject */
  13. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  14. /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
  15. /** @typedef {import("./util/entrypoints").ExportsField} ExportsField */
  16. /** @typedef {import("./util/entrypoints").FieldProcessor} FieldProcessor */
  17. module.exports = class ExportsFieldPlugin {
  18. /**
  19. * @param {string | ResolveStepHook} source source
  20. * @param {Set<string>} conditionNames condition names
  21. * @param {string | string[]} fieldNamePath name path
  22. * @param {string | ResolveStepHook} target target
  23. */
  24. constructor(source, conditionNames, fieldNamePath, target) {
  25. this.source = source;
  26. this.target = target;
  27. this.conditionNames = conditionNames;
  28. this.fieldName = fieldNamePath;
  29. /** @type {WeakMap<JsonObject, FieldProcessor>} */
  30. this.fieldProcessorCache = new WeakMap();
  31. }
  32. /**
  33. * @param {Resolver} resolver the resolver
  34. * @returns {void}
  35. */
  36. apply(resolver) {
  37. const target = resolver.ensureHook(this.target);
  38. resolver
  39. .getHook(this.source)
  40. .tapAsync("ExportsFieldPlugin", (request, resolveContext, callback) => {
  41. // When there is no description file, abort
  42. if (!request.descriptionFilePath) return callback();
  43. if (
  44. // When the description file is inherited from parent, abort
  45. // (There is no description file inside of this package)
  46. request.relativePath !== "." ||
  47. request.request === undefined
  48. )
  49. return callback();
  50. const remainingRequest =
  51. request.query || request.fragment
  52. ? (request.request === "." ? "./" : request.request) +
  53. request.query +
  54. request.fragment
  55. : request.request;
  56. const exportsField =
  57. /** @type {ExportsField|null|undefined} */
  58. (
  59. DescriptionFileUtils.getField(
  60. /** @type {JsonObject} */ (request.descriptionFileData),
  61. this.fieldName
  62. )
  63. );
  64. if (!exportsField) return callback();
  65. if (request.directory) {
  66. return callback(
  67. new Error(
  68. `Resolving to directories is not possible with the exports field (request was ${remainingRequest}/)`
  69. )
  70. );
  71. }
  72. /** @type {string[]} */
  73. let paths;
  74. try {
  75. // We attach the cache to the description file instead of the exportsField value
  76. // because we use a WeakMap and the exportsField could be a string too.
  77. // Description file is always an object when exports field can be accessed.
  78. let fieldProcessor = this.fieldProcessorCache.get(
  79. /** @type {JsonObject} */ (request.descriptionFileData)
  80. );
  81. if (fieldProcessor === undefined) {
  82. fieldProcessor = processExportsField(exportsField);
  83. this.fieldProcessorCache.set(
  84. /** @type {JsonObject} */ (request.descriptionFileData),
  85. fieldProcessor
  86. );
  87. }
  88. paths = fieldProcessor(remainingRequest, this.conditionNames);
  89. } catch (/** @type {unknown} */ err) {
  90. if (resolveContext.log) {
  91. resolveContext.log(
  92. `Exports field in ${request.descriptionFilePath} can't be processed: ${err}`
  93. );
  94. }
  95. return callback(/** @type {Error} */ (err));
  96. }
  97. if (paths.length === 0) {
  98. return callback(
  99. new Error(
  100. `Package path ${remainingRequest} is not exported from package ${request.descriptionFileRoot} (see exports field in ${request.descriptionFilePath})`
  101. )
  102. );
  103. }
  104. forEachBail(
  105. paths,
  106. /**
  107. * @param {string} p path
  108. * @param {(err?: null|Error, result?: null|ResolveRequest) => void} callback callback
  109. * @returns {void}
  110. */
  111. (p, callback) => {
  112. const parsedIdentifier = parseIdentifier(p);
  113. if (!parsedIdentifier) return callback();
  114. const [relativePath, query, fragment] = parsedIdentifier;
  115. const error = checkImportsExportsFieldTarget(relativePath);
  116. if (error) {
  117. return callback(error);
  118. }
  119. /** @type {ResolveRequest} */
  120. const obj = {
  121. ...request,
  122. request: undefined,
  123. path: resolver.join(
  124. /** @type {string} */ (request.descriptionFileRoot),
  125. relativePath
  126. ),
  127. relativePath,
  128. query,
  129. fragment
  130. };
  131. resolver.doResolve(
  132. target,
  133. obj,
  134. "using exports field: " + p,
  135. resolveContext,
  136. callback
  137. );
  138. },
  139. /**
  140. * @param {null|Error} [err] error
  141. * @param {null|ResolveRequest} [result] result
  142. * @returns {void}
  143. */
  144. (err, result) => callback(err, result || null)
  145. );
  146. });
  147. }
  148. };