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-11-19 07:50:10 +08:00
const StackedSetMap = require ( "../util/StackedSetMap" ) ;
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 ) {
2017-12-06 22:01:25 +08:00
compiler . hooks . compilation . tap ( "ModuleConcatenationPlugin" , ( compilation , {
normalModuleFactory
} ) => {
2017-12-14 17:22:27 +08:00
const handler = ( parser , parserOptions ) => {
2017-05-10 19:15:14 +08:00
parser . plugin ( "call eval" , ( ) => {
2017-12-16 22:15:02 +08:00
// Because of variable renaming we can't use modules with eval.
parser . state . module . buildMeta . moduleConcatenationBailout = "eval()" ;
2017-05-10 19:15:14 +08:00
} ) ;
2017-12-14 17:22:27 +08:00
} ;
normalModuleFactory . hooks . parser . for ( "javascript/auto" ) . tap ( "ModuleConcatenationPlugin" , handler ) ;
normalModuleFactory . hooks . parser . for ( "javascript/dynamic" ) . tap ( "ModuleConcatenationPlugin" , handler ) ;
normalModuleFactory . hooks . parser . for ( "javascript/esm" ) . tap ( "ModuleConcatenationPlugin" , handler ) ;
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-12-06 22:01:25 +08:00
compilation . hooks . optimizeChunkModules . tap ( "ModuleConcatenationPlugin" , ( 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-12-06 19:09:17 +08:00
if ( ! module . buildMeta || ! module . buildMeta . 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-12-16 22:15:02 +08:00
// Some expressions are not compatible with module concatenation
// because they may produce unexpected results. The plugin bails out
// if some were detected upfront.
if ( module . buildMeta && module . buildMeta . moduleConcatenationBailout ) {
setBailoutReason ( module , ` Module uses ${ module . buildMeta . moduleConcatenationBailout } ` ) ;
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)
2017-12-07 00:39:42 +08:00
if ( ! Array . isArray ( module . buildMeta . providedExports ) ) {
2017-08-07 19:56:50 +08:00
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
2017-11-20 22:41:30 +08:00
const nonHarmonyReasons = module . reasons . filter ( reason => ! reason . dependency || ! ( reason . dependency instanceof HarmonyImportDependency ) ) ;
2017-06-15 04:46:26 +08:00
if ( nonHarmonyReasons . length > 0 ) {
2017-11-20 22:41:30 +08:00
const importingModules = new Set ( nonHarmonyReasons . map ( r => r . module ) . filter ( Boolean ) ) ;
2017-11-22 01:52:35 +08:00
const importingExplanations = new Set ( nonHarmonyReasons . map ( r => r . explanation ) . filter ( Boolean ) ) ;
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 ( ) ;
2017-11-22 01:52:35 +08:00
const explanations = Array . from ( importingExplanations ) . sort ( ) ;
if ( names . length > 0 && explanations . length === 0 )
2017-11-20 22:41:30 +08:00
return ` Module is referenced from these modules with unsupported syntax: ${ names . join ( ", " ) } ` ;
2017-11-22 01:52:35 +08:00
else if ( names . length === 0 && explanations . length > 0 )
return ` Module is referenced by: ${ explanations . join ( ", " ) } ` ;
else if ( names . length > 0 && explanations . length > 0 )
return ` Module is referenced from these modules with unsupported syntax: ${ names . join ( ", " ) } and by: ${ explanations . join ( ", " ) } ` ;
2017-11-20 22:41:30 +08:00
else
return "Module is referenced in a unsupported way" ;
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 ) ;
2017-11-19 07:50:10 +08:00
for ( const module of currentConfiguration . getModules ( ) ) {
2017-06-15 04:46:26 +08:00
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-11-19 07:50:10 +08:00
const modules = concatConfiguration . getModules ( ) ;
const newModule = new ConcatenatedModule ( concatConfiguration . rootModule , modules ) ;
for ( const warning of concatConfiguration . getWarningsSorted ( ) ) {
2017-06-15 04:46:26 +08:00
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-11-19 07:50:10 +08:00
for ( const m of 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-12-07 00:39:42 +08:00
. filter ( ref => Array . isArray ( ref . importedNames ) || Array . isArray ( ref . module . buildMeta . 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-12-06 19:09:17 +08:00
if ( reason . module . factoryMeta . 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 {
2017-11-19 07:50:10 +08:00
constructor ( rootModule , cloneFrom ) {
2017-05-10 19:15:14 +08:00
this . rootModule = rootModule ;
2017-11-19 07:50:10 +08:00
if ( cloneFrom ) {
this . modules = cloneFrom . modules . createChild ( 5 ) ;
this . warnings = cloneFrom . warnings . createChild ( 5 ) ;
} else {
this . modules = new StackedSetMap ( ) ;
this . modules . add ( rootModule ) ;
this . warnings = new StackedSetMap ( ) ;
}
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-11-19 07:50:10 +08:00
getWarningsSorted ( ) {
return new Map ( this . warnings . asPairArray ( ) . sort ( ( a , b ) => {
2017-06-15 05:20:30 +08:00
const ai = a [ 0 ] . identifier ( ) ;
const bi = b [ 0 ] . identifier ( ) ;
if ( ai < bi ) return - 1 ;
if ( ai > bi ) return 1 ;
return 0 ;
} ) ) ;
}
2017-11-19 07:50:10 +08:00
getModules ( ) {
return this . modules . asArray ( ) ;
}
2017-05-10 19:15:14 +08:00
clone ( ) {
2017-11-19 07:50:10 +08:00
return new ConcatConfiguration ( this . rootModule , this ) ;
2017-05-10 19:15:14 +08:00
}
set ( config ) {
this . rootModule = config . rootModule ;
2017-11-19 07:50:10 +08:00
this . modules = config . modules ;
this . warnings = config . warnings ;
2017-05-10 19:15:14 +08:00
}
}
module . exports = ModuleConcatenationPlugin ;