ResolverFactory.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const Factory = require("enhanced-resolve").ResolverFactory;
  7. const { HookMap, SyncHook, SyncWaterfallHook } = require("tapable");
  8. const {
  9. cachedCleverMerge,
  10. removeOperations,
  11. resolveByProperty
  12. } = require("./util/cleverMerge");
  13. /** @typedef {import("enhanced-resolve").ResolveContext} ResolveContext */
  14. /** @typedef {import("enhanced-resolve").ResolveOptions} ResolveOptions */
  15. /** @typedef {import("enhanced-resolve").ResolveRequest} ResolveRequest */
  16. /** @typedef {import("enhanced-resolve").Resolver} Resolver */
  17. /** @typedef {import("../declarations/WebpackOptions").ResolveOptions} WebpackResolveOptions */
  18. /** @typedef {import("../declarations/WebpackOptions").ResolvePluginInstance} ResolvePluginInstance */
  19. /** @typedef {WebpackResolveOptions & {dependencyType?: string, resolveToContext?: boolean }} ResolveOptionsWithDependencyType */
  20. /**
  21. * @typedef {Object} WithOptions
  22. * @property {function(Partial<ResolveOptionsWithDependencyType>): ResolverWithOptions} withOptions create a resolver with additional/different options
  23. */
  24. /** @typedef {Resolver & WithOptions} ResolverWithOptions */
  25. // need to be hoisted on module level for caching identity
  26. const EMPTY_RESOLVE_OPTIONS = {};
  27. /**
  28. * @param {ResolveOptionsWithDependencyType} resolveOptionsWithDepType enhanced options
  29. * @returns {ResolveOptions} merged options
  30. */
  31. const convertToResolveOptions = resolveOptionsWithDepType => {
  32. const { dependencyType, plugins, ...remaining } = resolveOptionsWithDepType;
  33. // check type compat
  34. /** @type {Partial<ResolveOptions>} */
  35. const partialOptions = {
  36. ...remaining,
  37. plugins:
  38. plugins &&
  39. /** @type {ResolvePluginInstance[]} */ (
  40. plugins.filter(item => item !== "...")
  41. )
  42. };
  43. if (!partialOptions.fileSystem) {
  44. throw new Error(
  45. "fileSystem is missing in resolveOptions, but it's required for enhanced-resolve"
  46. );
  47. }
  48. // These weird types validate that we checked all non-optional properties
  49. const options =
  50. /** @type {Partial<ResolveOptions> & Pick<ResolveOptions, "fileSystem">} */ (
  51. partialOptions
  52. );
  53. return removeOperations(
  54. resolveByProperty(options, "byDependency", dependencyType)
  55. );
  56. };
  57. /**
  58. * @typedef {Object} ResolverCache
  59. * @property {WeakMap<Object, ResolverWithOptions>} direct
  60. * @property {Map<string, ResolverWithOptions>} stringified
  61. */
  62. module.exports = class ResolverFactory {
  63. constructor() {
  64. this.hooks = Object.freeze({
  65. /** @type {HookMap<SyncWaterfallHook<[ResolveOptionsWithDependencyType]>>} */
  66. resolveOptions: new HookMap(
  67. () => new SyncWaterfallHook(["resolveOptions"])
  68. ),
  69. /** @type {HookMap<SyncHook<[Resolver, ResolveOptions, ResolveOptionsWithDependencyType]>>} */
  70. resolver: new HookMap(
  71. () => new SyncHook(["resolver", "resolveOptions", "userResolveOptions"])
  72. )
  73. });
  74. /** @type {Map<string, ResolverCache>} */
  75. this.cache = new Map();
  76. }
  77. /**
  78. * @param {string} type type of resolver
  79. * @param {ResolveOptionsWithDependencyType=} resolveOptions options
  80. * @returns {ResolverWithOptions} the resolver
  81. */
  82. get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) {
  83. let typedCaches = this.cache.get(type);
  84. if (!typedCaches) {
  85. typedCaches = {
  86. direct: new WeakMap(),
  87. stringified: new Map()
  88. };
  89. this.cache.set(type, typedCaches);
  90. }
  91. const cachedResolver = typedCaches.direct.get(resolveOptions);
  92. if (cachedResolver) {
  93. return cachedResolver;
  94. }
  95. const ident = JSON.stringify(resolveOptions);
  96. const resolver = typedCaches.stringified.get(ident);
  97. if (resolver) {
  98. typedCaches.direct.set(resolveOptions, resolver);
  99. return resolver;
  100. }
  101. const newResolver = this._create(type, resolveOptions);
  102. typedCaches.direct.set(resolveOptions, newResolver);
  103. typedCaches.stringified.set(ident, newResolver);
  104. return newResolver;
  105. }
  106. /**
  107. * @param {string} type type of resolver
  108. * @param {ResolveOptionsWithDependencyType} resolveOptionsWithDepType options
  109. * @returns {ResolverWithOptions} the resolver
  110. */
  111. _create(type, resolveOptionsWithDepType) {
  112. /** @type {ResolveOptionsWithDependencyType} */
  113. const originalResolveOptions = { ...resolveOptionsWithDepType };
  114. const resolveOptions = convertToResolveOptions(
  115. this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
  116. );
  117. const resolver = /** @type {ResolverWithOptions} */ (
  118. Factory.createResolver(resolveOptions)
  119. );
  120. if (!resolver) {
  121. throw new Error("No resolver created");
  122. }
  123. /** @type {WeakMap<Partial<ResolveOptionsWithDependencyType>, ResolverWithOptions>} */
  124. const childCache = new WeakMap();
  125. resolver.withOptions = options => {
  126. const cacheEntry = childCache.get(options);
  127. if (cacheEntry !== undefined) return cacheEntry;
  128. const mergedOptions = cachedCleverMerge(originalResolveOptions, options);
  129. const resolver = this.get(type, mergedOptions);
  130. childCache.set(options, resolver);
  131. return resolver;
  132. };
  133. this.hooks.resolver
  134. .for(type)
  135. .call(resolver, resolveOptions, originalResolveOptions);
  136. return resolver;
  137. }
  138. };