AliasFieldPlugin.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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. const getInnerRequest = require("./getInnerRequest");
  8. /** @typedef {import("./Resolver")} Resolver */
  9. /** @typedef {import("./Resolver").JsonPrimitive} JsonPrimitive */
  10. /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
  11. /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
  12. // Sentinel stored in `_fieldDataCache` when a description file does not
  13. // contain a usable alias field object. Lets us distinguish "not cached yet"
  14. // from "no valid field" without calling back into `getField`.
  15. const NO_FIELD_OBJECT = Symbol("NoFieldObject");
  16. module.exports = class AliasFieldPlugin {
  17. /**
  18. * @param {string | ResolveStepHook} source source
  19. * @param {string | string[]} field field
  20. * @param {string | ResolveStepHook} target target
  21. */
  22. constructor(source, field, target) {
  23. this.source = source;
  24. this.field = field;
  25. this.target = target;
  26. // `this.field` is fixed for the plugin's lifetime, so caching
  27. // per description-file content is safe. The cached value is either
  28. // the resolved alias-map object or the `NO_FIELD_OBJECT` sentinel
  29. // meaning "description file has no usable alias field".
  30. /** @type {WeakMap<import("./Resolver").JsonObject, { [k: string]: JsonPrimitive } | typeof NO_FIELD_OBJECT>} */
  31. this._fieldDataCache = new WeakMap();
  32. }
  33. /**
  34. * @param {Resolver} resolver the resolver
  35. * @returns {void}
  36. */
  37. apply(resolver) {
  38. const target = resolver.ensureHook(this.target);
  39. resolver
  40. .getHook(this.source)
  41. .tapAsync("AliasFieldPlugin", (request, resolveContext, callback) => {
  42. if (!request.descriptionFileData) return callback();
  43. const innerRequest = getInnerRequest(resolver, request);
  44. if (!innerRequest) return callback();
  45. const { descriptionFileData } = request;
  46. let fieldData = this._fieldDataCache.get(descriptionFileData);
  47. if (fieldData === undefined) {
  48. const raw = DescriptionFileUtils.getField(
  49. descriptionFileData,
  50. this.field,
  51. );
  52. fieldData =
  53. raw === null || typeof raw !== "object"
  54. ? NO_FIELD_OBJECT
  55. : /** @type {{ [k: string]: JsonPrimitive }} */ (raw);
  56. this._fieldDataCache.set(descriptionFileData, fieldData);
  57. }
  58. if (fieldData === NO_FIELD_OBJECT) {
  59. if (resolveContext.log) {
  60. resolveContext.log(
  61. `Field '${this.field}' doesn't contain a valid alias configuration`,
  62. );
  63. }
  64. return callback();
  65. }
  66. /** @type {JsonPrimitive | undefined} */
  67. const data = Object.prototype.hasOwnProperty.call(
  68. fieldData,
  69. innerRequest,
  70. )
  71. ? /** @type {{ [Key in string]: JsonPrimitive }} */ (fieldData)[
  72. innerRequest
  73. ]
  74. : innerRequest.startsWith("./")
  75. ? /** @type {{ [Key in string]: JsonPrimitive }} */ (fieldData)[
  76. innerRequest.slice(2)
  77. ]
  78. : undefined;
  79. if (data === innerRequest) return callback();
  80. if (data === undefined) return callback();
  81. if (data === false) {
  82. /** @type {ResolveRequest} */
  83. const ignoreObj = {
  84. ...request,
  85. path: false,
  86. };
  87. if (typeof resolveContext.yield === "function") {
  88. resolveContext.yield(ignoreObj);
  89. return callback(null, null);
  90. }
  91. return callback(null, ignoreObj);
  92. }
  93. /** @type {ResolveRequest} */
  94. const obj = {
  95. ...request,
  96. path: /** @type {string} */ (request.descriptionFileRoot),
  97. request: /** @type {string} */ (data),
  98. fullySpecified: false,
  99. };
  100. resolver.doResolve(
  101. target,
  102. obj,
  103. `aliased from description file ${
  104. request.descriptionFilePath
  105. } with mapping '${innerRequest}' to '${/** @type {string} */ data}'`,
  106. resolveContext,
  107. (err, result) => {
  108. if (err) return callback(err);
  109. // Don't allow other aliasing or raw request
  110. if (result === undefined) return callback(null, null);
  111. callback(null, result);
  112. },
  113. );
  114. });
  115. }
  116. };