diff --git a/src/Bundle.js b/src/Bundle.js index 0fd7656..ffb3fc2 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -32,7 +32,8 @@ export default class Bundle { this.toExport = null; - this.modulePromises = blank(); + this.pending = blank(); + this.moduleById = blank(); this.modules = []; this.statements = null; @@ -44,8 +45,11 @@ export default class Bundle { } build () { - return this.fetchModule( this.entry, undefined ) + return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) ) + .then( id => this.fetchModule( id ) ) .then( entryModule => { + entryModule.bindImportSpecifiers(); + const defaultExport = entryModule.exports.default; this.entryModule = entryModule; @@ -78,12 +82,8 @@ export default class Bundle { } } - return entryModule.markAllStatements( true ); - }) - .then( () => { - return this.markAllModifierStatements(); - }) - .then( () => { + entryModule.markAllStatements( true ); + this.markAllModifierStatements(); this.orderedModules = this.sort(); }); } @@ -169,54 +169,64 @@ export default class Bundle { return allReplacements; } - fetchModule ( importee, importer ) { - return Promise.resolve( this.resolveId( importee, importer, this.resolveOptions ) ) - .then( id => { - if ( !id ) { - // external module - if ( !this.modulePromises[ importee ] ) { - const module = new ExternalModule( importee ); - this.externalModules.push( module ); - this.modulePromises[ importee ] = Promise.resolve( module ); - } + fetchModule ( id ) { + // short-circuit cycles + if ( this.pending[ id ] ) return null; + this.pending[ id ] = true; - return this.modulePromises[ importee ]; - } + return Promise.resolve( this.load( id, this.loadOptions ) ) + .then( source => { + let ast; - if ( id === importer ) { - throw new Error( `A module cannot import itself (${id})` ); + if ( typeof source === 'object' ) { + ast = source.ast; + source = source.code; } - if ( !this.modulePromises[ id ] ) { - this.modulePromises[ id ] = Promise.resolve( this.load( id, this.loadOptions ) ) - .then( source => { - let ast; + const module = new Module({ + id, + source, + ast, + bundle: this + }); - if ( typeof source === 'object' ) { - ast = source.ast; - source = source.code; - } + this.modules.push( module ); + this.moduleById[ id ] = module; - const module = new Module({ - id, - source, - ast, - bundle: this - }); + return this.fetchAllDependencies( module ).then( () => module ); + }); + } - this.modules.push( module ); + fetchAllDependencies ( module ) { + const promises = module.dependencies.map( source => { + return Promise.resolve( this.resolveId( source, module.id, this.resolveOptions ) ) + .then( resolvedId => { + module.resolvedIds[ source ] = resolvedId || source; - return module; - }); - } + // external module + if ( !resolvedId ) { + if ( !this.moduleById[ source ] ) { + const module = new ExternalModule( source ); + this.externalModules.push( module ); + this.moduleById[ source ] = module; + } + } - return this.modulePromises[ id ]; - }); + else if ( resolvedId === module.id ) { + throw new Error( `A module cannot import itself (${resolvedId})` ); + } + + else { + return this.fetchModule( resolvedId ); + } + }); + }); + + return Promise.all( promises ); } markAllModifierStatements () { let settled = true; - let promises = []; this.modules.forEach( module => { module.statements.forEach( statement => { @@ -233,38 +243,28 @@ export default class Bundle { if ( shouldMark ) { settled = false; - promises.push( statement.mark() ); + statement.mark(); return; } // special case - https://github.com/rollup/rollup/pull/40 + // TODO refactor this? it's a bit confusing const importDeclaration = module.imports[ name ]; - if ( !importDeclaration ) return; - - const promise = Promise.resolve( importDeclaration.module || this.fetchModule( importDeclaration.source, module.id ) ) - .then( module => { - if ( module.isExternal ) return null; - - importDeclaration.module = module; - const exportDeclaration = module.exports[ importDeclaration.name ]; - // TODO things like `export default a + b` don't apply here... right? - return module.findDefiningStatement( exportDeclaration.localName ); - }) - .then( definingStatement => { - if ( !definingStatement ) return; - - settled = false; - return statement.mark(); - }); + if ( !importDeclaration || importDeclaration.module.isExternal ) return; - promises.push( promise ); + const otherExportDeclaration = importDeclaration.module.exports[ importDeclaration.name ]; + // TODO things like `export default a + b` don't apply here... right? + const otherDefiningStatement = module.findDefiningStatement( otherExportDeclaration.localName ); + + if ( !otherDefiningStatement ) return; + + settled = false; + statement.mark(); }); }); }); - return Promise.all( promises ).then( () => { - if ( !settled ) return this.markAllModifierStatements(); - }); + if ( !settled ) this.markAllModifierStatements(); } render ( options = {} ) { @@ -501,14 +501,8 @@ export default class Bundle { const exportDeclaration = module.exports[ name ]; if ( exportDeclaration ) return this.trace( module, exportDeclaration.localName ); - for ( let i = 0; i < module.exportDelegates.length; i += 1 ) { - const delegate = module.exportDelegates[i]; - const delegateExportDeclaration = delegate.module.exports[ name ]; - - if ( delegateExportDeclaration ) { - return this.trace( delegate.module, delegateExportDeclaration.localName, es6 ); - } - } + const exportDelegate = module.exportDelegates[ name ]; + if ( exportDelegate ) return this.traceExport( exportDelegate.module, name, es6 ); throw new Error( `Could not trace binding '${name}' from ${module.id}` ); } diff --git a/src/Module.js b/src/Module.js index 9964ae2..1e79c68 100644 --- a/src/Module.js +++ b/src/Module.js @@ -1,15 +1,11 @@ -import { Promise } from 'sander'; import { parse } from 'acorn'; import MagicString from 'magic-string'; import Statement from './Statement'; import walk from './ast/walk'; import { blank, keys } from './utils/object'; -import { first, sequence } from './utils/promise'; import getLocation from './utils/getLocation'; import makeLegalIdentifier from './utils/makeLegalIdentifier'; -const emptyPromise = Promise.resolve(); - function deconflict ( name, names ) { while ( name in names ) { name = `_${name}`; @@ -52,20 +48,24 @@ export default class Module { this.statements = this.parse( ast ); - // imports and exports, indexed by ID + // all dependencies + this.dependencies = []; + this.resolvedIds = blank(); + this.boundImportSpecifiers = false; + + // imports and exports, indexed by local name this.imports = blank(); this.exports = blank(); this.reexports = blank(); + this.exportDelegates = blank(); - this.exportAlls = blank(); - - // array of all-export sources - this.exportDelegates = []; + this.exportAlls = []; this.replacements = blank(); this.varDeclarations = []; + this.marked = blank(); this.definitions = blank(); this.definitionPromises = blank(); this.modifications = blank(); @@ -79,10 +79,12 @@ export default class Module { // export { name } from './other' if ( source ) { + if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source ); + if ( node.type === 'ExportAllDeclaration' ) { // Store `export * from '...'` statements in an array of delegates. // When an unknown import is encountered, we see if one of them can satisfy it. - this.exportDelegates.push({ + this.exportAlls.push({ statement, source }); @@ -167,6 +169,8 @@ export default class Module { const node = statement.node; const source = node.source.value; + if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source ); + node.specifiers.forEach( specifier => { const isDefault = specifier.type === 'ImportDefaultSpecifier'; const isNamespace = specifier.type === 'ImportNamespaceSpecifier'; @@ -224,6 +228,34 @@ export default class Module { }); } + bindImportSpecifiers () { + if ( this.boundImportSpecifiers ) return; + this.boundImportSpecifiers = true; + + [ this.imports, this.reexports ].forEach( specifiers => { + keys( specifiers ).forEach( name => { + const specifier = specifiers[ name ]; + + if ( specifier.module ) return; + + const id = this.resolvedIds[ specifier.source ]; + specifier.module = this.bundle.moduleById[ id ]; + }); + }); + + this.exportAlls.forEach( delegate => { + const id = this.resolvedIds[ delegate.source ]; + delegate.module = this.bundle.moduleById[ id ]; + }); + + this.dependencies.forEach( source => { + const id = this.resolvedIds[ source ]; + const module = this.bundle.moduleById[ id ]; + + if ( !module.isExternal ) module.bindImportSpecifiers(); + }); + } + consolidateDependencies () { let strongDependencies = blank(); @@ -235,9 +267,12 @@ export default class Module { } 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 - strongDependencies[ statement.module.id ] = statement.module; // TODO is this right? `statement.module` should be `this`, surely? + const id = this.resolvedIds[ statement.node.source.value ]; + const module = this.bundle.moduleById[ id ]; + + if ( !module.isExternal ) strongDependencies[ module.id ] = module; } else if ( statement.isReexportDeclaration ) { @@ -262,7 +297,7 @@ export default class Module { keys( statement.stronglyDependsOn ).forEach( name => { if ( statement.defines[ name ] ) return; - addDependency( strongDependencies, this.exportAlls[ name ] ) || + addDependency( strongDependencies, this.exportDelegates[ name ] ) || addDependency( strongDependencies, this.imports[ name ] ); }); } @@ -274,7 +309,7 @@ export default class Module { keys( statement.dependsOn ).forEach( name => { if ( statement.defines[ name ] ) return; - addDependency( weakDependencies, this.exportAlls[ name ] ) || + addDependency( weakDependencies, this.exportDelegates[ name ] ) || addDependency( weakDependencies, this.imports[ name ] ); }); }); @@ -302,86 +337,74 @@ export default class Module { const importDeclaration = this.imports[ name ]; if ( !importDeclaration ) return null; - return Promise.resolve( importDeclaration.module || this.bundle.fetchModule( importDeclaration.source, this.id ) ) - .then( module => { - importDeclaration.module = module; - return module.findDefiningStatement( name ); - }); + return importDeclaration.module.findDefiningStatement( name ); } mark ( name ) { // shortcut cycles - if ( this.definitionPromises[ name ] ) { - return emptyPromise; - } - - let promise; + if ( this.marked[ name ] ) return; + this.marked[ name ] = true; // 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; + const module = importDeclaration.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; + // 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}`; - } + // special case - the module has its own import by this name + while ( !module.isExternal && module.imports[ suggestion ] ) { + suggestion = `_${suggestion}`; + } - module.suggestName( 'default', suggestion ); - } else if ( importDeclaration.name === '*' ) { - const localName = importDeclaration.localName; - const suggestion = this.suggestedNames[ localName ] || localName; - module.suggestName( '*', suggestion ); - module.suggestName( 'default', `${suggestion}__default` ); - } + module.suggestName( 'default', suggestion ); + } else if ( importDeclaration.name === '*' ) { + const localName = importDeclaration.localName; + const suggestion = this.suggestedNames[ localName ] || localName; + module.suggestName( '*', suggestion ); + module.suggestName( 'default', `${suggestion}__default` ); + } - if ( importDeclaration.name === 'default' ) { - module.needsDefault = true; - } else if ( importDeclaration.name === '*' ) { - module.needsAll = true; - } else { - module.needsNamed = true; - } + if ( importDeclaration.name === 'default' ) { + module.needsDefault = true; + } else if ( importDeclaration.name === '*' ) { + module.needsAll = true; + } else { + module.needsNamed = true; + } - if ( module.isExternal ) { - module.importedByBundle.push( importDeclaration ); - return emptyPromise; - } + if ( module.isExternal ) { + module.importedByBundle.push( importDeclaration ); + } - if ( importDeclaration.name === '*' ) { - // we need to create an internal namespace - if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) { - this.bundle.internalNamespaceModules.push( module ); - } + else if ( importDeclaration.name === '*' ) { + // we need to create an internal namespace + if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) { + this.bundle.internalNamespaceModules.push( module ); + } - return module.markAllExportStatements(); - } + module.markAllExportStatements(); + } - return module.markExport( importDeclaration.name, name, this ); - }); + else { + module.markExport( importDeclaration.name, name, this ); + } } else { const statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ]; - promise = statement && statement.mark(); + if ( statement ) statement.mark(); } - - this.definitionPromises[ name ] = promise || emptyPromise; - return this.definitionPromises[ name ]; } markAllStatements ( isEntryModule ) { - return sequence( this.statements, statement => { + this.statements.forEach( statement => { if ( statement.isIncluded ) return; // TODO can this happen? probably not... // skip import declarations... @@ -389,56 +412,42 @@ export default class Module { // ...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 if ( !statement.node.specifiers.length ) { - return this.bundle.fetchModule( statement.node.source.value, this.id ) - .then( module => { - statement.module = module; - if ( module.isExternal ) { - return; - } - return module.markAllStatements(); - }); - } + const id = this.resolvedIds[ statement.node.source.value ]; + const otherModule = this.bundle.moduleById[ id ]; - return; + if ( !otherModule.isExternal ) otherModule.markAllStatements(); + } } // 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 - if ( isEntryModule ) { - return statement.mark(); - } - - return; + if ( isEntryModule ) statement.mark(); } // include everything else - return statement.mark(); + else { + statement.mark(); + } }); } markAllExportStatements () { - return sequence( this.statements, statement => { - return statement.isExportDeclaration ? - statement.mark() : - null; + this.statements.forEach( statement => { + if ( statement.isExportDeclaration ) statement.mark(); }); } 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 reexport = this.reexports[ name ]; + const exportDeclaration = this.exports[ name ]; + + if ( reexport ) { + reexport.isUsed = true; + reexport.module.markExport( reexport.localName, suggestedName, this ); } - const exportDeclaration = this.exports[ name ]; - if ( exportDeclaration ) { + else if ( exportDeclaration ) { exportDeclaration.isUsed = true; if ( name === 'default' ) { this.needsDefault = true; @@ -446,30 +455,30 @@ export default class Module { return exportDeclaration.statement.mark(); } - return this.mark( exportDeclaration.localName ); + 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; + else { + // See if there exists an export delegate that defines `name`. + let i; + for ( i = 0; i < this.exportAlls.length; i += 1 ) { + const declaration = this.exportAlls[i]; + if ( declaration.module.exports[ name ] ) { // It's found! This module exports `name` through declaration. // It is however not imported into this scope. - this.exportAlls[ name ] = declaration; + this.exportDelegates[ name ] = declaration; + declaration.module.markExport( name ); declaration.statement.dependsOn[ name ] = - declaration.statement.stronglyDependsOn[ name ] = result; + declaration.statement.stronglyDependsOn[ name ] = true; - return result; - }); - }); - }); + return; + } + } + + throw new Error( `Module ${this.id} does not export ${name} (imported by ${importer.id})` ); + } } parse ( ast ) { diff --git a/src/Statement.js b/src/Statement.js index b68d216..27b70d8 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -1,5 +1,4 @@ import { blank, keys } from './utils/object'; -import { sequence } from './utils/promise'; import getLocation from './utils/getLocation'; import walk from './ast/walk'; import Scope from './ast/Scope'; @@ -264,26 +263,24 @@ export default class Statement { // `export { name } from './other'` is a special case if ( this.isReexportDeclaration ) { - return this.module.bundle.fetchModule( this.node.source.value, this.module.id ) - .then( otherModule => { - return sequence( this.node.specifiers, specifier => { - const reexport = this.module.reexports[ specifier.exported.name ]; + const id = this.module.resolvedIds[ this.node.source.value ]; + const otherModule = this.module.bundle.moduleById[ id ]; - reexport.isUsed = true; - reexport.module = otherModule; + this.node.specifiers.forEach( specifier => { + const reexport = this.module.reexports[ specifier.exported.name ]; - return otherModule.isExternal ? - null : - otherModule.markExport( specifier.local.name, specifier.exported.name, this.module ); - }); - }); - } + reexport.isUsed = true; + reexport.module = otherModule; // TODO still necessary? - const dependencies = Object.keys( this.dependsOn ); + if ( !otherModule.isExternal ) otherModule.markExport( specifier.local.name, specifier.exported.name, this.module ); + }); + + return; + } - return sequence( dependencies, name => { + Object.keys( this.dependsOn ).forEach( name => { if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place? - return this.module.mark( name ); + this.module.mark( name ); }); } diff --git a/src/ast/Scope.js b/src/ast/Scope.js index 6cbc6ce..aeb9da7 100644 --- a/src/ast/Scope.js +++ b/src/ast/Scope.js @@ -32,7 +32,7 @@ export default class Scope { this.parent.addDeclaration( name, declaration, isVar ); } else { this.declarations[ name ] = declaration; - if ( isVar ) this.varDeclarations.push( name ) + if ( isVar ) this.varDeclarations.push( name ); } } diff --git a/src/utils/promise.js b/src/utils/promise.js deleted file mode 100644 index e37489d..0000000 --- a/src/utils/promise.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Promise } from 'sander'; - -export function sequence ( arr, callback ) { - const len = arr.length; - let results = new Array( len ); - - let promise = Promise.resolve(); - - function next ( i ) { - return promise - .then( () => callback( arr[i], i ) ) - .then( result => results[i] = result ); - } - - let i; - - for ( i = 0; i < len; i += 1 ) { - promise = next( i ); - } - - return promise.then( () => results ); -} - - -export function first ( arr, fail, callback ) { - const len = arr.length; - - let promise = Promise.reject( fail ); - - function next ( i ) { - return promise - .catch(() => callback( arr[i], i )); - } - - let i; - - for ( i = 0; i < len; i += 1 ) { - promise = next( i ); - } - - return promise; -} diff --git a/test/function/export-all/_config.js b/test/function/export-all/_config.js index 24639c6..8060316 100644 --- a/test/function/export-all/_config.js +++ b/test/function/export-all/_config.js @@ -1,5 +1,3 @@ -var assert = require( 'assert' ); - module.exports = { description: 'allows export *' }; diff --git a/test/function/tracks-alias-mutations/_config.js b/test/function/tracks-alias-mutations/_config.js new file mode 100644 index 0000000..c3a0619 --- /dev/null +++ b/test/function/tracks-alias-mutations/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'tracks mutations of aliased objects', + skip: true +}; diff --git a/test/function/tracks-alias-mutations/bar.js b/test/function/tracks-alias-mutations/bar.js new file mode 100644 index 0000000..4ec5140 --- /dev/null +++ b/test/function/tracks-alias-mutations/bar.js @@ -0,0 +1,6 @@ +import { foo } from './foo'; + +var f = foo; +f.wasMutated = true; + +export var bar = 'whatever'; diff --git a/test/function/tracks-alias-mutations/foo.js b/test/function/tracks-alias-mutations/foo.js new file mode 100644 index 0000000..386f265 --- /dev/null +++ b/test/function/tracks-alias-mutations/foo.js @@ -0,0 +1 @@ +export var foo = {}; diff --git a/test/function/tracks-alias-mutations/main.js b/test/function/tracks-alias-mutations/main.js new file mode 100644 index 0000000..7cf7ee2 --- /dev/null +++ b/test/function/tracks-alias-mutations/main.js @@ -0,0 +1,4 @@ +import { foo } from './foo'; +import { bar } from './bar'; + +assert.ok( foo.wasMutated );