ResolverFactory.js 4.8 KB

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