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 ( ) ;
2017-05-29 05:31:58 +08:00
2017-06-15 04:46:26 +08:00
function setBailoutReason ( module , prefix , reason ) {
2017-05-28 21:25:07 +08:00
bailoutReasonMap . set ( module , reason ) ;
2017-06-15 04:46:26 +08:00
module . optimizationBailout . push ( typeof reason === "function" ? ( rs ) => ` ${ prefix } : ${ reason ( rs ) } ` : ` ${ prefix } : ${ reason } ` ) ;
2017-05-28 21:25:07 +08:00
}
2017-05-29 05:31:58 +08:00
2017-05-28 21:25:07 +08:00
function getBailoutReason ( module , requestShortener ) {
const reason = bailoutReasonMap . get ( module ) ;
if ( typeof reason === "function" ) return reason ( requestShortener ) ;
return reason ;
}
2017-05-29 05:31:58 +08:00
2017-05-10 19:15:14 +08:00
compilation . plugin ( "optimize-chunk-modules" , ( chunks , modules ) => {
2017-06-15 04:46:26 +08:00
const relevantModules = [ ] ;
const possibleInners = new Set ( ) ;
for ( const module of modules ) {
// Only harmony modules are valid for optimization
if ( ! module . meta || ! module . meta . harmonyModule ) {
continue ;
}
2017-05-28 21:25:07 +08:00
2017-06-15 04:46:26 +08:00
// Because of variable renaming we can't use modules with eval
if ( module . meta && module . meta . hasEval ) {
setBailoutReason ( module , "ModuleConcatenation" , "eval is used in the module" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
2017-06-15 04:46:26 +08:00
relevantModules . push ( module ) ;
2017-05-28 21:25:07 +08:00
2017-06-15 04:46:26 +08:00
// Module must not be the entry points
if ( module . getChunks ( ) . some ( chunk => chunk . entryModule === module ) ) {
setBailoutReason ( module , "ModuleConcatenation (inner)" , "module is an entrypoint" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
2017-06-15 04:46:26 +08:00
// Exports must be known (and not dynamic)
if ( ! Array . isArray ( module . providedExports ) ) {
setBailoutReason ( module , "ModuleConcatenation (inner)" , "exports are not known" ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
2017-06-15 04:46:26 +08:00
// Using dependency variables is not possible as this wraps the code in a function
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
2017-06-15 04:46:26 +08:00
// Module must only be used by Harmony Imports
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 , "ModuleConcatenation (inner)" , ( requestShortener ) => {
const names = Array . from ( importingModules ) . map ( m => m . readableIdentifier ( requestShortener ) ) ;
return ` module is used with non-harmony imports from ${ names . join ( ", " ) } ` ;
} ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
2017-06-15 04:46:26 +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 ;
} ) ;
const concatConfigurations = [ ] ;
const usedAsInner = new Set ( ) ;
for ( const currentRoot of relevantModules ) {
// 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
const currentConfiguration = new ConcatConfiguration ( currentRoot ) ;
// cache failures to add modules
const failureCache = new Map ( ) ;
// try to add all imports
for ( const imp of this . getImports ( currentRoot ) ) {
const problem = this . tryToAdd ( currentConfiguration , imp , possibleInners , failureCache ) ;
if ( problem ) {
failureCache . set ( imp , problem ) ;
currentConfiguration . addWarning ( imp , problem ) ;
2017-05-28 21:25:07 +08:00
}
}
2017-06-15 04:46:26 +08:00
if ( ! currentConfiguration . isEmpty ( ) ) {
concatConfigurations . push ( currentConfiguration ) ;
for ( const module of currentConfiguration . modules ) {
if ( module !== currentConfiguration . rootModule )
usedAsInner . add ( module ) ;
2017-05-28 21:25:07 +08:00
}
2017-05-10 19:15:14 +08:00
}
2017-06-15 04:46:26 +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)
concatConfigurations . sort ( ( a , b ) => {
return b . modules . size - a . modules . size ;
} ) ;
const usedModules = new Set ( ) ;
for ( const concatConfiguration of concatConfigurations ) {
if ( usedModules . has ( concatConfiguration . rootModule ) )
continue ;
const orderedModules = new Set ( ) ;
this . addInOrder ( concatConfiguration . rootModule , concatConfiguration . modules , orderedModules ) ;
const newModule = new ConcatenatedModule ( concatConfiguration . rootModule , Array . from ( orderedModules ) ) ;
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 } ` ;
} ) ;
}
const chunks = concatConfiguration . rootModule . getChunks ( ) ;
for ( const m of orderedModules ) {
usedModules . add ( m ) ;
chunks . forEach ( chunk => chunk . removeModule ( m ) ) ;
}
chunks . forEach ( chunk => {
2017-05-10 19:15:14 +08:00
chunk . addModule ( 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-06-15 04:46:26 +08:00
} ) ;
compilation . modules . push ( newModule ) ;
newModule . reasons . forEach ( reason => reason . dependency . module = newModule ) ;
newModule . dependencies . forEach ( dep => {
if ( dep . module ) {
dep . module . reasons . forEach ( reason => {
if ( reason . dependency === dep )
reason . module = newModule ;
} ) ;
}
} ) ;
}
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-06-15 04:46:26 +08:00
failureCache . set ( module , module ) ; // cache failures for performance
return module ;
}
// module must be in the same chunks
if ( ! config . rootModule . hasEqualsChunks ( module ) ) {
failureCache . set ( module , module ) ; // cache failures for performance
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 ;