Browse Source

Merge pull request #97 from rollup/syncify

Load all modules and syncify
contingency-plan
Rich Harris 9 years ago
parent
commit
db60e00f54
  1. 108
      src/Bundle.js
  2. 177
      src/Module.js
  3. 23
      src/Statement.js
  4. 2
      src/ast/Scope.js
  5. 42
      src/utils/promise.js
  6. 2
      test/function/export-all/_config.js
  7. 4
      test/function/tracks-alias-mutations/_config.js
  8. 6
      test/function/tracks-alias-mutations/bar.js
  9. 1
      test/function/tracks-alias-mutations/foo.js
  10. 4
      test/function/tracks-alias-mutations/main.js

108
src/Bundle.js

@ -32,7 +32,8 @@ export default class Bundle {
this.toExport = null; this.toExport = null;
this.modulePromises = blank(); this.pending = blank();
this.moduleById = blank();
this.modules = []; this.modules = [];
this.statements = null; this.statements = null;
@ -44,8 +45,11 @@ export default class Bundle {
} }
build () { build () {
return this.fetchModule( this.entry, undefined ) return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) )
.then( id => this.fetchModule( id ) )
.then( entryModule => { .then( entryModule => {
entryModule.bindImportSpecifiers();
const defaultExport = entryModule.exports.default; const defaultExport = entryModule.exports.default;
this.entryModule = entryModule; this.entryModule = entryModule;
@ -78,12 +82,8 @@ export default class Bundle {
} }
} }
return entryModule.markAllStatements( true ); entryModule.markAllStatements( true );
}) this.markAllModifierStatements();
.then( () => {
return this.markAllModifierStatements();
})
.then( () => {
this.orderedModules = this.sort(); this.orderedModules = this.sort();
}); });
} }
@ -169,26 +169,12 @@ export default class Bundle {
return allReplacements; return allReplacements;
} }
fetchModule ( importee, importer ) { fetchModule ( id ) {
return Promise.resolve( this.resolveId( importee, importer, this.resolveOptions ) ) // short-circuit cycles
.then( id => { if ( this.pending[ id ] ) return null;
if ( !id ) { this.pending[ id ] = true;
// external module
if ( !this.modulePromises[ importee ] ) {
const module = new ExternalModule( importee );
this.externalModules.push( module );
this.modulePromises[ importee ] = Promise.resolve( module );
}
return this.modulePromises[ importee ];
}
if ( id === importer ) {
throw new Error( `A module cannot import itself (${id})` );
}
if ( !this.modulePromises[ id ] ) { return Promise.resolve( this.load( id, this.loadOptions ) )
this.modulePromises[ id ] = Promise.resolve( this.load( id, this.loadOptions ) )
.then( source => { .then( source => {
let ast; let ast;
@ -205,18 +191,42 @@ export default class Bundle {
}); });
this.modules.push( module ); this.modules.push( module );
this.moduleById[ id ] = module;
return module; return this.fetchAllDependencies( module ).then( () => module );
}); });
} }
return this.modulePromises[ id ]; 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;
// external module
if ( !resolvedId ) {
if ( !this.moduleById[ source ] ) {
const module = new ExternalModule( source );
this.externalModules.push( module );
this.moduleById[ source ] = module;
}
}
else if ( resolvedId === module.id ) {
throw new Error( `A module cannot import itself (${resolvedId})` );
}
else {
return this.fetchModule( resolvedId );
}
}); });
});
return Promise.all( promises );
} }
markAllModifierStatements () { markAllModifierStatements () {
let settled = true; let settled = true;
let promises = [];
this.modules.forEach( module => { this.modules.forEach( module => {
module.statements.forEach( statement => { module.statements.forEach( statement => {
@ -233,38 +243,28 @@ export default class Bundle {
if ( shouldMark ) { if ( shouldMark ) {
settled = false; settled = false;
promises.push( statement.mark() ); statement.mark();
return; return;
} }
// special case - https://github.com/rollup/rollup/pull/40 // special case - https://github.com/rollup/rollup/pull/40
// TODO refactor this? it's a bit confusing
const importDeclaration = module.imports[ name ]; const importDeclaration = module.imports[ name ];
if ( !importDeclaration ) return; if ( !importDeclaration || importDeclaration.module.isExternal ) return;
const promise = Promise.resolve( importDeclaration.module || this.fetchModule( importDeclaration.source, module.id ) )
.then( module => {
if ( module.isExternal ) return null;
importDeclaration.module = module; const otherExportDeclaration = importDeclaration.module.exports[ importDeclaration.name ];
const exportDeclaration = module.exports[ importDeclaration.name ];
// TODO things like `export default a + b` don't apply here... right? // TODO things like `export default a + b` don't apply here... right?
return module.findDefiningStatement( exportDeclaration.localName ); const otherDefiningStatement = module.findDefiningStatement( otherExportDeclaration.localName );
})
.then( definingStatement => {
if ( !definingStatement ) return;
settled = false; if ( !otherDefiningStatement ) return;
return statement.mark();
});
promises.push( promise ); settled = false;
statement.mark();
}); });
}); });
}); });
return Promise.all( promises ).then( () => { if ( !settled ) this.markAllModifierStatements();
if ( !settled ) return this.markAllModifierStatements();
});
} }
render ( options = {} ) { render ( options = {} ) {
@ -501,14 +501,8 @@ export default class Bundle {
const exportDeclaration = module.exports[ name ]; const exportDeclaration = module.exports[ name ];
if ( exportDeclaration ) return this.trace( module, exportDeclaration.localName ); if ( exportDeclaration ) return this.trace( module, exportDeclaration.localName );
for ( let i = 0; i < module.exportDelegates.length; i += 1 ) { const exportDelegate = module.exportDelegates[ name ];
const delegate = module.exportDelegates[i]; if ( exportDelegate ) return this.traceExport( exportDelegate.module, name, es6 );
const delegateExportDeclaration = delegate.module.exports[ name ];
if ( delegateExportDeclaration ) {
return this.trace( delegate.module, delegateExportDeclaration.localName, es6 );
}
}
throw new Error( `Could not trace binding '${name}' from ${module.id}` ); throw new Error( `Could not trace binding '${name}' from ${module.id}` );
} }

177
src/Module.js

@ -1,15 +1,11 @@
import { Promise } from 'sander';
import { parse } from 'acorn'; 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 deconflict ( name, names ) { function deconflict ( name, names ) {
while ( name in names ) { while ( name in names ) {
name = `_${name}`; name = `_${name}`;
@ -52,20 +48,24 @@ export default class Module {
this.statements = this.parse( ast ); 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.imports = blank();
this.exports = blank(); this.exports = blank();
this.reexports = blank(); this.reexports = blank();
this.exportDelegates = blank();
this.exportAlls = blank(); this.exportAlls = [];
// array of all-export sources
this.exportDelegates = [];
this.replacements = blank(); this.replacements = blank();
this.varDeclarations = []; this.varDeclarations = [];
this.marked = blank();
this.definitions = blank(); this.definitions = blank();
this.definitionPromises = blank(); this.definitionPromises = blank();
this.modifications = blank(); this.modifications = blank();
@ -79,10 +79,12 @@ export default class Module {
// export { name } from './other' // export { name } from './other'
if ( source ) { if ( source ) {
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( 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({ this.exportAlls.push({
statement, statement,
source source
}); });
@ -167,6 +169,8 @@ export default class Module {
const node = statement.node; const node = statement.node;
const source = node.source.value; const source = node.source.value;
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
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';
@ -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 () { consolidateDependencies () {
let strongDependencies = blank(); let strongDependencies = blank();
@ -235,9 +267,12 @@ 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 id = this.resolvedIds[ statement.node.source.value ];
const module = this.bundle.moduleById[ id ];
if ( !module.isExternal ) strongDependencies[ module.id ] = module;
} }
else if ( statement.isReexportDeclaration ) { else if ( statement.isReexportDeclaration ) {
@ -262,7 +297,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.exportDelegates[ name ] ) ||
addDependency( strongDependencies, this.imports[ name ] ); addDependency( strongDependencies, this.imports[ name ] );
}); });
} }
@ -274,7 +309,7 @@ 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.exportDelegates[ name ] ) ||
addDependency( weakDependencies, this.imports[ name ] ); addDependency( weakDependencies, this.imports[ name ] );
}); });
}); });
@ -302,29 +337,20 @@ export default class Module {
const importDeclaration = this.imports[ name ]; const importDeclaration = this.imports[ name ];
if ( !importDeclaration ) return null; if ( !importDeclaration ) return null;
return Promise.resolve( importDeclaration.module || this.bundle.fetchModule( importDeclaration.source, this.id ) ) return importDeclaration.module.findDefiningStatement( name );
.then( module => {
importDeclaration.module = module;
return module.findDefiningStatement( name );
});
} }
mark ( name ) { mark ( name ) {
// shortcut cycles // shortcut cycles
if ( this.definitionPromises[ name ] ) { if ( this.marked[ name ] ) return;
return emptyPromise; this.marked[ name ] = true;
}
let promise;
// The definition for this name is in a different module // The definition for this name is in a different module
if ( this.imports[ name ] ) { if ( this.imports[ name ] ) {
const importDeclaration = this.imports[ name ]; const importDeclaration = this.imports[ name ];
importDeclaration.isUsed = true; importDeclaration.isUsed = true;
promise = this.bundle.fetchModule( importDeclaration.source, this.id ) const module = importDeclaration.module;
.then( module => {
importDeclaration.module = module;
// suggest names. TODO should this apply to non default/* imports? // suggest names. TODO should this apply to non default/* imports?
if ( importDeclaration.name === 'default' ) { if ( importDeclaration.name === 'default' ) {
@ -355,33 +381,30 @@ export default class Module {
if ( module.isExternal ) { if ( module.isExternal ) {
module.importedByBundle.push( importDeclaration ); module.importedByBundle.push( importDeclaration );
return emptyPromise;
} }
if ( importDeclaration.name === '*' ) { else if ( importDeclaration.name === '*' ) {
// we need to create an internal namespace // we need to create an internal namespace
if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) { if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) {
this.bundle.internalNamespaceModules.push( 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 { else {
const statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ]; 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 ) { 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,56 +412,42 @@ 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 id = this.resolvedIds[ statement.node.source.value ];
.then( module => { const otherModule = this.bundle.moduleById[ id ];
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 {
statement.mark();
}
}); });
} }
markAllExportStatements () { markAllExportStatements () {
return sequence( this.statements, statement => { this.statements.forEach( statement => {
return statement.isExportDeclaration ? if ( statement.isExportDeclaration ) statement.mark();
statement.mark() :
null;
}); });
} }
markExport ( name, suggestedName, importer ) { markExport ( name, suggestedName, importer ) {
const reexportDeclaration = this.reexports[ name ]; const reexport = this.reexports[ name ];
if ( reexportDeclaration ) { const exportDeclaration = this.exports[ name ];
reexportDeclaration.isUsed = true;
if ( reexport ) {
return this.bundle.fetchModule( reexportDeclaration.source, this.id ) reexport.isUsed = true;
.then( otherModule => { reexport.module.markExport( reexport.localName, suggestedName, this );
reexportDeclaration.module = otherModule;
return otherModule.markExport( reexportDeclaration.localName, suggestedName, this );
});
} }
const exportDeclaration = this.exports[ name ]; else if ( exportDeclaration ) {
if ( exportDeclaration ) {
exportDeclaration.isUsed = true; exportDeclaration.isUsed = true;
if ( name === 'default' ) { if ( name === 'default' ) {
this.needsDefault = true; this.needsDefault = true;
@ -446,30 +455,30 @@ export default class Module {
return exportDeclaration.statement.mark(); 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})` ); else {
// See if there exists an export delegate that defines `name`. // See if there exists an export delegate that defines `name`.
return first( this.exportDelegates, noExport, declaration => { let i;
return this.bundle.fetchModule( declaration.source, this.id ).then( submodule => { for ( i = 0; i < this.exportAlls.length; i += 1 ) {
declaration.module = submodule; const declaration = this.exportAlls[i];
return submodule.mark( name ).then( result => {
if ( !result.length ) throw noExport;
if ( declaration.module.exports[ name ] ) {
// It's found! This module exports `name` through declaration. // It's found! This module exports `name` through declaration.
// It is however not imported into this scope. // 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.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 ) { parse ( ast ) {

23
src/Statement.js

@ -1,5 +1,4 @@
import { blank, keys } from './utils/object'; import { blank, keys } from './utils/object';
import { sequence } from './utils/promise';
import getLocation from './utils/getLocation'; import getLocation from './utils/getLocation';
import walk from './ast/walk'; import walk from './ast/walk';
import Scope from './ast/Scope'; import Scope from './ast/Scope';
@ -264,26 +263,24 @@ export default class Statement {
// `export { name } from './other'` is a special case // `export { name } from './other'` is a special case
if ( this.isReexportDeclaration ) { if ( this.isReexportDeclaration ) {
return this.module.bundle.fetchModule( this.node.source.value, this.module.id ) const id = this.module.resolvedIds[ this.node.source.value ];
.then( otherModule => { const otherModule = this.module.bundle.moduleById[ id ];
return sequence( this.node.specifiers, specifier => {
this.node.specifiers.forEach( specifier => {
const reexport = this.module.reexports[ specifier.exported.name ]; const reexport = this.module.reexports[ specifier.exported.name ];
reexport.isUsed = true; reexport.isUsed = true;
reexport.module = otherModule; reexport.module = otherModule; // TODO still necessary?
return otherModule.isExternal ? if ( !otherModule.isExternal ) otherModule.markExport( specifier.local.name, specifier.exported.name, this.module );
null :
otherModule.markExport( specifier.local.name, specifier.exported.name, this.module );
});
}); });
}
const dependencies = Object.keys( this.dependsOn ); 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? if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place?
return this.module.mark( name ); this.module.mark( name );
}); });
} }

2
src/ast/Scope.js

@ -32,7 +32,7 @@ export default class Scope {
this.parent.addDeclaration( name, declaration, isVar ); this.parent.addDeclaration( name, declaration, isVar );
} else { } else {
this.declarations[ name ] = declaration; this.declarations[ name ] = declaration;
if ( isVar ) this.varDeclarations.push( name ) if ( isVar ) this.varDeclarations.push( name );
} }
} }

42
src/utils/promise.js

@ -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;
}

2
test/function/export-all/_config.js

@ -1,5 +1,3 @@
var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'allows export *' description: 'allows export *'
}; };

4
test/function/tracks-alias-mutations/_config.js

@ -0,0 +1,4 @@
module.exports = {
description: 'tracks mutations of aliased objects',
skip: true
};

6
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';

1
test/function/tracks-alias-mutations/foo.js

@ -0,0 +1 @@
export var foo = {};

4
test/function/tracks-alias-mutations/main.js

@ -0,0 +1,4 @@
import { foo } from './foo';
import { bar } from './bar';
assert.ok( foo.wasMutated );
Loading…
Cancel
Save