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.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,26 +169,12 @@ 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 );
}
return this.modulePromises[ importee ];
}
if ( id === importer ) {
throw new Error( `A module cannot import itself (${id})` );
}
fetchModule ( id ) {
// short-circuit cycles
if ( this.pending[ id ] ) return null;
this.pending[ id ] = true;
if ( !this.modulePromises[ id ] ) {
this.modulePromises[ id ] = Promise.resolve( this.load( id, this.loadOptions ) )
return Promise.resolve( this.load( id, this.loadOptions ) )
.then( source => {
let ast;
@ -205,18 +191,42 @@ export default class Bundle {
});
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 () {
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;
if ( !importDeclaration || importDeclaration.module.isExternal ) return;
importDeclaration.module = module;
const exportDeclaration = module.exports[ importDeclaration.name ];
const otherExportDeclaration = importDeclaration.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;
const otherDefiningStatement = module.findDefiningStatement( otherExportDeclaration.localName );
settled = false;
return statement.mark();
});
if ( !otherDefiningStatement ) return;
promises.push( promise );
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}` );
}

177
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,29 +337,20 @@ 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' ) {
@ -355,33 +381,30 @@ export default class Module {
if ( module.isExternal ) {
module.importedByBundle.push( importDeclaration );
return emptyPromise;
}
if ( importDeclaration.name === '*' ) {
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})` );
else {
// 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;
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 ) {

23
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 id = this.module.resolvedIds[ this.node.source.value ];
const otherModule = this.module.bundle.moduleById[ id ];
this.node.specifiers.forEach( specifier => {
const reexport = this.module.reexports[ specifier.exported.name ];
reexport.isUsed = true;
reexport.module = otherModule;
reexport.module = otherModule; // TODO still necessary?
return otherModule.isExternal ?
null :
otherModule.markExport( specifier.local.name, specifier.exported.name, this.module );
});
if ( !otherModule.isExternal ) 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?
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 );
} else {
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 = {
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