UNPKG

webpack-split-plugin

Version:
585 lines (523 loc) 18.3 kB
const log = require('fliplog') const Cleaner = require('./cleaner') const CollectionManager = require('./CollectionManager') const collection = new CollectionManager() let nextIdent = 0 class WebpackSplitPlugin { constructor(options) { this.shouldDebug = options.debug || false collection.debug(options.debug || false) if (options.totalSize) { let totalSize = options.totalSize if (typeof totalSize === 'string') { if (totalSize.includes('kb')) { totalSize = totalSize.split('kb').shift() * 1000 } else if (totalSize.includes('mb')) { totalSize = totalSize.split('mb').shift() * 10000 } } collection.totalSize(totalSize) } const normalizedOptions = this.normalizeOptions(options) this.chunkNames = normalizedOptions.chunkNames this.filenameTemplate = normalizedOptions.filenameTemplate this.minChunks = normalizedOptions.minChunks this.selectedChunks = normalizedOptions.selectedChunks this.children = normalizedOptions.children this.async = normalizedOptions.async this.minSize = normalizedOptions.minSize this.ident = __filename + nextIdent++ } // @TODO clean this up, see debug normalizeOptions(options) { if (Array.isArray(options)) { return { chunkNames: options, } } if (typeof options === 'string') { return { chunkNames: [options], } } // options.children and options.chunk may not be used together if (options.children && options.chunks) { throw new Error( 'You can\'t and it does not make any sense to use "children" and "chunk" options together.' ) } /** * options.async and options.filename are also not possible together * as filename specifies how the chunk is called but "async" implies * that webpack will take care of loading this file. */ if (options.async && options.filename) { throw new Error( `You can not specify a filename if you use the \"async\" option. You can however specify the name of the async chunk by passing the desired string as the \"async\" option.` ) } /** * Make sure this is either an array or undefined. * "name" can be a string and * "names" a string or an array */ const chunkNames = options.name || options.names ? [].concat(options.name || options.names) : undefined return { chunkNames: chunkNames, filenameTemplate: options.filename, minChunks: options.minChunks, selectedChunks: options.chunks, children: options.children, async: options.async, minSize: options.minSize, } } apply(compiler) { compiler.plugin('this-compilation', compilation => { // this will happen for each file compilation.plugin(['build-module'], module => { collection.handle(module) }) compilation.plugin( ['optimize-chunks', 'optimize-extracted-chunks'], chunks => { // only optimize once if (compilation[this.ident]) return compilation[this.ident] = true collection.complete() /** * Creates a list of "common"" chunks based on the options. * The list is made up of preexisting or newly created chunks. * - If chunk has the name as specified in the chunkNames it is put in the list * - If no chunk with the name as given in chunkNames exists a new chunk is created and added to the list * * These chunks are the "targets" for extracted modules. */ const targetChunks = this.getTargetChunks( chunks, compilation, this.chunkNames, this.children, this.async ) // log.bold('TARGETCHUNKS - CHUNKHACK - GETTARGETCHUNKS').echo() // this.debug(targetChunks) // @example this is only `noop` // iterate over all our new chunks targetChunks.forEach((targetChunk, idx) => { // this.debug({ targetChunk, idx }) log.data({ idx }).echo() /** * These chunks are subject to get "common" modules extracted and moved to the common chunk */ const affectedChunks = this.getAffectedChunks( compilation, chunks, targetChunk, targetChunks, idx, this.selectedChunks, this.async, this.children ) // THIS IS ONLY INDEX, NOT NOOP // this.debug(affectedChunks) // bail if no chunk is affected if (!affectedChunks) { return } // If we are async create an async chunk now // override the "commonChunk" with the newly created async one and use it as commonChunk from now on let asyncChunk if (this.async) { asyncChunk = this.createAsyncChunk( compilation, this.async, targetChunk ) targetChunk = asyncChunk } // @NOTE ------ MODIFIED THIS /** * Check which modules are "common" and could be extracted to a "common" chunk */ // const extractableModules = this.getExtractableModules( // this.minChunks, // affectedChunks, // targetChunk // ) const extractableModules = this.getModulesMagic(idx) // If the minSize option is set check if the size extracted from the chunk is reached // else bail out here. // As all modules/commons are interlinked with each other, common modules would be extracted // if we reach this mark at a later common chunk. (quirky I guess). if (this.minSize) { const modulesSize = this.calculateModulesSize(extractableModules) // if too small, bail if (modulesSize < this.minSize) return } // Remove modules that are moved to commons chunk from their original chunks // return all chunks that are affected by having modules removed - we need them later (apparently) const chunksWithExtractedModules = this.extractModulesAndReturnAffectedChunks( extractableModules, affectedChunks ) // log // .bold( // '\nBEFORE ADDING EXTRACTED MODULES - CHUNKHACK - addExtractedModulesToTargetChunk' // ) // .echo() // this.debug(targetChunk) // connect all extracted modules with the common chunk this.addExtractedModulesToTargetChunk( targetChunk, extractableModules ) // set filenameTemplate for chunk if (this.filenameTemplate) targetChunk.filenameTemplate = this.filenameTemplate // log // .bold( // '\AFTER ADDING EXTRACTED MODULES - CHUNKHACK - addExtractedModulesToTargetChunk' // ) // .echo() // log.quick(targetChunk) // this.debug(targetChunk) // if we are async connect the blocks of the "reallyUsedChunk" - the ones that had modules removed - // with the commonChunk and get the origins for the asyncChunk (remember "asyncChunk === commonChunk" at this moment). // bail out if (this.async) { this.moveExtractedChunkBlocksToTargetChunk( chunksWithExtractedModules, targetChunk ) asyncChunk.origins = this.extractOriginsOfChunksWithExtractedModules( chunksWithExtractedModules ) return } // 0 // this.debug(affectedChunks.length) // we are not in "async" mode // connect used chunks with commonChunk - shouldnt this be reallyUsedChunks here? this.makeTargetChunkParentOfAffectedChunks( affectedChunks, targetChunk ) // target chunk is `noop` // affectedChunks is ONLY OTHER CHUNKS (ENTRY) // JUST INDEX, AND INDEX HAS NOOP IN IT // // log.bold('\n MAKING TARGET PARENT!!! ') // this.debug(targetChunk) // log // .bold('\n affectedChunks!!! ') // .data(`- CHUNKHACK - makeTargetChunkParentOfAffectedChunks`) // .echo() // this.debug(affectedChunks) }) return true } ) }) } // - [x] getTargetChunks(allChunks, compilation, chunkNames, children, asyncOption) { const asyncOrNoSelectedChunk = children || asyncOption // we have specified chunk names if (chunkNames) { // map chunks by chunkName for quick access const allChunksNameMap = allChunks.reduce((map, chunk) => { if (chunk.name) { map.set(chunk.name, chunk) } return map }, new Map()) // Ensure we have a chunk per specified chunk name. // Reuse existing chunks if possible return chunkNames.map(chunkName => { if (allChunksNameMap.has(chunkName)) { return allChunksNameMap.get(chunkName) } // add the filtered chunks to the compilation return compilation.addChunk(chunkName) }) } // we dont have named chunks specified, so we just take all of them if (asyncOrNoSelectedChunk) { return allChunks.filter(chunk => !chunk.isInitial()) } /** * No chunk name(s) was specified nor is this an async/children commons chunk */ throw new Error( `You did not specify any valid target chunk settings. Take a look at the "name"/"names" or async/children option.` ) } getAffectedChunks( compilation, allChunks, targetChunk, targetChunks, currentIndex, selectedChunks, asyncOption, children ) { const asyncOrNoSelectedChunk = children || asyncOption if (Array.isArray(selectedChunks)) { return allChunks.filter(chunk => { const notCommmonChunk = chunk !== targetChunk const isSelectedChunk = selectedChunks.indexOf(chunk.name) > -1 return notCommmonChunk && isSelectedChunk }) } if (asyncOrNoSelectedChunk) { // nothing to do here if (!targetChunk.chunks) { return [] } return targetChunk.chunks.filter(chunk => { // we can only move modules from this chunk if the "commonChunk" is the only parent return asyncOption || chunk.parents.length === 1 }) } /** * past this point only entry chunks are allowed to become commonChunks */ if (targetChunk.parents.length > 0) { compilation.errors.push( new Error( "WebpackSplitPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + targetChunk.name + ')' ) ) return } /** * If we find a "targetchunk" that is also a normal chunk (meaning it is probably specified as an entry) * and the current target chunk comes after that and the found chunk has a runtime* * make that chunk be an 'affected' chunk of the current target chunk. * * To understand what that means take a look at the "examples/chunkhash", this basically will * result in the runtime to be extracted to the current target chunk. * * *runtime: the "runtime" is the "webpack"-block you may have seen in the bundles that resolves modules etc. */ return allChunks.filter(chunk => { const found = targetChunks.indexOf(chunk) if (found >= currentIndex) return false return chunk.hasRuntime() }) } createAsyncChunk(compilation, asyncOption, targetChunk) { const asyncChunk = compilation.addChunk( typeof asyncOption === 'string' ? asyncOption : undefined ) asyncChunk.chunkReason = 'async commons chunk' asyncChunk.extraAsync = true asyncChunk.addParent(targetChunk) targetChunk.addChunk(asyncChunk) return asyncChunk } // If minChunks is a function use that // otherwhise check if a module is used at least minChunks or 2 or usedChunks.length time getModuleFilter(minChunks, targetChunk, usedChunksLength) { if (typeof minChunks === 'function') { return minChunks } const minCount = minChunks || Math.max(2, usedChunksLength) const isUsedAtLeastMinTimes = (module, count) => count >= minCount return isUsedAtLeastMinTimes } getExtractableModules(minChunks, usedChunks, targetChunk) { if (minChunks === Infinity) { return [] } // count how many chunks contain a module const commonModulesToCountMap = usedChunks.reduce((map, chunk) => { for (let module of chunk.modules) { const count = map.has(module) ? map.get(module) : 0 map.set(module, count + 1) } return map }, new Map()) // filter by minChunks const moduleFilterCount = this.getModuleFilter( minChunks, targetChunk, usedChunks.length ) // filter by condition const moduleFilterCondition = (module, chunk) => { if (!module.chunkCondition) { return true } return module.chunkCondition(chunk) } return Array.from(commonModulesToCountMap) .filter(entry => { const module = entry[0] const count = entry[1] // if the module passes both filters, keep it. return ( moduleFilterCount(module, count) && moduleFilterCondition(module, targetChunk) ) }) .map(entry => entry[0]) } calculateModulesSize(modules) { return modules.reduce((totalSize, module) => totalSize + module.size(), 0) } addExtractedModulesToTargetChunk(chunk, modules) { for (let module of modules) { chunk.addModule(module) module.addChunk(chunk) } } // ******************************************** // @important ********************************* // ******************************************** /** * @NOTE after changing it around, this is unused in the CommonChunk hack * because it * * @desc * this is so we can remove grouped chunks from entry point * map groups to get back modules * map modules to userRequest, to simplify filtering entries * flatten * @param {[type]} index * @return {[type]} */ getFileNamesMagic(index) { const groups = collection.getCollections() return [].concat(...groups.map(group => group.map(file => file.filename)))[ index ] } /** * @desc return collection of modules we gathered during module build * using the index * @param {number} index * @return {Array<WebpackModule>} */ getModulesMagic(index) { const groups = collection.getCollections() return groups[index].map(file => file.module) } // - [x] extractModulesAndReturnAffectedChunks(reallyUsedModules, usedChunks) { return reallyUsedModules.reduce((affectedChunksSet, module) => { for (let chunk of usedChunks) { // removeChunk returns true if the chunk was contained and succesfully removed // false if the module did not have a connection to the chunk in question if (module.removeChunk(chunk)) { affectedChunksSet.add(chunk) } } return affectedChunksSet }, new Set()) } // - [ ] // confusingly named params // is `affectedChunks` & `targetChunk` makeTargetChunkParentOfAffectedChunks(usedChunks, commonChunk) { // log // .dim('it inserts chunk with parents...') // .verbose(4) // .data({ parents: chunk.parents, entrypoints: chunk.entrypoints }) // .echo() for (let chunk of usedChunks) { // set commonChunk as new sole parent chunk.parents = [commonChunk] // add chunk to commonChunk commonChunk.addChunk(chunk) for (let entrypoint of chunk.entrypoints) { entrypoint.insertChunk(commonChunk, chunk) } // log // .verbose(5) // .data({ parents: chunk.parents, entrypoints: chunk.entrypoints }) // .echo() } } // ******************************************** // @async ************************************* // ******************************************** // - [ ] moveExtractedChunkBlocksToTargetChunk(chunks, targetChunk) { for (let chunk of chunks) { for (let block of chunk.blocks) { block.chunks.unshift(targetChunk) targetChunk.addBlock(block) } } } // - [ ] extractOriginsOfChunksWithExtractedModules(chunks) { const origins = [] for (let chunk of chunks) { for (let origin of chunk.origins) { const newOrigin = Object.create(origin) newOrigin.reasons = (origin.reasons || []).concat('async commons') origins.push(newOrigin) } } return origins } // ******************************************** // @mine ********************************* // ******************************************** debug(chunks) { // this is blacklist, we also want to do whitelist // we'd like to keep `module`, `chunk`, `parnet` const cleanedChunks = Cleaner.init(chunks) // .debug() .keys([ /parser/, /loc/, /range/, /dependencies/, /dependenciesWarnings/, /strict/, /debug/, /loaders/, /assets/, /meta/, /warnings/, /used/, /rawRequest/, /resource/, /contextDependencies/, /_source/, /built/, /cachedSource/, /issuer/, /index/, /index2/, /id/, /portableId/, /lastId/, /cacheable/, /building/, /depth/, /buildTimestamp/, /optional/, /errors/, /variables/, /rendered/, /error/, /__NormalModuleFactoryCache/, ]) .clean() log.verbose(100).data({ cleanedChunks }).exit() } } module.exports = WebpackSplitPlugin