/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const mm = require("micromatch"); const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency"); const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency"); const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency"); /** @typedef {import("../Module")} Module */ /** @typedef {import("../Dependency")} Dependency */ /** * @typedef {Object} ExportInModule * @property {Module} module the module * @property {string} exportName the name of the export * @property {boolean} checked if the export is conditional */ /** * @typedef {Object} ReexportInfo * @property {Map} static * @property {Map>} dynamic */ /** * @param {ReexportInfo} info info object * @param {string} exportName name of export * @returns {ExportInModule | undefined} static export */ const getMappingFromInfo = (info, exportName) => { const staticMappings = info.static.get(exportName); if (staticMappings !== undefined) { if (staticMappings.length === 1) return staticMappings[0]; return undefined; } const dynamicMappings = Array.from(info.dynamic).filter( ([_, ignored]) => !ignored.has(exportName) ); if (dynamicMappings.length === 1) { return { module: dynamicMappings[0][0], exportName, checked: true }; } return undefined; }; /** * @param {ReexportInfo} info info object * @param {string} exportName name of export of source module * @param {Module} module the target module * @param {string} innerExportName name of export of target module * @param {boolean} checked true, if existence of target module is checked */ const addStaticReexport = ( info, exportName, module, innerExportName, checked ) => { let mappings = info.static.get(exportName); if (mappings !== undefined) { for (const mapping of mappings) { if (mapping.module === module && mapping.exportName === innerExportName) { mapping.checked = mapping.checked && checked; return; } } } else { mappings = []; info.static.set(exportName, mappings); } mappings.push({ module, exportName: innerExportName, checked }); }; /** * @param {ReexportInfo} info info object * @param {Module} module the reexport module * @param {Set} ignored ignore list * @returns {void} */ const addDynamicReexport = (info, module, ignored) => { const existingList = info.dynamic.get(module); if (existingList !== undefined) { for (const key of existingList) { if (!ignored.has(key)) existingList.delete(key); } } else { info.dynamic.set(module, new Set(ignored)); } }; class SideEffectsFlagPlugin { apply(compiler) { compiler.hooks.normalModuleFactory.tap("SideEffectsFlagPlugin", nmf => { nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => { const resolveData = data.resourceResolveData; if ( resolveData && resolveData.descriptionFileData && resolveData.relativePath ) { const sideEffects = resolveData.descriptionFileData.sideEffects; const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects( resolveData.relativePath, sideEffects ); if (!hasSideEffects) { module.factoryMeta.sideEffectFree = true; } } return module; }); nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => { if (data.settings.sideEffects === false) { module.factoryMeta.sideEffectFree = true; } else if (data.settings.sideEffects === true) { module.factoryMeta.sideEffectFree = false; } }); }); compiler.hooks.compilation.tap("SideEffectsFlagPlugin", compilation => { compilation.hooks.optimizeDependencies.tap( "SideEffectsFlagPlugin", modules => { /** @type {Map} */ const reexportMaps = new Map(); // Capture reexports of sideEffectFree modules for (const module of modules) { /** @type {Dependency[]} */ const removeDependencies = []; for (const dep of module.dependencies) { if (dep instanceof HarmonyImportSideEffectDependency) { if (dep.module && dep.module.factoryMeta.sideEffectFree) { removeDependencies.push(dep); } } else if ( dep instanceof HarmonyExportImportedSpecifierDependency ) { if (module.factoryMeta.sideEffectFree) { const mode = dep.getMode(true); if ( mode.type === "safe-reexport" || mode.type === "checked-reexport" || mode.type === "dynamic-reexport" || mode.type === "reexport-non-harmony-default" || mode.type === "reexport-non-harmony-default-strict" || mode.type === "reexport-named-default" ) { let info = reexportMaps.get(module); if (!info) { reexportMaps.set( module, (info = { static: new Map(), dynamic: new Map() }) ); } const targetModule = dep._module; switch (mode.type) { case "safe-reexport": for (const [key, id] of mode.map) { if (id) { addStaticReexport( info, key, targetModule, id, false ); } } break; case "checked-reexport": for (const [key, id] of mode.map) { if (id) { addStaticReexport( info, key, targetModule, id, true ); } } break; case "dynamic-reexport": addDynamicReexport(info, targetModule, mode.ignored); break; case "reexport-non-harmony-default": case "reexport-non-harmony-default-strict": case "reexport-named-default": addStaticReexport( info, mode.name, targetModule, "default", false ); break; } } } } } } // Flatten reexports for (const info of reexportMaps.values()) { const dynamicReexports = info.dynamic; info.dynamic = new Map(); for (const reexport of dynamicReexports) { let [targetModule, ignored] = reexport; for (;;) { const innerInfo = reexportMaps.get(targetModule); if (!innerInfo) break; for (const [key, reexports] of innerInfo.static) { if (ignored.has(key)) continue; for (const { module, exportName, checked } of reexports) { addStaticReexport(info, key, module, exportName, checked); } } // Follow dynamic reexport if there is only one if (innerInfo.dynamic.size !== 1) { // When there are more then one, we don't know which one break; } ignored = new Set(ignored); for (const [innerModule, innerIgnored] of innerInfo.dynamic) { for (const key of innerIgnored) { if (ignored.has(key)) continue; // This reexports ends here addStaticReexport(info, key, targetModule, key, true); ignored.add(key); } targetModule = innerModule; } } // Update reexport as all other cases has been handled addDynamicReexport(info, targetModule, ignored); } } for (const info of reexportMaps.values()) { const staticReexports = info.static; info.static = new Map(); for (const [key, reexports] of staticReexports) { for (let mapping of reexports) { for (;;) { const innerInfo = reexportMaps.get(mapping.module); if (!innerInfo) break; const newMapping = getMappingFromInfo( innerInfo, mapping.exportName ); if (!newMapping) break; mapping = newMapping; } addStaticReexport( info, key, mapping.module, mapping.exportName, mapping.checked ); } } } // Update imports along the reexports from sideEffectFree modules for (const pair of reexportMaps) { const module = pair[0]; const info = pair[1]; let newReasons = undefined; for (let i = 0; i < module.reasons.length; i++) { const reason = module.reasons[i]; const dep = reason.dependency; if ( (dep instanceof HarmonyExportImportedSpecifierDependency || (dep instanceof HarmonyImportSpecifierDependency && !dep.namespaceObjectAsContext)) && dep._id ) { const mapping = getMappingFromInfo(info, dep._id); if (mapping) { dep.redirectedModule = mapping.module; dep.redirectedId = mapping.exportName; mapping.module.addReason( reason.module, dep, reason.explanation ? reason.explanation + " (skipped side-effect-free modules)" : "(skipped side-effect-free modules)" ); // removing the currect reason, by not adding it to the newReasons array // lazily create the newReasons array if (newReasons === undefined) { newReasons = i === 0 ? [] : module.reasons.slice(0, i); } continue; } } if (newReasons !== undefined) newReasons.push(reason); } if (newReasons !== undefined) { module.reasons = newReasons; } } } ); }); } static moduleHasSideEffects(moduleName, flagValue) { switch (typeof flagValue) { case "undefined": return true; case "boolean": return flagValue; case "string": if (process.platform === "win32") { flagValue = flagValue.replace(/\\/g, "/"); } return mm.isMatch(moduleName, flagValue, { matchBase: true }); case "object": return flagValue.some(glob => SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob) ); } } } module.exports = SideEffectsFlagPlugin;