UNPKG

8.4 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const LazySet = require("../util/LazySet");
9const makeSerializable = require("../util/makeSerializable");
10
11/** @typedef {import("enhanced-resolve/lib/Resolver")} Resolver */
12/** @typedef {import("../CacheFacade").ItemCacheFacade} ItemCacheFacade */
13/** @typedef {import("../Compiler")} Compiler */
14/** @typedef {import("../FileSystemInfo")} FileSystemInfo */
15/** @typedef {import("../FileSystemInfo").Snapshot} Snapshot */
16
17class CacheEntry {
18 constructor(result, snapshot) {
19 this.result = result;
20 this.snapshot = snapshot;
21 }
22
23 serialize({ write }) {
24 write(this.result);
25 write(this.snapshot);
26 }
27
28 deserialize({ read }) {
29 this.result = read();
30 this.snapshot = read();
31 }
32}
33
34makeSerializable(CacheEntry, "webpack/lib/cache/ResolverCachePlugin");
35
36/**
37 * @template T
38 * @param {Set<T> | LazySet<T>} set set to add items to
39 * @param {Set<T> | LazySet<T>} otherSet set to add items from
40 * @returns {void}
41 */
42const addAllToSet = (set, otherSet) => {
43 if (set instanceof LazySet) {
44 set.addAll(otherSet);
45 } else {
46 for (const item of otherSet) {
47 set.add(item);
48 }
49 }
50};
51
52/**
53 * @param {Object} object an object
54 * @param {boolean} excludeContext if true, context is not included in string
55 * @returns {string} stringified version
56 */
57const objectToString = (object, excludeContext) => {
58 let str = "";
59 for (const key in object) {
60 if (excludeContext && key === "context") continue;
61 const value = object[key];
62 if (typeof value === "object" && value !== null) {
63 str += `|${key}=[${objectToString(value, false)}|]`;
64 } else {
65 str += `|${key}=|${value}`;
66 }
67 }
68 return str;
69};
70
71class ResolverCachePlugin {
72 /**
73 * Apply the plugin
74 * @param {Compiler} compiler the compiler instance
75 * @returns {void}
76 */
77 apply(compiler) {
78 const cache = compiler.getCache("ResolverCachePlugin");
79 /** @type {FileSystemInfo} */
80 let fileSystemInfo;
81 let snapshotOptions;
82 let realResolves = 0;
83 let cachedResolves = 0;
84 let cacheInvalidResolves = 0;
85 let concurrentResolves = 0;
86 compiler.hooks.thisCompilation.tap("ResolverCachePlugin", compilation => {
87 snapshotOptions = compilation.options.snapshot.resolve;
88 fileSystemInfo = compilation.fileSystemInfo;
89 compilation.hooks.finishModules.tap("ResolverCachePlugin", () => {
90 if (realResolves + cachedResolves > 0) {
91 const logger = compilation.getLogger("webpack.ResolverCachePlugin");
92 logger.log(
93 `${Math.round(
94 (100 * realResolves) / (realResolves + cachedResolves)
95 )}% really resolved (${realResolves} real resolves with ${cacheInvalidResolves} cached but invalid, ${cachedResolves} cached valid, ${concurrentResolves} concurrent)`
96 );
97 realResolves = 0;
98 cachedResolves = 0;
99 cacheInvalidResolves = 0;
100 concurrentResolves = 0;
101 }
102 });
103 });
104 /**
105 * @param {ItemCacheFacade} itemCache cache
106 * @param {Resolver} resolver the resolver
107 * @param {Object} resolveContext context for resolving meta info
108 * @param {Object} request the request info object
109 * @param {function((Error | null)=, Object=): void} callback callback function
110 * @returns {void}
111 */
112 const doRealResolve = (
113 itemCache,
114 resolver,
115 resolveContext,
116 request,
117 callback
118 ) => {
119 realResolves++;
120 const newRequest = {
121 _ResolverCachePluginCacheMiss: true,
122 ...request
123 };
124 const newResolveContext = {
125 ...resolveContext,
126 stack: new Set(),
127 missingDependencies: new LazySet(),
128 fileDependencies: new LazySet(),
129 contextDependencies: new LazySet()
130 };
131 const propagate = key => {
132 if (resolveContext[key]) {
133 addAllToSet(resolveContext[key], newResolveContext[key]);
134 }
135 };
136 const resolveTime = Date.now();
137 resolver.doResolve(
138 resolver.hooks.resolve,
139 newRequest,
140 "Cache miss",
141 newResolveContext,
142 (err, result) => {
143 propagate("fileDependencies");
144 propagate("contextDependencies");
145 propagate("missingDependencies");
146 if (err) return callback(err);
147 const fileDependencies = newResolveContext.fileDependencies;
148 const contextDependencies = newResolveContext.contextDependencies;
149 const missingDependencies = newResolveContext.missingDependencies;
150 fileSystemInfo.createSnapshot(
151 resolveTime,
152 fileDependencies,
153 contextDependencies,
154 missingDependencies,
155 snapshotOptions,
156 (err, snapshot) => {
157 if (err) return callback(err);
158 if (!snapshot) {
159 if (result) return callback(null, result);
160 return callback();
161 }
162 itemCache.store(new CacheEntry(result, snapshot), storeErr => {
163 if (storeErr) return callback(storeErr);
164 if (result) return callback(null, result);
165 callback();
166 });
167 }
168 );
169 }
170 );
171 };
172 compiler.resolverFactory.hooks.resolver.intercept({
173 factory(type, hook) {
174 /** @type {Map<string, (function(Error=, Object=): void)[]>} */
175 const activeRequests = new Map();
176 hook.tap(
177 "ResolverCachePlugin",
178 /**
179 * @param {Resolver} resolver the resolver
180 * @param {Object} options resolve options
181 * @param {Object} userOptions resolve options passed by the user
182 * @returns {void}
183 */
184 (resolver, options, userOptions) => {
185 if (options.cache !== true) return;
186 const optionsIdent = objectToString(userOptions, false);
187 const cacheWithContext =
188 options.cacheWithContext !== undefined
189 ? options.cacheWithContext
190 : false;
191 resolver.hooks.resolve.tapAsync(
192 {
193 name: "ResolverCachePlugin",
194 stage: -100
195 },
196 (request, resolveContext, callback) => {
197 if (request._ResolverCachePluginCacheMiss || !fileSystemInfo) {
198 return callback();
199 }
200 const identifier = `${type}${optionsIdent}${objectToString(
201 request,
202 !cacheWithContext
203 )}`;
204 const activeRequest = activeRequests.get(identifier);
205 if (activeRequest) {
206 activeRequest.push(callback);
207 return;
208 }
209 const itemCache = cache.getItemCache(identifier, null);
210 let callbacks;
211 const done = (err, result) => {
212 if (callbacks === undefined) {
213 callback(err, result);
214 callbacks = false;
215 } else {
216 for (const callback of callbacks) {
217 callback(err, result);
218 }
219 activeRequests.delete(identifier);
220 callbacks = false;
221 }
222 };
223 /**
224 * @param {Error=} err error if any
225 * @param {CacheEntry=} cacheEntry cache entry
226 * @returns {void}
227 */
228 const processCacheResult = (err, cacheEntry) => {
229 if (err) return done(err);
230
231 if (cacheEntry) {
232 const { snapshot, result } = cacheEntry;
233 fileSystemInfo.checkSnapshotValid(
234 snapshot,
235 (err, valid) => {
236 if (err || !valid) {
237 cacheInvalidResolves++;
238 return doRealResolve(
239 itemCache,
240 resolver,
241 resolveContext,
242 request,
243 done
244 );
245 }
246 cachedResolves++;
247 if (resolveContext.missingDependencies) {
248 addAllToSet(
249 resolveContext.missingDependencies,
250 snapshot.getMissingIterable()
251 );
252 }
253 if (resolveContext.fileDependencies) {
254 addAllToSet(
255 resolveContext.fileDependencies,
256 snapshot.getFileIterable()
257 );
258 }
259 if (resolveContext.contextDependencies) {
260 addAllToSet(
261 resolveContext.contextDependencies,
262 snapshot.getContextIterable()
263 );
264 }
265 done(null, result);
266 }
267 );
268 } else {
269 doRealResolve(
270 itemCache,
271 resolver,
272 resolveContext,
273 request,
274 done
275 );
276 }
277 };
278 itemCache.get(processCacheResult);
279 if (callbacks === undefined) {
280 callbacks = [callback];
281 activeRequests.set(identifier, callbacks);
282 }
283 }
284 );
285 }
286 );
287 return hook;
288 }
289 });
290 }
291}
292
293module.exports = ResolverCachePlugin;
294
\No newline at end of file