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" ) ;
2017-08-12 16:44:14 +08:00
const ModuleHotAcceptDependency = require ( "../dependencies/ModuleHotAcceptDependency" ) ;
const ModuleHotDeclineDependency = require ( "../dependencies/ModuleHotDeclineDependency" ) ;
2017-05-10 19:15:14 +08:00
const ConcatenatedModule = require ( "./ConcatenatedModule" ) ;
2017-07-25 18:53:18 +08:00
const HarmonyCompatibilityDependency = require ( "../dependencies/HarmonyCompatibilityDependency" ) ;
2017-05-10 19:15:14 +08:00
2017-11-08 18:32:05 +08:00
const formatBailoutReason = msg => {
2017-06-28 20:38:17 +08:00
return "ModuleConcatenation bailout: " + msg ;
2017-11-08 18:32:05 +08:00
} ;
2017-06-28 20:38:17 +08:00
2017-05-10 19:15:14 +08:00
class ModuleConcatenationPlugin {
constructor ( options ) {
if ( typeof options !== "object" ) options = { } ;
this . options = options ;
}
apply ( compiler ) {
compiler . plugin ( "compilation" , ( compilation , params ) => {
2017-11-12 01:48:29 +08:00
params . normalModuleFactory . plugin ( [ "parser javascript/auto" , "parser javascript/dynamic" , "parser javascript/esm" ] , ( parser , parserOptions ) => {
2017-05-10 19:15:14 +08:00
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-11-08 18:32:05 +08:00
const setBailoutReason = ( module , reason ) => {
2017-05-28 21:25:07 +08:00
bailoutReasonMap . set ( module , reason ) ;
2017-06-28 20:38:17 +08:00
module . optimizationBailout . push ( typeof reason === "function" ? ( rs ) => formatBailoutReason ( reason ( rs ) ) : formatBailoutReason ( reason ) ) ;
2017-11-08 18:32:05 +08:00
} ;
2017-05-29 05:31:58 +08:00
2017-11-08 18:32:05 +08:00
const getBailoutReason = ( module , requestShortener ) => {
2017-05-28 21:25:07 +08:00
const reason = bailoutReasonMap . get ( module ) ;
if ( typeof reason === "function" ) return reason ( requestShortener ) ;
return reason ;
2017-11-08 18:32:05 +08:00
} ;
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
2017-07-25 18:53:18 +08:00
if ( ! module . meta || ! module . meta . harmonyModule || ! module . dependencies . some ( d => d instanceof HarmonyCompatibilityDependency ) ) {
2017-06-28 20:38:17 +08:00
setBailoutReason ( module , "Module is not an ECMAScript module" ) ;
2017-06-15 04:46:26 +08:00
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 ) {
2017-06-28 20:38:17 +08:00
setBailoutReason ( module , "Module uses eval()" ) ;
2017-06-15 04:46:26 +08:00
continue ;
}
2017-05-10 19:15:14 +08:00
2017-08-07 19:56:50 +08:00
// Exports must be known (and not dynamic)
if ( ! Array . isArray ( module . providedExports ) ) {
setBailoutReason ( module , "Module exports are unknown" ) ;
continue ;
}
2017-08-08 14:15:18 +08:00
// Using dependency variables is not possible as this wraps the code in a function
if ( module . variables . length > 0 ) {
setBailoutReason ( module , ` Module uses injected variables ( ${ module . variables . map ( v => v . name ) . join ( ", " ) } ) ` ) ;
continue ;
}
2017-08-12 16:44:14 +08:00
// Hot Module Replacement need it's own module to work correctly
if ( module . dependencies . some ( dep => dep instanceof ModuleHotAcceptDependency || dep instanceof ModuleHotDeclineDependency ) ) {
setBailoutReason ( module , "Module uses Hot Module Replacement" ) ;
continue ;
}
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 ) ) {
2017-06-28 20:38:17 +08:00
setBailoutReason ( module , "Module is an entry point" ) ;
2017-06-15 04:46:26 +08:00
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 ) ) ;
2017-07-24 16:29:08 +08:00
const importingModuleTypes = new Map ( Array . from ( importingModules ) . map ( m => [ m , new Set ( nonHarmonyReasons . filter ( r => r . module === m ) . map ( r => r . dependency . type ) . sort ( ) ) ] ) ) ;
2017-06-28 20:38:17 +08:00
setBailoutReason ( module , ( requestShortener ) => {
2017-07-24 16:29:08 +08:00
const names = Array . from ( importingModules ) . map ( m => ` ${ m . readableIdentifier ( requestShortener ) } (referenced with ${ Array . from ( importingModuleTypes . get ( m ) ) . join ( ", " ) } ) ` ) . sort ( ) ;
return ` Module is referenced from these modules with unsupported syntax: ${ names . join ( ", " ) } ` ;
2017-06-15 04:46:26 +08:00
} ) ;
continue ;
}
2017-05-10 19:15:14 +08:00
2017-06-15 04:46:26 +08:00
possibleInners . add ( module ) ;
}
// sort by depth
2017-06-28 20:38:17 +08:00
// modules with lower depth are more likely suited as roots
2017-06-15 04:46:26 +08:00
// 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 ;
2017-08-08 02:33:47 +08:00
const newModule = new ConcatenatedModule ( concatConfiguration . rootModule , Array . from ( concatConfiguration . modules ) ) ;
2017-06-15 05:20:30 +08:00
concatConfiguration . sortWarnings ( ) ;
2017-06-15 04:46:26 +08:00
for ( const warning of concatConfiguration . warnings ) {
newModule . optimizationBailout . push ( ( requestShortener ) => {
const reason = getBailoutReason ( warning [ 0 ] , requestShortener ) ;
2017-06-28 20:38:17 +08:00
const reasonWithPrefix = reason ? ` (<- ${ reason } ) ` : "" ;
2017-06-15 04:46:26 +08:00
if ( warning [ 0 ] === warning [ 1 ] )
2017-07-24 16:29:08 +08:00
return formatBailoutReason ( ` Cannot concat with ${ warning [ 0 ] . readableIdentifier ( requestShortener ) } ${ reasonWithPrefix } ` ) ;
2017-06-15 04:46:26 +08:00
else
2017-07-24 16:29:08 +08:00
return formatBailoutReason ( ` Cannot concat with ${ warning [ 0 ] . readableIdentifier ( requestShortener ) } because of ${ warning [ 1 ] . readableIdentifier ( requestShortener ) } ${ reasonWithPrefix } ` ) ;
2017-06-15 04:46:26 +08:00
} ) ;
}
const chunks = concatConfiguration . rootModule . getChunks ( ) ;
2017-08-08 02:33:47 +08:00
for ( const m of concatConfiguration . modules ) {
2017-06-15 04:46:26 +08:00
usedModules . add ( m ) ;
chunks . forEach ( chunk => chunk . removeModule ( m ) ) ;
}
chunks . forEach ( chunk => {
2017-05-10 19:15:14 +08:00
chunk . addModule ( newModule ) ;
2017-08-16 20:05:06 +08:00
newModule . addChunk ( chunk ) ;
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 )
2017-08-08 15:32:43 +08:00
// Get reference info for this dependency
. map ( dep => dep . getReference ( ) )
// Reference is valid and has a module
. filter ( ref => ref && ref . module )
2017-05-10 19:15:14 +08:00
// Dependencies are simple enough to concat them
2017-08-08 15:32:43 +08:00
. filter ( ref => Array . isArray ( ref . importedNames ) || Array . isArray ( ref . module . providedExports ) )
2017-05-10 19:15:14 +08:00
// Take the imported module
2017-08-08 15:32:43 +08:00
. map ( ref => ref . module )
2017-05-10 19:15:14 +08:00
) ) ;
}
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-08-08 15:40:17 +08:00
// Modules that are not used can be ignored
2017-09-14 19:35:25 +08:00
if ( reason . module . sideEffectFree && reason . module . used === false ) continue ;
2017-08-08 15:40:17 +08:00
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
}
}
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-06-15 05:20:30 +08:00
sortWarnings ( ) {
this . warnings = new Map ( Array . from ( this . warnings ) . sort ( ( a , b ) => {
const ai = a [ 0 ] . identifier ( ) ;
const bi = b [ 0 ] . identifier ( ) ;
if ( ai < bi ) return - 1 ;
if ( ai > bi ) return 1 ;
return 0 ;
} ) ) ;
}
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 ;