webpack/lib/NormalModuleFactory.js

525 lines
13 KiB
JavaScript
Raw Normal View History

2013-01-30 18:49:25 +01:00
/*
2017-04-03 23:28:08 +02:00
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const path = require("path");
2018-02-11 05:27:09 +01:00
const asyncLib = require("neo-async");
const {
Tapable,
AsyncSeriesWaterfallHook,
SyncWaterfallHook,
SyncBailHook,
SyncHook,
HookMap
} = require("tapable");
2017-04-03 23:28:08 +02:00
const NormalModule = require("./NormalModule");
const RawModule = require("./RawModule");
const RuleSet = require("./RuleSet");
const cachedMerge = require("./util/cachedMerge");
const EMPTY_RESOLVE_OPTIONS = {};
const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/;
2017-11-08 11:32:05 +01:00
const loaderToIdent = data => {
if (!data.options) {
return data.loader;
}
if (typeof data.options === "string") {
return data.loader + "?" + data.options;
}
if (typeof data.options !== "object") {
throw new Error("loader options must be string or object");
}
if (data.ident) {
return data.loader + "??" + data.ident;
}
return data.loader + "?" + JSON.stringify(data.options);
2017-11-08 11:32:05 +01:00
};
2013-01-30 18:49:25 +01:00
2017-11-08 11:32:05 +01:00
const identToLoaderRequest = resultString => {
2017-04-04 10:26:16 +02:00
const idx = resultString.indexOf("?");
2018-02-25 02:00:20 +01:00
if (idx >= 0) {
2018-04-04 13:42:37 +02:00
const loader = resultString.substr(0, idx);
const options = resultString.substr(idx + 1);
return {
2018-04-04 13:42:37 +02:00
loader,
2017-04-03 23:28:08 +02:00
options
};
} else {
return {
loader: resultString,
2018-04-04 13:42:37 +02:00
options: undefined
2017-01-11 10:51:58 +01:00
};
}
2017-11-08 11:32:05 +01:00
};
const dependencyCache = new WeakMap();
2017-04-03 23:28:08 +02:00
class NormalModuleFactory extends Tapable {
constructor(context, resolverFactory, options) {
2017-04-03 23:28:08 +02:00
super();
2017-11-28 09:54:24 +01:00
this.hooks = {
resolver: new SyncWaterfallHook(["resolver"]),
factory: new SyncWaterfallHook(["factory"]),
beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
afterResolve: new AsyncSeriesWaterfallHook(["data"]),
createModule: new SyncBailHook(["data"]),
module: new SyncWaterfallHook(["module", "data"]),
createParser: new HookMap(() => new SyncBailHook(["parserOptions"])),
parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),
2018-02-25 02:00:20 +01:00
createGenerator: new HookMap(
() => new SyncBailHook(["generatorOptions"])
),
generator: new HookMap(
() => new SyncHook(["generator", "generatorOptions"])
)
2017-11-28 09:54:24 +01:00
};
this._pluginCompat.tap("NormalModuleFactory", options => {
2018-02-25 02:00:20 +01:00
switch (options.name) {
2017-11-28 09:54:24 +01:00
case "before-resolve":
case "after-resolve":
options.async = true;
break;
case "parser":
2018-02-25 02:00:20 +01:00
this.hooks.parser
.for("javascript/auto")
.tap(options.fn.name || "unnamed compat plugin", options.fn);
2017-11-28 09:54:24 +01:00
return true;
}
let match;
match = /^parser (.+)$/.exec(options.name);
2018-02-25 02:00:20 +01:00
if (match) {
this.hooks.parser
.for(match[1])
.tap(
options.fn.name || "unnamed compat plugin",
options.fn.bind(this)
);
2017-11-28 09:54:24 +01:00
return true;
}
match = /^create-parser (.+)$/.exec(options.name);
2018-02-25 02:00:20 +01:00
if (match) {
this.hooks.createParser
.for(match[1])
.tap(
options.fn.name || "unnamed compat plugin",
options.fn.bind(this)
);
2017-11-28 09:54:24 +01:00
return true;
}
});
this.resolverFactory = resolverFactory;
this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
2018-02-25 02:00:20 +01:00
this.cachePredicate =
typeof options.unsafeCache === "function"
? options.unsafeCache
: Boolean.bind(null, options.unsafeCache);
2017-04-03 23:28:08 +02:00
this.context = context || "";
this.parserCache = Object.create(null);
this.generatorCache = Object.create(null);
2017-12-13 21:35:39 +01:00
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
2017-11-28 09:54:24 +01:00
let resolver = this.hooks.resolver.call(null);
// Ignored
2018-02-25 02:00:20 +01:00
if (!resolver) return callback();
resolver(result, (err, data) => {
2018-02-25 02:00:20 +01:00
if (err) return callback(err);
// Ignored
2018-02-25 02:00:20 +01:00
if (!data) return callback();
// direct module
2018-02-25 02:00:20 +01:00
if (typeof data.source === "function") return callback(null, data);
2016-02-09 19:32:50 +01:00
2017-11-28 09:54:24 +01:00
this.hooks.afterResolve.callAsync(data, (err, result) => {
2018-02-25 02:00:20 +01:00
if (err) return callback(err);
2015-05-16 18:27:59 +02:00
// Ignored
2018-02-25 02:00:20 +01:00
if (!result) return callback();
2015-05-16 18:27:59 +02:00
2017-11-28 09:54:24 +01:00
let createdModule = this.hooks.createModule.call(result);
2018-02-25 02:00:20 +01:00
if (!createdModule) {
if (!result.request) {
return callback(new Error("Empty dependency (no request)"));
}
2017-04-03 23:28:08 +02:00
createdModule = new NormalModule(result);
2017-04-03 23:28:08 +02:00
}
2017-11-28 09:54:24 +01:00
createdModule = this.hooks.module.call(createdModule, result);
return callback(null, createdModule);
});
});
});
2017-12-13 21:35:39 +01:00
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
let matchResource = undefined;
let requestWithoutMatchResource = request;
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
if (matchResourceMatch) {
matchResource = matchResourceMatch[1];
if (/^\.\.?\//.test(matchResource)) {
matchResource = path.join(context, matchResource);
}
requestWithoutMatchResource = request.substr(
matchResourceMatch[0].length
);
}
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
const noAutoLoaders =
noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
let elements = requestWithoutMatchResource
2018-02-25 02:00:20 +01:00
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
let resource = elements.pop();
elements = elements.map(identToLoaderRequest);
2018-02-25 02:00:20 +01:00
asyncLib.parallel(
[
callback =>
this.resolveRequestArray(
contextInfo,
context,
elements,
loaderResolver,
callback
),
callback => {
if (resource === "" || resource[0] === "?") {
return callback(null, {
resource
});
}
2015-07-13 00:20:09 +02:00
2018-02-25 02:00:20 +01:00
normalResolver.resolve(
contextInfo,
context,
resource,
{},
(err, resource, resourceResolveData) => {
if (err) return callback(err);
callback(null, {
resourceResolveData,
resource
});
}
);
}
],
(err, results) => {
if (err) return callback(err);
let loaders = results[0];
const resourceResolveData = results[1].resourceResolveData;
resource = results[1].resource;
// translate option idents
try {
for (const item of loaders) {
if (typeof item.options === "string" && item.options[0] === "?") {
const ident = item.options.substr(1);
item.options = this.ruleSet.findOptionsByIdent(ident);
item.ident = ident;
}
2017-04-03 23:28:08 +02:00
}
2018-02-25 02:00:20 +01:00
} catch (e) {
return callback(e);
2018-01-22 13:52:43 +01:00
}
2018-02-25 02:00:20 +01:00
if (resource === false) {
// ignored
return callback(
null,
new RawModule(
"/* (ignored) */",
`ignored ${context} ${request}`,
`${request} (ignored)`
)
);
}
const userRequest =
(matchResource !== undefined ? `${matchResource}!=!` : "") +
loaders
.map(loaderToIdent)
.concat([resource])
.join("!");
2018-02-25 02:00:20 +01:00
let resourcePath =
matchResource !== undefined ? matchResource : resource;
2018-02-25 02:00:20 +01:00
let resourceQuery = "";
const queryIndex = resourcePath.indexOf("?");
if (queryIndex >= 0) {
resourceQuery = resourcePath.substr(queryIndex);
resourcePath = resourcePath.substr(0, queryIndex);
}
2018-02-25 02:00:20 +01:00
const result = this.ruleSet.exec({
resource: resourcePath,
realResource:
matchResource !== undefined
? resource.replace(/\?.*/, "")
: resourcePath,
2018-02-25 02:00:20 +01:00
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler
});
const settings = {};
const useLoadersPost = [];
const useLoaders = [];
const useLoadersPre = [];
for (const r of result) {
if (r.type === "use") {
if (r.enforce === "post" && !noPrePostAutoLoaders) {
2018-02-25 02:00:20 +01:00
useLoadersPost.push(r.value);
} else if (
2018-02-25 02:00:20 +01:00
r.enforce === "pre" &&
!noPreAutoLoaders &&
!noPrePostAutoLoaders
) {
2018-02-25 02:00:20 +01:00
useLoadersPre.push(r.value);
} else if (
!r.enforce &&
!noAutoLoaders &&
!noPrePostAutoLoaders
) {
2018-02-25 02:00:20 +01:00
useLoaders.push(r.value);
}
2018-02-25 02:00:20 +01:00
} else if (
typeof r.value === "object" &&
r.value !== null &&
typeof settings[r.type] === "object" &&
settings[r.type] !== null
) {
settings[r.type] = cachedMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}
2018-02-25 02:00:20 +01:00
asyncLib.parallel(
[
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoadersPost,
loaderResolver
),
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoaders,
loaderResolver
),
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoadersPre,
loaderResolver
)
],
(err, results) => {
if (err) return callback(err);
loaders = results[0].concat(loaders, results[1], results[2]);
process.nextTick(() => {
const type = settings.type;
const resolveOptions = settings.resolve;
callback(null, {
context: context,
request: loaders
.map(loaderToIdent)
.concat([resource])
.join("!"),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
matchResource,
2018-02-25 02:00:20 +01:00
resourceResolveData,
settings,
type,
parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),
resolveOptions
});
});
}
);
2018-01-22 13:52:43 +01:00
}
2018-02-25 02:00:20 +01:00
);
2017-04-03 23:28:08 +02:00
});
}
2013-01-30 18:49:25 +01:00
2017-04-03 23:28:08 +02:00
create(data, callback) {
2017-04-04 10:26:16 +02:00
const dependencies = data.dependencies;
const cacheEntry = dependencyCache.get(dependencies[0]);
2018-02-25 02:00:20 +01:00
if (cacheEntry) return callback(null, cacheEntry);
2017-04-04 10:26:16 +02:00
const context = data.context || this.context;
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
2017-04-04 10:26:16 +02:00
const request = dependencies[0].request;
const contextInfo = data.contextInfo || {};
2018-02-25 02:00:20 +01:00
this.hooks.beforeResolve.callAsync(
{
contextInfo,
resolveOptions,
context,
request,
dependencies
},
(err, result) => {
if (err) return callback(err);
2018-02-25 02:00:20 +01:00
// Ignored
if (!result) return callback();
2018-02-25 02:00:20 +01:00
const factory = this.hooks.factory.call(null);
2018-02-25 02:00:20 +01:00
// Ignored
if (!factory) return callback();
2018-02-25 02:00:20 +01:00
factory(result, (err, module) => {
if (err) return callback(err);
2017-04-03 23:28:08 +02:00
2018-02-25 02:00:20 +01:00
if (module && this.cachePredicate(module)) {
for (const d of dependencies) {
dependencyCache.set(d, module);
2018-02-25 02:00:20 +01:00
}
2018-01-22 13:52:43 +01:00
}
2017-04-03 23:28:08 +02:00
2018-02-25 02:00:20 +01:00
callback(null, module);
});
}
);
}
2017-04-03 23:28:08 +02:00
resolveRequestArray(contextInfo, context, array, resolver, callback) {
2018-02-25 02:00:20 +01:00
if (array.length === 0) return callback(null, []);
asyncLib.map(
array,
(item, callback) => {
resolver.resolve(
contextInfo,
context,
item.loader,
{},
(err, result) => {
if (
err &&
/^[^/]*$/.test(item.loader) &&
!/-loader$/.test(item.loader)
) {
return resolver.resolve(
contextInfo,
context,
item.loader + "-loader",
{},
err2 => {
if (!err2) {
err.message =
err.message +
"\n" +
"BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" +
2019-06-09 11:23:42 +02:00
` You need to specify '${item.loader}-loader' instead of '${item.loader}',\n` +
2018-05-24 16:05:56 +02:00
" see https://webpack.js.org/migrate/3/#automatic-loader-module-name-extension-removed";
2018-02-25 02:00:20 +01:00
}
callback(err);
}
);
2017-04-03 23:28:08 +02:00
}
2018-02-25 02:00:20 +01:00
if (err) return callback(err);
const optionsOnly = item.options
? {
options: item.options
2018-03-26 16:56:10 +02:00
}
2018-02-25 02:00:20 +01:00
: undefined;
return callback(
null,
Object.assign({}, item, identToLoaderRequest(result), optionsOnly)
);
}
);
},
callback
);
2017-04-03 23:28:08 +02:00
}
2017-10-30 13:56:57 +01:00
getParser(type, parserOptions) {
let ident = type;
2018-02-25 02:00:20 +01:00
if (parserOptions) {
if (parserOptions.ident) {
ident = `${type}|${parserOptions.ident}`;
} else {
ident = JSON.stringify([type, parserOptions]);
}
2017-04-03 23:28:08 +02:00
}
2018-02-25 02:00:20 +01:00
if (ident in this.parserCache) {
return this.parserCache[ident];
}
2018-02-25 02:00:20 +01:00
return (this.parserCache[ident] = this.createParser(type, parserOptions));
2017-04-03 23:28:08 +02:00
}
createParser(type, parserOptions = {}) {
2017-11-28 09:54:24 +01:00
const parser = this.hooks.createParser.for(type).call(parserOptions);
2018-02-25 02:00:20 +01:00
if (!parser) {
throw new Error(`No parser registered for ${type}`);
2017-10-30 13:56:57 +01:00
}
2017-11-28 09:54:24 +01:00
this.hooks.parser.for(type).call(parser, parserOptions);
return parser;
2017-04-03 23:28:08 +02:00
}
getGenerator(type, generatorOptions) {
let ident = type;
2018-02-25 02:00:20 +01:00
if (generatorOptions) {
if (generatorOptions.ident) {
ident = `${type}|${generatorOptions.ident}`;
} else {
ident = JSON.stringify([type, generatorOptions]);
}
}
2018-02-25 02:00:20 +01:00
if (ident in this.generatorCache) {
return this.generatorCache[ident];
}
2018-02-25 02:00:20 +01:00
return (this.generatorCache[ident] = this.createGenerator(
type,
generatorOptions
));
}
createGenerator(type, generatorOptions = {}) {
2018-02-25 02:00:20 +01:00
const generator = this.hooks.createGenerator
.for(type)
.call(generatorOptions);
if (!generator) {
throw new Error(`No generator registered for ${type}`);
}
this.hooks.generator.for(type).call(generator, generatorOptions);
return generator;
}
getResolver(type, resolveOptions) {
2018-02-25 02:00:20 +01:00
return this.resolverFactory.get(
type,
resolveOptions || EMPTY_RESOLVE_OPTIONS
);
}
2017-04-03 23:28:08 +02:00
}
module.exports = NormalModuleFactory;