UNPKG

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