Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const Dependency = require("./Dependency");
|
|
const { UsageState } = require("./ExportsInfo");
|
|
const ModuleGraphConnection = require("./ModuleGraphConnection");
|
|
const { STAGE_DEFAULT } = require("./OptimizationStages");
|
|
const ArrayQueue = require("./util/ArrayQueue");
|
|
const TupleQueue = require("./util/TupleQueue");
|
|
const { getEntryRuntime, mergeRuntimeOwned } = require("./util/runtime");
|
|
|
|
/** @typedef {import("./Compiler")} Compiler */
|
|
/** @typedef {import("./DependenciesBlock")} DependenciesBlock */
|
|
/** @typedef {import("./Dependency").ReferencedExport} ReferencedExport */
|
|
/** @typedef {import("./Dependency").ReferencedExports} ReferencedExports */
|
|
/** @typedef {import("./ExportsInfo")} ExportsInfo */
|
|
/** @typedef {import("./Module")} Module */
|
|
/** @typedef {import("./util/runtime").RuntimeSpec} RuntimeSpec */
|
|
|
|
const { NO_EXPORTS_REFERENCED, EXPORTS_OBJECT_REFERENCED } = Dependency;
|
|
|
|
const PLUGIN_NAME = "FlagDependencyUsagePlugin";
|
|
const PLUGIN_LOGGER_NAME = `webpack.${PLUGIN_NAME}`;
|
|
|
|
class FlagDependencyUsagePlugin {
|
|
/**
|
|
* @param {boolean} global do a global analysis instead of per runtime
|
|
*/
|
|
constructor(global) {
|
|
this.global = global;
|
|
}
|
|
|
|
/**
|
|
* Apply the plugin
|
|
* @param {Compiler} compiler the compiler instance
|
|
* @returns {void}
|
|
*/
|
|
apply(compiler) {
|
|
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
|
const moduleGraph = compilation.moduleGraph;
|
|
compilation.hooks.optimizeDependencies.tap(
|
|
{ name: PLUGIN_NAME, stage: STAGE_DEFAULT },
|
|
(modules) => {
|
|
if (compilation.moduleMemCaches) {
|
|
throw new Error(
|
|
"optimization.usedExports can't be used with cacheUnaffected as export usage is a global effect"
|
|
);
|
|
}
|
|
|
|
const logger = compilation.getLogger(PLUGIN_LOGGER_NAME);
|
|
/** @type {Map<ExportsInfo, Module>} */
|
|
const exportInfoToModuleMap = new Map();
|
|
|
|
/** @type {TupleQueue<Module, RuntimeSpec>} */
|
|
const queue = new TupleQueue();
|
|
|
|
/**
|
|
* @param {Module} module module to process
|
|
* @param {ReferencedExports} usedExports list of used exports
|
|
* @param {RuntimeSpec} runtime part of which runtime
|
|
* @param {boolean} forceSideEffects always apply side effects
|
|
* @returns {void}
|
|
*/
|
|
const processReferencedModule = (
|
|
module,
|
|
usedExports,
|
|
runtime,
|
|
forceSideEffects
|
|
) => {
|
|
const exportsInfo = moduleGraph.getExportsInfo(module);
|
|
if (usedExports.length > 0) {
|
|
if (!module.buildMeta || !module.buildMeta.exportsType) {
|
|
if (exportsInfo.setUsedWithoutInfo(runtime)) {
|
|
queue.enqueue(module, runtime);
|
|
}
|
|
return;
|
|
}
|
|
for (const usedExportInfo of usedExports) {
|
|
let usedExport;
|
|
let canMangle = true;
|
|
if (Array.isArray(usedExportInfo)) {
|
|
usedExport = usedExportInfo;
|
|
} else {
|
|
usedExport = usedExportInfo.name;
|
|
canMangle = usedExportInfo.canMangle !== false;
|
|
}
|
|
if (usedExport.length === 0) {
|
|
if (exportsInfo.setUsedInUnknownWay(runtime)) {
|
|
queue.enqueue(module, runtime);
|
|
}
|
|
} else {
|
|
let currentExportsInfo = exportsInfo;
|
|
for (let i = 0; i < usedExport.length; i++) {
|
|
const exportInfo = currentExportsInfo.getExportInfo(
|
|
usedExport[i]
|
|
);
|
|
if (canMangle === false) {
|
|
exportInfo.canMangleUse = false;
|
|
}
|
|
const lastOne = i === usedExport.length - 1;
|
|
if (!lastOne) {
|
|
const nestedInfo = exportInfo.getNestedExportsInfo();
|
|
if (nestedInfo) {
|
|
if (
|
|
exportInfo.setUsedConditionally(
|
|
(used) => used === UsageState.Unused,
|
|
UsageState.OnlyPropertiesUsed,
|
|
runtime
|
|
)
|
|
) {
|
|
const currentModule =
|
|
currentExportsInfo === exportsInfo
|
|
? module
|
|
: exportInfoToModuleMap.get(currentExportsInfo);
|
|
if (currentModule) {
|
|
queue.enqueue(currentModule, runtime);
|
|
}
|
|
}
|
|
currentExportsInfo = nestedInfo;
|
|
continue;
|
|
}
|
|
}
|
|
if (
|
|
exportInfo.setUsedConditionally(
|
|
(v) => v !== UsageState.Used,
|
|
UsageState.Used,
|
|
runtime
|
|
)
|
|
) {
|
|
const currentModule =
|
|
currentExportsInfo === exportsInfo
|
|
? module
|
|
: exportInfoToModuleMap.get(currentExportsInfo);
|
|
if (currentModule) {
|
|
queue.enqueue(currentModule, runtime);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// for a module without side effects we stop tracking usage here when no export is used
|
|
// This module won't be evaluated in this case
|
|
// TODO webpack 6 remove this check
|
|
if (
|
|
!forceSideEffects &&
|
|
module.factoryMeta !== undefined &&
|
|
module.factoryMeta.sideEffectFree
|
|
) {
|
|
return;
|
|
}
|
|
if (exportsInfo.setUsedForSideEffectsOnly(runtime)) {
|
|
queue.enqueue(module, runtime);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {DependenciesBlock} module the module
|
|
* @param {RuntimeSpec} runtime part of which runtime
|
|
* @param {boolean} forceSideEffects always apply side effects
|
|
* @returns {void}
|
|
*/
|
|
const processModule = (module, runtime, forceSideEffects) => {
|
|
/** @type {Map<Module, ReferencedExports | Map<string, string[] | ReferencedExport>>} */
|
|
const map = new Map();
|
|
|
|
/** @type {ArrayQueue<DependenciesBlock>} */
|
|
const queue = new ArrayQueue();
|
|
queue.enqueue(module);
|
|
for (;;) {
|
|
const block = queue.dequeue();
|
|
if (block === undefined) break;
|
|
for (const b of block.blocks) {
|
|
if (
|
|
!this.global &&
|
|
b.groupOptions &&
|
|
b.groupOptions.entryOptions
|
|
) {
|
|
processModule(
|
|
b,
|
|
b.groupOptions.entryOptions.runtime || undefined,
|
|
true
|
|
);
|
|
} else {
|
|
queue.enqueue(b);
|
|
}
|
|
}
|
|
for (const dep of block.dependencies) {
|
|
const connection = moduleGraph.getConnection(dep);
|
|
if (!connection || !connection.module) {
|
|
continue;
|
|
}
|
|
const activeState = connection.getActiveState(runtime);
|
|
if (activeState === false) continue;
|
|
const { module } = connection;
|
|
if (activeState === ModuleGraphConnection.TRANSITIVE_ONLY) {
|
|
processModule(module, runtime, false);
|
|
continue;
|
|
}
|
|
const oldReferencedExports = map.get(module);
|
|
if (oldReferencedExports === EXPORTS_OBJECT_REFERENCED) {
|
|
continue;
|
|
}
|
|
const referencedExports =
|
|
compilation.getDependencyReferencedExports(dep, runtime);
|
|
if (
|
|
oldReferencedExports === undefined ||
|
|
oldReferencedExports === NO_EXPORTS_REFERENCED ||
|
|
referencedExports === EXPORTS_OBJECT_REFERENCED
|
|
) {
|
|
map.set(module, referencedExports);
|
|
} else if (
|
|
oldReferencedExports !== undefined &&
|
|
referencedExports === NO_EXPORTS_REFERENCED
|
|
) {
|
|
continue;
|
|
} else {
|
|
let exportsMap;
|
|
if (Array.isArray(oldReferencedExports)) {
|
|
exportsMap = new Map();
|
|
for (const item of oldReferencedExports) {
|
|
if (Array.isArray(item)) {
|
|
exportsMap.set(item.join("\n"), item);
|
|
} else {
|
|
exportsMap.set(item.name.join("\n"), item);
|
|
}
|
|
}
|
|
map.set(module, exportsMap);
|
|
} else {
|
|
exportsMap = oldReferencedExports;
|
|
}
|
|
for (const item of referencedExports) {
|
|
if (Array.isArray(item)) {
|
|
const key = item.join("\n");
|
|
const oldItem = exportsMap.get(key);
|
|
if (oldItem === undefined) {
|
|
exportsMap.set(key, item);
|
|
}
|
|
// if oldItem is already an array we have to do nothing
|
|
// if oldItem is an ReferencedExport object, we don't have to do anything
|
|
// as canMangle defaults to true for arrays
|
|
} else {
|
|
const key = item.name.join("\n");
|
|
const oldItem = exportsMap.get(key);
|
|
if (oldItem === undefined || Array.isArray(oldItem)) {
|
|
exportsMap.set(key, item);
|
|
} else {
|
|
exportsMap.set(key, {
|
|
name: item.name,
|
|
canMangle: item.canMangle && oldItem.canMangle
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [module, referencedExports] of map) {
|
|
if (Array.isArray(referencedExports)) {
|
|
processReferencedModule(
|
|
module,
|
|
referencedExports,
|
|
runtime,
|
|
forceSideEffects
|
|
);
|
|
} else {
|
|
processReferencedModule(
|
|
module,
|
|
[...referencedExports.values()],
|
|
runtime,
|
|
forceSideEffects
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
logger.time("initialize exports usage");
|
|
for (const module of modules) {
|
|
const exportsInfo = moduleGraph.getExportsInfo(module);
|
|
exportInfoToModuleMap.set(exportsInfo, module);
|
|
exportsInfo.setHasUseInfo();
|
|
}
|
|
logger.timeEnd("initialize exports usage");
|
|
|
|
logger.time("trace exports usage in graph");
|
|
|
|
/**
|
|
* @param {Dependency} dep dependency
|
|
* @param {RuntimeSpec} runtime runtime
|
|
*/
|
|
const processEntryDependency = (dep, runtime) => {
|
|
const module = moduleGraph.getModule(dep);
|
|
if (module) {
|
|
processReferencedModule(
|
|
module,
|
|
NO_EXPORTS_REFERENCED,
|
|
runtime,
|
|
true
|
|
);
|
|
}
|
|
};
|
|
/** @type {RuntimeSpec} */
|
|
let globalRuntime;
|
|
for (const [
|
|
entryName,
|
|
{ dependencies: deps, includeDependencies: includeDeps, options }
|
|
] of compilation.entries) {
|
|
const runtime = this.global
|
|
? undefined
|
|
: getEntryRuntime(compilation, entryName, options);
|
|
for (const dep of deps) {
|
|
processEntryDependency(dep, runtime);
|
|
}
|
|
for (const dep of includeDeps) {
|
|
processEntryDependency(dep, runtime);
|
|
}
|
|
globalRuntime = mergeRuntimeOwned(globalRuntime, runtime);
|
|
}
|
|
for (const dep of compilation.globalEntry.dependencies) {
|
|
processEntryDependency(dep, globalRuntime);
|
|
}
|
|
for (const dep of compilation.globalEntry.includeDependencies) {
|
|
processEntryDependency(dep, globalRuntime);
|
|
}
|
|
|
|
while (queue.length) {
|
|
const [module, runtime] = /** @type {[Module, RuntimeSpec]} */ (
|
|
queue.dequeue()
|
|
);
|
|
processModule(module, runtime, false);
|
|
}
|
|
logger.timeEnd("trace exports usage in graph");
|
|
}
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = FlagDependencyUsagePlugin;
|