UNPKG

11.1 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 asyncLib = require("neo-async");
9const { AsyncSeriesWaterfallHook, SyncWaterfallHook } = require("tapable");
10const ContextModule = require("./ContextModule");
11const ModuleFactory = require("./ModuleFactory");
12const ContextElementDependency = require("./dependencies/ContextElementDependency");
13const LazySet = require("./util/LazySet");
14const { cachedSetProperty } = require("./util/cleverMerge");
15const { createFakeHook } = require("./util/deprecation");
16const { join } = require("./util/fs");
17
18/** @typedef {import("./ContextModule").ContextModuleOptions} ContextModuleOptions */
19/** @typedef {import("./ContextModule").ResolveDependenciesCallback} ResolveDependenciesCallback */
20/** @typedef {import("./Module")} Module */
21/** @typedef {import("./ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
22/** @typedef {import("./ModuleFactory").ModuleFactoryResult} ModuleFactoryResult */
23/** @typedef {import("./ResolverFactory")} ResolverFactory */
24/** @typedef {import("./dependencies/ContextDependency")} ContextDependency */
25/** @template T @typedef {import("./util/deprecation").FakeHook<T>} FakeHook<T> */
26/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
27
28const EMPTY_RESOLVE_OPTIONS = {};
29
30module.exports = class ContextModuleFactory extends ModuleFactory {
31 /**
32 * @param {ResolverFactory} resolverFactory resolverFactory
33 */
34 constructor(resolverFactory) {
35 super();
36 /** @type {AsyncSeriesWaterfallHook<[TODO[], ContextModuleOptions]>} */
37 const alternativeRequests = new AsyncSeriesWaterfallHook([
38 "modules",
39 "options"
40 ]);
41 this.hooks = Object.freeze({
42 /** @type {AsyncSeriesWaterfallHook<[TODO]>} */
43 beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
44 /** @type {AsyncSeriesWaterfallHook<[TODO]>} */
45 afterResolve: new AsyncSeriesWaterfallHook(["data"]),
46 /** @type {SyncWaterfallHook<[string[]]>} */
47 contextModuleFiles: new SyncWaterfallHook(["files"]),
48 /** @type {FakeHook<Pick<AsyncSeriesWaterfallHook<[TODO[]]>, "tap" | "tapAsync" | "tapPromise" | "name">>} */
49 alternatives: createFakeHook(
50 {
51 name: "alternatives",
52 /** @type {AsyncSeriesWaterfallHook<[TODO[]]>["intercept"]} */
53 intercept: interceptor => {
54 throw new Error(
55 "Intercepting fake hook ContextModuleFactory.hooks.alternatives is not possible, use ContextModuleFactory.hooks.alternativeRequests instead"
56 );
57 },
58 /** @type {AsyncSeriesWaterfallHook<[TODO[]]>["tap"]} */
59 tap: (options, fn) => {
60 alternativeRequests.tap(options, fn);
61 },
62 /** @type {AsyncSeriesWaterfallHook<[TODO[]]>["tapAsync"]} */
63 tapAsync: (options, fn) => {
64 alternativeRequests.tapAsync(options, (items, _options, callback) =>
65 fn(items, callback)
66 );
67 },
68 /** @type {AsyncSeriesWaterfallHook<[TODO[]]>["tapPromise"]} */
69 tapPromise: (options, fn) => {
70 alternativeRequests.tapPromise(options, fn);
71 }
72 },
73 "ContextModuleFactory.hooks.alternatives has deprecated in favor of ContextModuleFactory.hooks.alternativeRequests with an additional options argument.",
74 "DEP_WEBPACK_CONTEXT_MODULE_FACTORY_ALTERNATIVES"
75 ),
76 alternativeRequests
77 });
78 this.resolverFactory = resolverFactory;
79 }
80
81 /**
82 * @param {ModuleFactoryCreateData} data data object
83 * @param {function(Error=, ModuleFactoryResult=): void} callback callback
84 * @returns {void}
85 */
86 create(data, callback) {
87 const context = data.context;
88 const dependencies = data.dependencies;
89 const resolveOptions = data.resolveOptions;
90 const dependency = /** @type {ContextDependency} */ (dependencies[0]);
91 const fileDependencies = new LazySet();
92 const missingDependencies = new LazySet();
93 const contextDependencies = new LazySet();
94 this.hooks.beforeResolve.callAsync(
95 {
96 context: context,
97 dependencies: dependencies,
98 resolveOptions,
99 fileDependencies,
100 missingDependencies,
101 contextDependencies,
102 ...dependency.options
103 },
104 (err, beforeResolveResult) => {
105 if (err) {
106 return callback(err, {
107 fileDependencies,
108 missingDependencies,
109 contextDependencies
110 });
111 }
112
113 // Ignored
114 if (!beforeResolveResult) {
115 return callback(null, {
116 fileDependencies,
117 missingDependencies,
118 contextDependencies
119 });
120 }
121
122 const context = beforeResolveResult.context;
123 const request = beforeResolveResult.request;
124 const resolveOptions = beforeResolveResult.resolveOptions;
125
126 let loaders,
127 resource,
128 loadersPrefix = "";
129 const idx = request.lastIndexOf("!");
130 if (idx >= 0) {
131 let loadersRequest = request.substr(0, idx + 1);
132 let i;
133 for (
134 i = 0;
135 i < loadersRequest.length && loadersRequest[i] === "!";
136 i++
137 ) {
138 loadersPrefix += "!";
139 }
140 loadersRequest = loadersRequest
141 .substr(i)
142 .replace(/!+$/, "")
143 .replace(/!!+/g, "!");
144 if (loadersRequest === "") {
145 loaders = [];
146 } else {
147 loaders = loadersRequest.split("!");
148 }
149 resource = request.substr(idx + 1);
150 } else {
151 loaders = [];
152 resource = request;
153 }
154
155 const contextResolver = this.resolverFactory.get(
156 "context",
157 dependencies.length > 0
158 ? cachedSetProperty(
159 resolveOptions || EMPTY_RESOLVE_OPTIONS,
160 "dependencyType",
161 dependencies[0].category
162 )
163 : resolveOptions
164 );
165 const loaderResolver = this.resolverFactory.get("loader");
166
167 asyncLib.parallel(
168 [
169 callback => {
170 contextResolver.resolve(
171 {},
172 context,
173 resource,
174 {
175 fileDependencies,
176 missingDependencies,
177 contextDependencies
178 },
179 (err, result) => {
180 if (err) return callback(err);
181 callback(null, result);
182 }
183 );
184 },
185 callback => {
186 asyncLib.map(
187 loaders,
188 (loader, callback) => {
189 loaderResolver.resolve(
190 {},
191 context,
192 loader,
193 {
194 fileDependencies,
195 missingDependencies,
196 contextDependencies
197 },
198 (err, result) => {
199 if (err) return callback(err);
200 callback(null, result);
201 }
202 );
203 },
204 callback
205 );
206 }
207 ],
208 (err, result) => {
209 if (err) {
210 return callback(err, {
211 fileDependencies,
212 missingDependencies,
213 contextDependencies
214 });
215 }
216
217 this.hooks.afterResolve.callAsync(
218 {
219 addon:
220 loadersPrefix +
221 result[1].join("!") +
222 (result[1].length > 0 ? "!" : ""),
223 resource: result[0],
224 resolveDependencies: this.resolveDependencies.bind(this),
225 ...beforeResolveResult
226 },
227 (err, result) => {
228 if (err) {
229 return callback(err, {
230 fileDependencies,
231 missingDependencies,
232 contextDependencies
233 });
234 }
235
236 // Ignored
237 if (!result) {
238 return callback(null, {
239 fileDependencies,
240 missingDependencies,
241 contextDependencies
242 });
243 }
244
245 return callback(null, {
246 module: new ContextModule(result.resolveDependencies, result),
247 fileDependencies,
248 missingDependencies,
249 contextDependencies
250 });
251 }
252 );
253 }
254 );
255 }
256 );
257 }
258
259 /**
260 * @param {InputFileSystem} fs file system
261 * @param {ContextModuleOptions} options options
262 * @param {ResolveDependenciesCallback} callback callback function
263 * @returns {void}
264 */
265 resolveDependencies(fs, options, callback) {
266 const cmf = this;
267 const {
268 resource,
269 resourceQuery,
270 resourceFragment,
271 recursive,
272 regExp,
273 include,
274 exclude,
275 referencedExports,
276 category
277 } = options;
278 if (!regExp || !resource) return callback(null, []);
279
280 const addDirectoryChecked = (directory, visited, callback) => {
281 fs.realpath(directory, (err, realPath) => {
282 if (err) return callback(err);
283 if (visited.has(realPath)) return callback(null, []);
284 let recursionStack;
285 addDirectory(
286 directory,
287 (dir, callback) => {
288 if (recursionStack === undefined) {
289 recursionStack = new Set(visited);
290 recursionStack.add(realPath);
291 }
292 addDirectoryChecked(dir, recursionStack, callback);
293 },
294 callback
295 );
296 });
297 };
298
299 const addDirectory = (directory, addSubDirectory, callback) => {
300 fs.readdir(directory, (err, files) => {
301 if (err) return callback(err);
302 const processedFiles = cmf.hooks.contextModuleFiles.call(
303 /** @type {string[]} */ (files).map(file => file.normalize("NFC"))
304 );
305 if (!processedFiles || processedFiles.length === 0)
306 return callback(null, []);
307 asyncLib.map(
308 processedFiles.filter(p => p.indexOf(".") !== 0),
309 (segment, callback) => {
310 const subResource = join(fs, directory, segment);
311
312 if (!exclude || !subResource.match(exclude)) {
313 fs.stat(subResource, (err, stat) => {
314 if (err) {
315 if (err.code === "ENOENT") {
316 // ENOENT is ok here because the file may have been deleted between
317 // the readdir and stat calls.
318 return callback();
319 } else {
320 return callback(err);
321 }
322 }
323
324 if (stat.isDirectory()) {
325 if (!recursive) return callback();
326 addSubDirectory(subResource, callback);
327 } else if (
328 stat.isFile() &&
329 (!include || subResource.match(include))
330 ) {
331 const obj = {
332 context: resource,
333 request:
334 "." +
335 subResource.substr(resource.length).replace(/\\/g, "/")
336 };
337
338 this.hooks.alternativeRequests.callAsync(
339 [obj],
340 options,
341 (err, alternatives) => {
342 if (err) return callback(err);
343 alternatives = alternatives
344 .filter(obj => regExp.test(obj.request))
345 .map(obj => {
346 const dep = new ContextElementDependency(
347 obj.request + resourceQuery + resourceFragment,
348 obj.request,
349 category,
350 referencedExports
351 );
352 dep.optional = true;
353 return dep;
354 });
355 callback(null, alternatives);
356 }
357 );
358 } else {
359 callback();
360 }
361 });
362 } else {
363 callback();
364 }
365 },
366 (err, result) => {
367 if (err) return callback(err);
368
369 if (!result) return callback(null, []);
370
371 const flattenedResult = [];
372
373 for (const item of result) {
374 if (item) flattenedResult.push(...item);
375 }
376
377 callback(null, flattenedResult);
378 }
379 );
380 });
381 };
382
383 if (typeof fs.realpath === "function") {
384 addDirectoryChecked(resource, new Set(), callback);
385 } else {
386 const addSubDirectory = (dir, callback) =>
387 addDirectory(dir, addSubDirectory, callback);
388 addDirectory(resource, addSubDirectory, callback);
389 }
390 }
391};