Framework updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-03-04 23:20:19 +00:00
parent a89daf3d43
commit 3ed8517b2a
891 changed files with 11126 additions and 9600 deletions

View File

@@ -369,6 +369,7 @@ class CleanPlugin {
/** @param {CleanOptions} options options */
constructor(options = {}) {
validate(options);
/** @type {CleanOptions & { dry: boolean }} */
this.options = { dry: false, ...options };
}

View File

@@ -4019,7 +4019,7 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
* @param {string | ChunkGroupOptions} groupOptions options for the chunk group
* @param {Module=} module the module the references the chunk group
* @param {DependencyLocation=} loc the location from with the chunk group is referenced (inside of module)
* @param {string=} request the request from which the the chunk group is referenced
* @param {string=} request the request from which the chunk group is referenced
* @returns {ChunkGroup} the new or existing chunk group
*/
addChunkInGroup(groupOptions, module, loc, request) {
@@ -4067,7 +4067,7 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
* @param {EntryOptions} options options for the entrypoint
* @param {Module} module the module the references the chunk group
* @param {DependencyLocation} loc the location from with the chunk group is referenced (inside of module)
* @param {string} request the request from which the the chunk group is referenced
* @param {string} request the request from which the chunk group is referenced
* @returns {Entrypoint} the new or existing entrypoint
*/
addAsyncEntrypoint(options, module, loc, request) {
@@ -4531,12 +4531,14 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
(e) => e.chunks[e.chunks.length - 1]
)
)) {
const otherInfo =
/** @type {RuntimeChunkInfo} */
(runtimeChunksMap.get(other));
otherInfo.referencedBy.push(info);
info.remaining++;
remaining++;
const otherInfo = runtimeChunksMap.get(other);
// other may be a non-runtime chunk (e.g. worker chunk)
// when you have a worker chunk in your app.js (new Worker(...)) and as a separate entry point
if (otherInfo) {
otherInfo.referencedBy.push(info);
info.remaining++;
remaining++;
}
}
}
/** @type {Chunk[]} */

View File

@@ -119,6 +119,7 @@ const makeSerializable = require("./util/makeSerializable");
/** @typedef {Record<ModuleId, FakeMapType>} FakeMap */
/** @typedef {Record<string, ModuleId>} UserRequestMap */
/** @typedef {Record<ModuleId, ModuleId[]>} UserRequestsMap */
class ContextModule extends Module {
/**
@@ -711,7 +712,7 @@ class ContextModule extends Module {
/**
* @param {Dependency[]} dependencies all dependencies
* @param {ChunkGraph} chunkGraph chunk graph
* @returns {Map<string, ModuleId[] | undefined>} map with user requests
* @returns {UserRequestsMap} map with user requests
*/
getModuleDeferredAsyncDepsMap(dependencies, chunkGraph) {
const moduleGraph = chunkGraph.moduleGraph;
@@ -726,6 +727,7 @@ class ContextModule extends Module {
)
.filter(Boolean)
.sort(comparator);
/** @type {UserRequestsMap} */
const map = Object.create(null);
for (const module of sortedModules) {
if (!(/** @type {BuildMeta} */ (module.buildMeta).async)) {
@@ -740,7 +742,7 @@ class ContextModule extends Module {
}
/**
* @param {false | Map<string, ModuleId[] | undefined>} asyncDepsMap fake map
* @param {false | UserRequestsMap} asyncDepsMap fake map
* @returns {string} async deps map init statement
*/
getModuleDeferredAsyncDepsMapInitStatement(asyncDepsMap) {
@@ -1168,12 +1170,16 @@ function webpackAsyncContext(req) {
])});
}`
: `function webpackAsyncContext(req) {
if(!${RuntimeGlobals.hasOwnProperty}(map, req)) {
return Promise.resolve().then(${runtimeTemplate.basicFunction("", [
'var e = new Error("Cannot find module \'" + req + "\'");',
"e.code = 'MODULE_NOT_FOUND';",
"throw e;"
])});
try {
if(!${RuntimeGlobals.hasOwnProperty}(map, req)) {
return Promise.resolve().then(${runtimeTemplate.basicFunction("", [
'var e = new Error("Cannot find module \'" + req + "\'");',
"e.code = 'MODULE_NOT_FOUND';",
"throw e;"
])});
}
} catch(err) {
return Promise.reject(err);
}
var ids = map[req], id = ids[0];

View File

@@ -227,7 +227,7 @@ class Dependency {
*/
getReference(moduleGraph) {
throw new Error(
"Dependency.getReference was removed in favor of Dependency.getReferencedExports, ModuleGraph.getModule and ModuleGraph.getConnection().active"
"Dependency.getReference was removed in favor of Dependency.getReferencedExports, ModuleGraph.getModule, ModuleGraph.getConnection(), and ModuleGraphConnection.getActiveState(runtime)"
);
}

View File

@@ -26,6 +26,7 @@ As a result, the code may not run as expected or may cause runtime errors.`;
/** @type {string} */
this.name = "EnvironmentNotSupportAsyncWarning";
/** @type {Module} */
this.module = module;
}

View File

@@ -43,8 +43,11 @@ class EvalDevToolModulePlugin {
* @param {EvalDevToolModulePluginOptions=} options options
*/
constructor(options = {}) {
/** @type {DevtoolNamespace} */
this.namespace = options.namespace || "";
/** @type {string} */
this.sourceUrlComment = options.sourceUrlComment || "\n//# sourceURL=[url]";
/** @type {DevtoolModuleFilenameTemplate} */
this.moduleFilenameTemplate =
options.moduleFilenameTemplate ||
"webpack://[namespace]/[resourcePath]?[loaders]";

View File

@@ -17,10 +17,13 @@ const { makePathsAbsolute } = require("./util/identifier");
/** @typedef {import("webpack-sources").RawSourceMap} RawSourceMap */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../declarations/WebpackOptions").DevtoolNamespace} DevtoolNamespace */
/** @typedef {import("../declarations/WebpackOptions").DevtoolModuleFilenameTemplate} DevtoolModuleFilenameTemplate */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").Rules} Rules */
/** @typedef {import("./ChunkGraph").ModuleId} ModuleId */
/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("./ChunkGraph").ModuleId} ModuleId */
/** @typedef {import("./TemplatedPathPlugin").TemplatePath} TemplatePath */
/** @type {WeakMap<Source, Source>} */
const cache = new WeakMap();
@@ -51,14 +54,18 @@ class EvalSourceMapDevToolPlugin {
} else {
options = inputOptions;
}
/** @type {string} */
this.sourceMapComment =
options.append && typeof options.append !== "function"
? options.append
: "//# sourceURL=[module]\n//# sourceMappingURL=[url]";
/** @type {DevtoolModuleFilenameTemplate} */
this.moduleFilenameTemplate =
options.moduleFilenameTemplate ||
"webpack://[namespace]/[resource-path]?[hash]";
/** @type {DevtoolNamespace} */
this.namespace = options.namespace || "";
/** @type {SourceMapDevToolPluginOptions} */
this.options = options;
}

View File

@@ -933,36 +933,6 @@ class ExportInfo {
this._maxTarget = undefined;
}
// TODO webpack 5 remove
/**
* @private
* @param {EXPECTED_ANY} v v
*/
set used(v) {
throw new Error("REMOVED");
}
// TODO webpack 5 remove
/** @private */
get used() {
throw new Error("REMOVED");
}
// TODO webpack 5 remove
/**
* @private
* @param {EXPECTED_ANY} v v
*/
set usedName(v) {
throw new Error("REMOVED");
}
// TODO webpack 5 remove
/** @private */
get usedName() {
throw new Error("REMOVED");
}
get canMangle() {
switch (this.canMangleProvide) {
case undefined:

View File

@@ -995,7 +995,7 @@ class ExternalModule extends Module {
);
/** @type {CodeGenerationResultData} */
const data = new Map();
data.set("url", { javascript: request });
data.set("url", { javascript: /** @type {string} */ (request) });
return { sources, runtimeRequirements: RUNTIME_REQUIREMENTS, data };
}
case "css-url": {
@@ -1003,7 +1003,7 @@ class ExternalModule extends Module {
const sources = new Map();
/** @type {CodeGenerationResultData} */
const data = new Map();
data.set("url", { "css-url": request });
data.set("url", { "css-url": /** @type {string} */ (request) });
return { sources, runtimeRequirements: RUNTIME_REQUIREMENTS, data };
}
case "css-import": {

50
node_modules/webpack/lib/Module.js generated vendored
View File

@@ -80,6 +80,8 @@ const makeSerializable = require("./util/makeSerializable");
/** @typedef {KnownSourceType | string} SourceType */
/** @typedef {ReadonlySet<SourceType>} SourceTypes */
/** @typedef {ReadonlySet<typeof JAVASCRIPT_TYPE | string>} BasicSourceTypes */
// TODO webpack 6: compilation will be required in CodeGenerationContext
/**
* @typedef {object} CodeGenerationContext
@@ -104,11 +106,36 @@ const makeSerializable = require("./util/makeSerializable");
/** @typedef {Set<string>} RuntimeRequirements */
/** @typedef {ReadonlySet<string>} ReadOnlyRuntimeRequirements */
/** @typedef {Map<"topLevelDeclarations", Set<string>> & Map<"chunkInitFragments", InitFragment<EXPECTED_ANY>[]>} KnownCodeGenerationResultDataForJavascriptModules */
/** @typedef {Map<"url", { ["css-url"]: string }>} KnownCodeGenerationResultDataForCssModules */
/** @typedef {Map<"filename", string> & Map<"assetInfo", AssetInfo> & Map<"fullContentHash", string> & Map<"url", { javascript: string }>} KnownCodeGenerationResultDataForAssetModules */
/** @typedef {Map<"share-init", [{ shareScope: string, initStage: number, init: string }]>} KnownCodeGenerationResultForSharing */
/** @typedef {KnownCodeGenerationResultDataForJavascriptModules & KnownCodeGenerationResultDataForCssModules & KnownCodeGenerationResultDataForAssetModules & KnownCodeGenerationResultForSharing & Map<string, EXPECTED_ANY>} CodeGenerationResultData */
/**
* @typedef {object} AllCodeGenerationSchemas
* @property {Set<string>} topLevelDeclarations top level declarations for javascript modules
* @property {InitFragment<EXPECTED_ANY>[]} chunkInitFragments chunk init fragments for javascript modules
* @property {{ javascript?: string, ["css-url"]?: string }} url url for css and javascript modules
* @property {string} filename a filename for asset modules
* @property {AssetInfo} assetInfo an asset info for asset modules
* @property {string} fullContentHash a full content hash for asset modules
* @property {[{ shareScope: string, initStage: number, init: string }]} share-init share-init for modules federation
*/
/* eslint-disable jsdoc/type-formatting */
/**
* @template {string} K
* @typedef {K extends keyof AllCodeGenerationSchemas ? AllCodeGenerationSchemas[K] : EXPECTED_ANY} CodeGenValue
*/
/* eslint-enable jsdoc/type-formatting */
/* eslint-disable jsdoc/require-template */
/**
* @typedef {object} CodeGenMapOverloads
* @property {<K extends string>(key: K) => CodeGenValue<K> | undefined} get
* @property {<K extends string>(key: K, value: CodeGenValue<K>) => CodeGenerationResultData} set
* @property {<K extends string>(key: K) => boolean} has
* @property {<K extends string>(key: K) => boolean} delete
*/
/**
* @typedef {Omit<Map<string, EXPECTED_ANY>, "get" | "set" | "has" | "delete"> & CodeGenMapOverloads} CodeGenerationResultData
*/
/** @typedef {Map<SourceType, Source>} Sources */
@@ -941,6 +968,19 @@ class Module extends DependenciesBlock {
return JAVASCRIPT_TYPES;
}
/**
* Basic source types are high-level categories like javascript, css, webassembly, etc.
* We only have built-in knowledge about the javascript basic type here; other basic types may be
* added or changed over time by generators and do not need to be handled or detected here.
*
* Some modules, e.g. RemoteModule, may return non-basic source types like "remote" and "share-init"
* from getSourceTypes(), but their generated output is still JavaScript, i.e. their basic type is JS.
* @returns {BasicSourceTypes} types available (do not mutate)
*/
getSourceBasicTypes() {
return this.getSourceTypes();
}
/**
* @abstract
* @deprecated Use codeGeneration() instead

View File

@@ -184,15 +184,6 @@ class ModuleGraphConnection {
this.conditional = false;
this._active = value;
}
// TODO webpack 5 remove
get active() {
throw new Error("Use getActiveState instead");
}
set active(value) {
throw new Error("Use setActive instead");
}
}
/** @typedef {typeof TRANSITIVE_ONLY} TRANSITIVE_ONLY */

View File

@@ -8,6 +8,7 @@
const identifierUtils = require("./util/identifier");
/** @typedef {import("../declarations/WebpackOptions").StatsOptions} StatsOptions */
/** @typedef {import("../declarations/WebpackOptions").StatsValue} StatsValue */
/** @typedef {import("./Compilation").CreateStatsOptionsContext} CreateStatsOptionsContext */
/** @typedef {import("./Compilation").NormalizedStatsOptions} NormalizedStatsOptions */
/** @typedef {import("./Stats")} Stats */
@@ -25,8 +26,7 @@ const indent = (str, prefix) => {
return prefix + rem;
};
/** @typedef {undefined | string | boolean | StatsOptions} ChildrenStatsOptions */
/** @typedef {Omit<StatsOptions, "children"> & { children?: ChildrenStatsOptions | ChildrenStatsOptions[] }} MultiStatsOptions */
/** @typedef {StatsOptions} MultiStatsOptions */
/** @typedef {{ version: boolean, hash: boolean, errorsCount: boolean, warningsCount: boolean, errors: boolean, warnings: boolean, children: NormalizedStatsOptions[] }} ChildOptions */
class MultiStats {
@@ -56,7 +56,7 @@ class MultiStats {
}
/**
* @param {undefined | string | boolean | MultiStatsOptions} options stats options
* @param {undefined | StatsValue} options stats options
* @param {CreateStatsOptionsContext} context context
* @returns {ChildOptions} context context
*/
@@ -109,7 +109,7 @@ class MultiStats {
}
/**
* @param {(string | boolean | MultiStatsOptions)=} options stats options
* @param {StatsValue=} options stats options
* @returns {StatsCompilation} json output
*/
toJson(options) {
@@ -184,7 +184,7 @@ class MultiStats {
}
/**
* @param {(string | boolean | MultiStatsOptions)=} options stats options
* @param {StatsValue=} options stats options
* @returns {string} string output
*/
toString(options) {

View File

@@ -8,7 +8,10 @@
const { RawSource } = require("webpack-sources");
const OriginalSource = require("webpack-sources").OriginalSource;
const Module = require("./Module");
const { RUNTIME_TYPES } = require("./ModuleSourceTypeConstants");
const {
JAVASCRIPT_TYPES,
RUNTIME_TYPES
} = require("./ModuleSourceTypeConstants");
const { WEBPACK_MODULE_TYPE_RUNTIME } = require("./ModuleTypeConstants");
/** @typedef {import("./config/defaults").WebpackOptionsNormalizedWithDefaults} WebpackOptions */
@@ -29,6 +32,7 @@ const { WEBPACK_MODULE_TYPE_RUNTIME } = require("./ModuleTypeConstants");
/** @typedef {import("./ResolverFactory").ResolverWithOptions} ResolverWithOptions */
/** @typedef {import("./util/Hash")} Hash */
/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
/** @typedef {import("./Module").BasicSourceTypes} BasicSourceTypes */
class RuntimeModule extends Module {
/**
@@ -138,6 +142,19 @@ class RuntimeModule extends Module {
return RUNTIME_TYPES;
}
/**
* Basic source types are high-level categories like javascript, css, webassembly, etc.
* We only have built-in knowledge about the javascript basic type here; other basic types may be
* added or changed over time by generators and do not need to be handled or detected here.
*
* Some modules, e.g. RemoteModule, may return non-basic source types like "remote" and "share-init"
* from getSourceTypes(), but their generated output is still JavaScript, i.e. their basic type is JS.
* @returns {BasicSourceTypes} types available (do not mutate)
*/
getSourceBasicTypes() {
return JAVASCRIPT_TYPES;
}
/**
* @param {CodeGenerationContext} context context for code generation
* @returns {CodeGenerationResult} result

View File

@@ -17,6 +17,7 @@ class SourceMapDevToolModuleOptionsPlugin {
* @param {SourceMapDevToolPluginOptions=} options options
*/
constructor(options = {}) {
/** @type {SourceMapDevToolPluginOptions} */
this.options = options;
}

View File

@@ -19,6 +19,9 @@ const { makePathsAbsolute } = require("./util/identifier");
/** @typedef {import("webpack-sources").MapOptions} MapOptions */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../declarations/WebpackOptions").DevtoolNamespace} DevtoolNamespace */
/** @typedef {import("../declarations/WebpackOptions").DevtoolModuleFilenameTemplate} DevtoolModuleFilenameTemplate */
/** @typedef {import("../declarations/WebpackOptions").DevtoolFallbackModuleFilenameTemplate} DevtoolFallbackModuleFilenameTemplate */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").Rules} Rules */
/** @typedef {import("./CacheFacade").ItemCacheFacade} ItemCacheFacade */
@@ -140,19 +143,24 @@ class SourceMapDevToolPlugin {
constructor(options = {}) {
validate(options);
this.sourceMapFilename = /** @type {string | false} */ (options.filename);
/** @type {false | TemplatePath}} */
/** @type {undefined | null | false | string} */
this.sourceMapFilename = options.filename;
/** @type {false | TemplatePath} */
this.sourceMappingURLComment =
options.append === false
? false
: // eslint-disable-next-line no-useless-concat
options.append || "\n//# source" + "MappingURL=[url]";
/** @type {DevtoolModuleFilenameTemplate} */
this.moduleFilenameTemplate =
options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
/** @type {DevtoolFallbackModuleFilenameTemplate} */
this.fallbackModuleFilenameTemplate =
options.fallbackModuleFilenameTemplate ||
"webpack://[namespace]/[resourcePath]?[hash]";
/** @type {DevtoolNamespace} */
this.namespace = options.namespace || "";
/** @type {SourceMapDevToolPluginOptions} */
this.options = options;
}

5
node_modules/webpack/lib/Stats.js generated vendored
View File

@@ -6,6 +6,7 @@
"use strict";
/** @typedef {import("../declarations/WebpackOptions").StatsOptions} StatsOptions */
/** @typedef {import("../declarations/WebpackOptions").StatsValue} StatsValue */
/** @typedef {import("./Compilation")} Compilation */
/** @typedef {import("./stats/DefaultStatsFactoryPlugin").StatsCompilation} StatsCompilation */
@@ -50,7 +51,7 @@ class Stats {
}
/**
* @param {(string | boolean | StatsOptions)=} options stats options
* @param {StatsValue=} options stats options
* @returns {StatsCompilation} json output
*/
toJson(options) {
@@ -66,7 +67,7 @@ class Stats {
}
/**
* @param {(string | boolean | StatsOptions)=} options stats options
* @param {StatsValue=} options stats options
* @returns {string} string output
*/
toString(options) {

View File

@@ -60,6 +60,7 @@ const URLPlugin = require("./dependencies/URLPlugin");
const WorkerPlugin = require("./dependencies/WorkerPlugin");
const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin");
const JavascriptParser = require("./javascript/JavascriptParser");
const JsonModulesPlugin = require("./json/JsonModulesPlugin");
@@ -371,7 +372,9 @@ class WebpackOptionsApply extends OptionsApply {
new JavascriptModulesPlugin().apply(compiler);
new JsonModulesPlugin().apply(compiler);
new AssetModulesPlugin().apply(compiler);
new AssetModulesPlugin({
sideEffectFree: options.experiments.futureDefaults
}).apply(compiler);
if (!options.experiments.outputModule) {
if (options.output.module) {
@@ -483,11 +486,18 @@ class WebpackOptionsApply extends OptionsApply {
new HttpUriPlugin(httpOptions).apply(compiler);
}
if (options.experiments.deferImport) {
const JavascriptParser = require("./javascript/JavascriptParser");
if (
options.experiments.deferImport &&
!(
/** @type {typeof JavascriptParser & { __importPhasesExtended?: true }} */
(JavascriptParser).__importPhasesExtended
)
) {
const importPhases = require("acorn-import-phases");
JavascriptParser.extend(importPhases({ source: false }));
/** @type {typeof JavascriptParser & { __importPhasesExtended?: true }} */
(JavascriptParser).__importPhasesExtended = true;
}
new EntryOptionPlugin().apply(compiler);

View File

@@ -86,7 +86,19 @@ const getNormalModule = memoize(() => require("../NormalModule"));
const type = ASSET_MODULE_TYPE;
const PLUGIN_NAME = "AssetModulesPlugin";
/**
* @typedef {object} AssetModulesPluginOptions
* @property {boolean=} sideEffectFree
*/
class AssetModulesPlugin {
/**
* @param {AssetModulesPluginOptions} options options
*/
constructor(options) {
this.options = options;
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
@@ -112,7 +124,10 @@ class AssetModulesPlugin {
/** @type {NormalModuleCreateData} */
(createData)
);
module.factoryMeta = { sideEffectFree: true };
if (this.options.sideEffectFree) {
module.factoryMeta = { sideEffectFree: true };
}
return module;
});
}

View File

@@ -42,9 +42,13 @@ class RawDataUrlModule extends Module {
*/
constructor(url, identifier, readableIdentifier) {
super(ASSET_MODULE_TYPE_RAW_DATA_URL, null);
/** @type {string} */
this.url = url;
/** @type {Buffer | undefined} */
this.urlBuffer = url ? Buffer.from(url) : undefined;
this.identifierStr = identifier || this.url;
/** @type {string} */
this.identifierStr = identifier;
/** @type {string} */
this.readableIdentifierStr = readableIdentifier || this.identifierStr;
}

View File

@@ -7,7 +7,10 @@
const { RawSource } = require("webpack-sources");
const Module = require("../Module");
const { REMOTE_AND_SHARE_INIT_TYPES } = require("../ModuleSourceTypeConstants");
const {
JAVASCRIPT_TYPES,
REMOTE_AND_SHARE_INIT_TYPES
} = require("../ModuleSourceTypeConstants");
const { WEBPACK_MODULE_TYPE_REMOTE } = require("../ModuleTypeConstants");
const RuntimeGlobals = require("../RuntimeGlobals");
const makeSerializable = require("../util/makeSerializable");
@@ -34,6 +37,7 @@ const RemoteToExternalDependency = require("./RemoteToExternalDependency");
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
/** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
/** @typedef {import("../Module").BasicSourceTypes} BasicSourceTypes */
const RUNTIME_REQUIREMENTS = new Set([RuntimeGlobals.module]);
@@ -137,6 +141,19 @@ class RemoteModule extends Module {
return REMOTE_AND_SHARE_INIT_TYPES;
}
/**
* Basic source types are high-level categories like javascript, css, webassembly, etc.
* We only have built-in knowledge about the javascript basic type here; other basic types may be
* added or changed over time by generators and do not need to be handled or detected here.
*
* Some modules, e.g. RemoteModule, may return non-basic source types like "remote" and "share-init"
* from getSourceTypes(), but their generated output is still JavaScript, i.e. their basic type is JS.
* @returns {BasicSourceTypes} types available (do not mutate)
*/
getSourceBasicTypes() {
return JAVASCRIPT_TYPES;
}
/**
* @param {ModuleGraph} moduleGraph the module graph
* @param {boolean | undefined} strict the importing module is strict

View File

@@ -19,7 +19,7 @@ const {
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const CssImportDependency = require("../dependencies/CssImportDependency");
const EntryDependency = require("../dependencies/EntryDependency");
const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
const { getUndoPath } = require("../util/identifier");
const memoize = require("../util/memoize");
@@ -470,22 +470,27 @@ class CssGenerator extends Generator {
getTypes(module) {
const exportType = /** @type {BuildMeta} */ (module.buildMeta).exportType;
const sourceTypes = new Set();
const connections = this._moduleGraph.getIncomingConnections(module);
let isEntryModule = false;
for (const connection of connections) {
if (connection.dependency instanceof EntryDependency) {
isEntryModule = true;
}
if (
exportType === "link" &&
connection.dependency instanceof CssImportDependency
) {
continue;
}
// when no hmr required, css module js output contains no sideEffects at all
// js sideeffect connection doesn't require js type output
if (connection.dependency instanceof HarmonyImportSideEffectDependency) {
continue;
}
if (!connection.originModule) {
continue;
}
if (connection.originModule.type.split("/")[0] !== CSS_TYPE) {
sourceTypes.add(JAVASCRIPT_TYPE);
} else {
@@ -503,7 +508,7 @@ class CssGenerator extends Generator {
}
}
if (this._generatesJsOnly(module)) {
if (sourceTypes.has(JAVASCRIPT_TYPE) || isEntryModule) {
if (sourceTypes.has(JAVASCRIPT_TYPE)) {
return JAVASCRIPT_TYPES;
}
return new Set();
@@ -578,3 +583,5 @@ class CssGenerator extends Generator {
}
module.exports = CssGenerator;
module.exports = CssGenerator;

View File

@@ -386,6 +386,13 @@ class CssModulesPlugin {
compilation
).renderModuleContent.tap(PLUGIN_NAME, (source, module) => {
if (module instanceof CssModule && module.hot) {
const exportType = /** @type {BuildMeta} */ (module.buildMeta)
.exportType;
// When exportType !== "link", modules behave like JavaScript modules
if (exportType && exportType !== "link") {
return source;
}
// For exportType === "link", we can optimize with self-acceptance
const cssData = /** @type {BuildInfo} */ (module.buildInfo).cssData;
if (!cssData) {
return source;

View File

@@ -65,6 +65,10 @@ class CommonJsExportRequireDependency extends ModuleDependency {
return "cjs export require";
}
get category() {
return "commonjs";
}
/**
* @returns {boolean | TRANSITIVE} true, when changes to the referenced module could affect the referencing module; TRANSITIVE, when changes to the referenced module could affect referencing modules of the referencing module
*/

View File

@@ -5,13 +5,9 @@
"use strict";
const { fileURLToPath } = require("url");
const CommentCompilationWarning = require("../CommentCompilationWarning");
const RuntimeGlobals = require("../RuntimeGlobals");
const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
const WebpackError = require("../WebpackError");
const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
const { VariableInfo } = require("../javascript/JavascriptParser");
const {
evaluateToIdentifier,
evaluateToString,
@@ -36,6 +32,7 @@ const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency
/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
/** @typedef {import("../javascript/JavascriptParser").ImportSource} ImportSource */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("../javascript/JavascriptParser").Members} Members */
@@ -48,11 +45,300 @@ const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency
* @property {string} context
*/
const createRequireSpecifierTag = Symbol("createRequire");
const createdRequireIdentifierTag = Symbol("createRequire()");
const PLUGIN_NAME = "CommonJsImportsParserPlugin";
/**
* @param {JavascriptParser} parser parser
* @returns {(expr: Expression) => boolean} handler
*/
const createRequireCacheDependency = (parser) =>
toConstantDependency(parser, RuntimeGlobals.moduleCache, [
RuntimeGlobals.moduleCache,
RuntimeGlobals.moduleId,
RuntimeGlobals.moduleLoaded
]);
/**
* @param {JavascriptParser} parser parser
* @param {JavascriptParserOptions} options options
* @param {() => undefined | string} getContext context accessor
* @returns {(expr: Expression) => boolean} handler
*/
const createRequireAsExpressionHandler =
(parser, options, getContext) => (expr) => {
const dep = new CommonJsRequireContextDependency(
{
request: /** @type {string} */ (options.unknownContextRequest),
recursive: /** @type {boolean} */ (options.unknownContextRecursive),
regExp: /** @type {RegExp} */ (options.unknownContextRegExp),
mode: "sync"
},
/** @type {Range} */ (expr.range),
undefined,
parser.scope.inShorthand,
getContext()
);
dep.critical =
options.unknownContextCritical &&
"require function is used in a way in which dependencies cannot be statically extracted";
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
};
/**
* @param {JavascriptParser} parser parser
* @param {JavascriptParserOptions} options options
* @param {() => undefined | string} getContext context accessor
* @returns {(callNew: boolean) => (expr: CallExpression | NewExpression) => (boolean | void)} handler factory
*/
const createRequireCallHandler = (parser, options, getContext) => {
/**
* @param {CallExpression | NewExpression} expr expression
* @param {BasicEvaluatedExpression} param param
* @returns {boolean | void} true when handled
*/
const processRequireItem = (expr, param) => {
if (param.isString()) {
const dep = new CommonJsRequireDependency(
/** @type {string} */ (param.string),
/** @type {Range} */ (param.range),
getContext()
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
}
};
/**
* @param {CallExpression | NewExpression} expr expression
* @param {BasicEvaluatedExpression} param param
* @returns {boolean | void} true when handled
*/
const processRequireContext = (expr, param) => {
const dep = ContextDependencyHelpers.create(
CommonJsRequireContextDependency,
/** @type {Range} */ (expr.range),
param,
expr,
options,
{
category: "commonjs"
},
parser,
undefined,
getContext()
);
if (!dep) return;
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
};
return (callNew) => (expr) => {
if (options.commonjsMagicComments) {
const { options: requireOptions, errors: commentErrors } =
parser.parseCommentOptions(/** @type {Range} */ (expr.range));
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
/** @type {DependencyLocation} */ (comment.loc)
)
);
}
}
if (requireOptions && requireOptions.webpackIgnore !== undefined) {
if (typeof requireOptions.webpackIgnore !== "boolean") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${requireOptions.webpackIgnore}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
} else if (requireOptions.webpackIgnore) {
// Do not instrument `require()` if `webpackIgnore` is `true`
return true;
}
}
}
if (expr.arguments.length !== 1) return;
/** @type {null | LocalModule} */
let localModule;
const param = parser.evaluateExpression(expr.arguments[0]);
if (param.isConditional()) {
let isExpression = false;
for (const p of /** @type {BasicEvaluatedExpression[]} */ (
param.options
)) {
const result = processRequireItem(expr, p);
if (result === undefined) {
isExpression = true;
}
}
if (!isExpression) {
const dep = new RequireHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return true;
}
}
if (
param.isString() &&
(localModule = getLocalModule(
parser.state,
/** @type {string} */ (param.string)
))
) {
localModule.flagUsed();
const dep = new LocalModuleDependency(
localModule,
/** @type {Range} */ (expr.range),
callNew
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
} else {
const result = processRequireItem(expr, param);
if (result === undefined) {
processRequireContext(expr, param);
} else {
const dep = new RequireHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
}
}
return true;
};
};
/**
* @param {JavascriptParser} parser parser
* @param {JavascriptParserOptions} options options
* @param {() => undefined | string} getContext context accessor
* @returns {(expr: CallExpression, weak: boolean) => (boolean | void)} resolver
*/
const createProcessResolveHandler = (parser, options, getContext) => {
/**
* @param {CallExpression} expr call expression
* @param {BasicEvaluatedExpression} param param
* @param {boolean} weak weak
* @returns {boolean | void} true when handled
*/
const processResolveItem = (expr, param, weak) => {
if (param.isString()) {
const dep = new RequireResolveDependency(
/** @type {string} */ (param.string),
/** @type {Range} */ (param.range),
getContext()
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
dep.weak = weak;
parser.state.current.addDependency(dep);
return true;
}
};
/**
* @param {CallExpression} expr call expression
* @param {BasicEvaluatedExpression} param param
* @param {boolean} weak weak
* @returns {boolean | void} true when handled
*/
const processResolveContext = (expr, param, weak) => {
const dep = ContextDependencyHelpers.create(
RequireResolveContextDependency,
/** @type {Range} */ (param.range),
param,
expr,
options,
{
category: "commonjs",
mode: weak ? "weak" : "sync"
},
parser,
getContext()
);
if (!dep) return;
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
};
return (expr, weak) => {
if (!weak && options.commonjsMagicComments) {
const { options: requireOptions, errors: commentErrors } =
parser.parseCommentOptions(/** @type {Range} */ (expr.range));
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
/** @type {DependencyLocation} */ (comment.loc)
)
);
}
}
if (requireOptions && requireOptions.webpackIgnore !== undefined) {
if (typeof requireOptions.webpackIgnore !== "boolean") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${requireOptions.webpackIgnore}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
} else if (requireOptions.webpackIgnore) {
// Do not instrument `require()` if `webpackIgnore` is `true`
return true;
}
}
}
if (expr.arguments.length !== 1) return;
const param = parser.evaluateExpression(expr.arguments[0]);
if (param.isConditional()) {
for (const option of /** @type {BasicEvaluatedExpression[]} */ (
param.options
)) {
const result = processResolveItem(expr, option, weak);
if (result === undefined) {
processResolveContext(expr, option, weak);
}
}
const dep = new RequireResolveHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return true;
}
const result = processResolveItem(expr, param, weak);
if (result === undefined) {
processResolveContext(expr, param, weak);
}
const dep = new RequireResolveHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return true;
};
};
class CommonJsImportsParserPlugin {
/**
* @param {JavascriptParserOptions} options parser options
@@ -98,20 +384,6 @@ class CommonJsImportsParserPlugin {
evaluateToIdentifier(expression, "require", getMembers, true)
);
};
/**
* @param {string | symbol} tag tag
*/
const tapRequireExpressionTag = (tag) => {
parser.hooks.typeof
.for(tag)
.tap(
PLUGIN_NAME,
toConstantDependency(parser, JSON.stringify("function"))
);
parser.hooks.evaluateTypeof
.for(tag)
.tap(PLUGIN_NAME, evaluateToString("function"));
};
tapRequireExpression("require", () => []);
tapRequireExpression("require.resolve", () => ["resolve"]);
tapRequireExpression("require.resolveWeak", () => ["resolveWeak"]);
@@ -176,15 +448,7 @@ class CommonJsImportsParserPlugin {
// #endregion
// #region Inspection
const requireCache = toConstantDependency(
parser,
RuntimeGlobals.moduleCache,
[
RuntimeGlobals.moduleCache,
RuntimeGlobals.moduleId,
RuntimeGlobals.moduleLoaded
]
);
const requireCache = createRequireCacheDependency(parser);
parser.hooks.expression.for("require.cache").tap(PLUGIN_NAME, requireCache);
// #endregion
@@ -194,163 +458,26 @@ class CommonJsImportsParserPlugin {
* @param {Expression} expr expression
* @returns {boolean} true when handled
*/
const requireAsExpressionHandler = (expr) => {
const dep = new CommonJsRequireContextDependency(
{
request: /** @type {string} */ (options.unknownContextRequest),
recursive: /** @type {boolean} */ (options.unknownContextRecursive),
regExp: /** @type {RegExp} */ (options.unknownContextRegExp),
mode: "sync"
},
/** @type {Range} */ (expr.range),
undefined,
parser.scope.inShorthand,
getContext()
);
dep.critical =
options.unknownContextCritical &&
"require function is used in a way in which dependencies cannot be statically extracted";
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
};
const requireAsExpressionHandler = createRequireAsExpressionHandler(
parser,
options,
getContext
);
parser.hooks.expression
.for("require")
.tap(PLUGIN_NAME, requireAsExpressionHandler);
// #endregion
// #region Require
/**
* @param {CallExpression | NewExpression} expr expression
* @param {BasicEvaluatedExpression} param param
* @returns {boolean | void} true when handled
*/
const processRequireItem = (expr, param) => {
if (param.isString()) {
const dep = new CommonJsRequireDependency(
/** @type {string} */ (param.string),
/** @type {Range} */ (param.range),
getContext()
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
}
};
/**
* @param {CallExpression | NewExpression} expr expression
* @param {BasicEvaluatedExpression} param param
* @returns {boolean | void} true when handled
*/
const processRequireContext = (expr, param) => {
const dep = ContextDependencyHelpers.create(
CommonJsRequireContextDependency,
/** @type {Range} */ (expr.range),
param,
expr,
options,
{
category: "commonjs"
},
parser,
undefined,
getContext()
);
if (!dep) return;
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
};
/**
* @param {boolean} callNew true, when require is called with new
* @returns {(expr: CallExpression | NewExpression) => (boolean | void)} handler
*/
const createRequireHandler = (callNew) => (expr) => {
if (options.commonjsMagicComments) {
const { options: requireOptions, errors: commentErrors } =
parser.parseCommentOptions(/** @type {Range} */ (expr.range));
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
/** @type {DependencyLocation} */ (comment.loc)
)
);
}
}
if (requireOptions && requireOptions.webpackIgnore !== undefined) {
if (typeof requireOptions.webpackIgnore !== "boolean") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${requireOptions.webpackIgnore}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
} else if (requireOptions.webpackIgnore) {
// Do not instrument `require()` if `webpackIgnore` is `true`
return true;
}
}
}
if (expr.arguments.length !== 1) return;
/** @type {null | LocalModule} */
let localModule;
const param = parser.evaluateExpression(expr.arguments[0]);
if (param.isConditional()) {
let isExpression = false;
for (const p of /** @type {BasicEvaluatedExpression[]} */ (
param.options
)) {
const result = processRequireItem(expr, p);
if (result === undefined) {
isExpression = true;
}
}
if (!isExpression) {
const dep = new RequireHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return true;
}
}
if (
param.isString() &&
(localModule = getLocalModule(
parser.state,
/** @type {string} */ (param.string)
))
) {
localModule.flagUsed();
const dep = new LocalModuleDependency(
localModule,
/** @type {Range} */ (expr.range),
callNew
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
} else {
const result = processRequireItem(expr, param);
if (result === undefined) {
processRequireContext(expr, param);
} else {
const dep = new RequireHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
}
}
return true;
};
const createRequireHandler = createRequireCallHandler(
parser,
options,
getContext
);
parser.hooks.call
.for("require")
.tap(PLUGIN_NAME, createRequireHandler(false));
@@ -460,112 +587,11 @@ class CommonJsImportsParserPlugin {
* @param {boolean} weak weak
* @returns {boolean | void} true when handled
*/
const processResolve = (expr, weak) => {
if (!weak && options.commonjsMagicComments) {
const { options: requireOptions, errors: commentErrors } =
parser.parseCommentOptions(/** @type {Range} */ (expr.range));
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
/** @type {DependencyLocation} */ (comment.loc)
)
);
}
}
if (requireOptions && requireOptions.webpackIgnore !== undefined) {
if (typeof requireOptions.webpackIgnore !== "boolean") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${requireOptions.webpackIgnore}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
} else if (requireOptions.webpackIgnore) {
// Do not instrument `require()` if `webpackIgnore` is `true`
return true;
}
}
}
if (expr.arguments.length !== 1) return;
const param = parser.evaluateExpression(expr.arguments[0]);
if (param.isConditional()) {
for (const option of /** @type {BasicEvaluatedExpression[]} */ (
param.options
)) {
const result = processResolveItem(expr, option, weak);
if (result === undefined) {
processResolveContext(expr, option, weak);
}
}
const dep = new RequireResolveHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return true;
}
const result = processResolveItem(expr, param, weak);
if (result === undefined) {
processResolveContext(expr, param, weak);
}
const dep = new RequireResolveHeaderDependency(
/** @type {Range} */ (expr.callee.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return true;
};
/**
* @param {CallExpression} expr call expression
* @param {BasicEvaluatedExpression} param param
* @param {boolean} weak weak
* @returns {boolean | void} true when handled
*/
const processResolveItem = (expr, param, weak) => {
if (param.isString()) {
const dep = new RequireResolveDependency(
/** @type {string} */ (param.string),
/** @type {Range} */ (param.range),
getContext()
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
dep.weak = weak;
parser.state.current.addDependency(dep);
return true;
}
};
/**
* @param {CallExpression} expr call expression
* @param {BasicEvaluatedExpression} param param
* @param {boolean} weak weak
* @returns {boolean | void} true when handled
*/
const processResolveContext = (expr, param, weak) => {
const dep = ContextDependencyHelpers.create(
RequireResolveContextDependency,
/** @type {Range} */ (param.range),
param,
expr,
options,
{
category: "commonjs",
mode: weak ? "weak" : "sync"
},
parser,
getContext()
);
if (!dep) return;
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
dep.optional = Boolean(parser.scope.inTry);
parser.state.current.addDependency(dep);
return true;
};
const processResolve = createProcessResolveHandler(
parser,
options,
getContext
);
parser.hooks.call
.for("require.resolve")
@@ -574,232 +600,12 @@ class CommonJsImportsParserPlugin {
.for("require.resolveWeak")
.tap(PLUGIN_NAME, (expr) => processResolve(expr, true));
// #endregion
// #region Create require
if (!options.createRequire) return;
/** @type {ImportSource[]} */
let moduleName = [];
/** @type {string | undefined} */
let specifierName;
if (options.createRequire === true) {
moduleName = ["module", "node:module"];
specifierName = "createRequire";
} else {
/** @type {undefined | string} */
let moduleName;
const match = /^(.*) from (.*)$/.exec(options.createRequire);
if (match) {
[, specifierName, moduleName] = match;
}
if (!specifierName || !moduleName) {
const err = new WebpackError(
`Parsing javascript parser option "createRequire" failed, got ${JSON.stringify(
options.createRequire
)}`
);
err.details =
'Expected string in format "createRequire from module", where "createRequire" is specifier name and "module" name of the module';
throw err;
}
}
tapRequireExpressionTag(createdRequireIdentifierTag);
tapRequireExpressionTag(createRequireSpecifierTag);
parser.hooks.evaluateCallExpression
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr) => {
const context = parseCreateRequireArguments(expr);
if (context === undefined) return;
const ident = parser.evaluatedVariable({
tag: createdRequireIdentifierTag,
data: { context },
next: undefined
});
return new BasicEvaluatedExpression()
.setIdentifier(ident, ident, () => [])
.setSideEffects(false)
.setRange(/** @type {Range} */ (expr.range));
});
parser.hooks.unhandledExpressionMemberChain
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, (expr, members) =>
expressionIsUnsupported(
parser,
`createRequire().${members.join(".")} is not supported by webpack.`
)(expr)
);
parser.hooks.canRename
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, () => true);
parser.hooks.canRename
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, () => true);
parser.hooks.rename
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, defineUndefined);
parser.hooks.expression
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, requireAsExpressionHandler);
parser.hooks.call
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, createRequireHandler(false));
/**
* @param {CallExpression} expr call expression
* @returns {string | void} context
*/
const parseCreateRequireArguments = (expr) => {
const args = expr.arguments;
if (args.length !== 1) {
const err = new WebpackError(
"module.createRequire supports only one argument."
);
err.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addWarning(err);
return;
}
const arg = args[0];
const evaluated = parser.evaluateExpression(arg);
if (!evaluated.isString()) {
const err = new WebpackError(
"module.createRequire failed parsing argument."
);
err.loc = /** @type {DependencyLocation} */ (arg.loc);
parser.state.module.addWarning(err);
return;
}
const ctx = /** @type {string} */ (evaluated.string).startsWith("file://")
? fileURLToPath(/** @type {string} */ (evaluated.string))
: /** @type {string} */ (evaluated.string);
// argument always should be a filename
return ctx.slice(0, ctx.lastIndexOf(ctx.startsWith("/") ? "/" : "\\"));
};
parser.hooks.import.tap(
{
name: PLUGIN_NAME,
stage: -10
},
(statement, source) => {
if (
!moduleName.includes(source) ||
statement.specifiers.length !== 1 ||
statement.specifiers[0].type !== "ImportSpecifier" ||
statement.specifiers[0].imported.type !== "Identifier" ||
statement.specifiers[0].imported.name !== specifierName
) {
return;
}
// clear for 'import { createRequire as x } from "module"'
// if any other specifier was used import module
const clearDep = new ConstDependency(
parser.isAsiPosition(/** @type {Range} */ (statement.range)[0])
? ";"
: "",
/** @type {Range} */ (statement.range)
);
clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
parser.state.module.addPresentationalDependency(clearDep);
parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
return true;
}
);
parser.hooks.importSpecifier.tap(
{
name: PLUGIN_NAME,
stage: -10
},
(statement, source, id, name) => {
if (!moduleName.includes(source) || id !== specifierName) return;
parser.tagVariable(name, createRequireSpecifierTag);
return true;
}
);
parser.hooks.preDeclarator.tap(PLUGIN_NAME, (declarator) => {
if (
declarator.id.type !== "Identifier" ||
!declarator.init ||
declarator.init.type !== "CallExpression" ||
declarator.init.callee.type !== "Identifier"
) {
return;
}
const variableInfo = parser.getVariableInfo(declarator.init.callee.name);
if (
variableInfo instanceof VariableInfo &&
variableInfo.tagInfo &&
variableInfo.tagInfo.tag === createRequireSpecifierTag
) {
const context = parseCreateRequireArguments(declarator.init);
if (context === undefined) return;
parser.tagVariable(declarator.id.name, createdRequireIdentifierTag, {
name: declarator.id.name,
context
});
return true;
}
});
parser.hooks.memberChainOfCallMemberChain
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr, calleeMembers, callExpr, members) => {
if (
calleeMembers.length !== 0 ||
members.length !== 1 ||
members[0] !== "cache"
) {
return;
}
// createRequire().cache
const context = parseCreateRequireArguments(callExpr);
if (context === undefined) return;
return requireCache(expr);
});
parser.hooks.callMemberChainOfCallMemberChain
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr, calleeMembers, innerCallExpression, members) => {
if (
calleeMembers.length !== 0 ||
members.length !== 1 ||
members[0] !== "resolve"
) {
return;
}
// createRequire().resolve()
return processResolve(expr, false);
});
parser.hooks.expressionMemberChain
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, (expr, members) => {
// require.cache
if (members.length === 1 && members[0] === "cache") {
return requireCache(expr);
}
});
parser.hooks.callMemberChain
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, (expr, members) => {
// require.resolve()
if (members.length === 1 && members[0] === "resolve") {
return processResolve(expr, false);
}
});
parser.hooks.call
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr) => {
const clearDep = new ConstDependency(
"/* createRequire() */ undefined",
/** @type {Range} */ (expr.range)
);
clearDep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(clearDep);
return true;
});
// #endregion
}
}
module.exports = CommonJsImportsParserPlugin;
module.exports.createProcessResolveHandler = createProcessResolveHandler;
module.exports.createRequireAsExpressionHandler =
createRequireAsExpressionHandler;
module.exports.createRequireCacheDependency = createRequireCacheDependency;
module.exports.createRequireHandler = createRequireCallHandler;

View File

@@ -0,0 +1,356 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { fileURLToPath } = require("url");
const WebpackError = require("../WebpackError");
const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
const { VariableInfo } = require("../javascript/JavascriptParser");
const {
evaluateToString,
expressionIsUnsupported,
toConstantDependency
} = require("../javascript/JavascriptParserHelpers");
const CommonJsImportsParserPlugin = require("./CommonJsImportsParserPlugin");
const ConstDependency = require("./ConstDependency");
/** @typedef {import("estree").CallExpression} CallExpression */
/** @typedef {import("estree").Expression} Expression */
/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../javascript/JavascriptParser").ImportSource} ImportSource */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/**
* @typedef {object} CommonJsImportSettings
* @property {string=} name
* @property {string} context
*/
const createRequireSpecifierTag = Symbol("createRequire");
const createdRequireIdentifierTag = Symbol("createRequire()");
const PLUGIN_NAME = "CreateRequireParserPlugin";
const {
createProcessResolveHandler,
createRequireAsExpressionHandler,
createRequireCacheDependency,
createRequireHandler
} = CommonJsImportsParserPlugin;
class CreateRequireParserPlugin {
/**
* @param {JavascriptParserOptions} options parser options
*/
constructor(options) {
this.options = options;
}
/**
* @param {JavascriptParser} parser the parser
* @returns {void}
*/
apply(parser) {
const options = this.options;
if (!options.createRequire) return;
const getContext = () => {
if (parser.currentTagData) {
const { context } =
/** @type {CommonJsImportSettings} */
(parser.currentTagData);
return context;
}
};
/**
* @param {string | symbol} tag tag
*/
const tapRequireExpressionTag = (tag) => {
parser.hooks.typeof
.for(tag)
.tap(
PLUGIN_NAME,
toConstantDependency(parser, JSON.stringify("function"))
);
parser.hooks.evaluateTypeof
.for(tag)
.tap(PLUGIN_NAME, evaluateToString("function"));
};
/**
* @param {Expression} expr expression
* @returns {boolean} true when set undefined
*/
const defineUndefined = (expr) => {
const dep = new ConstDependency(
"undefined",
/** @type {Range} */ (expr.range)
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);
return false;
};
const requireCache = createRequireCacheDependency(parser);
const requireAsExpressionHandler = createRequireAsExpressionHandler(
parser,
options,
getContext
);
const createRequireCallHandler = createRequireHandler(
parser,
options,
getContext
);
const processResolve = createProcessResolveHandler(
parser,
options,
getContext
);
/** @type {ImportSource[]} */
let moduleNames = [];
/** @type {string | undefined} */
let specifierName;
if (options.createRequire === true) {
moduleNames = ["module", "node:module"];
specifierName = "createRequire";
} else if (typeof options.createRequire === "string") {
/** @type {undefined | string} */
let parsedModuleName;
const match = /^(.*) from (.*)$/.exec(options.createRequire);
if (match) {
[, specifierName, parsedModuleName] = match;
}
if (!specifierName || !parsedModuleName) {
const err = new WebpackError(
`Parsing javascript parser option "createRequire" failed, got ${JSON.stringify(
options.createRequire
)}`
);
err.details =
'Expected string in format "createRequire from module", where "createRequire" is specifier name and "module" name of the module';
throw err;
}
moduleNames = [parsedModuleName];
} else {
return;
}
/**
* @param {CallExpression} expr call expression
* @returns {string | void} context
*/
const parseCreateRequireArguments = (expr) => {
const args = expr.arguments;
if (args.length !== 1) {
const err = new WebpackError(
"module.createRequire supports only one argument."
);
err.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addWarning(err);
return;
}
const arg = args[0];
const evaluated = parser.evaluateExpression(arg);
if (!evaluated.isString()) {
const err = new WebpackError(
"module.createRequire failed parsing argument."
);
err.loc = /** @type {DependencyLocation} */ (arg.loc);
parser.state.module.addWarning(err);
return;
}
const ctx = /** @type {string} */ (evaluated.string).startsWith("file://")
? fileURLToPath(/** @type {string} */ (evaluated.string))
: /** @type {string} */ (evaluated.string);
// argument always should be a filename
return ctx.slice(0, ctx.lastIndexOf(ctx.startsWith("/") ? "/" : "\\"));
};
tapRequireExpressionTag(createdRequireIdentifierTag);
tapRequireExpressionTag(createRequireSpecifierTag);
parser.hooks.evaluateCallExpression
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr) => {
const context = parseCreateRequireArguments(expr);
if (context === undefined) return;
const ident = parser.evaluatedVariable({
tag: createdRequireIdentifierTag,
data: { context },
next: undefined
});
return new BasicEvaluatedExpression()
.setIdentifier(ident, ident, () => [])
.setSideEffects(false)
.setRange(/** @type {Range} */ (expr.range));
});
parser.hooks.unhandledExpressionMemberChain
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, (expr, members) =>
expressionIsUnsupported(
parser,
`createRequire().${members.join(".")} is not supported by webpack.`
)(expr)
);
parser.hooks.canRename
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, () => true);
parser.hooks.canRename
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, () => true);
parser.hooks.rename
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, defineUndefined);
parser.hooks.expression
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, requireAsExpressionHandler);
parser.hooks.call
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, createRequireCallHandler(false));
parser.hooks.import.tap(
{
name: PLUGIN_NAME,
stage: -10
},
(statement, source) => {
if (
!moduleNames.includes(source) ||
statement.specifiers.length !== 1 ||
statement.specifiers[0].type !== "ImportSpecifier" ||
statement.specifiers[0].imported.type !== "Identifier" ||
statement.specifiers[0].imported.name !== specifierName
) {
return;
}
// clear for 'import { createRequire as x } from "module"'
// if any other specifier was used import module
const clearDep = new ConstDependency(
parser.isAsiPosition(/** @type {Range} */ (statement.range)[0])
? ";"
: "",
/** @type {Range} */ (statement.range)
);
clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
parser.state.module.addPresentationalDependency(clearDep);
parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
return true;
}
);
parser.hooks.importSpecifier.tap(
{
name: PLUGIN_NAME,
stage: -10
},
(statement, source, id, name) => {
if (!moduleNames.includes(source) || id !== specifierName) return;
parser.tagVariable(name, createRequireSpecifierTag);
return true;
}
);
parser.hooks.preDeclarator.tap(PLUGIN_NAME, (declarator) => {
if (
declarator.id.type !== "Identifier" ||
!declarator.init ||
declarator.init.type !== "CallExpression" ||
declarator.init.callee.type !== "Identifier"
) {
return;
}
const variableInfo = parser.getVariableInfo(declarator.init.callee.name);
if (
variableInfo instanceof VariableInfo &&
variableInfo.tagInfo &&
variableInfo.tagInfo.tag === createRequireSpecifierTag
) {
const context = parseCreateRequireArguments(declarator.init);
if (context === undefined) return;
parser.tagVariable(declarator.id.name, createdRequireIdentifierTag, {
name: declarator.id.name,
context
});
return true;
}
});
parser.hooks.memberChainOfCallMemberChain
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr, calleeMembers, callExpr, members) => {
if (
calleeMembers.length !== 0 ||
members.length !== 1 ||
members[0] !== "cache"
) {
return;
}
// createRequire().cache
const context = parseCreateRequireArguments(callExpr);
if (context === undefined) return;
return requireCache(expr);
});
parser.hooks.callMemberChainOfCallMemberChain
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr, calleeMembers, innerCallExpression, members) => {
if (
calleeMembers.length !== 0 ||
members.length !== 1 ||
members[0] !== "resolve"
) {
return;
}
// createRequire().resolve()
return processResolve(expr, false);
});
parser.hooks.expressionMemberChain
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, (expr, members) => {
// require.cache
if (members.length === 1 && members[0] === "cache") {
return requireCache(expr);
}
});
parser.hooks.callMemberChain
.for(createdRequireIdentifierTag)
.tap(PLUGIN_NAME, (expr, members) => {
// require.resolve()
if (members.length === 1 && members[0] === "resolve") {
return processResolve(expr, false);
}
});
parser.hooks.expression
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr) => {
const clearDep = new ConstDependency(
"/* createRequire */ undefined",
/** @type {Range} */ (expr.range)
);
clearDep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(clearDep);
return true;
});
parser.hooks.call
.for(createRequireSpecifierTag)
.tap(PLUGIN_NAME, (expr) => {
const clearDep = new ConstDependency(
"/* createRequire() */ undefined",
/** @type {Range} */ (expr.range)
);
clearDep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(clearDep);
return true;
});
}
}
module.exports = CreateRequireParserPlugin;

View File

@@ -39,14 +39,10 @@ module.exports = class HarmonyExportDependencyParserPlugin {
*/
constructor(options) {
this.options = options;
this.exportPresenceMode =
options.reexportExportsPresence !== undefined
? ExportPresenceModes.fromUserOption(options.reexportExportsPresence)
: options.exportsPresence !== undefined
? ExportPresenceModes.fromUserOption(options.exportsPresence)
: options.strictExportPresence
? ExportPresenceModes.ERROR
: ExportPresenceModes.AUTO;
this.exportPresenceMode = ExportPresenceModes.resolveFromOptions(
options.reexportExportsPresence,
options
);
}
/**

View File

@@ -53,9 +53,38 @@ const ExportPresenceModes = {
default:
throw new Error(`Invalid export presence value ${str}`);
}
},
/**
* Resolve export presence mode from parser options with a specific key and shared fallbacks.
* @param {string | false | undefined} specificValue the type-specific option value (e.g. importExportsPresence or reexportExportsPresence)
* @param {import("../../declarations/WebpackOptions").JavascriptParserOptions} options parser options
* @returns {ExportPresenceMode} resolved mode
*/
resolveFromOptions(specificValue, options) {
if (specificValue !== undefined) {
return ExportPresenceModes.fromUserOption(specificValue);
}
if (options.exportsPresence !== undefined) {
return ExportPresenceModes.fromUserOption(options.exportsPresence);
}
return options.strictExportPresence
? ExportPresenceModes.ERROR
: ExportPresenceModes.AUTO;
}
};
/**
* Get the non-optional leading part of a member chain.
* @param {string[]} members members
* @param {boolean[]} membersOptionals optionality for each member
* @returns {string[]} the non-optional prefix
*/
const getNonOptionalPart = (members, membersOptionals) => {
let i = 0;
while (i < members.length && membersOptionals[i] === false) i++;
return i !== members.length ? members.slice(0, i) : members;
};
/** @typedef {string[]} Ids */
class HarmonyImportDependency extends ModuleDependency {
@@ -427,3 +456,4 @@ HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends
};
module.exports.ExportPresenceModes = ExportPresenceModes;
module.exports.getNonOptionalPart = getNonOptionalPart;

View File

@@ -18,12 +18,16 @@ const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency");
const HarmonyExports = require("./HarmonyExports");
const { ExportPresenceModes } = require("./HarmonyImportDependency");
const {
ExportPresenceModes,
getNonOptionalPart
} = require("./HarmonyImportDependency");
const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
const { ImportPhaseUtils, createGetImportPhase } = require("./ImportPhase");
/** @typedef {import("estree").Expression} Expression */
/** @typedef {import("estree").PrivateIdentifier} PrivateIdentifier */
/** @typedef {import("estree").Identifier} Identifier */
/** @typedef {import("estree").MemberExpression} MemberExpression */
/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
@@ -63,17 +67,44 @@ const harmonySpecifierGuardTag = Symbol("harmony import guard");
const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
/** @type {(members: Members) => string} */
const getMembersKey = (members) => members.join(".");
/**
* Strip the root binding name if needed
* @param {HarmonySettings} settings settings
* @param {Ids} ids ids
* @returns {Ids} ids for presence check
* @param {JavascriptParser} parser the parser
* @param {PrivateIdentifier | Expression} left left expression
* @param {Expression} right right expression
* @returns {{ leftPart: string, members: Members, settings: HarmonySettings } | undefined} info
*/
const getIdsForPresence = (settings, ids) =>
settings.ids.length ? ids.slice(1) : ids;
const getInOperatorHarmonyImportInfo = (parser, left, right) => {
const leftPartEvaluated = parser.evaluateExpression(left);
if (leftPartEvaluated.couldHaveSideEffects()) return;
/** @type {string | undefined} */
const leftPart = leftPartEvaluated.asString();
if (!leftPart) return;
const rightPart = parser.evaluateExpression(right);
if (!rightPart.isIdentifier()) return;
const rootInfo = rightPart.rootInfo;
const root =
typeof rootInfo === "string"
? rootInfo
: rootInfo instanceof VariableInfo
? rootInfo.name
: undefined;
if (!root) return;
const settings = /** @type {HarmonySettings | undefined} */ (
parser.getTagData(root, harmonySpecifierTag)
);
if (!settings) {
return;
}
return {
leftPart,
members: /** @type {(() => Members)} */ (rightPart.getMembers)(),
settings
};
};
module.exports = class HarmonyImportDependencyParserPlugin {
/**
@@ -82,23 +113,23 @@ module.exports = class HarmonyImportDependencyParserPlugin {
constructor(options) {
this.options = options;
/** @type {ExportPresenceMode} */
this.exportPresenceMode =
options.importExportsPresence !== undefined
? ExportPresenceModes.fromUserOption(options.importExportsPresence)
: options.exportsPresence !== undefined
? ExportPresenceModes.fromUserOption(options.exportsPresence)
: options.strictExportPresence
? ExportPresenceModes.ERROR
: ExportPresenceModes.AUTO;
this.exportPresenceMode = ExportPresenceModes.resolveFromOptions(
options.importExportsPresence,
options
);
this.strictThisContextOnImports = options.strictThisContextOnImports;
}
/**
* @param {JavascriptParser} parser the parser
* @param {HarmonySettings} settings settings
* @param {Ids} ids ids
* @returns {ExportPresenceMode} exportPresenceMode
*/
getExportPresenceMode(parser, ids) {
getExportPresenceMode(parser, settings, ids) {
// Guards only apply to namespace imports
if (settings.ids.length) return this.exportPresenceMode;
const harmonySettings = /** @type {HarmonySettings=} */ (
parser.currentTagData
);
@@ -107,9 +138,12 @@ module.exports = class HarmonyImportDependencyParserPlugin {
const data = /** @type {HarmonySpecifierGuards=} */ (
parser.getTagData(harmonySettings.name, harmonySpecifierGuardTag)
);
return data && data.guards && data.guards.has(getMembersKey(ids))
? ExportPresenceModes.NONE
: this.exportPresenceMode;
if (data && data.guards && data.guards.has(ids[0])) {
return ExportPresenceModes.NONE;
}
return this.exportPresenceMode;
}
/**
@@ -119,17 +153,6 @@ module.exports = class HarmonyImportDependencyParserPlugin {
apply(parser) {
const getImportPhase = createGetImportPhase(this.options.deferImport);
/**
* @param {Members} members members
* @param {MembersOptionals} membersOptionals members Optionals
* @returns {Ids} a non optional part
*/
function getNonOptionalPart(members, membersOptionals) {
let i = 0;
while (i < members.length && membersOptionals[i] === false) i++;
return i !== members.length ? members.slice(0, i) : members;
}
/**
* @param {MemberExpression} node member expression
* @param {number} count count
@@ -207,31 +230,14 @@ module.exports = class HarmonyImportDependencyParserPlugin {
);
parser.hooks.binaryExpression.tap(PLUGIN_NAME, (expression) => {
if (expression.operator !== "in") return;
const info = getInOperatorHarmonyImportInfo(
parser,
expression.left,
expression.right
);
if (!info) return;
const leftPartEvaluated = parser.evaluateExpression(expression.left);
if (leftPartEvaluated.couldHaveSideEffects()) return;
/** @type {string | undefined} */
const leftPart = leftPartEvaluated.asString();
if (!leftPart) return;
const rightPart = parser.evaluateExpression(expression.right);
if (!rightPart.isIdentifier()) return;
const rootInfo = rightPart.rootInfo;
if (
typeof rootInfo === "string" ||
!rootInfo ||
!rootInfo.tagInfo ||
rootInfo.tagInfo.tag !== harmonySpecifierTag
) {
return;
}
const settings =
/** @type {HarmonySettings} */
(rootInfo.tagInfo.data);
const members =
/** @type {(() => Members)} */
(rightPart.getMembers)();
const { leftPart, members, settings } = info;
const dep = new HarmonyEvaluatedImportSpecifierDependency(
settings.source,
settings.sourceOrder,
@@ -276,10 +282,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
settings.name,
/** @type {Range} */
(expr.range),
this.getExportPresenceMode(
parser,
getIdsForPresence(settings, settings.ids)
),
this.exportPresenceMode,
settings.phase,
settings.attributes,
[]
@@ -329,10 +332,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
settings.name,
/** @type {Range} */
(expr.range),
this.getExportPresenceMode(
parser,
getIdsForPresence(settings, ids)
),
this.getExportPresenceMode(parser, settings, ids),
settings.phase,
settings.attributes,
ranges
@@ -382,10 +382,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
ids,
settings.name,
/** @type {Range} */ (expr.range),
this.getExportPresenceMode(
parser,
getIdsForPresence(settings, ids)
),
this.getExportPresenceMode(parser, settings, ids),
settings.phase,
settings.attributes,
ranges
@@ -453,160 +450,6 @@ module.exports = class HarmonyImportDependencyParserPlugin {
}
});
/**
* @param {Expression} expression expression
* @returns {{ root: string, members: Members } | undefined} info
*/
const getHarmonyImportInfo = (expression) => {
const nameInfo = parser.getNameForExpression(expression);
if (!nameInfo) return;
const rootInfo = nameInfo.rootInfo;
const root =
typeof rootInfo === "string"
? rootInfo
: rootInfo instanceof VariableInfo
? rootInfo.name
: undefined;
if (!root) return;
if (!parser.getTagData(root, harmonySpecifierTag)) return;
return { root, members: nameInfo.getMembers() };
};
/**
* @param {Guards} guards guards
* @param {string} root root name
* @param {Members} members members
*/
const addToGuards = (guards, root, members) => {
const membersKey = getMembersKey(members);
const guardedMembers = guards.get(root);
if (guardedMembers) {
guardedMembers.add(membersKey);
return;
}
guards.set(
root,
// Adding `foo.bar` implies guarding `foo` as well
membersKey === "" ? new Set([""]) : new Set([membersKey, ""])
);
};
/**
* @param {Expression} expression expression
* @param {Guards} guards guards
* @param {boolean} needTruthy need to be truthy
*/
const collect = (expression, guards, needTruthy) => {
// !foo
if (
expression.type === "UnaryExpression" &&
expression.operator === "!"
) {
collect(expression.argument, guards, !needTruthy);
return;
} else if (expression.type === "LogicalExpression" && needTruthy) {
// foo && bar
if (expression.operator === "&&") {
collect(expression.left, guards, true);
collect(expression.right, guards, true);
}
// falsy || foo
else if (expression.operator === "||") {
const leftEvaluation = parser.evaluateExpression(expression.left);
const leftBool = leftEvaluation.asBool();
if (leftBool === false) {
collect(expression.right, guards, true);
}
}
// nullish ?? foo
else if (expression.operator === "??") {
const leftEvaluation = parser.evaluateExpression(expression.left);
const leftNullish = leftEvaluation.asNullish();
if (leftNullish === true) {
collect(expression.right, guards, true);
}
}
return;
}
if (!needTruthy) return;
/**
* @param {Expression} targetExpression expression
* @returns {boolean} is added
*/
const addGuardForExpression = (targetExpression) => {
const info = getHarmonyImportInfo(targetExpression);
if (!info) return false;
addToGuards(guards, info.root, info.members);
return true;
};
/**
* @param {Expression} left left expression
* @param {Expression} right right expression
* @param {(evaluation: ReturnType<JavascriptParser["evaluateExpression"]>) => boolean} matcher matcher
* @returns {boolean} is added
*/
const addGuardForNullishCompare = (left, right, matcher) => {
const leftEval = parser.evaluateExpression(left);
if (leftEval && matcher(leftEval)) {
return addGuardForExpression(right);
}
const rightEval = parser.evaluateExpression(right);
if (rightEval && matcher(rightEval)) {
return addGuardForExpression(/** @type {Expression} */ (left));
}
return false;
};
if (expression.type === "BinaryExpression") {
// "bar" in foo
if (expression.operator === "in") {
const leftEvaluation = parser.evaluateExpression(expression.left);
if (leftEvaluation.couldHaveSideEffects()) return;
const propertyName = leftEvaluation.asString();
if (!propertyName) return;
parser.evaluateExpression(expression.right);
const info = getHarmonyImportInfo(expression.right);
if (!info) return;
if (info.members.length) {
for (const member of info.members) {
addToGuards(guards, info.root, [member]);
}
}
addToGuards(guards, info.root, [...info.members, propertyName]);
return;
}
// foo !== undefined
else if (
expression.operator === "!==" &&
addGuardForNullishCompare(
/** @type {Expression} */ (expression.left),
expression.right,
(evaluation) => evaluation.isUndefined()
)
) {
return;
}
// foo != undefined
// foo != null
else if (
expression.operator === "!=" &&
addGuardForNullishCompare(
/** @type {Expression} */ (expression.left),
expression.right,
(evaluation) => Boolean(evaluation.asNullish())
)
) {
return;
}
}
addGuardForExpression(expression);
};
/**
* @param {Guards} guards guards
* @param {() => void} walk walk callback
@@ -657,7 +500,68 @@ module.exports = class HarmonyImportDependencyParserPlugin {
if (parser.scope.isAsmJs) return;
/** @type {Guards} */
const guards = new Map();
collect(expression, guards, true);
/**
* @param {Expression} expression expression
* @param {boolean} needTruthy need to be truthy
*/
const collect = (expression, needTruthy) => {
if (
expression.type === "UnaryExpression" &&
expression.operator === "!"
) {
collect(expression.argument, !needTruthy);
return;
} else if (expression.type === "LogicalExpression" && needTruthy) {
if (expression.operator === "&&") {
collect(expression.left, true);
collect(expression.right, true);
} else if (expression.operator === "||") {
const leftEvaluation = parser.evaluateExpression(expression.left);
const leftBool = leftEvaluation.asBool();
if (leftBool === false) {
collect(expression.right, true);
}
} else if (expression.operator === "??") {
const leftEvaluation = parser.evaluateExpression(expression.left);
const leftNullish = leftEvaluation.asNullish();
if (leftNullish === true) {
collect(expression.right, true);
}
}
return;
}
if (!needTruthy) return;
// Direct `"x" in ns` guards
if (
expression.type === "BinaryExpression" &&
expression.operator === "in"
) {
if (expression.right.type !== "Identifier") {
return;
}
const info = getInOperatorHarmonyImportInfo(
parser,
expression.left,
expression.right
);
if (!info) return;
const { settings, leftPart, members } = info;
// Only direct namespace guards
if (members.length > 0) return;
const guarded = guards.get(settings.name);
if (guarded) {
guarded.add(leftPart);
return;
}
guards.set(settings.name, new Set([leftPart]));
}
};
collect(expression, true);
if (guards.size === 0) return;
return (walk) => {

View File

@@ -5,6 +5,7 @@
"use strict";
const { JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants");
const makeSerializable = require("../util/makeSerializable");
const HarmonyImportDependency = require("./HarmonyImportDependency");
@@ -72,11 +73,16 @@ HarmonyImportSideEffectDependency.Template = class HarmonyImportSideEffectDepend
*/
apply(dependency, source, templateContext) {
const { moduleGraph, concatenationScope } = templateContext;
if (concatenationScope) {
const module = /** @type {Module} */ (moduleGraph.getModule(dependency));
if (concatenationScope.isModuleInScope(module)) {
return;
}
const module = /** @type {Module} */ (moduleGraph.getModule(dependency));
if (module && !module.getSourceBasicTypes().has(JAVASCRIPT_TYPE)) {
// no need to render import
return;
}
if (concatenationScope && concatenationScope.isModuleInScope(module)) {
return;
}
super.apply(dependency, source, templateContext);
}

View File

@@ -9,6 +9,7 @@ const {
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_ESM
} = require("../ModuleTypeConstants");
const CreateRequireParserPlugin = require("./CreateRequireParserPlugin");
const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
const HarmonyCompatibilityDependency = require("./HarmonyCompatibilityDependency");
@@ -138,6 +139,9 @@ class HarmonyModulesPlugin {
new HarmonyImportDependencyParserPlugin(parserOptions).apply(parser);
new HarmonyExportDependencyParserPlugin(parserOptions).apply(parser);
new HarmonyTopLevelThisParserPlugin().apply(parser);
if (parserOptions.createRequire) {
new CreateRequireParserPlugin(parserOptions).apply(parser);
}
};
normalModuleFactory.hooks.parser

View File

@@ -14,6 +14,7 @@ const {
} = require("../javascript/JavascriptParser");
const traverseDestructuringAssignmentProperties = require("../util/traverseDestructuringAssignmentProperties");
const ContextDependencyHelpers = require("./ContextDependencyHelpers");
const { getNonOptionalPart } = require("./HarmonyImportDependency");
const ImportContextDependency = require("./ImportContextDependency");
const ImportDependency = require("./ImportDependency");
const ImportEagerDependency = require("./ImportEagerDependency");
@@ -178,17 +179,6 @@ class ImportParserPlugin {
* @returns {void}
*/
apply(parser) {
/**
* @param {Members} members members
* @param {MembersOptionals} membersOptionals members Optionals
* @returns {string[]} a non optional part
*/
function getNonOptionalPart(members, membersOptionals) {
let i = 0;
while (i < members.length && membersOptionals[i] === false) i++;
return i !== members.length ? members.slice(0, i) : members;
}
parser.hooks.collectDestructuringAssignmentProperties.tap(
PLUGIN_NAME,
(expr) => {

View File

@@ -27,12 +27,16 @@ const ImportPhase = Object.freeze({
/**
* @typedef {object} ImportPhaseUtils
* @property {(phase: ImportPhaseType) => boolean} isEvaluation true if phase is evaluation
* @property {(phase: ImportPhaseType) => boolean} isDefer true if phase is defer
* @property {(phase: ImportPhaseType) => boolean} isSource true if phase is source
*/
/** @type {ImportPhaseUtils} */
const ImportPhaseUtils = {
isEvaluation(phase) {
return phase === ImportPhase.Evaluation;
},
isDefer(phase) {
return phase === ImportPhase.Defer;
},

View File

@@ -56,6 +56,7 @@ const JavascriptParser = require("./JavascriptParser");
/** @typedef {import("../config/defaults").OutputNormalizedWithDefaults} OutputOptions */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../ChunkGraph").EntryModuleWithChunkGroup} EntryModuleWithChunkGroup */
/** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
/** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */
/** @typedef {import("../Compilation").ExecuteModuleObject} ExecuteModuleObject */
@@ -75,37 +76,75 @@ const JavascriptParser = require("./JavascriptParser");
/** @typedef {import("../util/concatenate").ScopeSet} ScopeSet */
/** @typedef {import("../util/concatenate").UsedNamesInScopeInfo} UsedNamesInScopeInfo */
/** @type {WeakMap<ChunkGraph, WeakMap<Chunk, boolean>>} */
const chunkHasJsCache = new WeakMap();
/**
* @param {Chunk} chunk a chunk
* @param {ChunkGraph} chunkGraph the chunk graph
* @returns {boolean} true, when a JS file is needed for this chunk
*/
const chunkHasJs = (chunk, chunkGraph) => {
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) return true;
const _chunkHasJs = (chunk, chunkGraph) => {
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
return true;
}
}
}
return Boolean(
chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
);
};
/**
* @param {Chunk} chunk a chunk
* @param {ChunkGraph} chunkGraph the chunk graph
* @returns {boolean} true, when a JS file is needed for this chunk
*/
const chunkHasJs = (chunk, chunkGraph) => {
let innerCache = chunkHasJsCache.get(chunkGraph);
if (innerCache === undefined) {
innerCache = new WeakMap();
chunkHasJsCache.set(chunkGraph, innerCache);
}
const cachedResult = innerCache.get(chunk);
if (cachedResult !== undefined) {
return cachedResult;
}
const result = _chunkHasJs(chunk, chunkGraph);
innerCache.set(chunk, result);
return result;
};
/**
* @param {Chunk} chunk a chunk
* @param {ChunkGraph} chunkGraph the chunk graph
* @returns {boolean} true, when a JS file is needed for this chunk
*/
const chunkHasRuntimeOrJs = (chunk, chunkGraph) => {
if (chunkHasJs(chunk, chunkGraph)) {
return true;
}
if (
chunkGraph.getChunkModulesIterableBySourceType(
chunk,
WEBPACK_MODULE_TYPE_RUNTIME
)
) {
return true;
for (const chunkGroup of chunk.groupsIterable) {
for (const c of chunkGroup.chunks) {
if (chunkHasJs(c, chunkGraph)) return true;
}
}
return false;
}
return Boolean(
chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
);
return false;
};
/**
@@ -1312,17 +1351,22 @@ class JavascriptModulesPlugin {
const runtimeRequirements =
chunkGraph.getTreeRuntimeRequirements(chunk);
buf2.push("// Load entry module and return exports");
let i = chunkGraph.getNumberOfEntryModules(chunk);
/** @type {EntryModuleWithChunkGroup[]} */
const jsEntries = [];
for (const [
entryModule,
entrypoint
] of chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)) {
if (
!chunkGraph.getModuleSourceTypes(entryModule).has(JAVASCRIPT_TYPE)
chunkGraph.getModuleSourceTypes(entryModule).has(JAVASCRIPT_TYPE)
) {
i--;
jsEntries.push([entryModule, entrypoint]);
continue;
}
}
let i = jsEntries.length;
for (const [entryModule, entrypoint] of jsEntries) {
const chunks =
/** @type {Entrypoint} */
(entrypoint).chunks.filter((c) => c !== chunk);
@@ -1546,20 +1590,42 @@ class JavascriptModulesPlugin {
runtimeTemplate: { outputOptions }
} = renderContext;
const runtimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk);
/**
* @param {string} condition guard expression
* @returns {string[]} source
*/
const renderMissingModuleError = (condition) =>
outputOptions.pathinfo
? [
`if (${condition}) {`,
Template.indent([
"delete __webpack_module_cache__[moduleId];",
'var e = new Error("Cannot find module \'" + moduleId + "\'");',
"e.code = 'MODULE_NOT_FOUND';",
"throw e;"
]),
"}"
]
: [];
const moduleExecution = runtimeRequirements.has(
RuntimeGlobals.interceptModuleExecution
)
? Template.asString([
`var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: ${RuntimeGlobals.require} };`,
`${RuntimeGlobals.interceptModuleExecution}.forEach(function(handler) { handler(execOptions); });`,
...renderMissingModuleError("!execOptions.factory"),
"module = execOptions.module;",
"execOptions.factory.call(module.exports, module, module.exports, execOptions.require);"
])
: runtimeRequirements.has(RuntimeGlobals.thisAsExports)
? Template.asString([
...renderMissingModuleError("!(moduleId in __webpack_modules__)"),
`__webpack_modules__[moduleId].call(module.exports, module, module.exports, ${RuntimeGlobals.require});`
])
: Template.asString([
...renderMissingModuleError("!(moduleId in __webpack_modules__)"),
`__webpack_modules__[moduleId](module, module.exports, ${RuntimeGlobals.require});`
]);
const needModuleId = runtimeRequirements.has(RuntimeGlobals.moduleId);
@@ -1580,19 +1646,6 @@ class JavascriptModulesPlugin {
])
: Template.indent("return cachedModule.exports;"),
"}",
// Add helpful error message in development mode when module is not found
...(outputOptions.pathinfo
? [
"// Check if module exists (development only)",
"if (__webpack_modules__[moduleId] === undefined) {",
Template.indent([
'var e = new Error("Cannot find module \'" + moduleId + "\'");',
"e.code = 'MODULE_NOT_FOUND';",
"throw e;"
]),
"}"
]
: []),
"// Create a new module (and put it into the cache)",
"var module = __webpack_module_cache__[moduleId] = {",
Template.indent([

View File

@@ -318,13 +318,13 @@ class VariableInfo {
/** @typedef {Literal | string | null | undefined} ImportSource */
/**
* @typedef {Omit<ParseOptions, "sourceType"> & { sourceType: "module" | "script" | "auto" }} InternalParseOptions
* @typedef {Omit<ParseOptions, "sourceType" | "ecmaVersion"> & { sourceType: "module" | "script" | "auto" }} InternalParseOptions
*/
/**
* @typedef {object} ParseOptions
* @property {"module" | "script"} sourceType
* @property {EcmaVersion=} ecmaVersion
* @property {EcmaVersion} ecmaVersion
* @property {boolean=} locations
* @property {boolean=} comments
* @property {boolean=} ranges

View File

@@ -1004,6 +1004,9 @@ class ConcatenatedModule extends Module {
if (!(connection.dependency instanceof HarmonyImportDependency)) {
return false;
}
if (!connection.module.getSourceBasicTypes().has(JAVASCRIPT_TYPE)) {
return false;
}
return (
connection &&
connection.resolvedOriginModule === module &&

View File

@@ -466,7 +466,10 @@ class ModuleConcatenationPlugin {
chunk,
m
);
if (sourceTypes.size === 1) {
if (
sourceTypes.size === 1 &&
sourceTypes.has(JAVASCRIPT_TYPE)
) {
chunkGraph.disconnectChunkAndModule(chunk, m);
} else {
const newSourceTypes = new Set(sourceTypes);

View File

@@ -28,6 +28,7 @@ Assets: ${assetLists}`);
/** @type {string} */
this.name = "AssetsOverSizeLimitWarning";
/** @type {AssetDetails[]} */
this.assets = assetsOverSizeLimit;
}
}

View File

@@ -31,6 +31,7 @@ Entrypoints:${entrypointList}\n`);
/** @type {string} */
this.name = "EntrypointsOverSizeLimitWarning";
/** @type {EntrypointDetails[]} */
this.entrypoints = entrypoints;
}
}

View File

@@ -46,6 +46,7 @@ module.exports = class SizeLimitsPlugin {
* @param {PerformanceOptions} options the plugin options
*/
constructor(options) {
/** @type {PerformanceOptions["hints"]} */
this.hints = options.hints;
/** @type {number | undefined} */
this.maxAssetSize = options.maxAssetSize;

View File

@@ -29,30 +29,38 @@ class ToBinaryRuntimeModule extends RuntimeModule {
const isNodePlatform = compilation.compiler.platform.node;
const isWebPlatform = compilation.compiler.platform.web;
const isNeutralPlatform = runtimeTemplate.isNeutralPlatform();
const toImmutableBytes = runtimeTemplate.basicFunction("value", [
runtimeTemplate.destructureObject(["buffer"], "value"),
`${runtimeTemplate.renderConst()} throwErr = ${runtimeTemplate.basicFunction("", ["throw new TypeError('ArrayBuffer is immutable');"])};`,
"Object.defineProperties(buffer, { immutable: { value: true }, resize: { value: throwErr }, transfer: { value: throwErr }, transferToFixedLength: { value: throwErr } });",
"Object.freeze(buffer);",
"return value;"
]);
return Template.asString([
"// define to binary helper",
`${runtimeTemplate.renderConst()} toImmutableBytes = ${toImmutableBytes}`,
`${fn} = ${isNeutralPlatform ? "typeof Buffer !== 'undefined' ? " : ""}${
isNodePlatform || isNeutralPlatform
? `${runtimeTemplate.returningFunction("new Uint8Array(Buffer.from(base64, 'base64'))", "base64")}`
? `${runtimeTemplate.returningFunction("toImmutableBytes(new Uint8Array(Buffer.from(base64, 'base64')))", "base64")}`
: ""
} ${isNeutralPlatform ? ": " : ""}${
isWebPlatform || isNeutralPlatform
? `(${runtimeTemplate.basicFunction("", [
"var table = new Uint8Array(128);",
`${runtimeTemplate.renderConst()} table = new Uint8Array(128);`,
"for (var i = 0; i < 64; i++) table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;",
`return ${runtimeTemplate.basicFunction("base64", [
"var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == '=') - (base64[n - 2] == '=')) * 3 / 4 | 0);",
`${runtimeTemplate.renderConst()} n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == '=') - (base64[n - 2] == '=')) * 3 / 4 | 0);`,
"for (var i = 0, j = 0; i < n;) {",
Template.indent([
"var c0 = table[base64.charCodeAt(i++)], c1 = table[base64.charCodeAt(i++)];",
"var c2 = table[base64.charCodeAt(i++)], c3 = table[base64.charCodeAt(i++)];",
`${runtimeTemplate.renderConst()} c0 = table[base64.charCodeAt(i++)], c1 = table[base64.charCodeAt(i++)];`,
`${runtimeTemplate.renderConst()} c2 = table[base64.charCodeAt(i++)], c3 = table[base64.charCodeAt(i++)];`,
"bytes[j++] = (c0 << 2) | (c1 >> 4);",
"bytes[j++] = (c1 << 4) | (c2 >> 2);",
"bytes[j++] = (c2 << 6) | c3;"
]),
"}",
"return bytes"
"return toImmutableBytes(bytes)"
])}`
])})();`
: ""

View File

@@ -8,7 +8,10 @@
const { RawSource } = require("webpack-sources");
const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
const Module = require("../Module");
const { CONSUME_SHARED_TYPES } = require("../ModuleSourceTypeConstants");
const {
CONSUME_SHARED_TYPES,
JAVASCRIPT_TYPES
} = require("../ModuleSourceTypeConstants");
const {
WEBPACK_MODULE_TYPE_CONSUME_SHARED_MODULE
} = require("../ModuleTypeConstants");
@@ -41,6 +44,7 @@ const fallbackModuleCache = new WeakMap();
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
/** @typedef {import("../util/semver").SemVerRange} SemVerRange */
/** @typedef {import("../Module").BasicSourceTypes} BasicSourceTypes */
/**
* @typedef {object} ConsumeOptions
@@ -159,6 +163,19 @@ class ConsumeSharedModule extends Module {
return CONSUME_SHARED_TYPES;
}
/**
* Basic source types are high-level categories like javascript, css, webassembly, etc.
* We only have built-in knowledge about the javascript basic type here; other basic types may be
* added or changed over time by generators and do not need to be handled or detected here.
*
* Some modules, e.g. RemoteModule, may return non-basic source types like "remote" and "share-init"
* from getSourceTypes(), but their generated output is still JavaScript, i.e. their basic type is JS.
* @returns {BasicSourceTypes} types available (do not mutate)
*/
getSourceBasicTypes() {
return JAVASCRIPT_TYPES;
}
/**
* @param {ModuleGraph} moduleGraph the module graph
* @returns {Module | null} fallback module

View File

@@ -43,7 +43,9 @@ class AppendOnlyStackedSet {
clear() {
this._sets = [];
if (this._current) this._current.clear();
if (this._current) {
this._current = undefined;
}
}
/**
@@ -52,6 +54,25 @@ class AppendOnlyStackedSet {
createChild() {
return new AppendOnlyStackedSet(this._sets.length ? [...this._sets] : []);
}
/**
* @returns {Iterator<T>} iterable iterator
*/
[Symbol.iterator]() {
const iterators = this._sets.map((map) => map[Symbol.iterator]());
let current = iterators.pop();
return {
next() {
if (!current) return { done: true, value: undefined };
let result = current.next();
while (result.done && iterators.length > 0) {
current = /** @type {SetIterator<T>} */ (iterators.pop());
result = current.next();
}
return result;
}
};
}
}
module.exports = AppendOnlyStackedSet;

View File

@@ -8,19 +8,13 @@
const NO_MARKER = 0;
const IN_PROGRESS_MARKER = 1;
const DONE_MARKER = 2;
const DONE_MAYBE_ROOT_CYCLE_MARKER = 3;
const DONE_AND_ROOT_MARKER = 4;
const CANDIDATE_MARKER = 3;
/**
* @template T
* @typedef {Set<Node<T>>} Nodes
*/
/**
* @template T
* @typedef {Set<Cycle<T>>} Cycles
*/
/**
* @template T
*/
@@ -32,20 +26,24 @@ class Node {
this.item = item;
/** @type {Nodes<T>} */
this.dependencies = new Set();
this.marker = NO_MARKER;
/** @type {Cycle<T> | undefined} */
this.cycle = undefined;
/** @type {SCC<T>} */
this.scc = new SCC();
// Each node starts as a single-node SCC
this.scc.nodes.add(this);
/** @type {number} */
this.incoming = 0;
}
}
/**
* SCC (strongly connected component)
* @template T
*/
class Cycle {
class SCC {
constructor() {
/** @type {Nodes<T>} */
this.nodes = new Set();
this.marker = NO_MARKER;
}
}
@@ -70,10 +68,10 @@ module.exports = (items, getDependencies) => {
itemToNode.set(item, node);
}
// early exit when there is only a single item
// Early exit when there is only one node
if (itemToNode.size <= 1) return items;
// grab all the dependencies
// Build graph edges
for (const node of itemToNode.values()) {
for (const dep of getDependencies(node.item)) {
const depNode = itemToNode.get(dep);
@@ -83,27 +81,16 @@ module.exports = (items, getDependencies) => {
}
}
// Set of current root modules
// items will be removed if a new reference to it has been found
/** @type {Nodes<T>} */
const roots = new Set();
// All candidate root SCCs, they will be removed once an incoming edge is found
/** @type {Set<SCC<T>>} */
const rootSCCs = new Set();
// Set of current cycles without references to it
// cycles will be removed if a new reference to it has been found
// that is not part of the cycle
/** @type {Cycles<T>} */
const rootCycles = new Set();
// For all non-marked nodes
for (const selectedNode of itemToNode.values()) {
if (selectedNode.marker === NO_MARKER) {
// deep-walk all referenced modules
// in a non-recursive way
// DFS walk only once per unseen SCC
if (selectedNode.scc.marker === NO_MARKER) {
selectedNode.scc.marker = IN_PROGRESS_MARKER;
// start by entering the selected node
selectedNode.marker = IN_PROGRESS_MARKER;
// keep a stack to avoid recursive walk
// Keep a stack to avoid recursive walk
/** @type {StackEntry<T>[]} */
const stack = [
{
@@ -112,130 +99,113 @@ module.exports = (items, getDependencies) => {
}
];
// process the top item until stack is empty
while (stack.length > 0) {
const topOfStack = stack[stack.length - 1];
// Are there still edges unprocessed in the current node?
// Process one unvisited outgoing edge if available
if (topOfStack.openEdges.length > 0) {
// Process one dependency
const dependency =
/** @type {Node<T>} */
(topOfStack.openEdges.pop());
switch (dependency.marker) {
const depSCC = dependency.scc;
switch (depSCC.marker) {
case NO_MARKER:
// dependency has not be visited yet
// mark it as in-progress and recurse
// First time we see this SCC: enter it
stack.push({
node: dependency,
openEdges: [...dependency.dependencies]
});
dependency.marker = IN_PROGRESS_MARKER;
depSCC.marker = IN_PROGRESS_MARKER;
break;
case IN_PROGRESS_MARKER: {
// It's a in-progress cycle
let cycle = dependency.cycle;
if (!cycle) {
cycle = new Cycle();
cycle.nodes.add(dependency);
dependency.cycle = cycle;
}
// set cycle property for each node in the cycle
// if nodes are already part of a cycle
// we merge the cycles to a shared cycle
// Back-edge to an SCC that is still on the stack
// Example:
// A -> B -> C -> D
// ^ |
// |_________|
// If we are at `D` and traverse `D` -> `B`, then `B/C/D` must be in one SCC
/** @type {Set<SCC<T>>} */
const sccsToMerge = new Set();
for (
let i = stack.length - 1;
stack[i].node !== dependency;
stack[i].node.scc !== depSCC;
i--
) {
const node = stack[i].node;
if (node.cycle) {
if (node.cycle !== cycle) {
// merge cycles
for (const cycleNode of node.cycle.nodes) {
cycleNode.cycle = cycle;
cycle.nodes.add(cycleNode);
}
}
} else {
node.cycle = cycle;
cycle.nodes.add(node);
sccsToMerge.add(stack[i].node.scc);
}
for (const sccToMerge of sccsToMerge) {
for (const nodeInMergedSCC of sccToMerge.nodes) {
nodeInMergedSCC.scc = depSCC;
depSCC.nodes.add(nodeInMergedSCC);
}
}
// don't recurse into dependencies
// these are already on the stack
break;
}
case DONE_AND_ROOT_MARKER:
// This node has be visited yet and is currently a root node
// But as this is a new reference to the node
// it's not really a root
// so we have to convert it to a normal node
dependency.marker = DONE_MARKER;
roots.delete(dependency);
case CANDIDATE_MARKER:
// This finished SCC was previously considered as root SCC
// We just found a new incoming edge, so it is no longer a candidate
rootSCCs.delete(/** @type {SCC<T>} */ (depSCC));
depSCC.marker = DONE_MARKER;
break;
case DONE_MAYBE_ROOT_CYCLE_MARKER:
// This node has be visited yet and
// is maybe currently part of a completed root cycle
// we found a new reference to the cycle
// so it's not really a root cycle
// remove the cycle from the root cycles
// and convert it to a normal node
rootCycles.delete(/** @type {Cycle<T>} */ (dependency.cycle));
dependency.marker = DONE_MARKER;
case DONE_MARKER:
// Already finalized and not a candidate
break;
// DONE_MARKER: nothing to do, don't recurse into dependencies
}
} else {
// All dependencies of the current node has been visited
// we leave the node
// All dependencies of the current node have been processed
// So we leave the node
stack.pop();
topOfStack.node.marker = DONE_MARKER;
// Mark an SCC as DONE only when the popped node is the last
// node from that SCC remaining on the current stack.
// A -> B -> C -> D
// ^ |
// |_________|
// If `B` is popped and the new stack top is `A`, they are in
// different SCCs, so B's SCC can be finalized.
if (
stack.length &&
topOfStack.node.scc !== stack[stack.length - 1].node.scc
) {
topOfStack.node.scc.marker = DONE_MARKER;
}
}
}
const cycle = selectedNode.cycle;
if (cycle) {
for (const node of cycle.nodes) {
node.marker = DONE_MAYBE_ROOT_CYCLE_MARKER;
}
rootCycles.add(cycle);
} else {
selectedNode.marker = DONE_AND_ROOT_MARKER;
roots.add(selectedNode);
}
const scc = selectedNode.scc;
// This SCC is complete and currently has no known incoming edge
scc.marker = CANDIDATE_MARKER;
rootSCCs.add(scc);
}
}
// Extract roots from root cycles
// We take the nodes with most incoming edges
// inside of the cycle
for (const cycle of rootCycles) {
/** @type {Set<T>} */
const rootNodes = new Set();
// For each root SCC, we select node with the most incoming edges
// from within the same SCC
for (const scc of rootSCCs) {
let max = 0;
/** @type {Nodes<T>} */
const cycleRoots = new Set();
const nodes = cycle.nodes;
for (const node of nodes) {
const nodes = new Set(scc.nodes);
for (const node of scc.nodes) {
for (const dep of node.dependencies) {
if (nodes.has(dep)) {
if (scc.nodes.has(dep)) {
dep.incoming++;
if (dep.incoming < max) continue;
if (dep.incoming > max) {
cycleRoots.clear();
nodes.clear();
max = dep.incoming;
}
cycleRoots.add(dep);
nodes.add(dep);
}
}
}
for (const cycleRoot of cycleRoots) {
roots.add(cycleRoot);
for (const node of nodes) {
rootNodes.add(node.item);
}
}
// When roots were found, return them
if (roots.size > 0) {
return Array.from(roots, (r) => r.item);
}
// When root nodes were found, return them
if (rootNodes.size > 0) return rootNodes;
throw new Error("Implementation of findGraphRoots is broken");
};