@ -1,30 +1,51 @@
import { Promise } from 'sander ';
import { basename , extname } from './utils/path ';
import { parse } from 'acorn/src/index ' ;
import { parse } from 'acorn' ;
import MagicString from 'magic-string' ;
import MagicString from 'magic-string' ;
import Statement from './Statement' ;
import Statement from './Statement' ;
import walk from './ast/walk' ;
import walk from './ast/walk' ;
import { blank , keys } from './utils/object' ;
import { blank , keys } from './utils/object' ;
import { first , sequence } from './utils/promise' ;
import getLocation from './utils/getLocation' ;
import getLocation from './utils/getLocation' ;
import makeLegalIdentifier from './utils/makeLegalIdentifier' ;
import makeLegalIdentifier from './utils/makeLegalIdentifier' ;
const emptyPromise = Promise . resolve ( ) ;
function isEmptyExportedVarDeclaration ( node , exports , toExport ) {
if ( node . type !== 'VariableDeclaration' || node . declarations [ 0 ] . init ) return false ;
const name = node . declarations [ 0 ] . id . name ;
function deconflict ( name , names ) {
const id = exports . lookup ( name ) ;
while ( name in names ) {
name = ` _ ${ name } ` ;
return id && id . name in toExport ;
}
}
return name ;
function removeSourceMappingURLComments ( source , magicString ) {
const pattern = /\/\/#\s+sourceMappingURL=.+\n?/g ;
let match ;
while ( match = pattern . exec ( source ) ) {
magicString . remove ( match . index , match . index + match [ 0 ] . length ) ;
}
}
}
function isEmptyExportedVarDeclaration ( node , allBundleExports , moduleReplacements ) {
function assign ( target , source ) {
if ( node . type !== 'VariableDeclaration' || node . declarations [ 0 ] . init ) return false ;
for ( let key in source ) target [ key ] = source [ key ] ;
}
const name = node . declarations [ 0 ] . id . name ;
class Id {
const canonicalName = moduleReplacements [ name ] || name ;
constructor ( module , name , statement ) {
this . originalName = this . name = name ;
this . module = module ;
this . statement = statement ;
this . modifierStatements = [ ] ;
return canonicalName in allBundleExports ;
// modifiers
this . isUsed = false ;
}
mark ( ) {
this . isUsed = true ;
this . statement . mark ( ) ;
this . modifierStatements . forEach ( stmt => stmt . mark ( ) ) ;
}
}
}
export default class Module {
export default class Module {
@ -33,6 +54,16 @@ export default class Module {
this . bundle = bundle ;
this . bundle = bundle ;
this . id = id ;
this . id = id ;
this . module = this ;
this . isModule = true ;
// Implement Identifier interface.
this . name = makeLegalIdentifier ( basename ( id ) . slice ( 0 , - extname ( id ) . length ) ) ;
// HACK: If `id` isn't a path, the above code yields the empty string.
if ( ! this . name ) {
this . name = makeLegalIdentifier ( id ) ;
}
// By default, `id` is the filename. Custom resolvers and loaders
// By default, `id` is the filename. Custom resolvers and loaders
// can change that, but it makes sense to use it for the source filename
// can change that, but it makes sense to use it for the source filename
@ -40,37 +71,60 @@ export default class Module {
filename : id
filename : id
} ) ;
} ) ;
// remove existing sourceMappingURL comments
removeSourceMappingURLComments ( source , this . magicString ) ;
const pattern = /\/\/#\s+sourceMappingURL=.+\n?/g ;
let match ;
while ( match = pattern . exec ( source ) ) {
this . magicString . remove ( match . index , match . index + match [ 0 ] . length ) ;
}
this . suggestedNames = blank ( ) ;
this . comments = [ ] ;
this . comments = [ ] ;
this . statements = this . parse ( ast ) ;
this . statements = this . parse ( ast ) ;
// imports and exports, indexed by ID
// all dependencies
this . imports = blank ( ) ;
this . resolvedIds = blank ( ) ;
this . exports = blank ( ) ;
this . reexports = blank ( ) ;
// Virtual scopes for the local and exported names.
this . locals = bundle . scope . virtual ( true ) ;
this . exports = bundle . scope . virtual ( false ) ;
const { reference , inScope } = this . exports ;
this . exports . reference = name => {
// If we have it, grab it.
if ( inScope . call ( this . exports , name ) ) {
return reference . call ( this . exports , name ) ;
}
// ... otherwise search allExportsFrom
for ( let i = 0 ; i < this . allExportsFrom . length ; i += 1 ) {
const module = this . allExportsFrom [ i ] ;
if ( module . exports . inScope ( name ) ) {
return module . exports . reference ( name ) ;
}
}
// throw new Error( `The name "${name}" is never exported (from ${this.id})!` );
return reference . call ( this . exports , name ) ;
} ;
this . exports . inScope = name => {
if ( inScope . call ( this . exports , name ) ) return true ;
this . exportAlls = blank ( ) ;
return this . allExportsFrom . some ( module => module . exports . inScope ( name ) ) ;
} ;
// array of all-export sources
// Create a unique virtual scope for references to the module.
this . exportDelegates = [ ] ;
// const unique = bundle.scope.virtual();
// unique.define( this.name, this );
// this.reference = unique.reference( this.name );
this . replacements = blank ( ) ;
// As far as we know, all our exported bindings have been resolved.
this . allExportsResolved = true ;
this . allExportsFrom = [ ] ;
this . varDeclarations = [ ] ;
this . reassignment s = [ ] ;
this . definitions = blank ( ) ;
// TODO: change to false, and detect when it's necessary.
this . definitionPromises = blank ( ) ;
this . needsDynamicAccess = false ;
this . modifications = blank ( ) ;
this . analyse ( ) ;
this . dependencies = this . collectDependencies ( ) ;
}
}
addExport ( statement ) {
addExport ( statement ) {
@ -79,22 +133,32 @@ export default class Module {
// export { name } from './other'
// export { name } from './other'
if ( source ) {
if ( source ) {
const module = this . getModule ( source ) ;
if ( node . type === 'ExportAllDeclaration' ) {
if ( node . type === 'ExportAllDeclaration' ) {
// Store `export * from '...'` statements in an array of delegates.
// Store `export * from '...'` statements in an array of delegates.
// When an unknown import is encountered, we see if one of them can satisfy it.
// When an unknown import is encountered, we see if one of them can satisfy it.
this . exportDelegates . push ( {
statement ,
if ( module . isExternal ) {
source
let err = new Error ( ` Cannot trace 'export *' references through external modules. ` ) ;
} ) ;
err . file = this . id ;
err . loc = getLocation ( this . source , node . start ) ;
throw err ;
}
// It seems like we must re-export all exports from another module...
this . allExportsResolved = false ;
if ( ! ~ this . allExportsFrom . indexOf ( module ) ) {
this . allExportsFrom . push ( module ) ;
}
}
}
else {
else {
node . specifiers . forEach ( specifier => {
node . specifiers . forEach ( specifier => {
this . reexports [ specifier . exported . name ] = {
// Bind the export of this module, to the export of the other.
source ,
this . exports . bind ( specifier . exported . name ,
localName : specifier . local . name ,
module . exports . reference ( specifier . local . name ) ) ;
module : null // filled in later
} ;
} ) ;
} ) ;
}
}
}
}
@ -111,16 +175,18 @@ export default class Module {
node . declaration . type === 'Identifier' ?
node . declaration . type === 'Identifier' ?
node . declaration . name :
node . declaration . name :
null ;
null ;
const name = identifier || this . name ;
this . exports . default = {
// Always define a new `Identifier` for the default export.
statement ,
const id = new Id ( this , name , statement ) ;
name : 'default' ,
localName : identifier || 'default' ,
// Keep the identifier name, if one exists.
identifier ,
// We can optimize the newly created default `Identifier` away,
isDeclaration ,
// if it is never modified.
isAnonymous ,
// in case of `export default foo; foo = somethingElse`
isModified : false // in case of `export default foo; foo = somethingElse`
assign ( id , { isDeclaration , isAnonymous , identifier } ) ;
} ;
this . exports . define ( 'default' , id ) ;
}
}
// export { foo, bar, baz }
// export { foo, bar, baz }
@ -133,11 +199,7 @@ export default class Module {
const localName = specifier . local . name ;
const localName = specifier . local . name ;
const exportedName = specifier . exported . name ;
const exportedName = specifier . exported . name ;
this . exports [ exportedName ] = {
this . exports . bind ( exportedName , this . locals . reference ( localName ) ) ;
statement ,
localName ,
exportedName
} ;
} ) ;
} ) ;
}
}
@ -154,38 +216,49 @@ export default class Module {
name = declaration . id . name ;
name = declaration . id . name ;
}
}
this . exports [ name ] = {
this . locals . define ( name , new Id ( this , name , statement ) ) ;
statement ,
this . exports . bind ( name , this . locals . reference ( name ) ) ;
localName : name ,
expression : declaration
} ;
}
}
}
}
}
}
addImport ( statement ) {
addImport ( statement ) {
const node = statement . node ;
const node = statement . node ;
const source = node . source . value ;
const module = this . getModule ( node . source . value ) ;
node . specifiers . forEach ( specifier => {
node . specifiers . forEach ( specifier => {
const isDefault = specifier . type === 'ImportDefaultSpecifier' ;
const isDefault = specifier . type === 'ImportDefaultSpecifier' ;
const isNamespace = specifier . type === 'ImportNamespaceSpecifier' ;
const isNamespace = specifier . type === 'ImportNamespaceSpecifier' ;
const localName = specifier . local . name ;
const localName = specifier . local . name ;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier . imported . name ;
if ( this . imports [ localName ] ) {
if ( this . locals . defines ( localName ) ) {
const err = new Error ( ` Duplicated import ' ${ localName } ' ` ) ;
const err = new Error ( ` Duplicated import ' ${ localName } ' ` ) ;
err . file = this . id ;
err . file = this . id ;
err . loc = getLocation ( this . source , specifier . start ) ;
err . loc = getLocation ( this . source , specifier . start ) ;
throw err ;
throw err ;
}
}
this . imports [ localName ] = {
if ( isNamespace ) {
source ,
// If it's a namespace import, we bind the localName to the module itself.
name ,
module . needsAll = true ;
localName
module . name = localName ;
} ;
this . locals . bind ( localName , module ) ;
} else {
const name = isDefault ? 'default' : specifier . imported . name ;
this . locals . bind ( localName , module . exports . reference ( name ) ) ;
// For compliance with earlier Rollup versions.
// If the module is external, and we access the default.
// Rewrite the module name, and the default name to the
// `localName` we use for it.
if ( module . isExternal && isDefault ) {
const id = module . exports . lookup ( name ) ;
module . name = id . name = localName ;
id . name += '__default' ;
}
}
} ) ;
} ) ;
}
}
@ -199,15 +272,36 @@ export default class Module {
// consolidate names that are defined/modified in this module
// consolidate names that are defined/modified in this module
keys ( statement . defines ) . forEach ( name => {
keys ( statement . defines ) . forEach ( name => {
this . definitions [ name ] = statement ;
this . locals . define ( name , new Id ( this , name , statement ) ) ;
} ) ;
} ) ;
} ) ;
statement . scope . varDeclarations . forEach ( name => {
// If all exports aren't resolved, but all our delegate modules are...
this . varDeclarations . push ( name ) ;
if ( ! this . allExportsResolved && this . allExportsFrom . every ( module => module . allExportsResolved ) ) {
// .. then all our exports should be as well.
this . allExportsResolved = true ;
// For all modules we export all from, iterate through its exported names.
// If we don't already define the binding 'name',
// bind the name to the other module's reference.
this . allExportsFrom . forEach ( module => {
module . exports . getNames ( ) . forEach ( name => {
if ( ! this . exports . defines ( name ) ) {
this . exports . bind ( name , module . exports . reference ( name ) ) ;
}
} ) ;
} ) ;
} ) ;
}
keys ( statement . modifies ) . forEach ( name => {
// discover variables that are reassigned inside function
( this . modifications [ name ] || ( this . modifications [ name ] = [ ] ) ) . push ( statement ) ;
// bodies, so we can keep bindings live, e.g.
//
// export var count = 0;
// export function incr () { count += 1 }
let reassigned = blank ( ) ;
this . statements . forEach ( statement => {
keys ( statement . reassigns ) . forEach ( name => {
reassigned [ name ] = true ;
} ) ;
} ) ;
} ) ;
} ) ;
@ -216,12 +310,47 @@ export default class Module {
this . statements . forEach ( statement => {
this . statements . forEach ( statement => {
if ( statement . isReexportDeclaration ) return ;
if ( statement . isReexportDeclaration ) return ;
// while we're here, mark reassignments
statement . scope . varDeclarations . forEach ( name => {
if ( reassigned [ name ] && ! ~ this . reassignments . indexOf ( name ) ) {
this . reassignments . push ( name ) ;
}
} ) ;
keys ( statement . dependsOn ) . forEach ( name => {
keys ( statement . dependsOn ) . forEach ( name => {
if ( ! this . definitions [ name ] && ! this . imports [ name ] ) {
// For each name we depend on that isn't in scope,
this . bundle . assumedGlobals [ name ] = true ;
// add a new global and bind the local name to it.
if ( ! this . locals . inScope ( name ) ) {
this . bundle . globals . define ( name , {
originalName : name ,
name ,
mark ( ) { }
} ) ;
this . locals . bind ( name , this . bundle . globals . reference ( name ) ) ;
}
}
} ) ;
} ) ;
} ) ;
} ) ;
// OPTIMIZATION!
// If we have a default export and it's value is never modified,
// bind to it directly.
const def = this . exports . lookup ( 'default' ) ;
if ( def && ! def . isModified && def . identifier ) {
this . exports . bind ( 'default' , this . locals . reference ( def . identifier ) ) ;
}
}
// Returns the set of imported module ids by going through all import/exports statements.
collectDependencies ( ) {
const importedModules = blank ( ) ;
this . statements . forEach ( statement => {
if ( statement . isImportDeclaration || ( statement . isExportDeclaration && statement . node . source ) ) {
importedModules [ statement . node . source . value ] = true ;
}
} ) ;
return keys ( importedModules ) ;
}
}
consolidateDependencies ( ) {
consolidateDependencies ( ) {
@ -235,25 +364,21 @@ export default class Module {
}
}
this . statements . forEach ( statement => {
this . statements . forEach ( statement => {
if ( statement . isImportDeclaration && ! statement . node . specifiers . length && ! statement . module . isExternal ) {
if ( statement . isImportDeclaration && ! statement . node . specifiers . length ) {
// include module for its side-effects
// include module for its side-effects
strongDependencies [ statement . module . id ] = statement . module ; // TODO is this right? `statement.module` should be `this`, surely?
const module = this . getModule ( statement . node . source . value ) ;
if ( ! module . isExternal ) strongDependencies [ module . id ] = module ;
}
}
else if ( statement . isReexportDeclaration ) {
else if ( statement . isReexportDeclaration ) {
if ( statement . node . specifiers ) {
if ( statement . node . specifiers ) {
statement . node . specifiers . forEach ( specifier => {
statement . node . specifiers . forEach ( specifier => {
let reexport ;
let module = this ;
let name = specifier . exported . name ;
let name = specifier . exported . name ;
while ( ! module . isExternal && module . reexports [ name ] && module . reexports [ name ] . isUsed ) {
reexport = module . reexports [ name ] ;
module = reexport . module ;
name = reexport . localName ;
}
addDependency ( strongDependencies , reexport ) ;
let id = this . exports . lookup ( name ) ;
addDependency ( strongDependencies , id ) ;
} ) ;
} ) ;
}
}
}
}
@ -262,8 +387,7 @@ export default class Module {
keys ( statement . stronglyDependsOn ) . forEach ( name => {
keys ( statement . stronglyDependsOn ) . forEach ( name => {
if ( statement . defines [ name ] ) return ;
if ( statement . defines [ name ] ) return ;
addDependency ( strongDependencies , this . exportAlls [ name ] ) ||
addDependency ( strongDependencies , this . locals . lookup ( name ) ) ;
addDependency ( strongDependencies , this . imports [ name ] ) ;
} ) ;
} ) ;
}
}
} ) ;
} ) ;
@ -274,114 +398,53 @@ export default class Module {
keys ( statement . dependsOn ) . forEach ( name => {
keys ( statement . dependsOn ) . forEach ( name => {
if ( statement . defines [ name ] ) return ;
if ( statement . defines [ name ] ) return ;
addDependency ( weakDependencies , this . exportAlls [ name ] ) ||
addDependency ( weakDependencies , this . locals . lookup ( name ) ) ;
addDependency ( weakDependencies , this . imports [ name ] ) ;
} ) ;
} ) ;
} ) ;
} ) ;
return { strongDependencies , weakDependencies } ;
// Go through all our local and exported ids and make us depend on
}
// the defining modules as well as
this . exports . getIds ( ) . concat ( this . locals . getIds ( ) ) . forEach ( id => {
defaultName ( ) {
if ( id . module && ! id . module . isExternal ) {
const defaultExport = this . exports . default ;
weakDependencies [ id . module . id ] = id . module ;
if ( ! defaultExport ) return null ;
const name = defaultExport . identifier && ! defaultExport . isModified ?
defaultExport . identifier :
this . replacements . default ;
return this . replacements [ name ] || name ;
}
}
findDefiningStatement ( name ) {
if ( ! id . modifierStatements ) return ;
if ( this . definitions [ name ] ) return this . definitions [ name ] ;
// TODO what about `default`/`*`?
const importDeclaration = this . imports [ name ] ;
id . modifierStatements . forEach ( statement => {
if ( ! importDeclaration ) return null ;
const module = statement . module ;
weakDependencies [ module . id ] = module ;
return Promise . resolve ( importDeclaration . module || this . bundle . fetchModule ( importDeclaration . source , this . id ) )
} ) ;
. then ( module => {
importDeclaration . module = module ;
return module . findDefiningStatement ( name ) ;
} ) ;
} ) ;
}
mark ( name ) {
// shortcut cycles
if ( this . definitionPromises [ name ] ) {
return emptyPromise ;
}
let promise ;
// The definition for this name is in a different module
if ( this . imports [ name ] ) {
const importDeclaration = this . imports [ name ] ;
importDeclaration . isUsed = true ;
promise = this . bundle . fetchModule ( importDeclaration . source , this . id )
. then ( module => {
importDeclaration . module = module ;
// suggest names. TODO should this apply to non default/* imports?
if ( importDeclaration . name === 'default' ) {
// TODO this seems ropey
const localName = importDeclaration . localName ;
let suggestion = this . suggestedNames [ localName ] || localName ;
// special case - the module has its own import by this name
while ( ! module . isExternal && module . imports [ suggestion ] ) {
suggestion = ` _ ${ suggestion } ` ;
}
module . suggestName ( 'default' , suggestion ) ;
// `Bundle.sort` gets stuck in an infinite loop if a module has
} else if ( importDeclaration . name === '*' ) {
// `strongDependencies` to itself. Make sure it doesn't happen.
const localName = importDeclaration . localName ;
delete strongDependencies [ this . id ] ;
const suggestion = this . suggestedNames [ localName ] || localName ;
delete weakDependencies [ this . id ] ;
module . suggestName ( '*' , suggestion ) ;
module . suggestName ( 'default' , ` ${ suggestion } __default ` ) ;
}
if ( importDeclaration . name === 'default' ) {
return { strongDependencies , weakDependencies } ;
module . needsDefault = true ;
} else if ( importDeclaration . name === '*' ) {
module . needsAll = true ;
} else {
module . needsNamed = true ;
}
}
if ( module . isExternal ) {
getModule ( source ) {
module . importedByBundle . push ( importDeclaration ) ;
return this . bundle . moduleById [ this . resolvedIds [ source ] ] ;
return emptyPromise ;
}
}
if ( importDeclaration . name === '*' ) {
// If a module is marked, enforce dynamic access of its properties.
// we need to create an internal namespace
mark ( ) {
if ( ! ~ this . bundle . internalNamespaceModules . indexOf ( module ) ) {
if ( this . needsDynamicAccess ) return ;
this . bundle . internalNamespaceModules . push ( module ) ;
this . needsDynamicAccess = true ;
}
return module . markAllExportStatemen ts ( ) ;
this . markAllExports ( ) ;
}
}
return module . markExport ( importDeclaration . name , name , this ) ;
markAllSideEffects ( ) {
this . statements . forEach ( statement => {
statement . markSideEffect ( ) ;
} ) ;
} ) ;
}
}
else {
const statement = name === 'default' ? this . exports . default . statement : this . definitions [ name ] ;
promise = statement && statement . mark ( ) ;
}
this . definitionPromises [ name ] = promise || emptyPromise ;
return this . definitionPromises [ name ] ;
}
markAllStatements ( isEntryModule ) {
markAllStatements ( isEntryModule ) {
return sequence ( this . statements , statement => {
this . statements . forEach ( statement => {
if ( statement . isIncluded ) return ; // TODO can this happen? probably not...
if ( statement . isIncluded ) return ; // TODO can this happen? probably not...
// skip import declarations...
// skip import declarations...
@ -389,87 +452,33 @@ export default class Module {
// ...unless they're empty, in which case assume we're importing them for the side-effects
// ...unless they're empty, in which case assume we're importing them for the side-effects
// THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar
// THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar
if ( ! statement . node . specifiers . length ) {
if ( ! statement . node . specifiers . length ) {
return this . bundle . fetchModule ( statement . node . source . value , this . id )
const otherModule = this . getModule ( statement . node . source . value ) ;
. then ( module => {
statement . module = module ;
if ( module . isExternal ) {
return ;
}
return module . markAllStatements ( ) ;
} ) ;
}
return ;
if ( ! otherModule . isExternal ) otherModule . markAllStatements ( ) ;
}
}
}
// skip `export { foo, bar, baz }`...
// skip `export { foo, bar, baz }`...
if ( statement . node . type === 'ExportNamedDeclaration' && statement . node . specifiers . length ) {
else if ( statement . node . type === 'ExportNamedDeclaration' && statement . node . specifiers . length ) {
// ...but ensure they are defined, if this is the entry module
// ...but ensure they are defined, if this is the entry module
if ( isEntryModule ) {
if ( isEntryModule ) statement . mark ( ) ;
return statement . mark ( ) ;
}
return ;
}
}
// include everything else
// include everything else
return statement . mark ( ) ;
else {
} ) ;
// Be sure to mark the default export for the entry module.
if ( isEntryModule && statement . node . type === 'ExportDefaultDeclaration' ) {
this . exports . lookup ( 'default' ) . mark ( ) ;
}
}
markAllExportStatements ( ) {
statement . mark ( ) ;
return sequence ( this . statements , statement => {
return statement . isExportDeclaration ?
statement . mark ( ) :
null ;
} ) ;
}
}
markExport ( name , suggestedName , importer ) {
const reexportDeclaration = this . reexports [ name ] ;
if ( reexportDeclaration ) {
reexportDeclaration . isUsed = true ;
return this . bundle . fetchModule ( reexportDeclaration . source , this . id )
. then ( otherModule => {
reexportDeclaration . module = otherModule ;
return otherModule . markExport ( reexportDeclaration . localName , suggestedName , this ) ;
} ) ;
} ) ;
}
}
const exportDeclaration = this . exports [ name ] ;
// Marks all exported identifiers.
if ( exportDeclaration ) {
markAllExports ( ) {
exportDeclaration . isUsed = true ;
this . exports . getIds ( ) . forEach ( id => id . mark ( ) ) ;
if ( name === 'default' ) {
this . needsDefault = true ;
this . suggestName ( 'default' , suggestedName ) ;
return exportDeclaration . statement . mark ( ) ;
}
return this . mark ( exportDeclaration . localName ) ;
}
const noExport = new Error ( ` Module ${ this . id } does not export ${ name } (imported by ${ importer . id } ) ` ) ;
// See if there exists an export delegate that defines `name`.
return first ( this . exportDelegates , noExport , declaration => {
return this . bundle . fetchModule ( declaration . source , this . id ) . then ( submodule => {
declaration . module = submodule ;
return submodule . mark ( name ) . then ( result => {
if ( ! result . length ) throw noExport ;
// It's found! This module exports `name` through declaration.
// It is however not imported into this scope.
this . exportAlls [ name ] = declaration ;
declaration . statement . dependsOn [ name ] =
declaration . statement . stronglyDependsOn [ name ] = result ;
return result ;
} ) ;
} ) ;
} ) ;
}
}
parse ( ast ) {
parse ( ast ) {
@ -555,11 +564,7 @@ export default class Module {
return statements ;
return statements ;
}
}
rename ( name , replacement ) {
render ( toExport , direct ) {
this . replacements [ name ] = replacement ;
}
render ( allBundleExports , moduleReplacements ) {
let magicString = this . magicString . clone ( ) ;
let magicString = this . magicString . clone ( ) ;
this . statements . forEach ( statement => {
this . statements . forEach ( statement => {
@ -577,7 +582,7 @@ export default class Module {
}
}
// skip `export var foo;` if foo is exported
// skip `export var foo;` if foo is exported
if ( isEmptyExportedVarDeclaration ( statement . node . declaration , allBundleExports , moduleReplacements ) ) {
if ( isEmptyExportedVarDeclaration ( statement . node . declaration , this . exports , toExport ) ) {
magicString . remove ( statement . start , statement . next ) ;
magicString . remove ( statement . start , statement . next ) ;
return ;
return ;
}
}
@ -585,7 +590,7 @@ export default class Module {
// skip empty var declarations for exported bindings
// skip empty var declarations for exported bindings
// (otherwise we're left with `exports.foo;`, which is useless)
// (otherwise we're left with `exports.foo;`, which is useless)
if ( isEmptyExportedVarDeclaration ( statement . node , allBundleExports , moduleReplacements ) ) {
if ( isEmptyExportedVarDeclaration ( statement . node , this . exports , toExport ) ) {
magicString . remove ( statement . start , statement . next ) ;
magicString . remove ( statement . start , statement . next ) ;
return ;
return ;
}
}
@ -593,7 +598,7 @@ export default class Module {
// split up/remove var declarations as necessary
// split up/remove var declarations as necessary
if ( statement . node . isSynthetic ) {
if ( statement . node . isSynthetic ) {
// insert `var/let/const` if necessary
// insert `var/let/const` if necessary
if ( ! allBundleExports [ statement . node . declarations [ 0 ] . id . name ] ) {
if ( ! toExport [ statement . node . declarations [ 0 ] . id . name ] ) {
magicString . insert ( statement . start , ` ${ statement . node . kind } ` ) ;
magicString . insert ( statement . start , ` ${ statement . node . kind } ` ) ;
}
}
@ -603,14 +608,34 @@ export default class Module {
let replacements = blank ( ) ;
let replacements = blank ( ) ;
let bundleExports = blank ( ) ;
let bundleExports = blank ( ) ;
// Indirect identifier access.
if ( ! direct ) {
keys ( statement . dependsOn )
. forEach ( name => {
const id = this . locals . lookup ( name ) ;
// We shouldn't create a replacement for `id` if
// 1. `id` is a Global, in which case it has no module property
// 2. `id.module` isn't external, which means we have direct access
// 3. `id` is its own module, in the case of namespace imports
if ( id . module && id . module . isExternal && id . module !== id ) {
replacements [ name ] = id . originalName === 'default' ?
// default names are always directly accessed
id . name :
// other names are indirectly accessed
` ${ id . module . name } . ${ id . originalName } ` ;
}
} ) ;
}
keys ( statement . dependsOn )
keys ( statement . dependsOn )
. concat ( keys ( statement . defines ) )
. concat ( keys ( statement . defines ) )
. forEach ( name => {
. forEach ( name => {
const bundleName = moduleReplacements [ name ] || name ;
const bundleName = this . locals . lookup ( name ) . name ;
if ( allBundleExports [ bundleName ] ) {
if ( toExport [ bundleName ] ) {
bundleExports [ name ] = replacements [ name ] = allBundleExports [ bundleName ] ;
bundleExports [ name ] = replacements [ name ] = toExport [ bundleName ] ;
} else if ( bundleName !== name ) { // TODO weird structure
} else if ( bundleName !== name && ! replacements [ name ] ) { // TODO weird structure
replacements [ name ] = bundleName ;
replacements [ name ] = bundleName ;
}
}
} ) ;
} ) ;
@ -624,6 +649,11 @@ export default class Module {
magicString . remove ( statement . node . start , statement . node . declaration . start ) ;
magicString . remove ( statement . node . start , statement . node . declaration . start ) ;
}
}
else if ( statement . node . type === 'ExportAllDeclaration' ) {
// TODO: remove once `export * from 'external'` is supported.
magicString . remove ( statement . start , statement . next ) ;
}
// remove `export` from `export class Foo {...}` or `export default Foo`
// remove `export` from `export class Foo {...}` or `export default Foo`
// TODO default exports need different treatment
// TODO default exports need different treatment
else if ( statement . node . declaration . id ) {
else if ( statement . node . declaration . id ) {
@ -631,24 +661,25 @@ export default class Module {
}
}
else if ( statement . node . type === 'ExportDefaultDeclaration' ) {
else if ( statement . node . type === 'ExportDefaultDeclaration' ) {
const canonicalName = this . defaultName ( ) ;
const def = this . exports . lookup ( 'default' ) ;
if ( statement . node . declaration . type === 'Identifier' && canonicalName === ( moduleReplacements [ statement . node . declaration . name ] || statement . node . declaration . name ) ) {
// FIXME: dunno what to do here yet.
if ( statement . node . declaration . type === 'Identifier' && def . name === ( replacements [ statement . node . declaration . name ] || statement . node . declaration . name ) ) {
magicString . remove ( statement . start , statement . next ) ;
magicString . remove ( statement . start , statement . next ) ;
return ;
return ;
}
}
// prevent `var undefined = sideEffectyDefault(foo)`
// prevent `var undefined = sideEffectyDefault(foo)`
if ( canonicalName === undefin ed ) {
if ( ! def . isUs ed ) {
magicString . remove ( statement . start , statement . node . declaration . start ) ;
magicString . remove ( statement . start , statement . node . declaration . start ) ;
return ;
return ;
}
}
// anonymous functions should be converted into declarations
// anonymous functions should be converted into declarations
if ( statement . node . declaration . type === 'FunctionExpression' ) {
if ( statement . node . declaration . type === 'FunctionExpression' ) {
magicString . overwrite ( statement . node . start , statement . node . declaration . start + 8 , ` function ${ canonicalN ame} ` ) ;
magicString . overwrite ( statement . node . start , statement . node . declaration . start + 8 , ` function ${ def . n ame} ` ) ;
} else {
} else {
magicString . overwrite ( statement . node . start , statement . node . declaration . start , ` var ${ canonicalN ame} = ` ) ;
magicString . overwrite ( statement . node . start , statement . node . declaration . start , ` var ${ def . n ame} = ` ) ;
}
}
}
}
@ -660,15 +691,4 @@ export default class Module {
return magicString . trim ( ) ;
return magicString . trim ( ) ;
}
}
suggestName ( defaultOrBatch , suggestion ) {
// deconflict anonymous default exports with this module's definitions
const shouldDeconflict = this . exports . default && this . exports . default . isAnonymous ;
if ( shouldDeconflict ) suggestion = deconflict ( suggestion , this . definitions ) ;
if ( ! this . suggestedNames [ defaultOrBatch ] ) {
this . suggestedNames [ defaultOrBatch ] = makeLegalIdentifier ( suggestion ) ;
}
}
}
}