UNPKG

13.7 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 cachedMerge = require("./util/cachedMerge");
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] = cachedMerge(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 loaders = results[0].concat(loaders, results[1], results[2]);
339 process.nextTick(() => {
340 const type = settings.type;
341 const resolveOptions = settings.resolve;
342 callback(null, {
343 context: context,
344 request: loaders
345 .map(loaderToIdent)
346 .concat([resource])
347 .join("!"),
348 dependencies: data.dependencies,
349 userRequest,
350 rawRequest: request,
351 loaders,
352 resource,
353 matchResource,
354 resourceResolveData,
355 settings,
356 type,
357 parser: this.getParser(type, settings.parser),
358 generator: this.getGenerator(type, settings.generator),
359 resolveOptions
360 });
361 });
362 }
363 );
364 }
365 );
366 });
367 }
368
369 create(data, callback) {
370 const dependencies = data.dependencies;
371 const cacheEntry = dependencyCache.get(dependencies[0]);
372 if (cacheEntry) return callback(null, cacheEntry);
373 const context = data.context || this.context;
374 const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
375 const request = dependencies[0].request;
376 const contextInfo = data.contextInfo || {};
377 this.hooks.beforeResolve.callAsync(
378 {
379 contextInfo,
380 resolveOptions,
381 context,
382 request,
383 dependencies
384 },
385 (err, result) => {
386 if (err) return callback(err);
387
388 // Ignored
389 if (!result) return callback();
390
391 const factory = this.hooks.factory.call(null);
392
393 // Ignored
394 if (!factory) return callback();
395
396 factory(result, (err, module) => {
397 if (err) return callback(err);
398
399 if (module && this.cachePredicate(module)) {
400 for (const d of dependencies) {
401 dependencyCache.set(d, module);
402 }
403 }
404
405 callback(null, module);
406 });
407 }
408 );
409 }
410
411 resolveRequestArray(contextInfo, context, array, resolver, callback) {
412 if (array.length === 0) return callback(null, []);
413 asyncLib.map(
414 array,
415 (item, callback) => {
416 resolver.resolve(
417 contextInfo,
418 context,
419 item.loader,
420 {},
421 (err, result) => {
422 if (
423 err &&
424 /^[^/]*$/.test(item.loader) &&
425 !/-loader$/.test(item.loader)
426 ) {
427 return resolver.resolve(
428 contextInfo,
429 context,
430 item.loader + "-loader",
431 {},
432 err2 => {
433 if (!err2) {
434 err.message =
435 err.message +
436 "\n" +
437 "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" +
438 ` You need to specify '${
439 item.loader
440 }-loader' instead of '${item.loader}',\n` +
441 " see https://webpack.js.org/migrate/3/#automatic-loader-module-name-extension-removed";
442 }
443 callback(err);
444 }
445 );
446 }
447 if (err) return callback(err);
448
449 const optionsOnly = item.options
450 ? {
451 options: item.options
452 }
453 : undefined;
454 return callback(
455 null,
456 Object.assign({}, item, identToLoaderRequest(result), optionsOnly)
457 );
458 }
459 );
460 },
461 callback
462 );
463 }
464
465 getParser(type, parserOptions) {
466 let ident = type;
467 if (parserOptions) {
468 if (parserOptions.ident) {
469 ident = `${type}|${parserOptions.ident}`;
470 } else {
471 ident = JSON.stringify([type, parserOptions]);
472 }
473 }
474 if (ident in this.parserCache) {
475 return this.parserCache[ident];
476 }
477 return (this.parserCache[ident] = this.createParser(type, parserOptions));
478 }
479
480 createParser(type, parserOptions = {}) {
481 const parser = this.hooks.createParser.for(type).call(parserOptions);
482 if (!parser) {
483 throw new Error(`No parser registered for ${type}`);
484 }
485 this.hooks.parser.for(type).call(parser, parserOptions);
486 return parser;
487 }
488
489 getGenerator(type, generatorOptions) {
490 let ident = type;
491 if (generatorOptions) {
492 if (generatorOptions.ident) {
493 ident = `${type}|${generatorOptions.ident}`;
494 } else {
495 ident = JSON.stringify([type, generatorOptions]);
496 }
497 }
498 if (ident in this.generatorCache) {
499 return this.generatorCache[ident];
500 }
501 return (this.generatorCache[ident] = this.createGenerator(
502 type,
503 generatorOptions
504 ));
505 }
506
507 createGenerator(type, generatorOptions = {}) {
508 const generator = this.hooks.createGenerator
509 .for(type)
510 .call(generatorOptions);
511 if (!generator) {
512 throw new Error(`No generator registered for ${type}`);
513 }
514 this.hooks.generator.for(type).call(generator, generatorOptions);
515 return generator;
516 }
517
518 getResolver(type, resolveOptions) {
519 return this.resolverFactory.get(
520 type,
521 resolveOptions || EMPTY_RESOLVE_OPTIONS
522 );
523 }
524}
525
526module.exports = NormalModuleFactory;