1 |
|
2 |
|
3 |
|
4 |
|
5 | "use strict";
|
6 |
|
7 | const path = require("path");
|
8 | const asyncLib = require("neo-async");
|
9 | const {
|
10 | Tapable,
|
11 | AsyncSeriesWaterfallHook,
|
12 | SyncWaterfallHook,
|
13 | SyncBailHook,
|
14 | SyncHook,
|
15 | HookMap
|
16 | } = require("tapable");
|
17 | const NormalModule = require("./NormalModule");
|
18 | const RawModule = require("./RawModule");
|
19 | const RuleSet = require("./RuleSet");
|
20 | const cachedMerge = require("./util/cachedMerge");
|
21 |
|
22 | const EMPTY_RESOLVE_OPTIONS = {};
|
23 |
|
24 | const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/;
|
25 |
|
26 | const 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 |
|
42 | const 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 |
|
59 | const dependencyCache = new WeakMap();
|
60 |
|
61 | class 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 |
|
127 | if (!resolver) return callback();
|
128 |
|
129 | resolver(result, (err, data) => {
|
130 | if (err) return callback(err);
|
131 |
|
132 |
|
133 | if (!data) return callback();
|
134 |
|
135 |
|
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 |
|
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 |
|
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 |
|
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 |
|
389 | if (!result) return callback();
|
390 |
|
391 | const factory = this.hooks.factory.call(null);
|
392 |
|
393 |
|
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 |
|
526 | module.exports = NormalModuleFactory;
|