390 lines
11 KiB
JavaScript
390 lines
11 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const asyncLib = require("neo-async");
|
|
const { AsyncSeriesWaterfallHook, SyncWaterfallHook } = require("tapable");
|
|
const ContextModule = require("./ContextModule");
|
|
const ModuleFactory = require("./ModuleFactory");
|
|
const ContextElementDependency = require("./dependencies/ContextElementDependency");
|
|
const { cachedSetProperty } = require("./util/cleverMerge");
|
|
const { createFakeHook } = require("./util/deprecation");
|
|
const { join } = require("./util/fs");
|
|
|
|
/** @typedef {import("./ContextModule").ContextModuleOptions} ContextModuleOptions */
|
|
/** @typedef {import("./ContextModule").ResolveDependenciesCallback} ResolveDependenciesCallback */
|
|
/** @typedef {import("./Module")} Module */
|
|
/** @typedef {import("./ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
|
|
/** @typedef {import("./ModuleFactory").ModuleFactoryResult} ModuleFactoryResult */
|
|
/** @typedef {import("./ResolverFactory")} ResolverFactory */
|
|
/** @typedef {import("./dependencies/ContextDependency")} ContextDependency */
|
|
/** @template T @typedef {import("./util/deprecation").FakeHook<T>} FakeHook<T> */
|
|
/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
|
|
|
|
const EMPTY_RESOLVE_OPTIONS = {};
|
|
|
|
module.exports = class ContextModuleFactory extends ModuleFactory {
|
|
/**
|
|
* @param {ResolverFactory} resolverFactory resolverFactory
|
|
*/
|
|
constructor(resolverFactory) {
|
|
super();
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO[], ContextModuleOptions]>} */
|
|
const alternativeRequests = new AsyncSeriesWaterfallHook([
|
|
"modules",
|
|
"options"
|
|
]);
|
|
this.hooks = Object.freeze({
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO]>} */
|
|
beforeResolve: new AsyncSeriesWaterfallHook(["data"]),
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO]>} */
|
|
afterResolve: new AsyncSeriesWaterfallHook(["data"]),
|
|
/** @type {SyncWaterfallHook<[string[]]>} */
|
|
contextModuleFiles: new SyncWaterfallHook(["files"]),
|
|
/** @type {FakeHook<Pick<AsyncSeriesWaterfallHook<[TODO[]]>, "tap" | "tapAsync" | "tapPromise" | "name">>} */
|
|
alternatives: createFakeHook(
|
|
{
|
|
name: "alternatives",
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO[]]>["intercept"]} */
|
|
intercept: interceptor => {
|
|
throw new Error(
|
|
"Intercepting fake hook ContextModuleFactory.hooks.alternatives is not possible, use ContextModuleFactory.hooks.alternativeRequests instead"
|
|
);
|
|
},
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO[]]>["tap"]} */
|
|
tap: (options, fn) => {
|
|
alternativeRequests.tap(options, fn);
|
|
},
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO[]]>["tapAsync"]} */
|
|
tapAsync: (options, fn) => {
|
|
alternativeRequests.tapAsync(options, (items, _options, callback) =>
|
|
fn(items, callback)
|
|
);
|
|
},
|
|
/** @type {AsyncSeriesWaterfallHook<[TODO[]]>["tapPromise"]} */
|
|
tapPromise: (options, fn) => {
|
|
alternativeRequests.tapPromise(options, fn);
|
|
}
|
|
},
|
|
"ContextModuleFactory.hooks.alternatives has deprecated in favor of ContextModuleFactory.hooks.alternativeRequests with an additional options argument.",
|
|
"DEP_WEBPACK_CONTEXT_MODULE_FACTORY_ALTERNATIVES"
|
|
),
|
|
alternativeRequests
|
|
});
|
|
this.resolverFactory = resolverFactory;
|
|
}
|
|
|
|
/**
|
|
* @param {ModuleFactoryCreateData} data data object
|
|
* @param {function(Error=, ModuleFactoryResult=): void} callback callback
|
|
* @returns {void}
|
|
*/
|
|
create(data, callback) {
|
|
const context = data.context;
|
|
const dependencies = data.dependencies;
|
|
const resolveOptions = data.resolveOptions;
|
|
const dependency = /** @type {ContextDependency} */ (dependencies[0]);
|
|
const fileDependencies = new Set();
|
|
const missingDependencies = new Set();
|
|
const contextDependencies = new Set();
|
|
this.hooks.beforeResolve.callAsync(
|
|
{
|
|
context: context,
|
|
dependencies: dependencies,
|
|
resolveOptions,
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies,
|
|
...dependency.options
|
|
},
|
|
(err, beforeResolveResult) => {
|
|
if (err) {
|
|
return callback(err, {
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
});
|
|
}
|
|
|
|
// Ignored
|
|
if (!beforeResolveResult) {
|
|
return callback(null, {
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
});
|
|
}
|
|
|
|
const context = beforeResolveResult.context;
|
|
const request = beforeResolveResult.request;
|
|
const resolveOptions = beforeResolveResult.resolveOptions;
|
|
|
|
let loaders,
|
|
resource,
|
|
loadersPrefix = "";
|
|
const idx = request.lastIndexOf("!");
|
|
if (idx >= 0) {
|
|
let loadersRequest = request.substr(0, idx + 1);
|
|
let i;
|
|
for (
|
|
i = 0;
|
|
i < loadersRequest.length && loadersRequest[i] === "!";
|
|
i++
|
|
) {
|
|
loadersPrefix += "!";
|
|
}
|
|
loadersRequest = loadersRequest
|
|
.substr(i)
|
|
.replace(/!+$/, "")
|
|
.replace(/!!+/g, "!");
|
|
if (loadersRequest === "") {
|
|
loaders = [];
|
|
} else {
|
|
loaders = loadersRequest.split("!");
|
|
}
|
|
resource = request.substr(idx + 1);
|
|
} else {
|
|
loaders = [];
|
|
resource = request;
|
|
}
|
|
|
|
const contextResolver = this.resolverFactory.get(
|
|
"context",
|
|
dependencies.length > 0
|
|
? cachedSetProperty(
|
|
resolveOptions || EMPTY_RESOLVE_OPTIONS,
|
|
"dependencyType",
|
|
dependencies[0].category
|
|
)
|
|
: resolveOptions
|
|
);
|
|
const loaderResolver = this.resolverFactory.get("loader");
|
|
|
|
asyncLib.parallel(
|
|
[
|
|
callback => {
|
|
contextResolver.resolve(
|
|
{},
|
|
context,
|
|
resource,
|
|
{
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
},
|
|
(err, result) => {
|
|
if (err) return callback(err);
|
|
callback(null, result);
|
|
}
|
|
);
|
|
},
|
|
callback => {
|
|
asyncLib.map(
|
|
loaders,
|
|
(loader, callback) => {
|
|
loaderResolver.resolve(
|
|
{},
|
|
context,
|
|
loader,
|
|
{
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
},
|
|
(err, result) => {
|
|
if (err) return callback(err);
|
|
callback(null, result);
|
|
}
|
|
);
|
|
},
|
|
callback
|
|
);
|
|
}
|
|
],
|
|
(err, result) => {
|
|
if (err) {
|
|
return callback(err, {
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
});
|
|
}
|
|
|
|
this.hooks.afterResolve.callAsync(
|
|
{
|
|
addon:
|
|
loadersPrefix +
|
|
result[1].join("!") +
|
|
(result[1].length > 0 ? "!" : ""),
|
|
resource: result[0],
|
|
resolveDependencies: this.resolveDependencies.bind(this),
|
|
...beforeResolveResult
|
|
},
|
|
(err, result) => {
|
|
if (err) {
|
|
return callback(err, {
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
});
|
|
}
|
|
|
|
// Ignored
|
|
if (!result) {
|
|
return callback(null, {
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
});
|
|
}
|
|
|
|
return callback(null, {
|
|
module: new ContextModule(result.resolveDependencies, result),
|
|
fileDependencies,
|
|
missingDependencies,
|
|
contextDependencies
|
|
});
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {InputFileSystem} fs file system
|
|
* @param {ContextModuleOptions} options options
|
|
* @param {ResolveDependenciesCallback} callback callback function
|
|
* @returns {void}
|
|
*/
|
|
resolveDependencies(fs, options, callback) {
|
|
const cmf = this;
|
|
const {
|
|
resource,
|
|
resourceQuery,
|
|
resourceFragment,
|
|
recursive,
|
|
regExp,
|
|
include,
|
|
exclude,
|
|
referencedExports,
|
|
category
|
|
} = options;
|
|
if (!regExp || !resource) return callback(null, []);
|
|
|
|
const addDirectoryChecked = (directory, visited, callback) => {
|
|
fs.realpath(directory, (err, realPath) => {
|
|
if (err) return callback(err);
|
|
if (visited.has(realPath)) return callback(null, []);
|
|
let recursionStack;
|
|
addDirectory(
|
|
directory,
|
|
(dir, callback) => {
|
|
if (recursionStack === undefined) {
|
|
recursionStack = new Set(visited);
|
|
recursionStack.add(realPath);
|
|
}
|
|
addDirectoryChecked(dir, recursionStack, callback);
|
|
},
|
|
callback
|
|
);
|
|
});
|
|
};
|
|
|
|
const addDirectory = (directory, addSubDirectory, callback) => {
|
|
fs.readdir(directory, (err, files) => {
|
|
if (err) return callback(err);
|
|
const processedFiles = cmf.hooks.contextModuleFiles.call(
|
|
/** @type {string[]} */ (files).map(file => file.normalize("NFC"))
|
|
);
|
|
if (!processedFiles || processedFiles.length === 0)
|
|
return callback(null, []);
|
|
asyncLib.map(
|
|
processedFiles.filter(p => p.indexOf(".") !== 0),
|
|
(segment, callback) => {
|
|
const subResource = join(fs, directory, segment);
|
|
|
|
if (!exclude || !subResource.match(exclude)) {
|
|
fs.stat(subResource, (err, stat) => {
|
|
if (err) {
|
|
if (err.code === "ENOENT") {
|
|
// ENOENT is ok here because the file may have been deleted between
|
|
// the readdir and stat calls.
|
|
return callback();
|
|
} else {
|
|
return callback(err);
|
|
}
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
if (!recursive) return callback();
|
|
addSubDirectory(subResource, callback);
|
|
} else if (
|
|
stat.isFile() &&
|
|
(!include || subResource.match(include))
|
|
) {
|
|
const obj = {
|
|
context: resource,
|
|
request:
|
|
"." +
|
|
subResource.substr(resource.length).replace(/\\/g, "/")
|
|
};
|
|
|
|
this.hooks.alternativeRequests.callAsync(
|
|
[obj],
|
|
options,
|
|
(err, alternatives) => {
|
|
if (err) return callback(err);
|
|
alternatives = alternatives
|
|
.filter(obj => regExp.test(obj.request))
|
|
.map(obj => {
|
|
const dep = new ContextElementDependency(
|
|
obj.request + resourceQuery + resourceFragment,
|
|
obj.request,
|
|
category,
|
|
referencedExports
|
|
);
|
|
dep.optional = true;
|
|
return dep;
|
|
});
|
|
callback(null, alternatives);
|
|
}
|
|
);
|
|
} else {
|
|
callback();
|
|
}
|
|
});
|
|
} else {
|
|
callback();
|
|
}
|
|
},
|
|
(err, result) => {
|
|
if (err) return callback(err);
|
|
|
|
if (!result) return callback(null, []);
|
|
|
|
const flattenedResult = [];
|
|
|
|
for (const item of result) {
|
|
if (item) flattenedResult.push(...item);
|
|
}
|
|
|
|
callback(null, flattenedResult);
|
|
}
|
|
);
|
|
});
|
|
};
|
|
|
|
if (typeof fs.realpath === "function") {
|
|
addDirectoryChecked(resource, new Set(), callback);
|
|
} else {
|
|
const addSubDirectory = (dir, callback) =>
|
|
addDirectory(dir, addSubDirectory, callback);
|
|
addDirectory(resource, addSubDirectory, callback);
|
|
}
|
|
}
|
|
};
|