2017-05-10 19:15:14 +08:00
/ *
MIT License http : //www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @ sokra
* /
"use strict" ;
const HarmonyImportDependency = require ( "../dependencies/HarmonyImportDependency" ) ;
const ConcatenatedModule = require ( "./ConcatenatedModule" ) ;
const HarmonyExportImportedSpecifierDependency = require ( "../dependencies/HarmonyExportImportedSpecifierDependency" ) ;
class ModuleConcatenationPlugin {
constructor ( options ) {
if ( typeof options !== "object" ) options = { } ;
this . options = options ;
}
apply ( compiler ) {
compiler . plugin ( "compilation" , ( compilation , params ) => {
params . normalModuleFactory . plugin ( "parser" , ( parser , parserOptions ) => {
parser . plugin ( "call eval" , ( ) => {
parser . state . module . meta . hasEval = true ;
} ) ;
} ) ;
2017-05-28 21:25:07 +08:00
const bailoutReasonMap = new Map ( ) ;
function setBailoutReason ( module , reason ) {
bailoutReasonMap . set ( module , reason ) ;
module . optimizationBailout . push ( reason ) ;
}
function getBailoutReason ( module , requestShortener ) {
const reason = bailoutReasonMap . get ( module ) ;
if ( typeof reason === "function" ) return reason ( requestShortener ) ;
return reason ;
}
2017-05-10 19:15:14 +08:00
compilation . plugin ( "optimize-chunk-modules" , ( chunks , modules ) => {
chunks . forEach ( chunk => {
2017-05-28 21:25:07 +08:00
const relevantModules = [ ] ;
const possibleInners = new Set ( ) ;
for ( const module of chunk . modulesIterable ) {
// Only harmony modules are valid for optimization
if ( ! module . meta || ! module . meta . harmonyModule ) {
continue ;
}
2017-05-10 19:15:14 +08:00
// Module must not be in other chunks
// TODO add an option to allow module to be in other entry points
2017-05-28 21:25:07 +08:00
if ( module . getNumberOfChunks ( ) !== 1 ) {
setBailoutReason ( module , "ModuleConcatenation: module is in multiple chunks" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
// Because of variable renaming we can't use modules with eval
2017-05-28 21:25:07 +08:00
if ( module . meta && module . meta . hasEval ) {
setBailoutReason ( module , "ModuleConcatenation: eval is used in the module" ) ;
continue ;
}
relevantModules . push ( module ) ;
2017-05-10 19:15:14 +08:00
// Module must not be the entry points
2017-05-28 21:25:07 +08:00
if ( chunk . entryModule === module ) {
setBailoutReason ( module , "ModuleConcatenation (inner): module is an entrypoint" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
// Exports must be known (and not dynamic)
2017-05-28 21:25:07 +08:00
if ( ! Array . isArray ( module . providedExports ) ) {
setBailoutReason ( module , "ModuleConcatenation (inner): exports are not known" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
// Using dependency variables is not possible as this wraps the code in a function
2017-05-28 21:25:07 +08:00
if ( module . variables . length > 0 ) {
setBailoutReason ( module , "ModuleConcatenation (inner): dependency variables are used (i. e. ProvidePlugin)" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
// Module must only be used by Harmony Imports
2017-05-28 21:25:07 +08:00
const nonHarmonyReasons = module . reasons . filter ( reason => ! ( reason . dependency instanceof HarmonyImportDependency ) ) ;
if ( nonHarmonyReasons . length > 0 ) {
const importingModules = new Set ( nonHarmonyReasons . map ( r => r . module ) ) ;
setBailoutReason ( module , ( requestShortener ) => {
const names = Array . from ( importingModules ) . map ( m => m . readableIdentifier ( requestShortener ) ) ;
return ` ModuleConcatenation (inner): module is used with non-harmony imports from ${ names . join ( ", " ) } ` ;
} ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
2017-05-28 21:25:07 +08:00
possibleInners . add ( module ) ;
}
// sort by depth
// modules with lower depth are more likly suited as roots
// this improves performance, because modules already selected as inner are skipped
relevantModules . sort ( ( a , b ) => {
return a . depth - b . depth ;
} ) ;
2017-05-10 19:15:14 +08:00
const concatConfigurations = [ ] ;
2017-05-28 21:25:07 +08:00
const usedAsInner = new Set ( ) ;
2017-05-21 13:14:10 +08:00
for ( const currentRoot of relevantModules ) {
2017-05-28 21:25:07 +08:00
// when used by another configuration as inner:
// the other configuration is better and we can skip this one
if ( usedAsInner . has ( currentRoot ) )
continue ;
// create a configuration with the root
2017-05-10 19:15:14 +08:00
const currentConfiguration = new ConcatConfiguration ( currentRoot ) ;
2017-05-28 21:25:07 +08:00
// cache failures to add modules
const failureCache = new Map ( ) ;
// try to add all imports
2017-05-21 12:56:24 +08:00
for ( const imp of this . getImports ( currentRoot ) ) {
2017-05-28 21:25:07 +08:00
const problem = this . tryToAdd ( currentConfiguration , imp , possibleInners , failureCache ) ;
if ( problem ) {
failureCache . set ( imp , problem ) ;
currentConfiguration . addWarning ( imp , problem ) ;
}
2017-05-10 19:15:14 +08:00
}
2017-05-28 21:25:07 +08:00
if ( ! currentConfiguration . isEmpty ( ) ) {
2017-05-10 19:15:14 +08:00
concatConfigurations . push ( currentConfiguration ) ;
2017-05-28 21:25:07 +08:00
for ( const module of currentConfiguration . modules ) {
if ( module !== currentConfiguration . rootModule )
usedAsInner . add ( module ) ;
}
}
2017-05-10 19:15:14 +08:00
}
2017-05-21 13:14:10 +08:00
// HACK: Sort configurations by length and start with the longest one
// to get the biggers groups possible. Used modules are marked with usedModules
// TODO: Allow to reuse existing configuration while trying to add dependencies.
// This would improve performance. O(n^2) -> O(n)
2017-05-10 19:15:14 +08:00
concatConfigurations . sort ( ( a , b ) => {
return b . modules . size - a . modules . size ;
} ) ;
const usedModules = new Set ( ) ;
2017-05-21 13:14:10 +08:00
for ( const concatConfiguration of concatConfigurations ) {
if ( usedModules . has ( concatConfiguration . rootModule ) )
2017-05-10 19:15:14 +08:00
continue ;
const orderedModules = new Set ( ) ;
2017-05-21 13:14:10 +08:00
this . addInOrder ( concatConfiguration . rootModule , concatConfiguration . modules , orderedModules ) ;
const newModule = new ConcatenatedModule ( concatConfiguration . rootModule , Array . from ( orderedModules ) ) ;
2017-05-28 21:25:07 +08:00
for ( const warning of concatConfiguration . warnings ) {
newModule . optimizationBailout . push ( ( requestShortener ) => {
const reason = getBailoutReason ( warning [ 0 ] , requestShortener ) ;
const reasonPrefix = reason ? ` : ${ reason } ` : "" ;
if ( warning [ 0 ] === warning [ 1 ] )
return ` ModuleConcatenation: Cannot concat with ${ warning [ 0 ] . readableIdentifier ( requestShortener ) } ${ reasonPrefix } ` ;
else
return ` ModuleConcatenation: Cannot concat with ${ warning [ 0 ] . readableIdentifier ( requestShortener ) } because of ${ warning [ 1 ] . readableIdentifier ( requestShortener ) } ${ reasonPrefix } ` ;
} ) ;
}
2017-05-10 19:15:14 +08:00
for ( const m of orderedModules ) {
usedModules . add ( m ) ;
chunk . removeModule ( m ) ;
}
chunk . addModule ( newModule ) ;
compilation . modules . push ( newModule ) ;
2017-05-21 13:14:10 +08:00
if ( chunk . entryModule === concatConfiguration . rootModule )
2017-05-10 19:15:14 +08:00
chunk . entryModule = newModule ;
2017-05-28 21:25:07 +08:00
newModule . reasons . forEach ( reason => reason . dependency . module = newModule ) ;
2017-05-28 21:29:26 +08:00
newModule . dependencies . forEach ( dep => {
if ( dep . module ) {
dep . module . reasons . forEach ( reason => {
if ( reason . dependency === dep )
reason . module = newModule ;
} ) ;
}
} ) ;
2017-05-10 19:15:14 +08:00
}
2017-05-24 17:22:42 +08:00
compilation . modules = compilation . modules . filter ( m => ! usedModules . has ( m ) ) ;
2017-05-10 19:15:14 +08:00
} ) ;
} ) ;
} ) ;
}
getImports ( module ) {
return Array . from ( new Set ( module . dependencies
// Only harmony Dependencies
. filter ( dep => dep instanceof HarmonyImportDependency && dep . module )
// Dependencies are simple enough to concat them
. filter ( dep => {
return ! module . dependencies . some ( d =>
d instanceof HarmonyExportImportedSpecifierDependency &&
d . importDependency === dep &&
! d . id &&
! Array . isArray ( dep . module . providedExports )
) ;
} )
// Take the imported module
. map ( dep => dep . module )
) ) ;
}
2017-05-28 21:25:07 +08:00
tryToAdd ( config , module , possibleModules , failureCache ) {
const cacheEntry = failureCache . get ( module ) ;
if ( cacheEntry ) {
return cacheEntry ;
}
2017-05-10 19:15:14 +08:00
// Already added?
if ( config . has ( module ) ) {
2017-05-28 21:25:07 +08:00
return null ;
2017-05-10 19:15:14 +08:00
}
// Not possible to add?
if ( ! possibleModules . has ( module ) ) {
2017-05-28 21:25:07 +08:00
return module ;
2017-05-10 19:15:14 +08:00
}
// Clone config to make experimental changes
const testConfig = config . clone ( ) ;
// Add the module
testConfig . add ( module ) ;
// Every module which depends on the added module must be in the configuration too.
for ( const reason of module . reasons ) {
2017-05-28 21:25:07 +08:00
const problem = this . tryToAdd ( testConfig , reason . module , possibleModules , failureCache ) ;
if ( problem ) {
failureCache . set ( module , problem ) ; // cache failures for performance
return problem ;
2017-05-10 19:15:14 +08:00
}
}
// Eagerly try to add imports too if possible
2017-05-28 21:25:07 +08:00
for ( const imp of this . getImports ( module ) ) {
const problem = this . tryToAdd ( testConfig , imp , possibleModules , failureCache ) ;
if ( problem ) {
config . addWarning ( module , problem ) ;
}
}
2017-05-10 19:15:14 +08:00
// Commit experimental changes
config . set ( testConfig ) ;
2017-05-28 21:25:07 +08:00
return null ;
2017-05-10 19:15:14 +08:00
}
addInOrder ( module , unorderedSet , orderedSet ) {
if ( orderedSet . has ( module ) ) return ;
if ( ! unorderedSet . has ( module ) ) return ;
orderedSet . add ( module ) ;
for ( const imp of this . getImports ( module ) )
this . addInOrder ( imp , unorderedSet , orderedSet ) ;
orderedSet . delete ( module ) ;
orderedSet . add ( module ) ;
}
}
class ConcatConfiguration {
constructor ( rootModule ) {
this . rootModule = rootModule ;
this . modules = new Set ( [ rootModule ] ) ;
2017-05-28 21:25:07 +08:00
this . warnings = new Map ( ) ;
2017-05-10 19:15:14 +08:00
}
add ( module ) {
this . modules . add ( module ) ;
}
has ( module ) {
return this . modules . has ( module ) ;
}
isEmpty ( ) {
return this . modules . size === 1 ;
}
2017-05-28 21:25:07 +08:00
addWarning ( module , problem ) {
this . warnings . set ( module , problem ) ;
}
2017-05-10 19:15:14 +08:00
clone ( ) {
const clone = new ConcatConfiguration ( this . rootModule ) ;
for ( const module of this . modules )
clone . add ( module ) ;
2017-05-28 21:25:07 +08:00
for ( const pair of this . warnings )
clone . addWarning ( pair [ 0 ] , pair [ 1 ] ) ;
2017-05-10 19:15:14 +08:00
return clone ;
}
set ( config ) {
this . rootModule = config . rootModule ;
this . modules = new Set ( config . modules ) ;
2017-05-28 21:25:07 +08:00
this . warnings = new Map ( config . warnings ) ;
2017-05-10 19:15:14 +08:00
}
}
module . exports = ModuleConcatenationPlugin ;