ExportsFieldPlugin.js 4.3 KB

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