ImportsFieldPlugin.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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 { processImportsField } = require("./util/entrypoints");
  9. const { parseIdentifier } = require("./util/identifier");
  10. const {
  11. invalidSegmentRegEx,
  12. deprecatedInvalidSegmentRegEx
  13. } = require("./util/path");
  14. /** @typedef {import("./Resolver")} Resolver */
  15. /** @typedef {import("./Resolver").JsonObject} JsonObject */
  16. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  17. /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
  18. /** @typedef {import("./util/entrypoints").FieldProcessor} FieldProcessor */
  19. /** @typedef {import("./util/entrypoints").ImportsField} ImportsField */
  20. const dotCode = ".".charCodeAt(0);
  21. module.exports = class ImportsFieldPlugin {
  22. /**
  23. * @param {string | ResolveStepHook} source source
  24. * @param {Set<string>} conditionNames condition names
  25. * @param {string | string[]} fieldNamePath name path
  26. * @param {string | ResolveStepHook} targetFile target file
  27. * @param {string | ResolveStepHook} targetPackage target package
  28. */
  29. constructor(
  30. source,
  31. conditionNames,
  32. fieldNamePath,
  33. targetFile,
  34. targetPackage
  35. ) {
  36. this.source = source;
  37. this.targetFile = targetFile;
  38. this.targetPackage = targetPackage;
  39. this.conditionNames = conditionNames;
  40. this.fieldName = fieldNamePath;
  41. /** @type {WeakMap<JsonObject, FieldProcessor>} */
  42. this.fieldProcessorCache = new WeakMap();
  43. }
  44. /**
  45. * @param {Resolver} resolver the resolver
  46. * @returns {void}
  47. */
  48. apply(resolver) {
  49. const targetFile = resolver.ensureHook(this.targetFile);
  50. const targetPackage = resolver.ensureHook(this.targetPackage);
  51. resolver
  52. .getHook(this.source)
  53. .tapAsync("ImportsFieldPlugin", (request, resolveContext, callback) => {
  54. // When there is no description file, abort
  55. if (!request.descriptionFilePath || request.request === undefined) {
  56. return callback();
  57. }
  58. const remainingRequest =
  59. request.request + request.query + request.fragment;
  60. const importsField =
  61. /** @type {ImportsField|null|undefined} */
  62. (
  63. DescriptionFileUtils.getField(
  64. /** @type {JsonObject} */ (request.descriptionFileData),
  65. this.fieldName
  66. )
  67. );
  68. if (!importsField) return callback();
  69. if (request.directory) {
  70. return callback(
  71. new Error(
  72. `Resolving to directories is not possible with the imports field (request was ${remainingRequest}/)`
  73. )
  74. );
  75. }
  76. /** @type {string[]} */
  77. let paths;
  78. /** @type {string | null} */
  79. let usedField;
  80. try {
  81. // We attach the cache to the description file instead of the importsField value
  82. // because we use a WeakMap and the importsField could be a string too.
  83. // Description file is always an object when exports field can be accessed.
  84. let fieldProcessor = this.fieldProcessorCache.get(
  85. /** @type {JsonObject} */ (request.descriptionFileData)
  86. );
  87. if (fieldProcessor === undefined) {
  88. fieldProcessor = processImportsField(importsField);
  89. this.fieldProcessorCache.set(
  90. /** @type {JsonObject} */ (request.descriptionFileData),
  91. fieldProcessor
  92. );
  93. }
  94. [paths, usedField] = fieldProcessor(
  95. remainingRequest,
  96. this.conditionNames
  97. );
  98. } catch (/** @type {unknown} */ err) {
  99. if (resolveContext.log) {
  100. resolveContext.log(
  101. `Imports field in ${request.descriptionFilePath} can't be processed: ${err}`
  102. );
  103. }
  104. return callback(/** @type {Error} */ (err));
  105. }
  106. if (paths.length === 0) {
  107. return callback(
  108. new Error(
  109. `Package import ${remainingRequest} is not imported from package ${request.descriptionFileRoot} (see imports field in ${request.descriptionFilePath})`
  110. )
  111. );
  112. }
  113. forEachBail(
  114. paths,
  115. /**
  116. * @param {string} p path
  117. * @param {(err?: null|Error, result?: null|ResolveRequest) => void} callback callback
  118. * @param {number} i index
  119. * @returns {void}
  120. */
  121. (p, callback, i) => {
  122. const parsedIdentifier = parseIdentifier(p);
  123. if (!parsedIdentifier) return callback();
  124. const [path_, query, fragment] = parsedIdentifier;
  125. switch (path_.charCodeAt(0)) {
  126. // should be relative
  127. case dotCode: {
  128. if (
  129. invalidSegmentRegEx.exec(path_.slice(2)) !== null &&
  130. deprecatedInvalidSegmentRegEx.test(path_.slice(2)) !== null
  131. ) {
  132. if (paths.length === i) {
  133. return callback(
  134. new Error(
  135. `Invalid "imports" target "${p}" defined for "${usedField}" in the package config ${request.descriptionFilePath}, targets must start with "./"`
  136. )
  137. );
  138. }
  139. return callback();
  140. }
  141. /** @type {ResolveRequest} */
  142. const obj = {
  143. ...request,
  144. request: undefined,
  145. path: resolver.join(
  146. /** @type {string} */ (request.descriptionFileRoot),
  147. path_
  148. ),
  149. relativePath: path_,
  150. query,
  151. fragment
  152. };
  153. resolver.doResolve(
  154. targetFile,
  155. obj,
  156. "using imports field: " + p,
  157. resolveContext,
  158. (err, result) => {
  159. if (err) return callback(err);
  160. // Don't allow to continue - https://github.com/webpack/enhanced-resolve/issues/400
  161. if (result === undefined) return callback(null, null);
  162. callback(null, result);
  163. }
  164. );
  165. break;
  166. }
  167. // package resolving
  168. default: {
  169. /** @type {ResolveRequest} */
  170. const obj = {
  171. ...request,
  172. request: path_,
  173. relativePath: path_,
  174. fullySpecified: true,
  175. query,
  176. fragment
  177. };
  178. resolver.doResolve(
  179. targetPackage,
  180. obj,
  181. "using imports field: " + p,
  182. resolveContext,
  183. (err, result) => {
  184. if (err) return callback(err);
  185. // Don't allow to continue - https://github.com/webpack/enhanced-resolve/issues/400
  186. if (result === undefined) return callback(null, null);
  187. callback(null, result);
  188. }
  189. );
  190. }
  191. }
  192. },
  193. /**
  194. * @param {null|Error} [err] error
  195. * @param {null|ResolveRequest} [result] result
  196. * @returns {void}
  197. */
  198. (err, result) => callback(err, result || null)
  199. );
  200. });
  201. }
  202. };