Browse Source

Merge pull request #99 from Victorystick/scope2

RFC: Use Scopes for Bundles and Modules
gh-109
Rich Harris 9 years ago
parent
commit
16a38a2564
  1. 1
      .babelrc
  2. 6
      .eslintrc
  3. 4
      package.json
  4. 265
      src/Bundle.js
  5. 61
      src/ExternalModule.js
  6. 489
      src/Module.js
  7. 152
      src/Scope.js
  8. 182
      src/Statement.js
  9. 22
      src/finalisers/cjs.js
  10. 40
      src/finalisers/es6.js
  11. 22
      src/finalisers/shared/getExportBlock.js
  12. 11
      src/finalisers/shared/getInteropBlock.js
  13. 2
      src/utils/getExportMode.js
  14. 5
      src/utils/makeLegalIdentifier.js
  15. 2
      test/form/external-imports/_expected/cjs.js
  16. 4
      test/form/external-imports/_expected/es6.js
  17. 8
      test/form/internal-conflict-resolution/_expected/amd.js
  18. 8
      test/form/internal-conflict-resolution/_expected/cjs.js
  19. 8
      test/form/internal-conflict-resolution/_expected/es6.js
  20. 8
      test/form/internal-conflict-resolution/_expected/iife.js
  21. 8
      test/form/internal-conflict-resolution/_expected/umd.js
  22. 3
      test/form/namespace-optimization/_config.js
  23. 7
      test/form/namespace-optimization/_expected/amd.js
  24. 5
      test/form/namespace-optimization/_expected/cjs.js
  25. 3
      test/form/namespace-optimization/_expected/es6.js
  26. 7
      test/form/namespace-optimization/_expected/iife.js
  27. 11
      test/form/namespace-optimization/_expected/umd.js
  28. 3
      test/form/namespace-optimization/bar.js
  29. 3
      test/form/namespace-optimization/foo.js
  30. 3
      test/form/namespace-optimization/main.js
  31. 1
      test/form/namespace-optimization/quux.js
  32. 3
      test/function/dynamic-namespace-lookup/_config.js
  33. 2
      test/function/dynamic-namespace-lookup/foo.js
  34. 8
      test/function/dynamic-namespace-lookup/main.js
  35. 3
      test/function/shorthand-properties/baz.js
  36. 5
      test/function/shorthand-properties/foo.js
  37. 5
      test/function/shorthand-properties/main.js
  38. 7
      test/sourcemaps/names/_config.js
  39. 9
      test/test.js
  40. 120
      test/testScope.js

1
.babelrc

@ -5,6 +5,7 @@
"es6.classes",
"es6.constants",
"es6.destructuring",
"es6.modules",
"es6.parameters",
"es6.properties.shorthand",
"es6.spread",

6
.eslintrc

@ -1,15 +1,19 @@
{
"rules": {
"indent": [ 2, "tab", { "SwitchCase": 1}],
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
"quotes": [ 2, "single" ],
"linebreak-style": [ 2, "unix" ],
"semi": [ 2, "always" ],
"space-after-keywords": [ 2, "always" ],
"space-before-blocks": [ 2, "always" ],
"space-before-function-paren": [ 2, "always" ],
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
"no-cond-assign": [ 0 ]
},
"env": {
"es6": true,
"browser": true,
"mocha": true,
"node": true
},
"extends": "eslint:recommended",

4
package.json

@ -26,12 +26,16 @@
"optimizer"
],
"author": "Rich Harris",
"contributors": [
"Oskar Segersvärd <victorystick@gmail.com>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/rich-harris/rollup/issues"
},
"homepage": "https://github.com/rich-harris/rollup",
"devDependencies": {
"babel": "^5.8.21",
"babel-core": "^5.5.8",
"console-group": "^0.1.2",
"eslint": "^1.1.0",

265
src/Bundle.js

@ -1,17 +1,16 @@
import { basename, extname } from './utils/path';
import { Promise } from 'sander';
import MagicString from 'magic-string';
import { blank, keys } from './utils/object';
import Module from './Module';
import ExternalModule from './ExternalModule';
import finalisers from './finalisers/index';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
import ensureArray from './utils/ensureArray';
import { defaultResolver, defaultExternalResolver } from './utils/resolveId';
import { defaultLoader } from './utils/load';
import getExportMode from './utils/getExportMode';
import getIndentString from './utils/getIndentString';
import { unixizePath } from './utils/normalizePlatform.js';
import Scope from './Scope';
export default class Bundle {
constructor ( options ) {
@ -30,6 +29,18 @@ export default class Bundle {
transform: ensureArray( options.transform )
};
// The global scope, and the bundle's internal scope.
this.globals = new Scope();
this.scope = new Scope( this.globals );
// TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
// However, the deconfliction logic is greatly simplified by being the same for all formats.
this.globals.define( 'exports' );
this.scope.bind( 'exports', this.globals.reference( 'exports' ) );
// Alias for entryModule.exports.
this.exports = null;
this.toExport = null;
this.pending = blank();
@ -39,134 +50,37 @@ export default class Bundle {
this.statements = null;
this.externalModules = [];
this.internalNamespaceModules = [];
this.assumedGlobals = blank();
this.assumedGlobals.exports = true; // TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
}
build () {
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;
if ( defaultExport ) {
entryModule.needsDefault = true;
// `export default function foo () {...}` -
// use the declared name for the export
if ( defaultExport.identifier ) {
entryModule.suggestName( 'default', defaultExport.identifier );
}
// `export default a + b` - generate an export name
// based on the id of the entry module
else {
let defaultExportName = makeLegalIdentifier( basename( this.entryModule.id ).slice( 0, -extname( this.entryModule.id ).length ) );
// deconflict
let topLevelNames = [];
entryModule.statements.forEach( statement => {
keys( statement.defines ).forEach( name => topLevelNames.push( name ) );
});
while ( ~topLevelNames.indexOf( defaultExportName ) ) {
defaultExportName = `_${defaultExportName}`;
}
entryModule.suggestName( 'default', defaultExportName );
}
}
this.exports = entryModule.exports;
entryModule.markAllStatements( true );
this.markAllModifierStatements();
this.orderedModules = this.sort();
});
}
// TODO would be better to deconflict once, rather than per-render
deconflict ( es6 ) {
let usedNames = blank();
// ensure no conflicts with globals
keys( this.assumedGlobals ).forEach( name => usedNames[ name ] = true );
let allReplacements = blank();
// Assign names to external modules
this.externalModules.forEach( module => {
// while we're here...
allReplacements[ module.id ] = blank();
// TODO is this necessary in the ES6 case?
let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id );
module.name = getSafeName( name );
});
// Discover conflicts (i.e. two statements in separate modules both define `foo`)
let i = this.orderedModules.length;
while ( i-- ) {
const module = this.orderedModules[i];
// while we're here...
allReplacements[ module.id ] = blank();
keys( module.definitions ).forEach( name => {
const safeName = getSafeName( name );
if ( safeName !== name ) {
module.rename( name, safeName );
allReplacements[ module.id ][ name ] = safeName;
}
});
}
// Assign non-conflicting names to internal default/namespace export
this.orderedModules.forEach( module => {
if ( !module.needsDefault && !module.needsAll ) return;
if ( module.needsAll ) {
const namespaceName = getSafeName( module.suggestedNames[ '*' ] );
module.replacements[ '*' ] = namespaceName;
}
if ( module.needsDefault || module.needsAll && module.exports.default ) {
const defaultExport = module.exports.default;
// only create a new name if either
// a) it's an expression (`export default 42`) or
// b) it's a name that is reassigned to (`export var a = 1; a = 2`)
if ( defaultExport && defaultExport.identifier && !defaultExport.isModified ) return; // TODO encapsulate check for whether we need synthetic default name
this.exports.localIds().forEach( ([ , id ]) => {
// If the export is a module (namespace), we need
// all its exports dynamically accessible.
if ( id.module === id ) id.dynamicAccess();
});
const defaultName = getSafeName( module.suggestedNames.default );
module.replacements.default = defaultName;
}
});
// As a last step, deconflict all identifier names, once.
this.scope.deconflict();
this.orderedModules.forEach( module => {
keys( module.imports ).forEach( localName => {
if ( !module.imports[ localName ].isUsed ) return;
// Alias the default import to the external module named
// for external modules that don't need named imports.
this.externalModules.forEach( module => {
const externalDefault = module.exports.lookup( 'default' );
const bundleName = this.trace( module, localName, es6 );
if ( bundleName !== localName ) {
allReplacements[ module.id ][ localName ] = bundleName;
}
if ( externalDefault && !( module.needsNamed || module.needsAll ) ) {
externalDefault.name = module.name;
}
});
});
});
function getSafeName ( name ) {
while ( usedNames[ name ] ) {
name = `_${name}`;
}
usedNames[ name ] = true;
return name;
}
return allReplacements;
}
fetchModule ( id ) {
@ -193,7 +107,13 @@ export default class Bundle {
this.modules.push( module );
this.moduleById[ id ] = module;
return this.fetchAllDependencies( module ).then( () => module );
return this.fetchAllDependencies( module ).then( () => {
// Analyze the module once all its dependencies have been resolved.
// This means that any dependencies of a module has already been
// analysed when it's time for the module itself.
module.analyse();
return module;
});
});
}
@ -206,7 +126,7 @@ export default class Bundle {
// external module
if ( !resolvedId ) {
if ( !this.moduleById[ source ] ) {
const module = new ExternalModule( source );
const module = new ExternalModule( { id: source, bundle: this } );
this.externalModules.push( module );
this.moduleById[ source ] = module;
}
@ -225,51 +145,8 @@ export default class Bundle {
return Promise.all( promises );
}
markAllModifierStatements () {
let settled = true;
this.modules.forEach( module => {
module.statements.forEach( statement => {
if ( statement.isIncluded ) return;
keys( statement.modifies ).forEach( name => {
const definingStatement = module.definitions[ name ];
const exportDeclaration = module.exports[ name ] || module.reexports[ name ] || (
module.exports.default && module.exports.default.identifier === name && module.exports.default
);
const shouldMark = ( definingStatement && definingStatement.isIncluded ) ||
( exportDeclaration && exportDeclaration.isUsed );
if ( shouldMark ) {
settled = false;
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 || importDeclaration.module.isExternal ) return;
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();
});
});
});
if ( !settled ) this.markAllModifierStatements();
}
render ( options = {} ) {
const format = options.format || 'es6';
const allReplacements = this.deconflict( format === 'es6' );
// Determine export mode - 'default', 'named', 'none'
const exportMode = getExportMode( this, options.exports );
@ -296,15 +173,14 @@ export default class Bundle {
this.orderedModules.forEach( module => {
module.reassignments.forEach( name => {
isReassignedVarDeclaration[ module.replacements[ name ] || name ] = true;
isReassignedVarDeclaration[ module.locals.lookup( name ).name ] = true;
});
});
if ( format !== 'es6' && exportMode === 'named' ) {
keys( this.entryModule.exports )
.concat( keys( this.entryModule.reexports ) )
this.exports.getNames()
.forEach( name => {
const canonicalName = this.traceExport( this.entryModule, name );
const canonicalName = this.exports.lookup( name ).name;
if ( isReassignedVarDeclaration[ canonicalName ] ) {
varExports[ name ] = true;
@ -322,14 +198,13 @@ export default class Bundle {
// since we're rewriting variable exports, we want to
// ensure we don't try and export them again at the bottom
this.toExport = keys( this.entryModule.exports )
.concat( keys( this.entryModule.reexports ) )
this.toExport = this.exports.getNames()
.filter( key => !varExports[ key ] );
let magicString = new MagicString.Bundle({ separator: '\n\n' });
this.orderedModules.forEach( module => {
const source = module.render( allBundleExports, allReplacements[ module.id ], format );
const source = module.render( allBundleExports, format === 'es6' );
if ( source.toString().length ) {
magicString.addSource( source );
}
@ -337,15 +212,12 @@ export default class Bundle {
// prepend bundle with internal namespaces
const indentString = getIndentString( magicString, options );
const namespaceBlock = this.internalNamespaceModules.map( module => {
const exports = keys( module.exports )
.concat( keys( module.reexports ) )
.map( name => {
const canonicalName = this.traceExport( module, name );
return `${indentString}get ${name} () { return ${canonicalName}; }`;
});
const exports = module.exports.localIds().map( ( [ name, id ] ) =>
`${indentString}get ${name} () { return ${id.name}; }`);
return `var ${module.replacements['*']} = {\n` +
return `var ${module.name} = {\n` +
exports.join( ',\n' ) +
`\n};\n\n`;
}).join( '' );
@ -390,12 +262,17 @@ export default class Bundle {
}
sort () {
let seen = {};
// Set of visited module ids.
let seen = blank();
let ordered = [];
let hasCycles;
let strongDeps = {};
let stronglyDependsOn = {};
// Map from module id to list of modules.
let strongDeps = blank();
// Map from module id to boolean.
let stronglyDependsOn = blank();
function visit ( module ) {
seen[ module.id ] = true;
@ -472,38 +349,4 @@ export default class Bundle {
return ordered;
}
trace ( module, localName, es6 ) {
const importDeclaration = module.imports[ localName ];
// defined in this module
if ( !importDeclaration ) return module.replacements[ localName ] || localName;
// defined elsewhere
return this.traceExport( importDeclaration.module, importDeclaration.name, es6 );
}
traceExport ( module, name, es6 ) {
if ( module.isExternal ) {
if ( name === 'default' ) return module.needsNamed && !es6 ? `${module.name}__default` : module.name;
if ( name === '*' ) return module.name;
return es6 ? name : `${module.name}.${name}`;
}
const reexportDeclaration = module.reexports[ name ];
if ( reexportDeclaration ) {
return this.traceExport( reexportDeclaration.module, reexportDeclaration.localName );
}
if ( name === '*' ) return module.replacements[ '*' ];
if ( name === 'default' ) return module.defaultName();
const exportDeclaration = module.exports[ name ];
if ( exportDeclaration ) return this.trace( module, exportDeclaration.localName );
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}` );
}
}

61
src/ExternalModule.js

@ -1,16 +1,36 @@
import { blank } from './utils/object';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
// An external identifier.
class Id {
constructor ( module, name ) {
this.originalName = this.name = name;
this.module = module;
this.modifierStatements = [];
}
// Flags the identifier as imported by the bundle when marked.
mark () {
this.module.importedByBundle[ this.originalName ] = true;
this.modifierStatements.forEach( stmt => stmt.mark() );
}
}
export default class ExternalModule {
constructor ( id ) {
constructor ( { id, bundle } ) {
this.id = id;
this.name = null;
this.isExternal = true;
this.importedByBundle = [];
// Implement `Identifier` interface.
this.originalName = this.name = makeLegalIdentifier( id );
this.module = this;
this.isModule = true;
this.suggestedNames = blank();
// Define the external module's name in the bundle scope.
bundle.scope.define( id, this );
this.needsDefault = false;
this.isExternal = true;
this.importedByBundle = blank();
// Invariant: needsNamed and needsAll are never both true at once.
// Because an import with both a namespace and named import is invalid:
@ -19,19 +39,28 @@ export default class ExternalModule {
//
this.needsNamed = false;
this.needsAll = false;
}
findDefiningStatement () {
return null;
}
this.exports = bundle.scope.virtual( false );
const { reference } = this.exports;
// Override reference.
this.exports.reference = name => {
if ( name !== 'default' ) {
this.needsNamed = true;
}
if ( !this.exports.defines( name ) ) {
this.exports.define( name, new Id( this, name ) );
}
rename () {
// noop
return reference.call( this.exports, name );
};
}
suggestName ( exportName, suggestion ) {
if ( !this.suggestedNames[ exportName ] ) {
this.suggestedNames[ exportName ] = suggestion;
}
// External modules are always marked for inclusion in the bundle.
// Marking an external module signals its use as a namespace.
mark () {
this.needsAll = true;
}
}

489
src/Module.js

@ -1,3 +1,4 @@
import { basename, extname } from './utils/path';
import { parse } from 'acorn';
import MagicString from 'magic-string';
import Statement from './Statement';
@ -6,21 +7,45 @@ import { blank, keys } from './utils/object';
import getLocation from './utils/getLocation';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
function deconflict ( name, names ) {
while ( name in names ) {
name = `_${name}`;
function isEmptyExportedVarDeclaration ( node, exports, toExport ) {
if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false;
const name = node.declarations[0].id.name;
const id = exports.lookup( name );
return id && id.name in toExport;
}
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 );
}
}
return name;
function assign ( target, source ) {
for ( var key in source ) target[ key ] = source[ key ];
}
function isEmptyExportedVarDeclaration ( node, allBundleExports, moduleReplacements ) {
if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false;
class Id {
constructor ( module, name, statement ) {
this.originalName = this.name = name;
this.module = module;
this.statement = statement;
const name = node.declarations[0].id.name;
const canonicalName = moduleReplacements[ name ] || name;
this.modifierStatements = [];
// modifiers
this.isUsed = false;
}
return canonicalName in allBundleExports;
mark () {
this.isUsed = true;
this.statement.mark();
this.modifierStatements.forEach( stmt => stmt.mark() );
}
}
export default class Module {
@ -29,6 +54,16 @@ export default class Module {
this.bundle = bundle;
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
// can change that, but it makes sense to use it for the source filename
@ -36,41 +71,58 @@ export default class Module {
filename: id
});
// remove existing sourceMappingURL comments
const pattern = /\/\/#\s+sourceMappingURL=.+\n?/g;
let match;
while ( match = pattern.exec( source ) ) {
this.magicString.remove( match.index, match.index + match[0].length );
}
removeSourceMappingURLComments( source, this.magicString );
this.suggestedNames = blank();
this.comments = [];
this.statements = this.parse( ast );
// 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();
// Virtual scopes for the local and exported names.
this.locals = bundle.scope.virtual( true );
this.exports = bundle.scope.virtual( false );
this.exportAlls = [];
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 exportAlls
for ( let i = 0; i < this.exportAlls.length; i += 1 ) {
const module = this.exportAlls[i];
if ( module.exports.inScope( name ) ) {
return module.exports.reference( name );
}
}
this.replacements = blank();
// 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;
return this.exportAlls.some( module => module.exports.inScope( name ) );
};
// Create a unique virtual scope for references to the module.
// const unique = bundle.scope.virtual();
// unique.define( this.name, this );
// this.reference = unique.reference( this.name );
this.exportAlls = [];
this.reassignments = [];
this.marked = blank();
this.definitions = blank();
this.definitionPromises = blank();
this.modifications = blank();
// TODO: change to false, and detect when it's necessary.
this.needsDynamicAccess = false;
this.analyse();
this.dependencies = this.collectDependencies();
}
addExport ( statement ) {
@ -79,24 +131,24 @@ export default class Module {
// export { name } from './other'
if ( source ) {
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
const module = this.getModule( 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.exportAlls.push({
statement,
source
});
if ( module.isExternal ) {
throw new Error( `Cannot trace 'export *' references through external modules.` );
}
this.exportAlls.push( module );
}
else {
node.specifiers.forEach( specifier => {
this.reexports[ specifier.exported.name ] = {
source,
localName: specifier.local.name,
module: null // filled in later
};
// Bind the export of this module, to the export of the other.
this.exports.bind( specifier.exported.name,
module.exports.reference( specifier.local.name ) );
});
}
}
@ -113,16 +165,18 @@ export default class Module {
node.declaration.type === 'Identifier' ?
node.declaration.name :
null;
const name = identifier || this.name;
// Always define a new `Identifier` for the default export.
const id = new Id( this, name, statement );
this.exports.default = {
statement,
name: 'default',
localName: identifier || 'default',
identifier,
isDeclaration,
isAnonymous,
isModified: false // in case of `export default foo; foo = somethingElse`
};
// Keep the identifier name, if one exists.
// We can optimize the newly created default `Identifier` away,
// if it is never modified.
// in case of `export default foo; foo = somethingElse`
assign( id, { isDeclaration, isAnonymous, identifier } );
this.exports.define( 'default', id );
}
// export { foo, bar, baz }
@ -135,11 +189,7 @@ export default class Module {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports[ exportedName ] = {
statement,
localName,
exportedName
};
this.exports.bind( exportedName, this.locals.reference( localName ) );
});
}
@ -156,40 +206,49 @@ export default class Module {
name = declaration.id.name;
}
this.exports[ name ] = {
statement,
localName: name,
expression: declaration
};
this.locals.define( name, new Id( this, name, statement ) );
this.exports.bind( name, this.locals.reference( name ) );
}
}
}
addImport ( statement ) {
const node = statement.node;
const source = node.source.value;
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
const module = this.getModule( node.source.value );
node.specifiers.forEach( specifier => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
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}'` );
err.file = this.id;
err.loc = getLocation( this.source, specifier.start );
throw err;
}
this.imports[ localName ] = {
source,
name,
localName
};
if ( isNamespace ) {
// If it's a namespace import, we bind the localName to the module itself.
module.needsAll = true;
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';
}
}
});
}
@ -203,11 +262,7 @@ export default class Module {
// consolidate names that are defined/modified in this module
keys( statement.defines ).forEach( name => {
this.definitions[ name ] = statement;
});
keys( statement.modifies ).forEach( name => {
( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement );
this.locals.define( name, new Id( this, name, statement ) );
});
});
@ -236,39 +291,39 @@ export default class Module {
});
keys( statement.dependsOn ).forEach( name => {
if ( !this.definitions[ name ] && !this.imports[ name ] ) {
this.bundle.assumedGlobals[ name ] = true;
// For each name we depend on that isn't in scope,
// 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 ) );
}
});
});
}
bindImportSpecifiers () {
if ( this.boundImportSpecifiers ) return;
this.boundImportSpecifiers = true;
[ this.imports, this.reexports ].forEach( specifiers => {
keys( specifiers ).forEach( name => {
const specifier = specifiers[ 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 ) );
}
}
if ( specifier.module ) return;
// Returns the set of imported module ids by going through all import/exports statements.
collectDependencies () {
const importedModules = blank();
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.statements.forEach( statement => {
if ( statement.isImportDeclaration || ( statement.isExportDeclaration && statement.node.source ) ) {
importedModules[ statement.node.source.value ] = true;
}
});
this.dependencies.forEach( source => {
const id = this.resolvedIds[ source ];
const module = this.bundle.moduleById[ id ];
if ( !module.isExternal ) module.bindImportSpecifiers();
});
return keys( importedModules );
}
consolidateDependencies () {
@ -284,8 +339,7 @@ export default class Module {
this.statements.forEach( statement => {
if ( statement.isImportDeclaration && !statement.node.specifiers.length ) {
// include module for its side-effects
const id = this.resolvedIds[ statement.node.source.value ];
const module = this.bundle.moduleById[ id ];
const module = this.getModule( statement.node.source.value );
if ( !module.isExternal ) strongDependencies[ module.id ] = module;
}
@ -293,17 +347,11 @@ export default class Module {
else if ( statement.isReexportDeclaration ) {
if ( statement.node.specifiers ) {
statement.node.specifiers.forEach( specifier => {
let reexport;
let module = this;
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 );
});
}
}
@ -312,8 +360,7 @@ export default class Module {
keys( statement.stronglyDependsOn ).forEach( name => {
if ( statement.defines[ name ] ) return;
addDependency( strongDependencies, this.exportDelegates[ name ] ) ||
addDependency( strongDependencies, this.imports[ name ] );
addDependency( strongDependencies, this.locals.lookup( name ) );
});
}
});
@ -324,98 +371,47 @@ export default class Module {
keys( statement.dependsOn ).forEach( name => {
if ( statement.defines[ name ] ) return;
addDependency( weakDependencies, this.exportDelegates[ name ] ) ||
addDependency( weakDependencies, this.imports[ name ] );
addDependency( weakDependencies, this.locals.lookup( name ) );
});
});
return { strongDependencies, weakDependencies };
}
this.locals.getNames().forEach( name => {
const id = this.locals.lookup( name );
defaultName () {
const defaultExport = this.exports.default;
if ( !id.modifierStatements ) return;
if ( !defaultExport ) return null;
id.modifierStatements.forEach( statement => {
const module = statement.module;
weakDependencies[ module.id ] = module;
});
});
const name = defaultExport.identifier && !defaultExport.isModified ?
defaultExport.identifier :
this.replacements.default;
// `Bundle.sort` gets stuck in an infinite loop if a module has
// `strongDependencies` to itself. Make sure it doesn't happen.
delete strongDependencies[ this.id ];
delete weakDependencies[ this.id ];
return this.replacements[ name ] || name;
return { strongDependencies, weakDependencies };
}
findDefiningStatement ( name ) {
if ( this.definitions[ name ] ) return this.definitions[ name ];
// Enforce dynamic access of the module's properties.
dynamicAccess () {
if ( this.needsDynamicAccess ) return;
// TODO what about `default`/`*`?
this.needsDynamicAccess = true;
this.markAllExportStatements();
const importDeclaration = this.imports[ name ];
if ( !importDeclaration ) return null;
return importDeclaration.module.findDefiningStatement( name );
if ( !~this.bundle.internalNamespaceModules.indexOf( this ) ) {
this.bundle.internalNamespaceModules.push( this );
}
}
mark ( name ) {
// shortcut cycles
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;
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;
// 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` );
}
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 );
}
else if ( importDeclaration.name === '*' ) {
// we need to create an internal namespace
if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) {
this.bundle.internalNamespaceModules.push( module );
}
module.markAllExportStatements();
}
else {
module.markExport( importDeclaration.name, name, this );
}
}
getModule ( source ) {
return this.bundle.moduleById[ this.resolvedIds[ source ] ];
}
else {
const statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ];
if ( statement ) statement.mark();
}
mark () {
this.dynamicAccess();
}
markAllStatements ( isEntryModule ) {
@ -427,8 +423,7 @@ 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 ) {
const id = this.resolvedIds[ statement.node.source.value ];
const otherModule = this.bundle.moduleById[ id ];
const otherModule = this.getModule( statement.node.source.value );
if ( !otherModule.isExternal ) otherModule.markAllStatements();
}
@ -442,6 +437,11 @@ export default class Module {
// include everything else
else {
// Be sure to mark the default export for the entry module.
if ( isEntryModule && statement.node.type === 'ExportDefaultDeclaration' ) {
this.exports.lookup( 'default' ).mark();
}
statement.mark();
}
});
@ -453,49 +453,6 @@ export default class Module {
});
}
markExport ( name, suggestedName, importer ) {
const reexport = this.reexports[ name ];
const exportDeclaration = this.exports[ name ];
if ( reexport ) {
reexport.isUsed = true;
reexport.module.markExport( reexport.localName, suggestedName, this );
}
else if ( exportDeclaration ) {
exportDeclaration.isUsed = true;
if ( name === 'default' ) {
this.needsDefault = true;
this.suggestName( 'default', suggestedName );
return exportDeclaration.statement.mark();
}
this.mark( exportDeclaration.localName );
}
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.exportDelegates[ name ] = declaration;
declaration.module.markExport( name );
declaration.statement.dependsOn[ name ] =
declaration.statement.stronglyDependsOn[ name ] = true;
return;
}
}
throw new Error( `Module ${this.id} does not export ${name} (imported by ${importer.id})` );
}
}
parse ( ast ) {
// The ast can be supplied programmatically (but usually won't be)
if ( !ast ) {
@ -579,11 +536,7 @@ export default class Module {
return statements;
}
rename ( name, replacement ) {
this.replacements[ name ] = replacement;
}
render ( allBundleExports, moduleReplacements ) {
render ( toExport, direct ) {
let magicString = this.magicString.clone();
this.statements.forEach( statement => {
@ -601,7 +554,7 @@ export default class Module {
}
// 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 );
return;
}
@ -609,7 +562,7 @@ export default class Module {
// skip empty var declarations for exported bindings
// (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 );
return;
}
@ -617,7 +570,7 @@ export default class Module {
// split up/remove var declarations as necessary
if ( statement.node.isSynthetic ) {
// 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} ` );
}
@ -627,14 +580,34 @@ export default class Module {
let replacements = 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 )
.concat( keys( statement.defines ) )
.forEach( name => {
const bundleName = moduleReplacements[ name ] || name;
const bundleName = this.locals.lookup( name ).name;
if ( allBundleExports[ bundleName ] ) {
bundleExports[ name ] = replacements[ name ] = allBundleExports[ bundleName ];
} else if ( bundleName !== name ) { // TODO weird structure
if ( toExport[ bundleName ] ) {
bundleExports[ name ] = replacements[ name ] = toExport[ bundleName ];
} else if ( bundleName !== name && !replacements[ name ] ) { // TODO weird structure
replacements[ name ] = bundleName;
}
});
@ -655,24 +628,25 @@ export default class Module {
}
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 );
return;
}
// prevent `var undefined = sideEffectyDefault(foo)`
if ( canonicalName === undefined ) {
if ( !def.isUsed ) {
magicString.remove( statement.start, statement.node.declaration.start );
return;
}
// anonymous functions should be converted into declarations
if ( statement.node.declaration.type === 'FunctionExpression' ) {
magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${canonicalName}` );
magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${def.name}` );
} else {
magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${canonicalName} = ` );
magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${def.name} = ` );
}
}
@ -684,15 +658,4 @@ export default class Module {
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 );
}
}
}

152
src/Scope.js

@ -0,0 +1,152 @@
import { blank, keys } from './utils/object';
// A minimal `Identifier` implementation. Anything that has an `originalName`,
// and a mutable `name` property can be used as an `Identifier`.
function Identifier ( name ) {
this.originalName = this.name = name;
}
// A reference to an `Identifier`.
function Reference ( scope, index ) {
this.scope = scope;
this.index = index;
}
// Dereferences a `Reference`.
function dereference ( ref ) {
return ref.scope.ids[ ref.index ];
}
function isntReference ( id ) {
return !( id instanceof Reference );
}
// Prefix the argument with '_'.
function underscorePrefix ( x ) {
return '_' + x;
}
// ## Scope
// A Scope is a mapping from string names to `Identifiers`.
export default class Scope {
constructor ( parent ) {
this.ids = [];
this.names = blank();
this.parent = parent || null;
this.used = blank();
}
// Binds the `name` to the given reference `ref`.
bind ( name, ref ) {
this.ids[ this.index( name ) ] = ref;
}
// Deconflict all names within the scope,
// using the given renaming function.
// If no function is supplied, `underscorePrefix` is used.
deconflict ( rename = underscorePrefix ) {
const names = this.used;
this.ids.filter( ref => ref instanceof Reference ).forEach( ref => {
// Same scope.
if ( ref.scope.ids === this.ids ) return;
// Another scope!
while ( ref instanceof Reference ) {
ref = dereference( ref );
}
names[ ref.name ] = ref;
});
this.ids.filter( isntReference ).forEach( id => {
if ( typeof id === 'string' ) {
throw new Error( `Required name "${id}" is undefined!` );
}
let name = id.name;
while ( name in names && names[ name ] !== id ) {
name = rename( name );
}
names[ name ] = id;
id.name = name;
});
}
// Defines `name` in the scope to be `id`.
// If no `id` is supplied, a plain `Identifier` is created.
define ( name, id ) {
this.ids[ this.index( name ) ] = id || new Identifier( name );
}
// TODO: rename! Too similar to `define`.
defines ( name ) {
return name in this.names;
}
// Return the names referenced to in the scope.
getNames () {
return keys( this.names );
}
// *private, don't use*
//
// Return `name`'s index in the `ids` array if it exists,
// otherwise returns the index to a new placeholder slot.
index ( name ) {
if ( !( name in this.names ) ) {
return this.names[ name ] = this.ids.push( name ) - 1;
}
return this.names[ name ];
}
// Returns true if `name` is in Scope.
inScope ( name ) {
if ( name in this.names ) return true;
return this.parent ? this.parent.inScope( name ) : false;
}
// Returns a list of `[ name, identifier ]` tuples.
localIds () {
return keys( this.names ).map( name => [ name, this.lookup( name ) ] );
}
// Lookup the identifier referred to by `name`.
lookup ( name ) {
if ( !( name in this.names ) && this.parent ) {
return this.parent.lookup( name );
}
let id = this.ids[ this.names[ name ] ];
while ( id instanceof Reference ) {
id = dereference( id );
}
return id;
}
// Get a reference to the identifier `name` in this scope.
reference ( name ) {
return new Reference( this, this.index( name ) );
}
// Return the used names of the scope.
// Names aren't considered used unless they're deconflicted.
usedNames () {
return keys( this.used ).sort();
}
// Create and return a virtual `Scope` instance, bound to
// the actual scope of `this`, optionally inherit the parent scope.
virtual ( inheritParent ) {
const scope = new Scope( inheritParent ? this.parent : null );
scope.ids = this.ids;
return scope;
}
}

182
src/Statement.js

@ -15,6 +15,14 @@ function isFunctionDeclaration ( node, parent ) {
if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true;
}
function chainedMemberExpression ( node ) {
if ( node.object.type === 'MemberExpression' ) {
return chainedMemberExpression( node.object ) + '.' + node.property.name;
}
return node.object.name + '.' + node.property.name;
}
export default class Statement {
constructor ( node, module, start, end ) {
this.node = node;
@ -25,12 +33,15 @@ export default class Statement {
this.scope = new Scope();
this.defines = blank();
this.modifies = blank();
this.dependsOn = blank();
this.stronglyDependsOn = blank();
this.reassigns = blank();
// TODO: make this more efficient
this.dependantIds = [];
this.namespaceReplacements = [];
this.isIncluded = false;
this.isImportDeclaration = node.type === 'ImportDeclaration';
@ -41,6 +52,19 @@ export default class Statement {
analyse () {
if ( this.isImportDeclaration ) return; // nothing to analyse
// `export { name } from './other'` is a special case
if ( this.isReexportDeclaration ) {
this.node.specifiers && this.node.specifiers.forEach( specifier => {
const id = this.module.exports.lookup( specifier.exported.name );
if ( !~this.dependantIds.indexOf( id ) ) {
this.dependantIds.push( id );
}
});
return;
}
let scope = this.scope;
walk( this.node, {
@ -126,6 +150,11 @@ export default class Statement {
// /update expressions) need to be captured
let writeDepth = 0;
// Used to track
let topName;
let currentMemberExpression = null;
let namespace = null;
if ( !this.isImportDeclaration ) {
walk( this.node, {
enter: ( node, parent ) => {
@ -142,6 +171,85 @@ export default class Statement {
if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
if ( node._scope ) scope = scope.parent;
// Optimize namespace lookups, which manifest as MemberExpressions.
if ( node.type === 'MemberExpression' && ( !currentMemberExpression || node.object === currentMemberExpression ) ) {
currentMemberExpression = node;
if ( !namespace ) {
topName = node.object.name;
const id = this.module.locals.lookup( topName );
if ( !id || !id.isModule || id.isExternal ) return;
namespace = id;
}
// If a namespace is the left hand side of an assignment, throw an error.
if ( parent.type === 'AssignmentExpression' && parent.left === node ||
parent.type === 'UpdateExpression' && parent.argument === node ) {
const err = new Error( `Illegal reassignment to import '${chainedMemberExpression( node )}'` );
err.file = this.module.id;
err.loc = getLocation( this.module.magicString.toString(), node.start );
throw err;
}
// Extract the name of the accessed property, from and Identifier or Literal.
// Any eventual Literal value is converted to a string.
const name = !node.computed ? node.property.name :
( node.property.type === 'Literal' ? String( node.property.value ) : null );
// If we can't resolve the name being accessed statically,
// we require the namespace to be dynamically accessible.
//
// // resolvable
// console.log( javascript.keywords.for )
// console.log( javascript.keywords[ 'for' ] )
// console.log( javascript.keywords[ 6 ] )
//
// // unresolvable
// console.log( javascript.keywords[ index ] )
// console.log( javascript.keywords[ 1 + 5 ] )
if ( name === null ) {
namespace.dynamicAccess();
namespace = null;
currentMemberExpression = null;
return;
}
const id = namespace.exports.lookup( name );
// If the namespace doesn't define the given name,
// we can throw an error (even for nested namespaces).
if ( !id ) {
throw new Error( `Module doesn't define "${name}"!` );
}
// We can't resolve deeper. Replace the member chain.
if ( parent.type !== 'MemberExpression' || !( id.isModule && !id.isExternal ) ) {
if ( !~this.dependantIds.indexOf( id ) ) {
this.dependantIds.push( id );
}
// FIXME: do this better
// If we depend on this name...
if ( this.dependsOn[ topName ] ) {
// ... decrement the count...
if ( !--this.dependsOn[ topName ] ) {
// ... and remove it if the count is 0.
delete this.dependsOn[ topName ];
}
}
this.namespaceReplacements.push( [ node, id ] );
namespace = null;
currentMemberExpression = null;
return;
}
namespace = id;
}
}
});
}
@ -172,7 +280,11 @@ export default class Statement {
const definingScope = scope.findDefiningScope( node.name );
if ( !definingScope || definingScope.depth === 0 ) {
this.dependsOn[ node.name ] = true;
if ( !( node.name in this.dependsOn ) ) {
this.dependsOn[ node.name ] = 0;
}
this.dependsOn[ node.name ]++;
if ( strong ) this.stronglyDependsOn[ node.name ] = true;
}
}
@ -189,9 +301,9 @@ export default class Statement {
// disallow assignments/updates to imported bindings and namespaces
if ( isAssignment ) {
const importSpecifier = this.module.imports[ node.name ];
const importSpecifier = this.module.locals.lookup( node.name );
if ( importSpecifier && !scope.contains( node.name ) ) {
if ( importSpecifier && importSpecifier.module !== this.module && !scope.contains( node.name ) ) {
const minDepth = importSpecifier.name === '*' ?
2 : // cannot do e.g. `namespace.foo = bar`
1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
@ -207,11 +319,12 @@ export default class Statement {
// special case = `export default foo; foo += 1;` - we'll
// need to assign a new variable so that the exported
// value is not updated by the second statement
if ( this.module.exports.default && depth === 0 && this.module.exports.default.identifier === node.name ) {
const def = this.module.exports.lookup( 'default' );
if ( def && depth === 0 && def.name === node.name ) {
// but only if this is a) inside a function body or
// b) after the export declaration
if ( !!scope.parent || node.start > this.module.exports.default.statement.node.start ) {
this.module.exports.default.isModified = true;
if ( !!scope.parent || node.start > def.statement.node.start ) {
def.isModified = true;
}
}
@ -234,7 +347,11 @@ export default class Statement {
// anything (but we still need to call checkForWrites to
// catch illegal reassignments to imported bindings)
if ( writeDepth === 0 && node.type === 'Identifier' ) {
this.modifies[ node.name ] = true;
const id = this.module.locals.lookup( node.name );
if ( id && id.modifierStatements && !~id.modifierStatements.indexOf( this ) ) {
id.modifierStatements.push( this );
}
}
};
@ -260,31 +377,19 @@ export default class Statement {
if ( this.isIncluded ) return; // prevent infinite loops
this.isIncluded = true;
// `export { name } from './other'` is a special case
if ( this.isReexportDeclaration ) {
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; // TODO still necessary?
if ( !otherModule.isExternal ) otherModule.markExport( specifier.local.name, specifier.exported.name, this.module );
});
return;
}
this.dependantIds.forEach( id => id.mark() );
Object.keys( this.dependsOn ).forEach( name => {
// TODO: perhaps these could also be added?
keys( this.dependsOn ).forEach( name => {
if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place?
this.module.mark( name );
this.module.locals.lookup( name ).mark();
});
}
replaceIdentifiers ( magicString, names, bundleExports ) {
const replacementStack = [ names ];
const statement = this;
const replacementStack = [];
const nameList = keys( names );
let deshadowList = [];
@ -313,10 +418,12 @@ export default class Statement {
if ( node.type === 'VariableDeclaration' ) {
// if this contains a single declarator, and it's one that
// needs to be rewritten, we replace the whole lot
const name = node.declarations[0].id.name;
const id = node.declarations[0].id;
const name = id.name;
if ( node.declarations.length === 1 && bundleExports[ name ] ) {
magicString.overwrite( node.start, node.declarations[0].id.end, bundleExports[ name ], true );
node.declarations[0].id._skip = true;
magicString.overwrite( node.start, id.end, bundleExports[ name ], true );
id._skip = true;
}
// otherwise, we insert the `exports.foo = foo` after the declaration
@ -369,6 +476,18 @@ export default class Statement {
replacementStack.push( newNames );
}
if ( node.type === 'MemberExpression' ) {
const replacements = statement.namespaceReplacements;
for ( let i = 0; i < replacements.length; i += 1 ) {
const [ top, id ] = replacements[ i ];
if ( node === top ) {
magicString.overwrite( node.start, node.end, id.name );
return this.skip();
}
}
}
if ( node.type !== 'Identifier' ) return;
// if there's no replacement, or it's the same, there's nothing more to do
@ -399,8 +518,7 @@ export default class Statement {
if ( /^Function/.test( node.type ) ) depth -= 1;
if ( node._scope ) {
replacementStack.pop();
names = replacementStack[ replacementStack.length - 1 ];
names = replacementStack.pop();
}
}
});

22
src/finalisers/cjs.js

@ -1,21 +1,19 @@
import getInteropBlock from './shared/getInteropBlock';
import getExportBlock from './shared/getExportBlock';
export default function cjs ( bundle, magicString, { exportMode }, options ) {
let intro = options.useStrict === false ? `` : `'use strict';\n\n`;
// TODO handle empty imports, once they're supported
const importBlock = bundle.externalModules
.map( module => {
let requireStatement = `var ${module.name} = require('${module.id}');`;
if ( module.needsDefault ) {
requireStatement += '\n' + ( module.needsNamed ? `var ${module.name}__default = ` : `${module.name} = ` ) +
`'default' in ${module.name} ? ${module.name}['default'] : ${module.name};`;
}
return requireStatement;
})
.join( '\n' );
let importBlock = bundle.externalModules
.map( module => `var ${module.name} = require('${module.id}');`)
.join('\n');
const interopBlock = getInteropBlock( bundle );
if ( interopBlock ) {
importBlock += '\n' + interopBlock;
}
if ( importBlock ) {
intro += importBlock + '\n\n';

40
src/finalisers/es6.js

@ -1,13 +1,14 @@
import { blank, keys } from '../utils/object';
import { keys } from '../utils/object';
function uniqueNames ( declarations ) {
let uniques = blank();
function specifiersFor ( externalModule ) {
return keys( externalModule.importedByBundle )
.filter( notDefault )
.sort()
.map( name => {
const id = externalModule.exports.lookup( name );
declarations
.filter( declaration => !/^(default|\*)$/.test( declaration.name ) )
.forEach( declaration => uniques[ declaration.name ] = true );
return keys( uniques );
return name !== id.name ? `${name} as ${id.name}` : name;
});
}
function notDefault ( name ) {
@ -19,19 +20,18 @@ export default function es6 ( bundle, magicString ) {
.map( module => {
const specifiers = [];
if ( module.needsDefault ) {
specifiers.push( module.importedByBundle.filter( declaration =>
declaration.name === 'default' )[0].localName );
const id = module.exports.lookup( 'default' );
if ( id ) {
specifiers.push( id.name );
}
if ( module.needsAll ) {
specifiers.push( '* as ' + module.importedByBundle.filter( declaration =>
declaration.name === '*' )[0].localName );
specifiers.push( '* as ' + module.name );
}
if ( module.needsNamed ) {
specifiers.push( '{ ' + uniqueNames( module.importedByBundle )
.join( ', ' ) + ' }' );
specifiers.push( '{ ' + specifiersFor( module ).join( ', ' ) + ' }' );
}
return specifiers.length ?
@ -47,18 +47,18 @@ export default function es6 ( bundle, magicString ) {
const module = bundle.entryModule;
const specifiers = bundle.toExport.filter( notDefault ).map( name => {
const canonicalName = bundle.traceExport( module, name );
const id = bundle.exports.lookup( name );
return canonicalName === name ?
return id.name === name ?
name :
`${canonicalName} as ${name}`;
`${id.name} as ${name}`;
});
let exportBlock = specifiers.length ? `export { ${specifiers.join(', ')} };` : '';
const defaultExport = module.exports.default || module.reexports.default;
const defaultExport = module.exports.lookup( 'default' );
if ( defaultExport ) {
exportBlock += `export default ${bundle.traceExport(module,'default')};`;
exportBlock += `export default ${ defaultExport.name };`;
}
if ( exportBlock ) {

22
src/finalisers/shared/getExportBlock.js

@ -1,18 +1,24 @@
function wrapAccess ( id ) {
return ( id.originalName !== 'default' && id.module && id.module.isExternal ) ?
id.module.name + propertyAccess( id.originalName ) : id.name;
}
function propertyAccess ( name ) {
return name === 'default' ? `['default']` : `.${name}`;
}
export default function getExportBlock ( bundle, exportMode, mechanism = 'return' ) {
if ( exportMode === 'default' ) {
const defaultExport = bundle.entryModule.exports.default;
const defaultExportName = bundle.entryModule.replacements.default ||
defaultExport.identifier;
const id = bundle.exports.lookup( 'default' );
return `${mechanism} ${defaultExportName};`;
return `${mechanism} ${wrapAccess( id )};`;
}
return bundle.toExport
.map( name => {
const prop = name === 'default' ? `['default']` : `.${name}`;
name = bundle.traceExport( bundle.entryModule, name );
return `exports${prop} = ${name};`;
const id = bundle.exports.lookup( name );
return `exports${propertyAccess( name )} = ${wrapAccess( id )};`;
})
.join( '\n' );
}

11
src/finalisers/shared/getInteropBlock.js

@ -1,11 +1,12 @@
export default function getInteropBlock ( bundle ) {
return bundle.externalModules
.map( module => {
return module.needsDefault ?
( module.needsNamed ?
`var ${module.name}__default = 'default' in ${module.name} ? ${module.name}['default'] : ${module.name};` :
`${module.name} = 'default' in ${module.name} ? ${module.name}['default'] : ${module.name};` ) :
null;
const def = module.exports.lookup( 'default' );
if ( !def ) return;
return ( module.needsNamed ? 'var ' : '' ) +
`${def.name} = 'default' in ${module.name} ? ${module.name}['default'] : ${module.name};`;
})
.filter( Boolean )
.join( '\n' );

2
src/utils/getExportMode.js

@ -5,7 +5,7 @@ function badExports ( option, keys ) {
}
export default function getExportMode ( bundle, exportMode ) {
const exportKeys = keys( bundle.entryModule.exports ).concat( keys( bundle.entryModule.reexports ) );
const exportKeys = keys( bundle.entryModule.exports.names );
if ( exportMode === 'default' ) {
if ( exportKeys.length !== 1 || exportKeys[0] !== 'default' ) {

5
src/utils/makeLegalIdentifier.js

@ -8,7 +8,10 @@ reservedWords.concat( builtins ).forEach( word => blacklisted[ word ] = true );
export default function makeLegalIdentifier ( str ) {
str = str.replace( /[^$_a-zA-Z0-9]/g, '_' );
str = str
.replace( /-(\w)/g, ( _, letter ) => letter.toUpperCase() )
.replace( /[^$_a-zA-Z0-9]/g, '_' );
if ( /\d/.test( str[0] ) || blacklisted[ str ] ) str = `_${str}`;
return str;

2
test/form/external-imports/_expected/cjs.js

@ -1,10 +1,10 @@
'use strict';
var factory = require('factory');
factory = 'default' in factory ? factory['default'] : factory;
var baz = require('baz');
var containers = require('shipping-port');
var alphabet = require('alphabet');
factory = 'default' in factory ? factory['default'] : factory;
var alphabet__default = 'default' in alphabet ? alphabet['default'] : alphabet;
factory( null );

4
test/form/external-imports/_expected/es6.js

@ -1,10 +1,10 @@
import factory from 'factory';
import { bar, foo } from 'baz';
import * as containers from 'shipping-port';
import alphabet, { a } from 'alphabet';
import alphabet__default, { a } from 'alphabet';
factory( null );
foo( bar );
containers.forEach( console.log, console );
console.log( a );
console.log( alphabet.length );
console.log( alphabet__default.length );

8
test/form/internal-conflict-resolution/_expected/amd.js

@ -1,15 +1,15 @@
define(function () { 'use strict';
var _bar = 42;
var bar = 42;
function foo () {
return _bar;
return bar;
}
function bar () {
function _bar () {
alert( foo() );
}
bar();
_bar();
});

8
test/form/internal-conflict-resolution/_expected/cjs.js

@ -1,13 +1,13 @@
'use strict';
var _bar = 42;
var bar = 42;
function foo () {
return _bar;
return bar;
}
function bar () {
function _bar () {
alert( foo() );
}
bar();
_bar();

8
test/form/internal-conflict-resolution/_expected/es6.js

@ -1,11 +1,11 @@
var _bar = 42;
var bar = 42;
function foo () {
return _bar;
return bar;
}
function bar () {
function _bar () {
alert( foo() );
}
bar();
_bar();

8
test/form/internal-conflict-resolution/_expected/iife.js

@ -1,15 +1,15 @@
(function () { 'use strict';
var _bar = 42;
var bar = 42;
function foo () {
return _bar;
return bar;
}
function bar () {
function _bar () {
alert( foo() );
}
bar();
_bar();
})();

8
test/form/internal-conflict-resolution/_expected/umd.js

@ -4,16 +4,16 @@
factory();
}(this, function () { 'use strict';
var _bar = 42;
var bar = 42;
function foo () {
return _bar;
return bar;
}
function bar () {
function _bar () {
alert( foo() );
}
bar();
_bar();
}));

3
test/form/namespace-optimization/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'it does static lookup optimization of internal namespaces'
};

7
test/form/namespace-optimization/_expected/amd.js

@ -0,0 +1,7 @@
define(function () { 'use strict';
function a () {}
a();
});

5
test/form/namespace-optimization/_expected/cjs.js

@ -0,0 +1,5 @@
'use strict';
function a () {}
a();

3
test/form/namespace-optimization/_expected/es6.js

@ -0,0 +1,3 @@
function a () {}
a();

7
test/form/namespace-optimization/_expected/iife.js

@ -0,0 +1,7 @@
(function () { 'use strict';
function a () {}
a();
})();

11
test/form/namespace-optimization/_expected/umd.js

@ -0,0 +1,11 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
factory();
}(this, function () { 'use strict';
function a () {}
a();
}));

3
test/form/namespace-optimization/bar.js

@ -0,0 +1,3 @@
import * as quux from './quux';
export { quux };

3
test/form/namespace-optimization/foo.js

@ -0,0 +1,3 @@
import * as bar from './bar';
export { bar };

3
test/form/namespace-optimization/main.js

@ -0,0 +1,3 @@
import * as foo from './foo';
foo.bar.quux.a();

1
test/form/namespace-optimization/quux.js

@ -0,0 +1 @@
export function a () {}

3
test/function/dynamic-namespace-lookup/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'does namespace optimization when possible, but not for dynamic lookups'
};

2
test/function/dynamic-namespace-lookup/foo.js

@ -0,0 +1,2 @@
export var bar = 'bar';
export var baz = 'baz';

8
test/function/dynamic-namespace-lookup/main.js

@ -0,0 +1,8 @@
import * as foo from './foo';
var bar = 'baz';
assert.equal( foo.bar, 'bar' );
assert.equal( foo.baz, 'baz' );
assert.equal( foo[ bar ], 'baz' );

3
test/function/shorthand-properties/baz.js

@ -0,0 +1,3 @@
export default function bar () {
return 'main-bar';
}

5
test/function/shorthand-properties/foo.js

@ -1,7 +1,10 @@
import baz from './baz.js';
function bar () {
return 'foo-bar';
}
export var foo = {
bar
bar,
baz
};

5
test/function/shorthand-properties/main.js

@ -1,8 +1,5 @@
import bar from './baz.js';
import { foo } from './foo';
function bar () {
return 'main-bar';
}
assert.equal( bar(), 'main-bar' );
assert.equal( foo.bar(), 'foo-bar' );

7
test/sourcemaps/names/_config.js

@ -1,4 +1,3 @@
var path = require( 'path' );
var assert = require( 'assert' );
var getLocation = require( '../../utils/getLocation' );
var SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;
@ -12,18 +11,18 @@ module.exports = {
var match = /Object\.create\( ([^\.]+)\.prototype/.exec( code );
var deconflictedName = match[1];
if ( deconflictedName === 'Foo' ) throw new Error( 'Need to update this test!' );
if ( deconflictedName !== 'Foo' ) throw new Error( 'Need to update this test!' );
var smc = new SourceMapConsumer( map );
var index = code.indexOf( deconflictedName );
var generatedLoc = getLocation( code, index );
var originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.name, 'Foo' );
assert.equal( originalLoc.name, null );
index = code.indexOf( deconflictedName, index + 1 );
generatedLoc = getLocation( code, index );
originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.name, 'Bar' );
assert.equal( originalLoc.name, 'Foo' );
}
};

9
test/test.js

@ -6,7 +6,6 @@ var sander = require( 'sander' );
var assert = require( 'assert' );
var exec = require( 'child_process' ).exec;
var babel = require( 'babel-core' );
var sequence = require( './utils/promiseSequence' );
var rollup = require( '../dist/rollup' );
var FUNCTION = path.resolve( __dirname, 'function' );
@ -164,13 +163,11 @@ describe( 'rollup', function () {
entry: FORM + '/' + dir + '/main.js'
});
var bundlePromise = rollup.rollup( options );
PROFILES.forEach( function ( profile ) {
( config.skip ? it.skip : config.solo ? it.only : it )( 'generates ' + profile.format, function () {
if ( config.solo ) console.group( dir );
return bundlePromise.then( function ( bundle ) {
return rollup.rollup( options ).then( function ( bundle ) {
var options = extend( {}, config.options, {
dest: FORM + '/' + dir + '/_actual/' + profile.format + '.js',
format: profile.format
@ -219,11 +216,9 @@ describe( 'rollup', function () {
entry: SOURCEMAPS + '/' + dir + '/main.js'
});
var bundlePromise = rollup.rollup( options );
PROFILES.forEach( function ( profile ) {
( config.skip ? it.skip : config.solo ? it.only : it )( 'generates ' + profile.format, function () {
return bundlePromise.then( function ( bundle ) {
return rollup.rollup( options ).then( function ( bundle ) {
var options = extend( {}, config.options, {
format: profile.format,
sourceMap: true,

120
test/testScope.js

@ -0,0 +1,120 @@
require('babel/register');
var assert = require( 'assert' );
var Scope = require( '../src/Scope' );
describe( 'Scope', function () {
it( 'can define and bind names', function () {
const scope = new Scope();
// If I define 'a'...
scope.define( 'a' );
// ... and bind 'b' to a reference to 'a'...
scope.bind( 'b', scope.reference( 'a' ) );
// ... lookups for 'a' and 'b' should both
// resolve to the same identifier.
assert.equal( scope.lookup( 'b' ), scope.lookup( 'a' ) );
});
describe( 'parent:', function () {
var parent = new Scope(),
child = new Scope( parent );
it( 'allows children access to its names', function () {
parent.define( 'a' );
assert.equal( child.lookup( 'a' ), parent.lookup( 'a' ) );
});
it( 'names in the child scope shadows the parent', function () {
child.define( 'a' );
assert.notEqual( child.lookup( 'a' ), parent.lookup( 'a' ) );
child.define( 'b' );
assert.equal( parent.lookup( 'b' ), undefined );
});
});
describe( 'virtual scope:', function () {
var real, a, b;
beforeEach(function () {
real = new Scope();
a = real.virtual();
b = real.virtual();
});
it( 'is created within another scope', function () {
// The actual ids are the same.
assert.equal( real.ids, a.ids );
assert.equal( real.ids, b.ids );
});
it( 'lookups different identifiers', function () {
// If I define 'a' in both scopes...
a.define( 'a' );
b.define( 'a' );
// ... the name 'a' should lookup different identifiers.
assert.notEqual( a.lookup( 'a' ), b.lookup( 'b' ) );
});
it( 'can deconflict names', function () {
a.define( 'a' );
b.define( 'a' );
// Deconflicting the actual scope should make all identifiers unique.
real.deconflict();
assert.deepEqual( real.usedNames(), [ '_a', 'a' ] );
});
it( 'deconflicts with a custom function, if provided', function () {
for (var i = 0; i < 26; i++) {
// Create 26 scopes, all of which define 'a'.
real.virtual().define( 'a' );
}
// Custom deconfliction function which ignores the current name.
var num = 10;
real.deconflict( function () {
return (num++).toString(36);
});
assert.deepEqual( real.usedNames(), 'abcdefghijklmnopqrstuvwxyz'.split('') );
// Deconflicting twice has no additional effect.
real.deconflict();
assert.deepEqual( real.usedNames(), 'abcdefghijklmnopqrstuvwxyz'.split('') );
});
});
it( 'dedupes-external-imports', function () {
var real = new Scope();
var external = real.virtual(),
locals = real.virtual(),
exports = real.virtual();
external.define( 'Component' );
locals.bind( 'Comp', external.reference( 'Component' ) );
exports.bind( 'default', locals.reference( 'Foo' ) );
try {
real.deconflict();
assert.ok( false, 'Scope.deconflict should throw with "Foo" undefined' );
} catch ( ignore ) {
// as expected
}
locals.define( 'Foo' );
real.deconflict();
});
});
Loading…
Cancel
Save