webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
511 lines (469 loc) • 16.7 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const asyncLib = require("neo-async");
const Queue = require("./util/Queue");
const createHash = require("./util/createHash");
/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("./DependenciesBlock")} DependenciesBlock */
/** @typedef {import("./Dependency")} Dependency */
/** @typedef {import("./Dependency").ExportSpec} ExportSpec */
/** @typedef {import("./Dependency").ExportsSpec} ExportsSpec */
/** @typedef {import("./ExportsInfo")} ExportsInfo */
/** @typedef {import("./ExportsInfo").ExportInfoName} ExportInfoName */
/** @typedef {import("./ExportsInfo").RestoreProvidedData} RestoreProvidedData */
/** @typedef {import("../declarations/WebpackOptions").HashFunction} HashFunction */
/** @typedef {import("./LazyBarrel")} LazyBarrelController */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./Module").BuildInfo} BuildInfo */
const PLUGIN_NAME = "FlagDependencyExportsPlugin";
const PLUGIN_LOGGER_NAME = `webpack.${PLUGIN_NAME}`;
/** @typedef {{ etag: string, data: RestoreProvidedData }} MemCacheEntry */
/**
* @param {Module} module the module
* @param {LazyBarrelController} lazyBarrelController the compilation's lazy barrel controller
* @param {HashFunction} hashFunction hash function for long keys
* @returns {string} the hash
*/
const getLazyRequestsHash = (module, lazyBarrelController, hashFunction) => {
const lazyKeys = lazyBarrelController.getLazyRequests(module);
if (!lazyKeys) return "";
const joined = [...lazyKeys].join("|");
if (joined.length < 100) return joined;
const hash = createHash(hashFunction);
hash.update(joined);
return /** @type {string} */ (hash.digest("hex"));
};
class FlagDependencyExportsPlugin {
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
const moduleGraph = compilation.moduleGraph;
const cache = compilation.getCache(PLUGIN_NAME);
const lazyBarrelController = compilation._lazyBarrelController;
const hashFunction = compilation.outputOptions.hashFunction;
compilation.hooks.finishModules.tapAsync(
PLUGIN_NAME,
(modules, callback) => {
const logger = compilation.getLogger(PLUGIN_LOGGER_NAME);
let statRestoredFromMemCache = 0;
let statRestoredFromCache = 0;
let statNoExports = 0;
let statFlaggedUncached = 0;
let statNotCached = 0;
let statQueueItemsProcessed = 0;
const { moduleMemCaches } = compilation;
/** @type {Queue<Module>} */
const queue = new Queue();
// Step 1: Try to restore cached provided export info from cache
logger.time("restore cached provided exports");
asyncLib.each(
/** @type {import("neo-async").IterableCollection<Module>} */ (
/** @type {unknown} */ (modules)
),
(module, callback) => {
const exportsInfo = moduleGraph.getExportsInfo(module);
// If the module doesn't have an exportsType, it's a module
// without declared exports.
if (
(!module.buildMeta || !module.buildMeta.exportsType) &&
exportsInfo.otherExportsInfo.provided !== null
) {
// It's a module without declared exports
statNoExports++;
exportsInfo.setHasProvideInfo();
exportsInfo.setUnknownExportsProvided();
return callback();
}
// If the module has no hash, it's uncacheable
if (
typeof (/** @type {BuildInfo} */ (module.buildInfo).hash) !==
"string"
) {
statFlaggedUncached++;
// Enqueue uncacheable module for determining the exports
queue.enqueue(module);
exportsInfo.setHasProvideInfo();
return callback();
}
const memCache = moduleMemCaches && moduleMemCaches.get(module);
/** @type {MemCacheEntry | undefined} */
const memCacheEntry = memCache && memCache.get(this);
const hash = getLazyRequestsHash(
module,
lazyBarrelController,
hashFunction
);
// validated by the still-lazy request keys: reference-change
// detection cannot invalidate the mem cache when the un-lazied
// connections are unsafe-cached (they are skipped when comparing
// references)
if (memCacheEntry !== undefined && memCacheEntry.etag === hash) {
statRestoredFromMemCache++;
exportsInfo.restoreProvided(memCacheEntry.data);
return callback();
}
const buildHash = /** @type {string} */ (
/** @type {BuildInfo} */ (module.buildInfo).hash
);
cache.get(
module.identifier(),
hash ? `${buildHash}|${hash}` : buildHash,
(err, result) => {
if (err) return callback(err);
if (result !== undefined) {
statRestoredFromCache++;
exportsInfo.restoreProvided(result);
} else {
statNotCached++;
// Without cached info enqueue module for determining the exports
queue.enqueue(module);
exportsInfo.setHasProvideInfo();
}
callback();
}
);
},
(err) => {
logger.timeEnd("restore cached provided exports");
if (err) return callback(err);
/** @type {Set<Module>} */
const modulesToStore = new Set();
/** @type {Map<Module, Set<Module>>} */
const dependencies = new Map();
/** @type {Module} */
let module;
/** @type {ExportsInfo} */
let exportsInfo;
/** @type {Map<Dependency, ExportsSpec>} */
const exportsSpecsFromDependencies = new Map();
let cacheable = true;
let changed = false;
/**
* Process dependencies block.
* @param {DependenciesBlock} depBlock the dependencies block
* @returns {void}
*/
const processDependenciesBlock = (depBlock) => {
for (const dep of depBlock.dependencies) {
processDependency(dep);
}
for (const block of depBlock.blocks) {
processDependenciesBlock(block);
}
};
/**
* Process dependency.
* @param {Dependency} dep the dependency
* @returns {void}
*/
const processDependency = (dep) => {
const exportDesc = dep.getExports(moduleGraph);
if (!exportDesc) return;
exportsSpecsFromDependencies.set(dep, exportDesc);
};
/**
* Process exports spec.
* @param {Dependency} dep dependency
* @param {ExportsSpec} exportDesc info
* @returns {void}
*/
const processExportsSpec = (dep, exportDesc) => {
const exports = exportDesc.exports;
const globalCanMangle = exportDesc.canMangle;
const globalFrom = exportDesc.from;
const globalPriority = exportDesc.priority;
const globalTerminalBinding =
exportDesc.terminalBinding || false;
const globalPure = exportDesc.isPure || false;
const exportDeps = exportDesc.dependencies;
if (exportDesc.hideExports) {
for (const name of exportDesc.hideExports) {
const exportInfo = exportsInfo.getExportInfo(name);
exportInfo.unsetTarget(dep);
}
}
if (exports === true) {
// unknown exports
if (
exportsInfo.setUnknownExportsProvided(
globalCanMangle,
exportDesc.excludeExports,
globalFrom && dep,
globalFrom,
globalPriority
)
) {
changed = true;
}
} else if (Array.isArray(exports)) {
/**
* merge in new exports
* @param {ExportsInfo} exportsInfo own exports info
* @param {(ExportSpec | string)[]} exports list of exports
*/
const mergeExports = (exportsInfo, exports) => {
for (const exportNameOrSpec of exports) {
/** @type {ExportInfoName} */
let name;
let canMangle = globalCanMangle;
let terminalBinding = globalTerminalBinding;
let pure = globalPure;
/** @type {ExportSpec["exports"]} */
let exports;
let from = globalFrom;
/** @type {ExportSpec["export"]} */
let fromExport;
let priority = globalPriority;
let hidden = false;
/** @type {ExportSpec["inlined"]} */
let inlined;
if (typeof exportNameOrSpec === "string") {
name = exportNameOrSpec;
} else {
name = exportNameOrSpec.name;
if (exportNameOrSpec.canMangle !== undefined) {
canMangle = exportNameOrSpec.canMangle;
}
if (exportNameOrSpec.export !== undefined) {
fromExport = exportNameOrSpec.export;
}
if (exportNameOrSpec.exports !== undefined) {
exports = exportNameOrSpec.exports;
}
if (exportNameOrSpec.from !== undefined) {
from = exportNameOrSpec.from;
}
if (exportNameOrSpec.priority !== undefined) {
priority = exportNameOrSpec.priority;
}
if (exportNameOrSpec.terminalBinding !== undefined) {
terminalBinding = exportNameOrSpec.terminalBinding;
}
if (exportNameOrSpec.isPure !== undefined) {
pure = exportNameOrSpec.isPure;
}
if (exportNameOrSpec.hidden !== undefined) {
hidden = exportNameOrSpec.hidden;
}
if (exportNameOrSpec.inlined !== undefined) {
inlined = exportNameOrSpec.inlined;
}
}
const exportInfo = exportsInfo.getExportInfo(name);
if (
exportInfo.provided === false ||
exportInfo.provided === null
) {
exportInfo.provided = true;
changed = true;
}
if (
exportInfo.canMangleProvide !== false &&
canMangle === false
) {
exportInfo.canMangleProvide = false;
changed = true;
}
if (
inlined !== undefined &&
exportInfo.canInlineProvide === undefined
) {
exportInfo.canInlineProvide = inlined;
changed = true;
}
if (terminalBinding && !exportInfo.terminalBinding) {
exportInfo.terminalBinding = true;
changed = true;
}
if (pure && exportInfo.pureProvide !== true) {
exportInfo.pureProvide = true;
changed = true;
}
if (exports) {
const nestedExportsInfo =
exportInfo.createNestedExportsInfo();
mergeExports(
/** @type {ExportsInfo} */ (nestedExportsInfo),
exports
);
}
if (
from &&
(hidden
? exportInfo.unsetTarget(dep)
: exportInfo.setTarget(
dep,
from,
fromExport === undefined ? [name] : fromExport,
priority
))
) {
changed = true;
}
// Recalculate target exportsInfo
const target = exportInfo.getTarget(moduleGraph);
/** @type {undefined | ExportsInfo} */
let targetExportsInfo;
if (target) {
const targetModuleExportsInfo =
moduleGraph.getExportsInfo(target.module);
targetExportsInfo =
targetModuleExportsInfo.getNestedExportsInfo(
target.export
);
// add dependency for this module
const set = dependencies.get(target.module);
if (set === undefined) {
dependencies.set(target.module, new Set([module]));
} else {
set.add(module);
}
}
if (exportInfo.exportsInfoOwned) {
if (
/** @type {ExportsInfo} */
(exportInfo.exportsInfo).setRedirectNamedTo(
targetExportsInfo
)
) {
changed = true;
}
} else if (exportInfo.exportsInfo !== targetExportsInfo) {
exportInfo.exportsInfo = targetExportsInfo;
changed = true;
}
}
};
mergeExports(exportsInfo, exports);
}
// store dependencies
if (exportDeps) {
cacheable = false;
for (const exportDependency of exportDeps) {
// add dependency for this module
const set = dependencies.get(exportDependency);
if (set === undefined) {
dependencies.set(exportDependency, new Set([module]));
} else {
set.add(module);
}
}
}
};
const notifyDependencies = () => {
const deps = dependencies.get(module);
if (deps !== undefined) {
for (const dep of deps) {
queue.enqueue(dep);
}
}
};
logger.time("figure out provided exports");
while (queue.length > 0) {
module = /** @type {Module} */ (queue.dequeue());
statQueueItemsProcessed++;
exportsInfo = moduleGraph.getExportsInfo(module);
cacheable = true;
changed = false;
exportsSpecsFromDependencies.clear();
moduleGraph.freeze();
processDependenciesBlock(module);
moduleGraph.unfreeze();
for (const [dep, exportsSpec] of exportsSpecsFromDependencies) {
processExportsSpec(dep, exportsSpec);
}
if (cacheable) {
modulesToStore.add(module);
}
if (changed) {
notifyDependencies();
}
}
logger.timeEnd("figure out provided exports");
logger.log(
`${Math.round(
(100 * (statFlaggedUncached + statNotCached)) /
(statRestoredFromMemCache +
statRestoredFromCache +
statNotCached +
statFlaggedUncached +
statNoExports)
)}% of exports of modules have been determined (${statNoExports} no declared exports, ${statNotCached} not cached, ${statFlaggedUncached} flagged uncacheable, ${statRestoredFromCache} from cache, ${statRestoredFromMemCache} from mem cache, ${
statQueueItemsProcessed - statNotCached - statFlaggedUncached
} additional calculations due to dependencies)`
);
logger.time("store provided exports into cache");
asyncLib.each(
modulesToStore,
(module, callback) => {
if (
typeof (
/** @type {BuildInfo} */
(module.buildInfo).hash
) !== "string"
) {
// not cacheable
return callback();
}
const cachedData = moduleGraph
.getExportsInfo(module)
.getRestoreProvidedData();
const memCache =
moduleMemCaches && moduleMemCaches.get(module);
const hash = getLazyRequestsHash(
module,
lazyBarrelController,
hashFunction
);
if (memCache) {
/** @type {MemCacheEntry} */
const entry = {
etag: hash,
data: cachedData
};
memCache.set(this, entry);
}
const buildHash = /** @type {string} */ (
/** @type {BuildInfo} */ (module.buildInfo).hash
);
cache.store(
module.identifier(),
hash ? `${buildHash}|${hash}` : buildHash,
cachedData,
callback
);
},
(err) => {
logger.timeEnd("store provided exports into cache");
callback(err);
}
);
}
);
}
);
/** @type {WeakMap<Module, RestoreProvidedData>} */
const providedExportsCache = new WeakMap();
compilation.hooks.rebuildModule.tap(PLUGIN_NAME, (module) => {
providedExportsCache.set(
module,
moduleGraph.getExportsInfo(module).getRestoreProvidedData()
);
});
compilation.hooks.finishRebuildingModule.tap(PLUGIN_NAME, (module) => {
moduleGraph.getExportsInfo(module).restoreProvided(
/** @type {RestoreProvidedData} */
(providedExportsCache.get(module))
);
});
});
}
}
module.exports = FlagDependencyExportsPlugin;