MainFieldPlugin.js 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const DescriptionFileUtils = require("./DescriptionFileUtils");
  7. /** @typedef {import("./Resolver")} Resolver */
  8. /** @typedef {import("./Resolver").JsonObject} JsonObject */
  9. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  10. /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
  11. /** @typedef {{ name: string | string[], forceRelative: boolean }} MainFieldOptions */
  12. const alreadyTriedMainField = Symbol("alreadyTriedMainField");
  13. // Sentinel cached for description files where the main field resolves to a
  14. // value we cannot use (missing, non-string, ".", "./"). Cheaper to store and
  15. // check than to re-walk the description file on every resolve.
  16. const NO_MAIN = Symbol("NoMain");
  17. module.exports = class MainFieldPlugin {
  18. /**
  19. * @param {string | ResolveStepHook} source source
  20. * @param {MainFieldOptions} options options
  21. * @param {string | ResolveStepHook} target target
  22. */
  23. constructor(source, options, target) {
  24. this.source = source;
  25. this.options = options;
  26. this.target = target;
  27. // Cache the resolved `mainModule` per description-file content. The
  28. // options (`name`, `forceRelative`) are fixed for this plugin
  29. // instance, so caching against content alone is safe. Stores either
  30. // the ready-to-use request string or the `NO_MAIN` sentinel.
  31. /** @type {WeakMap<JsonObject, string | typeof NO_MAIN>} */
  32. this._mainModuleCache = new WeakMap();
  33. }
  34. /**
  35. * @param {Resolver} resolver the resolver
  36. * @returns {void}
  37. */
  38. apply(resolver) {
  39. const target = resolver.ensureHook(this.target);
  40. resolver
  41. .getHook(this.source)
  42. .tapAsync("MainFieldPlugin", (request, resolveContext, callback) => {
  43. if (
  44. request.path !== request.descriptionFileRoot ||
  45. /** @type {ResolveRequest & { [alreadyTriedMainField]?: string }} */
  46. (request)[alreadyTriedMainField] === request.descriptionFilePath ||
  47. !request.descriptionFilePath
  48. ) {
  49. return callback();
  50. }
  51. const descFileData = /** @type {JsonObject} */ (
  52. request.descriptionFileData
  53. );
  54. let mainModule = this._mainModuleCache.get(descFileData);
  55. if (mainModule === undefined) {
  56. let raw =
  57. /** @type {string | null | undefined} */
  58. (DescriptionFileUtils.getField(descFileData, this.options.name));
  59. if (!raw || typeof raw !== "string" || raw === "." || raw === "./") {
  60. this._mainModuleCache.set(descFileData, NO_MAIN);
  61. return callback();
  62. }
  63. if (this.options.forceRelative && !/^\.\.?\//.test(raw)) {
  64. raw = `./${raw}`;
  65. }
  66. mainModule = raw;
  67. this._mainModuleCache.set(descFileData, mainModule);
  68. } else if (mainModule === NO_MAIN) {
  69. return callback();
  70. }
  71. const filename = resolver.basename(request.descriptionFilePath);
  72. /** @type {ResolveRequest & { [alreadyTriedMainField]?: string }} */
  73. const obj = {
  74. ...request,
  75. request: mainModule,
  76. module: false,
  77. directory: mainModule.endsWith("/"),
  78. [alreadyTriedMainField]: request.descriptionFilePath,
  79. };
  80. return resolver.doResolve(
  81. target,
  82. obj,
  83. `use ${mainModule} from ${this.options.name} in ${filename}`,
  84. resolveContext,
  85. callback,
  86. );
  87. });
  88. }
  89. };