UNPKG

13.8 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4 */
5"use strict";
6
7const path = require("path");
8const asyncLib = require("neo-async");
9const {
10 Tapable,
11 AsyncSeriesWaterfallHook,
12 SyncWaterfallHook,
13 SyncBailHook,
14 SyncHook,
15 HookMap
16} = require("tapable");
17const NormalModule = require("./NormalModule");
18const RawModule = require("./RawModule");
19const RuleSet = require("./RuleSet");
20const { cachedCleverMerge } = require("./util/cleverMerge");
21
22const EMPTY_RESOLVE_OPTIONS = {};
23
24const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/;
25
26const loaderToIdent = data => {
27 if (!data.options) {
28 return data.loader;
29 }
30 if (typeof data.options === "string") {
31 return data.loader + "?" + data.options;
32 }
33 if (typeof data.options !== "object") {
34 throw new Error("loader options must be string or object");
35 }
36 if (data.ident) {
37 return data.loader + "??" + data.ident;
38 }
39 return data.loader + "?" + JSON.stringify(data.options);
40};
41
42const identToLoaderRequest = resultString => {
43 const idx = resultString.indexOf("?");
44 if (idx >= 0) {
45 const loader = resultString.substr(0, idx);
46 const options = resultString.substr(idx + 1);
47 return {
48 loader,
49 options
50 };
51 } else {
52 return {
53 loader: resultString,
54 options: undefined
55 };
56 }
57};
58
59const dependencyCache = new WeakMap();
60
61class NormalModuleFactory extends Tapable {
62 constructor(context, resolverFactory, options) {
63 super();
64 this.hooks = {
65 resolver: new SyncWaterfallHook(["resolver"]),
66 factory: new SyncWaterfallHook(["factory"]),
67 beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
68 afterResolve: new AsyncSeriesWaterfallHook(["data"]),
69 createModule: new SyncBailHook(["data"]),
70 module: new SyncWaterfallHook(["module", "data"]),
71 createParser: new HookMap(() => new SyncBailHook(["parserOptions"])),
72 parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),
73 createGenerator: new HookMap(
74 () => new SyncBailHook(["generatorOptions"])
75 ),
76 generator: new HookMap(
77 () => new SyncHook(["generator", "generatorOptions"])
78 )
79 };
80 this._pluginCompat.tap("NormalModuleFactory", options => {
81 switch (options.name) {
82 case "before-resolve":
83 case "after-resolve":
84 options.async = true;
85 break;
86 case "parser":
87 this.hooks.parser
88 .for("javascript/auto")
89 .tap(options.fn.name || "unnamed compat plugin", options.fn);
90 return true;
91 }
92 let match;
93 match = /^parser (.+)$/.exec(options.name);
94 if (match) {
95 this.hooks.parser
96 .for(match[1])
97 .tap(
98 options.fn.name || "unnamed compat plugin",
99 options.fn.bind(this)
100 );
101 return true;
102 }
103 match = /^create-parser (.+)$/.exec(options.name);
104 if (match) {
105 this.hooks.createParser
106 .for(match[1])
107 .tap(
108 options.fn.name || "unnamed compat plugin",
109 options.fn.bind(this)
110 );
111 return true;
112 }
113 });
114 this.resolverFactory = resolverFactory;
115 this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
116 this.cachePredicate =
117 typeof options.unsafeCache === "function"
118 ? options.unsafeCache
119 : Boolean.bind(null, options.unsafeCache);
120 this.context = context || "";
121 this.parserCache = Object.create(null);
122 this.generatorCache = Object.create(null);
123 this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
124 let resolver = this.hooks.resolver.call(null);
125
126 // Ignored
127 if (!resolver) return callback();
128
129 resolver(result, (err, data) => {
130 if (err) return callback(err);
131
132 // Ignored
133 if (!data) return callback();
134
135 // direct module
136 if (typeof data.source === "function") return callback(null, data);
137
138 this.hooks.afterResolve.callAsync(data, (err, result) => {
139 if (err) return callback(err);
140
141 // Ignored
142 if (!result) return callback();
143
144 let createdModule = this.hooks.createModule.call(result);
145 if (!createdModule) {
146 if (!result.request) {
147 return callback(new Error("Empty dependency (no request)"));
148 }
149
150 createdModule = new NormalModule(result);
151 }
152
153 createdModule = this.hooks.module.call(createdModule, result);
154
155 return callback(null, createdModule);
156 });
157 });
158 });
159 this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
160 const contextInfo = data.contextInfo;
161 const context = data.context;
162 const request = data.request;
163
164 const loaderResolver = this.getResolver("loader");
165 const normalResolver = this.getResolver("normal", data.resolveOptions);
166
167 let matchResource = undefined;
168 let requestWithoutMatchResource = request;
169 const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
170 if (matchResourceMatch) {
171 matchResource = matchResourceMatch[1];
172 if (/^\.\.?\//.test(matchResource)) {
173 matchResource = path.join(context, matchResource);
174 }
175 requestWithoutMatchResource = request.substr(
176 matchResourceMatch[0].length
177 );
178 }
179
180 const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
181 const noAutoLoaders =
182 noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
183 const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
184 let elements = requestWithoutMatchResource
185 .replace(/^-?!+/, "")
186 .replace(/!!+/g, "!")
187 .split("!");
188 let resource = elements.pop();
189 elements = elements.map(identToLoaderRequest);
190
191 asyncLib.parallel(
192 [
193 callback =>
194 this.resolveRequestArray(
195 contextInfo,
196 context,
197 elements,
198 loaderResolver,
199 callback
200 ),
201 callback => {
202 if (resource === "" || resource[0] === "?") {
203 return callback(null, {
204 resource
205 });
206 }
207
208 normalResolver.resolve(
209 contextInfo,
210 context,
211 resource,
212 {},
213 (err, resource, resourceResolveData) => {
214 if (err) return callback(err);
215 callback(null, {
216 resourceResolveData,
217 resource
218 });
219 }
220 );
221 }
222 ],
223 (err, results) => {
224 if (err) return callback(err);
225 let loaders = results[0];
226 const resourceResolveData = results[1].resourceResolveData;
227 resource = results[1].resource;
228
229 // translate option idents
230 try {
231 for (const item of loaders) {
232 if (typeof item.options === "string" && item.options[0] === "?") {
233 const ident = item.options.substr(1);
234 item.options = this.ruleSet.findOptionsByIdent(ident);
235 item.ident = ident;
236 }
237 }
238 } catch (e) {
239 return callback(e);
240 }
241
242 if (resource === false) {
243 // ignored
244 return callback(
245 null,
246 new RawModule(
247 "/* (ignored) */",
248 `ignored ${context} ${request}`,
249 `${request} (ignored)`
250 )
251 );
252 }
253
254 const userRequest =
255 (matchResource !== undefined ? `${matchResource}!=!` : "") +
256 loaders
257 .map(loaderToIdent)
258 .concat([resource])
259 .join("!");
260
261 let resourcePath =
262 matchResource !== undefined ? matchResource : resource;
263 let resourceQuery = "";
264 const queryIndex = resourcePath.indexOf("?");
265 if (queryIndex >= 0) {
266 resourceQuery = resourcePath.substr(queryIndex);
267 resourcePath = resourcePath.substr(0, queryIndex);
268 }
269
270 const result = this.ruleSet.exec({
271 resource: resourcePath,
272 realResource:
273 matchResource !== undefined
274 ? resource.replace(/\?.*/, "")
275 : resourcePath,
276 resourceQuery,
277 issuer: contextInfo.issuer,
278 compiler: contextInfo.compiler
279 });
280 const settings = {};
281 const useLoadersPost = [];
282 const useLoaders = [];
283 const useLoadersPre = [];
284 for (const r of result) {
285 if (r.type === "use") {
286 if (r.enforce === "post" && !noPrePostAutoLoaders) {
287 useLoadersPost.push(r.value);
288 } else if (
289 r.enforce === "pre" &&
290 !noPreAutoLoaders &&
291 !noPrePostAutoLoaders
292 ) {
293 useLoadersPre.push(r.value);
294 } else if (
295 !r.enforce &&
296 !noAutoLoaders &&
297 !noPrePostAutoLoaders
298 ) {
299 useLoaders.push(r.value);
300 }
301 } else if (
302 typeof r.value === "object" &&
303 r.value !== null &&
304 typeof settings[r.type] === "object" &&
305 settings[r.type] !== null
306 ) {
307 settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
308 } else {
309 settings[r.type] = r.value;
310 }
311 }
312 asyncLib.parallel(
313 [
314 this.resolveRequestArray.bind(
315 this,
316 contextInfo,
317 this.context,
318 useLoadersPost,
319 loaderResolver
320 ),
321 this.resolveRequestArray.bind(
322 this,
323 contextInfo,
324 this.context,
325 useLoaders,
326 loaderResolver
327 ),
328 this.resolveRequestArray.bind(
329 this,
330 contextInfo,
331 this.context,
332 useLoadersPre,
333 loaderResolver
334 )
335 ],
336 (err, results) => {
337 if (err) return callback(err);
338 if (matchResource === undefined) {
339 loaders = results[0].concat(loaders, results[1], results[2]);
340 } else {
341 loaders = results[0].concat(results[1], loaders, results[2]);
342 }
343 process.nextTick(() => {
344 const type = settings.type;
345 const resolveOptions = settings.resolve;
346 callback(null, {
347 context: context,
348 request: loaders
349 .map(loaderToIdent)
350 .concat([resource])
351 .join("!"),
352 dependencies: data.dependencies,
353 userRequest,
354 rawRequest: request,
355 loaders,
356 resource,
357 matchResource,
358 resourceResolveData,
359 settings,
360 type,
361 parser: this.getParser(type, settings.parser),
362 generator: this.getGenerator(type, settings.generator),
363 resolveOptions
364 });
365 });
366 }
367 );
368 }
369 );
370 });
371 }
372
373 create(data, callback) {
374 const dependencies = data.dependencies;
375 const cacheEntry = dependencyCache.get(dependencies[0]);
376 if (cacheEntry) return callback(null, cacheEntry);
377 const context = data.context || this.context;
378 const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
379 const request = dependencies[0].request;
380 const contextInfo = data.contextInfo || {};
381 this.hooks.beforeResolve.callAsync(
382 {
383 contextInfo,
384 resolveOptions,
385 context,
386 request,
387 dependencies
388 },
389 (err, result) => {
390 if (err) return callback(err);
391
392 // Ignored
393 if (!result) return callback();
394
395 const factory = this.hooks.factory.call(null);
396
397 // Ignored
398 if (!factory) return callback();
399
400 factory(result, (err, module) => {
401 if (err) return callback(err);
402
403 if (module && this.cachePredicate(module)) {
404 for (const d of dependencies) {
405 dependencyCache.set(d, module);
406 }
407 }
408
409 callback(null, module);
410 });
411 }
412 );
413 }
414
415 resolveRequestArray(contextInfo, context, array, resolver, callback) {
416 if (array.length === 0) return callback(null, []);
417 asyncLib.map(
418 array,
419 (item, callback) => {
420 resolver.resolve(
421 contextInfo,
422 context,
423 item.loader,
424 {},
425 (err, result) => {
426 if (
427 err &&
428 /^[^/]*$/.test(item.loader) &&
429 !/-loader$/.test(item.loader)
430 ) {
431 return resolver.resolve(
432 contextInfo,
433 context,
434 item.loader + "-loader",
435 {},
436 err2 => {
437 if (!err2) {
438 err.message =
439 err.message +
440 "\n" +
441 "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" +
442 ` You need to specify '${item.loader}-loader' instead of '${item.loader}',\n` +
443 " see https://webpack.js.org/migrate/3/#automatic-loader-module-name-extension-removed";
444 }
445 callback(err);
446 }
447 );
448 }
449 if (err) return callback(err);
450
451 const optionsOnly = item.options
452 ? {
453 options: item.options
454 }
455 : undefined;
456 return callback(
457 null,
458 Object.assign({}, item, identToLoaderRequest(result), optionsOnly)
459 );
460 }
461 );
462 },
463 callback
464 );
465 }
466
467 getParser(type, parserOptions) {
468 let ident = type;
469 if (parserOptions) {
470 if (parserOptions.ident) {
471 ident = `${type}|${parserOptions.ident}`;
472 } else {
473 ident = JSON.stringify([type, parserOptions]);
474 }
475 }
476 if (ident in this.parserCache) {
477 return this.parserCache[ident];
478 }
479 return (this.parserCache[ident] = this.createParser(type, parserOptions));
480 }
481
482 createParser(type, parserOptions = {}) {
483 const parser = this.hooks.createParser.for(type).call(parserOptions);
484 if (!parser) {
485 throw new Error(`No parser registered for ${type}`);
486 }
487 this.hooks.parser.for(type).call(parser, parserOptions);
488 return parser;
489 }
490
491 getGenerator(type, generatorOptions) {
492 let ident = type;
493 if (generatorOptions) {
494 if (generatorOptions.ident) {
495 ident = `${type}|${generatorOptions.ident}`;
496 } else {
497 ident = JSON.stringify([type, generatorOptions]);
498 }
499 }
500 if (ident in this.generatorCache) {
501 return this.generatorCache[ident];
502 }
503 return (this.generatorCache[ident] = this.createGenerator(
504 type,
505 generatorOptions
506 ));
507 }
508
509 createGenerator(type, generatorOptions = {}) {
510 const generator = this.hooks.createGenerator
511 .for(type)
512 .call(generatorOptions);
513 if (!generator) {
514 throw new Error(`No generator registered for ${type}`);
515 }
516 this.hooks.generator.for(type).call(generator, generatorOptions);
517 return generator;
518 }
519
520 getResolver(type, resolveOptions) {
521 return this.resolverFactory.get(
522 type,
523 resolveOptions || EMPTY_RESOLVE_OPTIONS
524 );
525 }
526}
527
528module.exports = NormalModuleFactory;