UNPKG

react-monaco-editor

Version:
520 lines (503 loc) 16.8 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const crypto = require("crypto"); const SortableSet = require("../util/SortableSet"); const GraphHelpers = require("../GraphHelpers"); const { isSubset } = require("../util/SetHelpers"); const hashFilename = name => { return crypto .createHash("md4") .update(name) .digest("hex") .slice(0, 8); }; const sortByIdentifier = (a, b) => { if (a.identifier() > b.identifier()) return 1; if (a.identifier() < b.identifier()) return -1; return 0; }; const getRequests = chunk => { let requests = 0; for (const chunkGroup of chunk.groupsIterable) { requests = Math.max(requests, chunkGroup.chunks.length); } return requests; }; const getModulesSize = modules => { let sum = 0; for (const m of modules) sum += m.size(); return sum; }; const isOverlap = (a, b) => { for (const item of a.keys()) { if (b.has(item)) return true; } return false; }; const compareEntries = (a, b) => { // 1. by priority const diffPriority = a.cacheGroup.priority - b.cacheGroup.priority; if (diffPriority) return diffPriority; // 2. by number of chunks const diffCount = a.chunks.size - b.chunks.size; if (diffCount) return diffCount; // 3. by size reduction const aSizeReduce = a.size * (a.chunks.size - 1); const bSizeReduce = b.size * (b.chunks.size - 1); const diffSizeReduce = aSizeReduce - bSizeReduce; if (diffSizeReduce) return diffSizeReduce; // 4. by number of modules (to be able to compare by identifier) const modulesA = a.modules; const modulesB = b.modules; const diff = modulesA.size - modulesB.size; if (diff) return diff; // 5. by module identifiers modulesA.sort(); modulesB.sort(); const aI = modulesA[Symbol.iterator](); const bI = modulesB[Symbol.iterator](); // eslint-disable-next-line no-constant-condition while (true) { const aItem = aI.next(); const bItem = bI.next(); if (aItem.done) return 0; const aModuleIdentifier = aItem.value.identifier(); const bModuleIdentifier = bItem.value.identifier(); if (aModuleIdentifier > bModuleIdentifier) return -1; if (aModuleIdentifier < bModuleIdentifier) return 1; } }; module.exports = class SplitChunksPlugin { constructor(options) { this.options = SplitChunksPlugin.normalizeOptions(options); } static normalizeOptions(options = {}) { return { chunksFilter: SplitChunksPlugin.normalizeChunksFilter( options.chunks || "all" ), minSize: options.minSize || 0, minChunks: options.minChunks || 1, maxAsyncRequests: options.maxAsyncRequests || 1, maxInitialRequests: options.maxInitialRequests || 1, getName: SplitChunksPlugin.normalizeName({ name: options.name, automaticNameDelimiter: options.automaticNameDelimiter }) || (() => {}), filename: options.filename || undefined, getCacheGroups: SplitChunksPlugin.normalizeCacheGroups({ cacheGroups: options.cacheGroups, automaticNameDelimiter: options.automaticNameDelimiter }) }; } static normalizeName({ name, automaticNameDelimiter }) { if (name === true) { const fn = (module, chunks, cacheGroup) => { const names = chunks.map(c => c.name); if (!names.every(Boolean)) return; names.sort(); let name = (cacheGroup && cacheGroup !== "default" ? cacheGroup + automaticNameDelimiter : "") + names.join(automaticNameDelimiter); // Filenames and paths can't be too long otherwise an // ENAMETOOLONG error is raised. If the generated name if too // long, it is truncated and a hash is appended. The limit has // been set to 100 to prevent `[name].[chunkhash].[ext]` from // generating a 256+ character string. if (name.length > 100) { name = name.slice(0, 100) + automaticNameDelimiter + hashFilename(name); } return name; }; return fn; } if (typeof name === "string") { const fn = () => { return name; }; return fn; } if (typeof name === "function") return name; } static normalizeChunksFilter(chunks) { if (chunks === "initial") { return chunk => chunk.canBeInitial(); } if (chunks === "async") { return chunk => !chunk.canBeInitial(); } if (chunks === "all") { return () => true; } if (typeof chunks === "function") return chunks; } static normalizeCacheGroups({ cacheGroups, automaticNameDelimiter }) { if (typeof cacheGroups === "function") { return cacheGroups; } if (cacheGroups && typeof cacheGroups === "object") { const fn = (module, chunks) => { let results; for (const key of Object.keys(cacheGroups)) { let option = cacheGroups[key]; if (option === false) continue; if (option instanceof RegExp || typeof option === "string") { option = { test: option }; } if (typeof option === "function") { let result = option(module); if (result) { if (results === undefined) results = []; for (const r of Array.isArray(result) ? result : [result]) { const result = Object.assign( { key }, r ); if (result.name) result.getName = () => result.name; if (result.chunks) { result.chunksFilter = SplitChunksPlugin.normalizeChunksFilter( result.chunks ); } results.push(result); } } } else if (SplitChunksPlugin.checkTest(option.test, module, chunks)) { if (results === undefined) results = []; results.push({ key: key, priority: option.priority, getName: SplitChunksPlugin.normalizeName({ name: option.name, automaticNameDelimiter }), chunksFilter: SplitChunksPlugin.normalizeChunksFilter( option.chunks ), enforce: option.enforce, minSize: option.minSize, minChunks: option.minChunks, maxAsyncRequests: option.maxAsyncRequests, maxInitialRequests: option.maxInitialRequests, filename: option.filename, reuseExistingChunk: option.reuseExistingChunk }); } } return results; }; return fn; } const fn = () => {}; return fn; } static checkTest(test, module, chunks) { if (test === undefined) return true; if (typeof test === "function") return test(module, chunks); if (typeof test === "boolean") return test; const names = chunks .map(c => c.name) .concat(module.nameForCondition ? [module.nameForCondition()] : []) .filter(Boolean); if (typeof test === "string") { for (const name of names) if (name.startsWith(test)) return true; return false; } if (test instanceof RegExp) { for (const name of names) if (test.test(name)) return true; return false; } return false; } apply(compiler) { compiler.hooks.thisCompilation.tap("SplitChunksPlugin", compilation => { let alreadyOptimized = false; compilation.hooks.unseal.tap("SplitChunksPlugin", () => { alreadyOptimized = false; }); compilation.hooks.optimizeChunksAdvanced.tap( "SplitChunksPlugin", chunks => { if (alreadyOptimized) return; alreadyOptimized = true; // Give each selected chunk an index (to create strings from chunks) const indexMap = new Map(); let index = 1; for (const chunk of chunks) { indexMap.set(chunk, index++); } const getKey = chunks => { return Array.from(chunks, c => indexMap.get(c)) .sort() .join(); }; // Create a list of possible combinations const chunkSetsInGraph = new Map(); // Map<string, Set<Chunk>> for (const module of compilation.modules) { const chunkIndices = getKey(module.chunksIterable); chunkSetsInGraph.set(chunkIndices, new Set(module.chunksIterable)); } const combinations = new Map(); // Map<string, Set<Chunk>[]> for (const [key, chunksSet] of chunkSetsInGraph) { var array = []; for (const set of chunkSetsInGraph.values()) { if (isSubset(chunksSet, set)) { array.push(set); } } combinations.set(key, array); } // Map a list of chunks to a list of modules // For the key the chunk "index" is used, the value is a SortableSet of modules const chunksInfoMap = new Map(); // Walk through all modules for (const module of compilation.modules) { // Get array of chunks const chunks = module.getChunks(); // Get cache group let cacheGroups = this.options.getCacheGroups(module, chunks); if (!Array.isArray(cacheGroups)) continue; for (const cacheGroupSource of cacheGroups) { const cacheGroup = { key: cacheGroupSource.key, priority: cacheGroupSource.priority || 0, chunksFilter: cacheGroupSource.chunksFilter || this.options.chunksFilter, minSize: cacheGroupSource.minSize !== undefined ? cacheGroupSource.minSize : cacheGroupSource.enforce ? 0 : this.options.minSize, minChunks: cacheGroupSource.minChunks !== undefined ? cacheGroupSource.minChunks : cacheGroupSource.enforce ? 1 : this.options.minChunks, maxAsyncRequests: cacheGroupSource.maxAsyncRequests !== undefined ? cacheGroupSource.maxAsyncRequests : cacheGroupSource.enforce ? Infinity : this.options.maxAsyncRequests, maxInitialRequests: cacheGroupSource.maxInitialRequests !== undefined ? cacheGroupSource.maxInitialRequests : cacheGroupSource.enforce ? Infinity : this.options.maxInitialRequests, getName: cacheGroupSource.getName !== undefined ? cacheGroupSource.getName : this.options.getName, filename: cacheGroupSource.filename !== undefined ? cacheGroupSource.filename : this.options.filename, reuseExistingChunk: cacheGroupSource.reuseExistingChunk }; // For all combination of chunk selection for (const chunkCombination of combinations.get(getKey(chunks))) { // Get indices of chunks in which this module occurs const chunkIndices = Array.from(chunkCombination, chunk => indexMap.get(chunk) ); // Break if minimum number of chunks is not reached if (chunkIndices.length < cacheGroup.minChunks) continue; // Select chunks by configuration const selectedChunks = Array.from(chunkCombination).filter( cacheGroup.chunksFilter ); // Break if minimum number of chunks is not reached if (selectedChunks.length < cacheGroup.minChunks) continue; // Determine name for split chunk const name = cacheGroup.getName( module, selectedChunks, cacheGroup.key ); // Create key for maps // When it has a name we use the name as key // Elsewise we create the key from chunks and cache group key // This automatically merges equal names const chunksKey = getKey(selectedChunks); const key = (name && `name:${name}`) || `chunks:${chunksKey} key:${cacheGroup.key}`; // Add module to maps let info = chunksInfoMap.get(key); if (info === undefined) { chunksInfoMap.set( key, (info = { modules: new SortableSet(undefined, sortByIdentifier), cacheGroup, name, chunks: new Map(), reusedableChunks: new Set(), chunksKeys: new Set() }) ); } info.modules.add(module); if (!info.chunksKeys.has(chunksKey)) { info.chunksKeys.add(chunksKey); for (const chunk of selectedChunks) { info.chunks.set(chunk, chunk.getNumberOfModules()); } } } } } for (const [key, info] of chunksInfoMap) { // Get size of module lists info.size = getModulesSize(info.modules); if (info.size < info.cacheGroup.minSize) { chunksInfoMap.delete(key); } } let changed = false; while (chunksInfoMap.size > 0) { // Find best matching entry let bestEntryKey; let bestEntry; for (const pair of chunksInfoMap) { const key = pair[0]; const info = pair[1]; if (bestEntry === undefined) { bestEntry = info; bestEntryKey = key; } else if (compareEntries(bestEntry, info) < 0) { bestEntry = info; bestEntryKey = key; } } const item = bestEntry; chunksInfoMap.delete(bestEntryKey); let chunkName = item.name; // Variable for the new chunk (lazy created) let newChunk; // When no chunk name, check if we can reuse a chunk instead of creating a new one let isReused = false; if (item.cacheGroup.reuseExistingChunk) { for (const pair of item.chunks) { if (pair[1] === item.modules.size) { const chunk = pair[0]; if (chunk.hasEntryModule()) continue; if (!newChunk || !newChunk.name) newChunk = chunk; else if ( chunk.name && chunk.name.length < newChunk.name.length ) newChunk = chunk; else if ( chunk.name && chunk.name.length === newChunk.name.length && chunk.name < newChunk.name ) newChunk = chunk; chunkName = undefined; isReused = true; } } } // Walk through all chunks for (const chunk of item.chunks.keys()) { // skip if we address ourself if (chunk.name === chunkName || chunk === newChunk) continue; // respect max requests when not enforced const maxRequests = chunk.isOnlyInitial() ? item.cacheGroup.maxInitialRequests : chunk.canBeInitial() ? Math.min( item.cacheGroup.maxInitialRequests, item.cacheGroup.maxAsyncRequests ) : item.cacheGroup.maxAsyncRequests; if (isFinite(maxRequests) && getRequests(chunk) >= maxRequests) continue; if (newChunk === undefined) { // Create the new chunk newChunk = compilation.addChunk(chunkName); } // Add graph connections for splitted chunk chunk.split(newChunk); // Remove all selected modules from the chunk for (const module of item.modules) { chunk.removeModule(module); module.rewriteChunkInReasons(chunk, [newChunk]); } } // If we successfully created a new chunk or reused one if (newChunk) { // Add a note to the chunk newChunk.chunkReason = isReused ? "reused as split chunk" : "split chunk"; if (item.cacheGroup.key) { newChunk.chunkReason += ` (cache group: ${ item.cacheGroup.key })`; } if (chunkName) { newChunk.chunkReason += ` (name: ${chunkName})`; // If the chosen name is already an entry point we remove the entry point const entrypoint = compilation.entrypoints.get(chunkName); if (entrypoint) { compilation.entrypoints.delete(chunkName); entrypoint.remove(); newChunk.entryModule = undefined; } } if (item.cacheGroup.filename) { if (!newChunk.isOnlyInitial()) { throw new Error( "SplitChunksPlugin: You are trying to set a filename for a chunk which is (also) loaded on demand. " + "The runtime can only handle loading of chunks which match the chunkFilename schema. " + "Using a custom filename would fail at runtime. " + `(cache group: ${item.cacheGroup.key})` ); } newChunk.filenameTemplate = item.cacheGroup.filename; } if (!isReused) { // Add all modules to the new chunk for (const module of item.modules) { GraphHelpers.connectChunkAndModule(newChunk, module); } } // remove all modules from other entries and update size for (const [key, info] of chunksInfoMap) { if (isOverlap(info.chunks, item.chunks)) { const oldSize = info.modules.size; for (const module of item.modules) { info.modules.delete(module); } if (info.modules.size === 0) { chunksInfoMap.delete(key); continue; } if (info.modules.size !== oldSize) { info.size = getModulesSize(info.modules); if (info.size < info.cacheGroup.minSize) chunksInfoMap.delete(key); } } } changed = true; } } if (changed) return true; } ); }); } };