You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
11 KiB
357 lines
11 KiB
1 month ago
|
/*
|
||
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
||
|
Author Tobias Koppers @sokra
|
||
|
*/
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
const ModuleNotFoundError = require("../ModuleNotFoundError");
|
||
|
const RuntimeGlobals = require("../RuntimeGlobals");
|
||
|
const WebpackError = require("../WebpackError");
|
||
|
const { parseOptions } = require("../container/options");
|
||
|
const LazySet = require("../util/LazySet");
|
||
|
const createSchemaValidation = require("../util/create-schema-validation");
|
||
|
const { parseRange } = require("../util/semver");
|
||
|
const ConsumeSharedFallbackDependency = require("./ConsumeSharedFallbackDependency");
|
||
|
const ConsumeSharedModule = require("./ConsumeSharedModule");
|
||
|
const ConsumeSharedRuntimeModule = require("./ConsumeSharedRuntimeModule");
|
||
|
const ProvideForSharedDependency = require("./ProvideForSharedDependency");
|
||
|
const { resolveMatchedConfigs } = require("./resolveMatchedConfigs");
|
||
|
const {
|
||
|
isRequiredVersion,
|
||
|
getDescriptionFile,
|
||
|
getRequiredVersionFromDescriptionFile
|
||
|
} = require("./utils");
|
||
|
|
||
|
/** @typedef {import("../../declarations/plugins/sharing/ConsumeSharedPlugin").ConsumeSharedPluginOptions} ConsumeSharedPluginOptions */
|
||
|
/** @typedef {import("../../declarations/plugins/sharing/ConsumeSharedPlugin").ConsumesConfig} ConsumesConfig */
|
||
|
/** @typedef {import("../Compiler")} Compiler */
|
||
|
/** @typedef {import("../ResolverFactory").ResolveOptionsWithDependencyType} ResolveOptionsWithDependencyType */
|
||
|
/** @typedef {import("../util/semver").SemVerRange} SemVerRange */
|
||
|
/** @typedef {import("./ConsumeSharedModule").ConsumeOptions} ConsumeOptions */
|
||
|
/** @typedef {import("./utils").DescriptionFile} DescriptionFile */
|
||
|
|
||
|
const validate = createSchemaValidation(
|
||
|
require("../../schemas/plugins/sharing/ConsumeSharedPlugin.check.js"),
|
||
|
() => require("../../schemas/plugins/sharing/ConsumeSharedPlugin.json"),
|
||
|
{
|
||
|
name: "Consume Shared Plugin",
|
||
|
baseDataPath: "options"
|
||
|
}
|
||
|
);
|
||
|
|
||
|
/** @type {ResolveOptionsWithDependencyType} */
|
||
|
const RESOLVE_OPTIONS = { dependencyType: "esm" };
|
||
|
const PLUGIN_NAME = "ConsumeSharedPlugin";
|
||
|
|
||
|
class ConsumeSharedPlugin {
|
||
|
/**
|
||
|
* @param {ConsumeSharedPluginOptions} options options
|
||
|
*/
|
||
|
constructor(options) {
|
||
|
if (typeof options !== "string") {
|
||
|
validate(options);
|
||
|
}
|
||
|
|
||
|
/** @type {[string, ConsumeOptions][]} */
|
||
|
this._consumes = parseOptions(
|
||
|
options.consumes,
|
||
|
(item, key) => {
|
||
|
if (Array.isArray(item)) throw new Error("Unexpected array in options");
|
||
|
/** @type {ConsumeOptions} */
|
||
|
const result =
|
||
|
item === key || !isRequiredVersion(item)
|
||
|
? // item is a request/key
|
||
|
{
|
||
|
import: key,
|
||
|
shareScope: options.shareScope || "default",
|
||
|
shareKey: key,
|
||
|
requiredVersion: undefined,
|
||
|
packageName: undefined,
|
||
|
strictVersion: false,
|
||
|
singleton: false,
|
||
|
eager: false
|
||
|
}
|
||
|
: // key is a request/key
|
||
|
// item is a version
|
||
|
{
|
||
|
import: key,
|
||
|
shareScope: options.shareScope || "default",
|
||
|
shareKey: key,
|
||
|
requiredVersion: parseRange(item),
|
||
|
strictVersion: true,
|
||
|
packageName: undefined,
|
||
|
singleton: false,
|
||
|
eager: false
|
||
|
};
|
||
|
return result;
|
||
|
},
|
||
|
(item, key) => ({
|
||
|
import: item.import === false ? undefined : item.import || key,
|
||
|
shareScope: item.shareScope || options.shareScope || "default",
|
||
|
shareKey: item.shareKey || key,
|
||
|
requiredVersion:
|
||
|
typeof item.requiredVersion === "string"
|
||
|
? parseRange(item.requiredVersion)
|
||
|
: item.requiredVersion,
|
||
|
strictVersion:
|
||
|
typeof item.strictVersion === "boolean"
|
||
|
? item.strictVersion
|
||
|
: item.import !== false && !item.singleton,
|
||
|
packageName: item.packageName,
|
||
|
singleton: Boolean(item.singleton),
|
||
|
eager: Boolean(item.eager)
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply the plugin
|
||
|
* @param {Compiler} compiler the compiler instance
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
apply(compiler) {
|
||
|
compiler.hooks.thisCompilation.tap(
|
||
|
PLUGIN_NAME,
|
||
|
(compilation, { normalModuleFactory }) => {
|
||
|
compilation.dependencyFactories.set(
|
||
|
ConsumeSharedFallbackDependency,
|
||
|
normalModuleFactory
|
||
|
);
|
||
|
|
||
|
/** @type {Map<string, ConsumeOptions>} */
|
||
|
let unresolvedConsumes;
|
||
|
/** @type {Map<string, ConsumeOptions>} */
|
||
|
let resolvedConsumes;
|
||
|
/** @type {Map<string, ConsumeOptions>} */
|
||
|
let prefixedConsumes;
|
||
|
const promise = resolveMatchedConfigs(compilation, this._consumes).then(
|
||
|
({ resolved, unresolved, prefixed }) => {
|
||
|
resolvedConsumes = resolved;
|
||
|
unresolvedConsumes = unresolved;
|
||
|
prefixedConsumes = prefixed;
|
||
|
}
|
||
|
);
|
||
|
|
||
|
const resolver = compilation.resolverFactory.get(
|
||
|
"normal",
|
||
|
RESOLVE_OPTIONS
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* @param {string} context issuer directory
|
||
|
* @param {string} request request
|
||
|
* @param {ConsumeOptions} config options
|
||
|
* @returns {Promise<ConsumeSharedModule>} create module
|
||
|
*/
|
||
|
const createConsumeSharedModule = (context, request, config) => {
|
||
|
/**
|
||
|
* @param {string} details details
|
||
|
*/
|
||
|
const requiredVersionWarning = details => {
|
||
|
const error = new WebpackError(
|
||
|
`No required version specified and unable to automatically determine one. ${details}`
|
||
|
);
|
||
|
error.file = `shared module ${request}`;
|
||
|
compilation.warnings.push(error);
|
||
|
};
|
||
|
const directFallback =
|
||
|
config.import &&
|
||
|
/^(\.\.?(\/|$)|\/|[A-Za-z]:|\\\\)/.test(config.import);
|
||
|
return Promise.all([
|
||
|
new Promise(
|
||
|
/**
|
||
|
* @param {(value?: string) => void} resolve resolve
|
||
|
*/
|
||
|
resolve => {
|
||
|
if (!config.import) {
|
||
|
resolve();
|
||
|
return;
|
||
|
}
|
||
|
const resolveContext = {
|
||
|
/** @type {LazySet<string>} */
|
||
|
fileDependencies: new LazySet(),
|
||
|
/** @type {LazySet<string>} */
|
||
|
contextDependencies: new LazySet(),
|
||
|
/** @type {LazySet<string>} */
|
||
|
missingDependencies: new LazySet()
|
||
|
};
|
||
|
resolver.resolve(
|
||
|
{},
|
||
|
directFallback ? compiler.context : context,
|
||
|
config.import,
|
||
|
resolveContext,
|
||
|
(err, result) => {
|
||
|
compilation.contextDependencies.addAll(
|
||
|
resolveContext.contextDependencies
|
||
|
);
|
||
|
compilation.fileDependencies.addAll(
|
||
|
resolveContext.fileDependencies
|
||
|
);
|
||
|
compilation.missingDependencies.addAll(
|
||
|
resolveContext.missingDependencies
|
||
|
);
|
||
|
if (err) {
|
||
|
compilation.errors.push(
|
||
|
new ModuleNotFoundError(null, err, {
|
||
|
name: `resolving fallback for shared module ${request}`
|
||
|
})
|
||
|
);
|
||
|
return resolve();
|
||
|
}
|
||
|
resolve(/** @type {string} */ (result));
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
),
|
||
|
new Promise(
|
||
|
/**
|
||
|
* @param {(value?: SemVerRange) => void} resolve resolve
|
||
|
*/
|
||
|
resolve => {
|
||
|
if (config.requiredVersion !== undefined) {
|
||
|
resolve(/** @type {SemVerRange} */ (config.requiredVersion));
|
||
|
return;
|
||
|
}
|
||
|
let packageName = config.packageName;
|
||
|
if (packageName === undefined) {
|
||
|
if (/^(\/|[A-Za-z]:|\\\\)/.test(request)) {
|
||
|
// For relative or absolute requests we don't automatically use a packageName.
|
||
|
// If wished one can specify one with the packageName option.
|
||
|
resolve();
|
||
|
return;
|
||
|
}
|
||
|
const match = /^((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(request);
|
||
|
if (!match) {
|
||
|
requiredVersionWarning(
|
||
|
"Unable to extract the package name from request."
|
||
|
);
|
||
|
resolve();
|
||
|
return;
|
||
|
}
|
||
|
packageName = match[0];
|
||
|
}
|
||
|
|
||
|
getDescriptionFile(
|
||
|
compilation.inputFileSystem,
|
||
|
context,
|
||
|
["package.json"],
|
||
|
(err, result) => {
|
||
|
if (err) {
|
||
|
requiredVersionWarning(
|
||
|
`Unable to read description file: ${err}`
|
||
|
);
|
||
|
return resolve();
|
||
|
}
|
||
|
const { data, path: descriptionPath } =
|
||
|
/** @type {DescriptionFile} */ (result);
|
||
|
if (!data) {
|
||
|
requiredVersionWarning(
|
||
|
`Unable to find description file in ${context}.`
|
||
|
);
|
||
|
return resolve();
|
||
|
}
|
||
|
if (data.name === packageName) {
|
||
|
// Package self-referencing
|
||
|
return resolve();
|
||
|
}
|
||
|
const requiredVersion =
|
||
|
getRequiredVersionFromDescriptionFile(data, packageName);
|
||
|
if (typeof requiredVersion !== "string") {
|
||
|
requiredVersionWarning(
|
||
|
`Unable to find required version for "${packageName}" in description file (${descriptionPath}). It need to be in dependencies, devDependencies or peerDependencies.`
|
||
|
);
|
||
|
return resolve();
|
||
|
}
|
||
|
resolve(parseRange(requiredVersion));
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
)
|
||
|
]).then(
|
||
|
([importResolved, requiredVersion]) =>
|
||
|
new ConsumeSharedModule(
|
||
|
directFallback ? compiler.context : context,
|
||
|
{
|
||
|
...config,
|
||
|
importResolved,
|
||
|
import: importResolved ? config.import : undefined,
|
||
|
requiredVersion
|
||
|
}
|
||
|
)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
normalModuleFactory.hooks.factorize.tapPromise(
|
||
|
PLUGIN_NAME,
|
||
|
({ context, request, dependencies }) =>
|
||
|
// wait for resolving to be complete
|
||
|
promise.then(() => {
|
||
|
if (
|
||
|
dependencies[0] instanceof ConsumeSharedFallbackDependency ||
|
||
|
dependencies[0] instanceof ProvideForSharedDependency
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
const match = unresolvedConsumes.get(request);
|
||
|
if (match !== undefined) {
|
||
|
return createConsumeSharedModule(context, request, match);
|
||
|
}
|
||
|
for (const [prefix, options] of prefixedConsumes) {
|
||
|
if (request.startsWith(prefix)) {
|
||
|
const remainder = request.slice(prefix.length);
|
||
|
return createConsumeSharedModule(context, request, {
|
||
|
...options,
|
||
|
import: options.import
|
||
|
? options.import + remainder
|
||
|
: undefined,
|
||
|
shareKey: options.shareKey + remainder
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
);
|
||
|
normalModuleFactory.hooks.createModule.tapPromise(
|
||
|
PLUGIN_NAME,
|
||
|
({ resource }, { context, dependencies }) => {
|
||
|
if (
|
||
|
dependencies[0] instanceof ConsumeSharedFallbackDependency ||
|
||
|
dependencies[0] instanceof ProvideForSharedDependency
|
||
|
) {
|
||
|
return Promise.resolve();
|
||
|
}
|
||
|
const options = resolvedConsumes.get(
|
||
|
/** @type {string} */ (resource)
|
||
|
);
|
||
|
if (options !== undefined) {
|
||
|
return createConsumeSharedModule(
|
||
|
context,
|
||
|
/** @type {string} */ (resource),
|
||
|
options
|
||
|
);
|
||
|
}
|
||
|
return Promise.resolve();
|
||
|
}
|
||
|
);
|
||
|
compilation.hooks.additionalTreeRuntimeRequirements.tap(
|
||
|
PLUGIN_NAME,
|
||
|
(chunk, set) => {
|
||
|
set.add(RuntimeGlobals.module);
|
||
|
set.add(RuntimeGlobals.moduleCache);
|
||
|
set.add(RuntimeGlobals.moduleFactoriesAddOnly);
|
||
|
set.add(RuntimeGlobals.shareScopeMap);
|
||
|
set.add(RuntimeGlobals.initializeSharing);
|
||
|
set.add(RuntimeGlobals.hasOwnProperty);
|
||
|
compilation.addRuntimeModule(
|
||
|
chunk,
|
||
|
new ConsumeSharedRuntimeModule(set)
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = ConsumeSharedPlugin;
|