Browse Source

brute force merge rewrite -> rewrite-master

better-aggressive
Rich-Harris 9 years ago
parent
commit
3e6fe19a39
  1. 20
      CHANGELOG.md
  2. 6
      appveyor.yml
  3. 3
      package.json
  4. 210
      src/Bundle.js
  5. 95
      src/ExternalModule.js
  6. 678
      src/Module.js
  7. 175
      src/Scope.js
  8. 479
      src/Statement.js
  9. 69
      src/ast/Scope.js
  10. 76
      src/ast/attachScopes.js
  11. 59
      src/ast/walk.js
  12. 2
      src/finalisers/amd.js
  13. 20
      src/finalisers/cjs.js
  14. 39
      src/finalisers/es6.js
  15. 2
      src/finalisers/iife.js
  16. 29
      src/finalisers/shared/getExportBlock.js
  17. 11
      src/finalisers/shared/getInteropBlock.js
  18. 2
      src/finalisers/umd.js
  19. 114
      src/optimise/namespace-lookup.js
  20. 4
      src/utils/getExportMode.js
  21. 3
      test/form/exports-at-end-if-possible/_config.js
  22. 2
      test/form/external-imports/_expected/cjs.js
  23. 4
      test/form/external-imports/_expected/es6.js
  24. 8
      test/form/internal-conflict-resolution/_expected/amd.js
  25. 8
      test/form/internal-conflict-resolution/_expected/cjs.js
  26. 8
      test/form/internal-conflict-resolution/_expected/es6.js
  27. 8
      test/form/internal-conflict-resolution/_expected/iife.js
  28. 8
      test/form/internal-conflict-resolution/_expected/umd.js
  29. 0
      test/form/shorthand-properties/_config.js
  30. 25
      test/form/shorthand-properties/_expected/amd.js
  31. 23
      test/form/shorthand-properties/_expected/cjs.js
  32. 21
      test/form/shorthand-properties/_expected/es6.js
  33. 25
      test/form/shorthand-properties/_expected/iife.js
  34. 29
      test/form/shorthand-properties/_expected/umd.js
  35. 7
      test/form/shorthand-properties/bar.js
  36. 7
      test/form/shorthand-properties/baz.js
  37. 7
      test/form/shorthand-properties/foo.js
  38. 7
      test/form/shorthand-properties/main.js
  39. 3
      test/function/assignment-to-exports/_config.js
  40. 2
      test/function/consistent-renaming-c/_config.js
  41. 3
      test/function/consistent-renaming-f/_config.js
  42. 3
      test/function/consistent-renaming-f/bar.js
  43. 9
      test/function/consistent-renaming-f/main.js
  44. 2
      test/function/export-from-no-local-binding/_config.js
  45. 4
      test/function/import-of-unexported-fails/_config.js
  46. 3
      test/function/module-sort-order/_config.js
  47. 20
      test/function/module-sort-order/a.js
  48. 1
      test/function/module-sort-order/b.js
  49. 3
      test/function/module-sort-order/c.js
  50. 4
      test/function/module-sort-order/main.js
  51. 5
      test/function/module-sort-order/z.js
  52. 5
      test/function/namespace-optimisation-before-exports/_config.js
  53. 1
      test/function/namespace-optimisation-before-exports/bar.js
  54. 1
      test/function/namespace-optimisation-before-exports/foo.js
  55. 6
      test/function/namespace-optimisation-before-exports/main.js
  56. 7
      test/function/namespace-optimisation-before-exports/zoo.js
  57. 3
      test/function/pass-namespace-to-function/_config.js
  58. 1
      test/function/pass-namespace-to-function/bar.js
  59. 7
      test/function/pass-namespace-to-function/foo.js
  60. 5
      test/function/pass-namespace-to-function/main.js
  61. 3
      test/function/shorthand-properties/baz.js
  62. 10
      test/function/shorthand-properties/foo.js
  63. 5
      test/function/shorthand-properties/main.js
  64. 3
      test/function/tracks-alias-mutations/_config.js
  65. 23
      test/sourcemaps/names/_config.js
  66. 26
      test/test.js
  67. 114
      test/testScope.js

20
CHANGELOG.md

@ -1,5 +1,25 @@
# rollup changelog
## 0.17.4
* Allow imports from hidden directories (replay of [#133](https://github.com/rollup/rollup/issues/133))
## 0.17.3
* Handle parenthesised default exports ([#136](https://github.com/rollup/rollup/issues/136))
## 0.17.2
* Allow use of scoped npm packages ([#131](https://github.com/rollup/rollup/issues/131))
## 0.17.1
* Allow namespaces to be passed to a function ([#149](https://github.com/rollup/rollup/issues/149))
## 0.17.0
* Roll back to 0.15.0 and reapply subsequent fixes pending resolution of ([#132](https://github.com/rollup/rollup/issues/132)) and related issues
## 0.16.4
* Fix import paths with `.` ([#133](https://github.com/rollup/rollup/issues/133))

6
appveyor.yml

@ -9,7 +9,11 @@ init:
environment:
matrix:
- nodejs_version: 4
# node.js
- nodejs_version: 0.10
- nodejs_version: 0.12
# io.js
- nodejs_version: 1
install:
- ps: Install-Product node $env:nodejs_version

3
package.json

@ -1,6 +1,6 @@
{
"name": "rollup",
"version": "0.16.4",
"version": "0.17.4",
"description": "Next-generation ES6 module bundler",
"main": "dist/rollup.js",
"jsnext:main": "src/rollup.js",
@ -59,6 +59,7 @@
"dependencies": {
"acorn": "^2.3.0",
"chalk": "^1.0.0",
"estree-walker": "^0.1.3",
"magic-string": "^0.7.0",
"minimist": "^1.1.1",
"sander": "^0.3.3",

210
src/Bundle.js

@ -10,9 +10,6 @@ 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';
import optimiseNamespaceLookups from './optimise/namespace-lookup.js';
export default class Bundle {
constructor ( options ) {
@ -31,69 +28,81 @@ 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 );
// Strictly speaking, these globals only apply to non-ES6, non-default-only bundles.
// However, the deconfliction logic is greatly simplified by being the same for all formats.
// * CommonJS needs `module` and `exports` ( and `require`? ) to be in scope.
// * SystemJS needs a reference to a function for its `exports`,
// and another one for any `module` it imports. These global names can be reused!
[ 'exports', 'module' ]
.forEach( name => {
this.globals.define( name );
this.scope.bind( name, this.globals.reference( name ) );
});
// Alias for entryModule.exports.
this.exports = null;
this.toExport = null;
this.pending = blank();
this.moduleById = blank();
this.modules = [];
this.statements = null;
this.externalModules = [];
this.internalNamespaces = [];
this.assumedGlobals = blank();
// TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
[ 'module', 'exports' ].forEach( global => this.assumedGlobals[ global ] = true );
}
build () {
return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) )
.then( id => this.fetchModule( id ) )
.then( entryModule => {
this.modules.forEach( module => {
module.statements.forEach( optimiseNamespaceLookups );
});
this.entryModule = entryModule;
this.exports = entryModule.exports;
entryModule.markAllStatements( true );
entryModule.markAllExports();
this.modules.forEach( module => module.bindImportSpecifiers() );
this.modules.forEach( module => module.bindAliases() );
this.modules.forEach( module => module.bindReferences() );
// Include all side-effects
this.modules.forEach( module => {
module.markAllSideEffects();
// mark all export statements
entryModule.getExports().forEach( name => {
const declaration = entryModule.traceExport( name );
declaration.isExported = true;
if ( declaration.statement ) declaration.use();
});
// Sort the modules.
let settled = false;
while ( !settled ) {
settled = true;
this.modules.forEach( module => {
if ( module.markAllSideEffects() ) settled = false;
});
}
this.orderedModules = this.sort();
this.deconflict();
});
}
// As a last step, deconflict all identifier names, once.
this.scope.deconflict();
deconflict () {
let used = blank();
// 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' );
// ensure no conflicts with globals
keys( this.assumedGlobals ).forEach( name => used[ name ] = 1 );
if ( externalDefault && !( module.needsNamed || module.needsAll ) ) {
externalDefault.name = module.name;
}
});
function getSafeName ( name ) {
if ( used[ name ] ) {
return `${name}$${used[name]++}`;
}
used[ name ] = 1;
return name;
}
this.externalModules.forEach( module => {
module.name = getSafeName( module.name );
});
this.modules.forEach( module => {
keys( module.declarations ).forEach( originalName => {
const declaration = module.declarations[ originalName ];
if ( originalName === 'default' ) {
if ( declaration.original && !declaration.original.isReassigned ) return;
}
declaration.name = getSafeName( declaration.name );
});
});
}
fetchModule ( id ) {
@ -110,23 +119,12 @@ export default class Bundle {
source = source.code;
}
const module = new Module({
id,
source,
ast,
bundle: this
});
const module = new Module({ id, source, ast, bundle: this });
this.modules.push( module );
this.moduleById[ id ] = 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;
});
return this.fetchAllDependencies( module ).then( () => module );
});
}
@ -139,7 +137,7 @@ export default class Bundle {
// external module
if ( !resolvedId ) {
if ( !this.moduleById[ source ] ) {
const module = new ExternalModule( { id: source, bundle: this } );
const module = new ExternalModule( source );
this.externalModules.push( module );
this.moduleById[ source ] = module;
}
@ -164,95 +162,19 @@ export default class Bundle {
// Determine export mode - 'default', 'named', 'none'
const exportMode = getExportMode( this, options.exports );
// If we have named exports from the bundle, and those exports
// are assigned to *within* the bundle, we may need to rewrite e.g.
//
// export let count = 0;
// export function incr () { count++ }
//
// might become...
//
// exports.count = 0;
// function incr () {
// exports.count += 1;
// }
// exports.incr = incr;
//
// This doesn't apply if the bundle is exported as ES6!
let allBundleExports = blank();
let isReassignedVarDeclaration = blank();
let varExports = blank();
let getterExports = [];
this.orderedModules.forEach( module => {
module.reassignments.forEach( name => {
isReassignedVarDeclaration[ module.locals.lookup( name ).name ] = true;
});
});
if ( format !== 'es6' && exportMode === 'named' ) {
this.exports.getNames()
.forEach( name => {
const canonicalName = this.exports.lookup( name ).name;
if ( isReassignedVarDeclaration[ canonicalName ] ) {
varExports[ name ] = true;
// if the same binding is exported multiple ways, we need to
// use getters to keep all exports in sync
if ( allBundleExports[ canonicalName ] ) {
getterExports.push({ key: name, value: allBundleExports[ canonicalName ] });
} else {
allBundleExports[ canonicalName ] = `exports.${name}`;
}
}
});
}
// since we're rewriting variable exports, we want to
// ensure we don't try and export them again at the bottom
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, format === 'es6' );
const source = module.render( format === 'es6' );
if ( source.toString().length ) {
magicString.addSource( source );
}
});
// prepend bundle with internal namespaces
const indentString = getIndentString( magicString, options );
const namespaceBlock = this.modules.filter( module => module.needsDynamicAccess ).map( module => {
const exports = module.exports.getNames().map( name => {
const id = module.exports.lookup( name );
return `${indentString}get ${name} () { return ${id.name}; }`;
});
return `var ${module.name} = {\n` +
exports.join( ',\n' ) +
`\n};\n\n`;
}).join( '' );
magicString.prepend( namespaceBlock );
if ( getterExports.length ) {
// TODO offer ES3-safe (but not spec-compliant) alternative?
const getterExportsBlock = `Object.defineProperties(exports, {\n` +
getterExports.map( ({ key, value }) => indentString + `${key}: { get: function () { return ${value}; } }` ).join( ',\n' ) +
`\n});`;
magicString.append( '\n\n' + getterExportsBlock );
}
const finalise = finalisers[ format ];
if ( !finalise ) {
throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
}
if ( !finalise ) throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options );
@ -277,19 +199,15 @@ export default class Bundle {
}
sort () {
// Set of visited module ids.
let seen = blank();
let seen = {};
let ordered = [];
let hasCycles;
// Map from module id to list of modules.
let strongDeps = blank();
// Map from module id to boolean.
let stronglyDependsOn = blank();
let strongDeps = {};
let stronglyDependsOn = {};
function visit ( module ) {
if ( seen[ module.id ] ) return;
seen[ module.id ] = true;
const { strongDependencies, weakDependencies } = module.consolidateDependencies();
@ -338,7 +256,7 @@ export default class Bundle {
ordered.push( module );
}
visit( this.entryModule );
this.modules.forEach( visit );
if ( hasCycles ) {
let unordered = ordered;

95
src/ExternalModule.js

@ -1,66 +1,75 @@
import { blank } from './utils/object';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
// An external identifier.
class Id {
class ExternalDeclaration {
constructor ( module, name ) {
this.originalName = this.name = name;
this.module = module;
this.name = name;
this.isExternal = true;
}
this.modifierStatements = [];
addAlias () {
// noop
}
// Flags the identifier as imported by the bundle when marked.
mark () {
this.module.importedByBundle[ this.originalName ] = true;
this.modifierStatements.forEach( stmt => stmt.mark() );
addReference ( reference ) {
reference.declaration = this;
if ( this.name === 'default' || this.name === '*' ) {
this.module.suggestName( reference.name );
}
}
}
export default class ExternalModule {
constructor ( { id, bundle } ) {
this.id = id;
render ( es6 ) {
if ( this.name === '*' ) {
return this.module.name;
}
// Implement `Identifier` interface.
this.originalName = this.name = makeLegalIdentifier( id );
this.module = this;
this.isModule = true;
if ( this.name === 'default' ) {
return !es6 && this.module.exportsNames ?
`${this.module.name}__default` :
this.module.name;
}
// Define the external module's name in the bundle scope.
bundle.scope.define( id, this );
return es6 ? this.name : `${this.module.name}.${this.name}`;
}
this.isExternal = true;
this.importedByBundle = blank();
use () {
// noop?
}
}
// Invariant: needsNamed and needsAll are never both true at once.
// Because an import with both a namespace and named import is invalid:
//
// import * as ns, { a } from '...'
//
this.needsNamed = false;
this.needsAll = false;
export default class ExternalModule {
constructor ( id ) {
this.id = id;
this.name = makeLegalIdentifier( id );
this.exports = bundle.scope.virtual( false );
this.nameSuggestions = blank();
this.mostCommonSuggestion = 0;
const { reference } = this.exports;
this.isExternal = true;
this.declarations = blank();
// Override reference.
this.exports.reference = name => {
if ( name !== 'default' ) {
this.needsNamed = true;
}
this.exportsNames = false;
}
if ( !this.exports.defines( name ) ) {
this.exports.define( name, new Id( this, name ) );
}
suggestName ( name ) {
if ( !this.nameSuggestions[ name ] ) this.nameSuggestions[ name ] = 0;
this.nameSuggestions[ name ] += 1;
return reference.call( this.exports, name );
};
if ( this.nameSuggestions[ name ] > this.mostCommonSuggestion ) {
this.mostCommonSuggestion = this.nameSuggestions[ name ];
this.name = name;
}
}
// External modules are always marked for inclusion in the bundle.
// Marking an external module signals its use as a namespace.
mark () {
this.needsAll = true;
traceExport ( name ) {
if ( name !== 'default' && name !== '*' ) {
this.exportsNames = true;
}
return this.declarations[ name ] || (
this.declarations[ name ] = new ExternalDeclaration( this, name )
);
}
}

678
src/Module.js

@ -1,135 +1,160 @@
import { basename, extname } from './utils/path';
import { parse } from 'acorn';
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import Statement from './Statement';
import walk from './ast/walk';
import { blank, keys } from './utils/object';
import { basename, extname } from './utils/path';
import getLocation from './utils/getLocation';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
import SOURCEMAPPING_URL from './utils/sourceMappingURL';
function removeSourceMappingURLComments ( source, magicString ) {
const SOURCEMAPPING_URL_PATTERN = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' );
let match;
class SyntheticDefaultDeclaration {
constructor ( node, statement, name ) {
this.node = node;
this.statement = statement;
this.name = name;
while ( match = SOURCEMAPPING_URL_PATTERN.exec( source ) ) {
magicString.remove( match.index, match.index + match[0].length );
this.original = null;
this.isExported = false;
this.aliases = [];
}
}
function assign ( target, source ) {
for ( let key in source ) target[ key ] = source[ key ];
}
addAlias ( declaration ) {
this.aliases.push( declaration );
}
class Id {
constructor ( module, name, statement ) {
this.originalName = this.name = name;
this.module = module;
this.statement = statement;
addReference ( reference ) {
reference.declaration = this;
this.name = reference.name;
}
this.modifierStatements = [];
bind ( declaration ) {
this.original = declaration;
}
// modifiers
this.isUsed = false;
render () {
return !this.original || this.original.isReassigned ?
this.name :
this.original.render();
}
mark () {
use () {
this.isUsed = true;
this.statement.mark();
this.modifierStatements.forEach( stmt => stmt.mark() );
this.aliases.forEach( alias => alias.use() );
}
}
class LateBoundIdPlaceholder {
constructor ( module, name ) {
class SyntheticNamespaceDeclaration {
constructor ( module ) {
this.module = module;
this.name = name;
this.placeholder = true;
this.name = null;
this.needsNamespaceBlock = false;
this.aliases = [];
this.originals = blank();
module.getExports().forEach( name => {
this.originals[ name ] = module.traceExport( name );
});
}
mark () {
throw new Error(`The imported name "${this.name}" is never exported by "${this.module.id}".`);
addAlias ( declaration ) {
this.aliases.push( declaration );
}
}
export default class Module {
constructor ({ id, source, ast, bundle }) {
this.source = source;
addReference ( reference ) {
// if we have e.g. `foo.bar`, we can optimise
// the reference by pointing directly to `bar`
if ( reference.parts.length ) {
reference.name = reference.parts.shift();
this.bundle = bundle;
this.id = id;
this.module = this;
this.isModule = true;
reference.end += reference.name.length + 1; // TODO this is brittle
// Implement Identifier interface.
this.name = makeLegalIdentifier( basename( id ).slice( 0, -extname( id ).length ) );
const original = this.originals[ reference.name ];
// HACK: If `id` isn't a path, the above code yields the empty string.
if ( !this.name ) {
this.name = makeLegalIdentifier( id );
original.addReference( reference );
return;
}
// By default, `id` is the filename. Custom resolvers and loaders
// can change that, but it makes sense to use it for the source filename
this.magicString = new MagicString( source, {
filename: id
});
// otherwise we're accessing the namespace directly,
// which means we need to mark all of this module's
// exports and render a namespace block in the bundle
if ( !this.needsNamespaceBlock ) {
this.needsNamespaceBlock = true;
this.module.bundle.internalNamespaces.push( this );
removeSourceMappingURLComments( source, this.magicString );
keys( this.originals ).forEach( name => {
const original = this.originals[ name ];
original.use();
});
}
this.comments = [];
reference.declaration = this;
this.name = reference.name;
}
this.statements = this.parse( ast );
renderBlock ( indentString ) {
const members = keys( this.originals ).map( name => {
const original = this.originals[ name ];
// all dependencies
this.resolvedIds = blank();
if ( original.isReassigned ) {
return `${indentString}get ${name} () { return ${original.render()}; }`;
}
// Virtual scopes for the local and exported names.
this.locals = bundle.scope.virtual( true );
this.exports = bundle.scope.virtual( false );
return `${indentString}${name}: ${original.render()}`;
});
const { reference, inScope } = this.exports;
return `var ${this.render()} = {\n${members.join( ',\n' )}\n};\n\n`;
}
this.exports.reference = name => {
// If we have it, grab it.
if ( inScope.call( this.exports, name ) ) {
return reference.call( this.exports, name );
}
render () {
return this.name;
}
// ... otherwise search allExportsFrom
for ( let i = 0; i < this.allExportsFrom.length; i += 1 ) {
const module = this.allExportsFrom[i];
if ( module.exports.inScope( name ) ) {
return module.exports.reference( name );
}
}
use () {
// noop?
this.aliases.forEach( alias => alias.use() );
}
}
// throw new Error( `The name "${name}" is never exported (from ${this.id})!` );
this.exports.define( name, new LateBoundIdPlaceholder( this, name ) );
return reference.call( this.exports, name );
};
export default class Module {
constructor ({ id, source, ast, bundle }) {
this.source = source;
this.bundle = bundle;
this.id = id;
this.exports.inScope = name => {
if ( inScope.call( this.exports, name ) ) return true;
// all dependencies
this.dependencies = [];
this.resolvedIds = blank();
return this.allExportsFrom.some( module => module.exports.inScope( name ) );
};
// imports and exports, indexed by local name
this.imports = blank();
this.exports = blank();
this.reexports = blank();
// 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.exportAllSources = [];
this.exportAllModules = null;
// As far as we know, all our exported bindings have been resolved.
this.allExportsResolved = true;
this.allExportsFrom = [];
// By default, `id` is the filename. Custom resolvers and loaders
// can change that, but it makes sense to use it for the source filename
this.magicString = new MagicString( source, {
filename: id
});
this.reassignments = [];
// remove existing sourceMappingURL comments
const pattern = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' );
let match;
while ( match = pattern.exec( source ) ) {
this.magicString.remove( match.index, match.index + match[0].length );
}
// TODO: change to false, and detect when it's necessary.
this.needsDynamicAccess = false;
this.comments = [];
this.statements = this.parse( ast );
this.dependencies = this.collectDependencies();
this.declarations = blank();
this.analyse();
}
addExport ( statement ) {
@ -138,32 +163,21 @@ export default class Module {
// export { name } from './other'
if ( source ) {
const module = this.getModule( 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.
if ( module.isExternal ) {
let err = new Error( `Cannot trace 'export *' references through external modules.` );
err.file = this.id;
err.loc = getLocation( this.source, node.start );
throw err;
}
// It seems like we must re-export all exports from another module...
this.allExportsResolved = false;
if ( !~this.allExportsFrom.indexOf( module ) ) {
this.allExportsFrom.push( module );
}
this.exportAllSources.push( source );
}
else {
node.specifiers.forEach( specifier => {
// Bind the export of this module, to the export of the other.
this.exports.bind( specifier.exported.name,
module.exports.reference( specifier.local.name ) );
this.reexports[ specifier.exported.name ] = {
source,
localName: specifier.local.name,
module: null // filled in later
};
});
}
}
@ -172,26 +186,15 @@ export default class Module {
// export default foo;
// export default 42;
else if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
const isAnonymous = /(?:Class|Function)Expression$/.test( node.declaration.type );
const identifier = isDeclaration ?
node.declaration.id.name :
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 );
// 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 );
const identifier = ( node.declaration.id && node.declaration.id.name ) || node.declaration.name;
this.exports.default = {
localName: 'default',
identifier
};
// create a synthetic declaration
this.declarations.default = new SyntheticDefaultDeclaration( node, statement, identifier || this.basename() );
}
// export { foo, bar, baz }
@ -204,7 +207,7 @@ export default class Module {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports.bind( exportedName, this.locals.reference( localName ) );
this.exports[ exportedName ] = { localName };
});
}
@ -221,49 +224,32 @@ export default class Module {
name = declaration.id.name;
}
this.locals.define( name, new Id( this, name, statement ) );
this.exports.bind( name, this.locals.reference( name ) );
this.exports[ name ] = { localName: name };
}
}
}
addImport ( statement ) {
const node = statement.node;
const module = this.getModule( node.source.value );
const source = node.source.value;
node.specifiers.forEach( specifier => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
node.specifiers.forEach( specifier => {
const localName = specifier.local.name;
if ( this.locals.defines( localName ) ) {
if ( this.imports[ localName ] ) {
const err = new Error( `Duplicated import '${localName}'` );
err.file = this.id;
err.loc = getLocation( this.source, specifier.start );
throw err;
}
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';
}
}
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
this.imports[ localName ] = { source, name, module: null };
});
}
@ -275,215 +261,146 @@ export default class Module {
statement.analyse();
// consolidate names that are defined/modified in this module
keys( statement.defines ).forEach( name => {
this.locals.define( name, new Id( this, name, statement ) );
statement.scope.eachDeclaration( ( name, declaration ) => {
this.declarations[ name ] = declaration;
});
});
}
// If all exports aren't resolved, but all our delegate modules are...
if ( !this.allExportsResolved && this.allExportsFrom.every( module => module.allExportsResolved )) {
// .. then all our exports should be as well.
this.allExportsResolved = true;
// For all modules we export all from, iterate through its exported names.
// If we don't already define the binding 'name',
// bind the name to the other module's reference.
this.allExportsFrom.forEach( module => {
module.exports.getNames().forEach( name => {
if ( name !== 'default' && !this.exports.defines( name ) ) {
this.exports.bind( name, module.exports.reference( name ) );
}
});
});
}
// discover variables that are reassigned inside function
// bodies, so we can keep bindings live, e.g.
//
// export var count = 0;
// export function incr () { count += 1 }
let reassigned = blank();
this.statements.forEach( statement => {
keys( statement.reassigns ).forEach( name => {
reassigned[ name ] = true;
});
});
basename () {
return makeLegalIdentifier( basename( this.id ).slice( 0, -extname( this.id ).length ) );
}
// if names are referenced that are neither defined nor imported
// in this module, we assume that they're globals
this.statements.forEach( statement => {
if ( statement.isReexportDeclaration ) return;
bindAliases () {
keys( this.declarations ).forEach( name => {
const declaration = this.declarations[ name ];
const statement = declaration.statement;
if ( statement.node.type !== 'VariableDeclaration' ) return;
// while we're here, mark reassignments
statement.scope.varDeclarations.forEach( name => {
if ( reassigned[ name ] && !~this.reassignments.indexOf( name ) ) {
this.reassignments.push( name );
}
});
statement.references.forEach( reference => {
if ( reference.name === name || !reference.isImmediatelyUsed ) return;
keys( statement.dependsOn ).forEach( name => {
// 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 ) );
}
const otherDeclaration = this.trace( reference.name );
if ( otherDeclaration ) otherDeclaration.addAlias( declaration );
});
});
// OPTIMIZATION!
// If we have a default export and it's value is never modified,
// bind to it directly.
const def = this.exports.lookup( 'default' );
if ( def && !def.isModified && def.identifier ) {
this.exports.bind( 'default', this.locals.reference( def.identifier ) );
}
}
// Returns the set of imported module ids by going through all import/exports statements.
collectDependencies () {
const importedModules = blank();
bindImportSpecifiers () {
[ this.imports, this.reexports ].forEach( specifiers => {
keys( specifiers ).forEach( name => {
const specifier = specifiers[ name ];
this.statements.forEach( statement => {
if ( statement.isImportDeclaration || ( statement.isExportDeclaration && statement.node.source ) ) {
importedModules[ statement.node.source.value ] = true;
}
const id = this.resolvedIds[ specifier.source ];
specifier.module = this.bundle.moduleById[ id ];
});
});
return keys( importedModules );
this.exportAllModules = this.exportAllSources.map( source => {
const id = this.resolvedIds[ source ];
return this.bundle.moduleById[ id ];
});
}
consolidateDependencies () {
let strongDependencies = blank();
function addDependency ( dependencies, declaration ) {
if ( declaration && declaration.module && !declaration.module.isExternal ) {
dependencies[ declaration.module.id ] = declaration.module;
return true;
bindReferences () {
if ( this.declarations.default ) {
if ( this.exports.default.identifier ) {
const declaration = this.trace( this.exports.default.identifier );
if ( declaration ) this.declarations.default.bind( declaration );
}
}
this.statements.forEach( statement => {
if ( statement.isImportDeclaration && !statement.node.specifiers.length ) {
// include module for its side-effects
const module = this.getModule( statement.node.source.value );
if ( !module.isExternal ) strongDependencies[ module.id ] = module;
// skip `export { foo, bar, baz }`...
if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
// ...unless this is the entry module
if ( this !== this.bundle.entryModule ) return;
}
else if ( statement.isReexportDeclaration ) {
if ( statement.node.specifiers ) {
statement.node.specifiers.forEach( specifier => {
let name = specifier.exported.name;
let id = this.exports.lookup( name );
statement.references.forEach( reference => {
const declaration = reference.scope.findDeclaration( reference.name ) ||
this.trace( reference.name );
addDependency( strongDependencies, id );
});
if ( declaration ) {
declaration.addReference( reference );
} else {
// TODO handle globals
this.bundle.assumedGlobals[ reference.name ] = true;
}
}
else {
keys( statement.stronglyDependsOn ).forEach( name => {
if ( statement.defines[ name ] ) return;
addDependency( strongDependencies, this.locals.lookup( name ) );
});
}
});
});
}
consolidateDependencies () {
let strongDependencies = blank();
let weakDependencies = blank();
this.statements.forEach( statement => {
keys( statement.dependsOn ).forEach( name => {
if ( statement.defines[ name ] ) return;
// treat all imports as weak dependencies
this.dependencies.forEach( source => {
const id = this.resolvedIds[ source ];
const dependency = this.bundle.moduleById[ id ];
addDependency( weakDependencies, this.locals.lookup( name ) );
});
if ( !dependency.isExternal ) {
weakDependencies[ dependency.id ] = dependency;
}
});
// Go through all our local and exported ids and make us depend on
// the defining modules as well as
this.exports.getIds().concat(this.locals.getIds()).forEach( id => {
if ( id.module && !id.module.isExternal ) {
weakDependencies[ id.module.id ] = id.module;
}
// identify strong dependencies to break ties in case of cycles
this.statements.forEach( statement => {
statement.references.forEach( reference => {
const declaration = reference.declaration;
if ( !id.modifierStatements ) return;
if ( declaration && declaration.statement ) {
const module = declaration.statement.module;
if ( module === this ) return;
id.modifierStatements.forEach( statement => {
const module = statement.module;
weakDependencies[ module.id ] = module;
// TODO disregard function declarations
if ( reference.isImmediatelyUsed ) {
strongDependencies[ module.id ] = module;
}
}
});
});
// `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 { strongDependencies, weakDependencies };
}
getModule ( source ) {
return this.bundle.moduleById[ this.resolvedIds[ source ] ];
}
// If a module is marked, enforce dynamic access of its properties.
mark () {
if ( this.needsDynamicAccess ) return;
this.needsDynamicAccess = true;
this.markAllExports();
}
getExports () {
let exports = blank();
markAllSideEffects () {
this.statements.forEach( statement => {
statement.markSideEffect();
keys( this.exports ).forEach( name => {
exports[ name ] = true;
});
}
markAllStatements ( isEntryModule ) {
this.statements.forEach( statement => {
if ( statement.isIncluded ) return; // TODO can this happen? probably not...
// skip import declarations...
if ( statement.isImportDeclaration ) {
// ...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 otherModule = this.getModule( statement.node.source.value );
keys( this.reexports ).forEach( name => {
exports[ name ] = true;
});
if ( !otherModule.isExternal ) otherModule.markAllStatements();
}
}
this.exportAllModules.forEach( module => {
module.getExports().forEach( name => {
if ( name !== 'default' ) exports[ name ] = true;
});
});
// skip `export { foo, bar, baz }`...
else if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
// ...but ensure they are defined, if this is the entry module
if ( isEntryModule ) statement.mark();
}
return keys( exports );
}
// 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();
}
markAllSideEffects () {
let hasSideEffect = false;
statement.mark();
}
this.statements.forEach( statement => {
if ( statement.markSideEffect() ) hasSideEffect = true;
});
return hasSideEffect;
}
// Marks all exported identifiers.
markAllExports () {
this.exports.getIds().forEach( id => id.mark() );
namespace () {
if ( !this.declarations['*'] ) {
this.declarations['*'] = new SyntheticNamespaceDeclaration( this );
}
return this.declarations['*'];
}
parse ( ast ) {
@ -575,7 +492,7 @@ export default class Module {
return statements;
}
render ( toExport, direct ) {
render ( es6 ) {
let magicString = this.magicString.clone();
this.statements.forEach( statement => {
@ -596,55 +513,61 @@ export default class Module {
// split up/remove var declarations as necessary
if ( statement.node.isSynthetic ) {
// insert `var/let/const` if necessary
if ( !toExport[ statement.node.declarations[0].id.name ] ) {
const declaration = this.declarations[ statement.node.declarations[0].id.name ];
if ( !( declaration.isExported && declaration.isReassigned ) ) { // TODO encapsulate this
magicString.insert( statement.start, `${statement.node.kind} ` );
}
magicString.overwrite( statement.end, statement.next, ';\n' ); // TODO account for trailing newlines
}
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}`;
}
});
}
let toDeshadow = blank();
statement.references.forEach( reference => {
const declaration = reference.declaration;
keys( statement.dependsOn )
.concat( keys( statement.defines ) )
.forEach( name => {
const bundleName = this.locals.lookup( name ).name;
if ( declaration ) {
const { start, end } = reference;
const name = declaration.render( es6 );
if ( toExport[ bundleName ] ) {
bundleExports[ name ] = replacements[ name ] = toExport[ bundleName ];
} else if ( bundleName !== name && !replacements[ name ] ) { // TODO weird structure
replacements[ name ] = bundleName;
// the second part of this check is necessary because of
// namespace optimisation – name of `foo.bar` could be `bar`
if ( reference.name === name && name.length === reference.end - reference.start ) return;
// prevent local variables from shadowing renamed references
const identifier = name.match( /[^\.]+/ )[0];
if ( reference.scope.contains( identifier ) ) {
toDeshadow[ identifier ] = `${identifier}$$`; // TODO more robust mechanism
}
});
statement.replaceIdentifiers( magicString, replacements, bundleExports );
if ( reference.isShorthandProperty ) {
magicString.insert( end, `: ${name}` );
} else {
magicString.overwrite( start, end, name, true );
}
}
});
if ( keys( toDeshadow ).length ) {
statement.references.forEach( reference => {
if ( reference.name in toDeshadow ) {
magicString.overwrite( reference.start, reference.end, toDeshadow[ reference.name ], true );
}
});
}
// modify exports as necessary
if ( statement.isExportDeclaration ) {
// remove `export` from `export var foo = 42`
if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.declaration.type === 'VariableDeclaration' ) {
magicString.remove( statement.node.start, statement.node.declaration.start );
const name = statement.node.declaration.declarations[0].id.name;
const declaration = this.declarations[ name ];
const end = declaration.isExported && declaration.isReassigned ?
statement.node.declaration.declarations[0].start :
statement.node.declaration.start;
magicString.remove( statement.node.start, end );
}
else if ( statement.node.type === 'ExportAllDeclaration' ) {
@ -659,25 +582,27 @@ export default class Module {
}
else if ( statement.node.type === 'ExportDefaultDeclaration' ) {
const def = this.exports.lookup( 'default' );
const defaultDeclaration = this.declarations.default;
// FIXME: dunno what to do here yet.
if ( statement.node.declaration.type === 'Identifier' && def.name === ( replacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) {
// prevent `var foo = foo`
if ( defaultDeclaration.original && !defaultDeclaration.original.isReassigned ) {
magicString.remove( statement.start, statement.next );
return;
}
const defaultName = defaultDeclaration.render();
// prevent `var undefined = sideEffectyDefault(foo)`
if ( !def.isUsed ) {
if ( !defaultDeclaration.isExported && !defaultDeclaration.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 ${def.name}` );
magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${defaultName}` );
} else {
magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${def.name} = ` );
magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${defaultName} = ` );
}
}
@ -687,6 +612,53 @@ export default class Module {
}
});
// add namespace block if necessary
const namespace = this.declarations['*'];
if ( namespace && namespace.needsNamespaceBlock ) {
magicString.append( '\n\n' + namespace.renderBlock( magicString.getIndentString() ) );
}
return magicString.trim();
}
trace ( name ) {
if ( name in this.declarations ) return this.declarations[ name ];
if ( name in this.imports ) {
const importDeclaration = this.imports[ name ];
const otherModule = importDeclaration.module;
if ( importDeclaration.name === '*' && !otherModule.isExternal ) {
return otherModule.namespace();
}
return otherModule.traceExport( importDeclaration.name, this );
}
return null;
}
traceExport ( name, importer ) {
// export { foo } from './other'
const reexportDeclaration = this.reexports[ name ];
if ( reexportDeclaration ) {
return reexportDeclaration.module.traceExport( reexportDeclaration.localName, this );
}
const exportDeclaration = this.exports[ name ];
if ( exportDeclaration ) {
return this.trace( exportDeclaration.localName );
}
for ( let i = 0; i < this.exportAllModules.length; i += 1 ) {
const module = this.exportAllModules[i];
const declaration = module.traceExport( name, this );
if ( declaration ) return declaration;
}
let errorMessage = `Module ${this.id} does not export ${name}`;
if ( importer ) errorMessage += ` (imported by ${importer.id})`;
throw new Error( errorMessage );
}
}

175
src/Scope.js

@ -1,175 +0,0 @@
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`.
class Identifier {
constructor ( name ) {
this.originalName = this.name = name;
}
mark () {
// noop
}
}
// 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 );
}
// Returns a function that will prefix its argument with '_'
// and append a number if called with the same argument more than once.
function underscorePrefix () {
function number ( x ) {
if ( !( x in map ) ) {
map[ x ] = 0;
return '';
}
return map[ x ]++;
}
var map = blank();
return x => `_${x}${number( 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 => {
// TODO: can this be removed?
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.
getIds () {
return keys( this.names ).map( 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 ) {
if ( !( name in this.names ) ) {
throw new Error( `Cannot reference undefined identifier "${name}"` );
}
return new Reference( this, this.names[ 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;
}
}

479
src/Statement.js

@ -1,12 +1,7 @@
import { blank, keys } from './utils/object';
import getLocation from './utils/getLocation';
import walk from './ast/walk';
import { walk } from 'estree-walker';
import Scope from './ast/Scope';
const blockDeclarations = {
'const': true,
'let': true
};
import attachScopes from './ast/attachScopes';
import getLocation from './utils/getLocation';
const modifierNodes = {
AssignmentExpression: 'left',
@ -17,12 +12,48 @@ function isIife ( node, parent ) {
return parent && parent.type === 'CallExpression' && node === parent.callee;
}
function isFunctionDeclaration ( node, parent ) {
// `function foo () {}`
if ( node.type === 'FunctionDeclaration' ) return true;
function isReference ( node, parent ) {
if ( node.type === 'MemberExpression' ) {
return !node.computed && isReference( node.object, node );
}
if ( node.type === 'Identifier' ) {
// TODO is this right?
if ( parent.type === 'MemberExpression' ) return parent.computed || node === parent.object;
// disregard the `bar` in { bar: foo }
if ( parent.type === 'Property' && node !== parent.value ) return false;
// disregard the `bar` in `class Foo { bar () {...} }`
if ( parent.type === 'MethodDefinition' ) return false;
// `var foo = function () {}` - same thing for present purposes
if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true;
// disregard the `bar` in `export { foo as bar }`
if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;
return true;
}
}
class Reference {
constructor ( node, scope ) {
this.node = node;
this.scope = scope;
this.declaration = null; // bound later
this.parts = [];
let root = node;
while ( root.type === 'MemberExpression' ) {
this.parts.unshift( root.property.name );
root = root.object;
}
this.name = root.name;
this.start = node.start;
this.end = node.start + this.name.length; // can be overridden in the case of namespace members
}
}
export default class Statement {
@ -34,15 +65,8 @@ export default class Statement {
this.next = null; // filled in later
this.scope = new Scope();
this.defines = blank();
this.dependsOn = blank();
this.stronglyDependsOn = blank();
this.reassigns = blank();
// TODO: make this more efficient
this.dependantIds = [];
this.namespaceReplacements = [];
this.references = [];
this.isIncluded = false;
@ -54,260 +78,99 @@ 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, {
enter ( node, parent ) {
let newScope;
switch ( node.type ) {
case 'FunctionDeclaration':
scope.addDeclaration( node, false, false );
break;
case 'BlockStatement':
if ( parent && /Function/.test( parent.type ) ) {
newScope = new Scope({
parent: scope,
block: false,
params: parent.params
});
// named function expressions - the name is considered
// part of the function's scope
if ( parent.type === 'FunctionExpression' && parent.id ) {
newScope.addDeclaration( parent, false, false );
}
} else {
newScope = new Scope({
parent: scope,
block: true
});
}
break;
// attach scopes
attachScopes( this );
case 'CatchClause':
newScope = new Scope({
parent: scope,
params: [ node.param ],
block: true
});
break;
case 'VariableDeclaration':
node.declarations.forEach( declarator => {
const isBlockDeclaration = node.type === 'VariableDeclaration' && blockDeclarations[ node.kind ];
scope.addDeclaration( declarator, isBlockDeclaration, true );
});
break;
case 'ClassDeclaration':
scope.addDeclaration( node, false, false );
break;
}
if ( newScope ) {
Object.defineProperty( node, '_scope', {
value: newScope,
configurable: true
});
scope = newScope;
}
},
leave ( node ) {
if ( node._scope ) {
scope = scope.parent;
}
}
// attach statement to each top-level declaration,
// so we can mark statements easily
this.scope.eachDeclaration( ( name, declaration ) => {
declaration.statement = this;
});
// This allows us to track whether we're looking at code that will
// be executed immediately (either outside a function, or immediately
// inside an IIFE), for the purposes of determining whether dependencies
// are strong or weak. It's not bulletproof, since it wouldn't catch...
//
// var calledImmediately = function () {
// doSomethingWith( strongDependency );
// }
// calledImmediately();
//
// ...but it's better than nothing
// find references
let { module, references, scope } = this;
let readDepth = 0;
// This allows us to track whether a modifying statement (i.e. assignment
// /update expressions) need to be captured
let writeDepth = 0;
if ( !this.isImportDeclaration ) {
walk( this.node, {
enter: ( node, parent ) => {
if ( isFunctionDeclaration( node, parent ) ) writeDepth += 1;
if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1;
if ( node._scope ) scope = node._scope;
this.checkForReads( scope, node, parent, !readDepth );
this.checkForWrites( scope, node, writeDepth );
},
leave: ( node, parent ) => {
if ( isFunctionDeclaration( node, parent ) ) writeDepth -= 1;
if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
if ( node._scope ) scope = scope.parent;
}
});
}
keys( scope.declarations ).forEach( name => {
this.defines[ name ] = true;
});
}
checkForReads ( scope, node, parent, strong ) {
if ( node.type === 'Identifier' ) {
// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) {
return;
}
// disregard the `bar` in { bar: foo }
if ( parent.type === 'Property' && node !== parent.value ) {
return;
}
// disregard the `bar` in `class Foo { bar () {...} }`
if ( parent.type === 'MethodDefinition' ) return;
// disregard the `bar` in `export { foo as bar }`
if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;
const definingScope = scope.findDefiningScope( node.name );
walk( this.node, {
enter ( node, parent ) {
if ( node._scope ) scope = node._scope;
if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1;
if ( !definingScope || definingScope.depth === 0 ) {
if ( !( node.name in this.dependsOn ) ) {
this.dependsOn[ node.name ] = 0;
// special case – shorthand properties. because node.key === node.value,
// we can't differentiate once we've descended into the node
if ( node.type === 'Property' && node.shorthand ) {
const reference = new Reference( node.key, scope );
reference.isShorthandProperty = true; // TODO feels a bit kludgy
references.push( reference );
return this.skip();
}
this.dependsOn[ node.name ]++;
if ( strong ) this.stronglyDependsOn[ node.name ] = true;
}
}
}
let isReassignment;
checkForWrites ( scope, node, writeDepth ) {
const addNode = ( node, isAssignment ) => {
let depth = 0; // determine whether we're illegally modifying a binding or namespace
if ( parent && parent.type in modifierNodes ) {
let subject = parent[ modifierNodes[ parent.type ] ];
let depth = 0;
while ( node.type === 'MemberExpression' ) {
node = node.object;
depth += 1;
}
// disallow assignments/updates to imported bindings and namespaces
if ( isAssignment ) {
const importSpecifier = this.module.locals.lookup( node.name );
while ( subject.type === 'MemberExpression' ) {
subject = subject.object;
depth += 1;
}
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
const importDeclaration = module.imports[ subject.name ];
if ( depth < minDepth ) {
const err = new Error( `Illegal reassignment to import '${node.name}'` );
err.file = this.module.id;
err.loc = getLocation( this.module.magicString.toString(), node.start );
throw err;
}
}
if ( !scope.contains( subject.name ) && importDeclaration ) {
const minDepth = importDeclaration.name === '*' ?
2 : // cannot do e.g. `namespace.foo = bar`
1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
// 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
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 > def.statement.node.start ) {
def.isModified = true;
if ( depth < minDepth ) {
const err = new Error( `Illegal reassignment to import '${subject.name}'` );
err.file = module.id;
err.loc = getLocation( module.magicString.toString(), subject.start );
throw err;
}
}
}
// we track updates/reassignments to variables, to know whether we
// need to rewrite it later from `foo` to `exports.foo` to keep
// bindings live
if (
depth === 0 &&
writeDepth > 0 &&
!scope.contains( node.name )
) {
this.reassigns[ node.name ] = true;
}
}
// we only care about writes that happen a) at the top level,
// or b) inside a function that could be immediately invoked.
// Writes inside named functions are only relevant if the
// function is called, in which case we don't need to do
// anything (but we still need to call checkForWrites to
// catch illegal reassignments to imported bindings)
if ( writeDepth === 0 && node.type === 'Identifier' ) {
const id = this.module.locals.lookup( node.name );
if ( id && id.modifierStatements && !~id.modifierStatements.indexOf( this ) ) {
id.modifierStatements.push( this );
isReassignment = !depth;
}
}
};
if ( node.type === 'AssignmentExpression' ) {
addNode( node.left, true );
}
if ( isReference( node, parent ) ) {
// function declaration IDs are a special case – they're associated
// with the parent scope
const referenceScope = parent.type === 'FunctionDeclaration' && node === parent.id ?
scope.parent :
scope;
else if ( node.type === 'UpdateExpression' ) {
addNode( node.argument, true );
}
const reference = new Reference( node, referenceScope );
references.push( reference );
else if ( node.type === 'CallExpression' ) {
node.arguments.forEach( arg => addNode( arg, false ) );
reference.isImmediatelyUsed = !readDepth;
reference.isReassignment = isReassignment;
// `foo.bar()` is assumed to mutate foo
if ( node.callee.type === 'MemberExpression' ) {
addNode( node.callee );
this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
}
},
leave ( node, parent ) {
if ( node._scope ) scope = scope.parent;
if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
}
}
});
}
mark () {
if ( this.isIncluded ) return; // prevent infinite loops
this.isIncluded = true;
this.dependantIds.forEach( id => id.mark() );
// 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.locals.lookup( name ).mark();
this.references.forEach( reference => {
if ( reference.declaration ) reference.declaration.use();
});
}
markSideEffect () {
if ( this.isIncluded ) return;
const statement = this;
let hasSideEffect = false;
walk( this.node, {
enter ( node, parent ) {
@ -315,141 +178,27 @@ export default class Statement {
// If this is a top-level call expression, or an assignment to a global,
// this statement will need to be marked
if ( node.type === 'CallExpression' ) {
statement.mark();
if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) {
hasSideEffect = true;
}
else if ( node.type in modifierNodes ) {
let subject = node[ modifierNodes[ node.type ] ];
while ( subject.type === 'MemberExpression' ) subject = subject.object;
if ( statement.module.bundle.globals.defines( subject.name ) ) statement.mark();
}
}
});
}
const declaration = statement.module.trace( subject.name );
replaceIdentifiers ( magicString, names, bundleExports ) {
const statement = this;
const replacementStack = [];
const nameList = keys( names );
let deshadowList = [];
nameList.forEach( name => {
const replacement = names[ name ];
deshadowList.push( replacement.split( '.' )[0] );
});
let topLevel = true;
let depth = 0;
walk( this.node, {
enter ( node, parent ) {
if ( node._skip ) return this.skip();
if ( /^Function/.test( node.type ) ) depth += 1;
// `this` is undefined at the top level of ES6 modules
if ( node.type === 'ThisExpression' && depth === 0 ) {
magicString.overwrite( node.start, node.end, 'undefined', true );
}
// special case - variable declarations that need to be rewritten
// as bundle exports
if ( topLevel ) {
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 id = node.declarations[0].id;
const name = id.name;
if ( node.declarations.length === 1 && bundleExports[ name ] ) {
magicString.overwrite( node.start, id.end, bundleExports[ name ], true );
id._skip = true;
}
}
}
const scope = node._scope;
if ( scope ) {
topLevel = false;
let newNames = blank();
// Consider a scope to have replacements if there are any namespaceReplacements.
let hasReplacements = statement.namespaceReplacements.length > 0;
keys( names ).forEach( name => {
if ( !scope.declarations[ name ] ) {
newNames[ name ] = names[ name ];
hasReplacements = true;
}
});
deshadowList.forEach( name => {
if ( scope.declarations[ name ] ) {
newNames[ name ] = name + '$$'; // TODO better mechanism
hasReplacements = true;
}
});
if ( !hasReplacements && depth > 0 ) {
return this.skip();
}
names = newNames;
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 ( !declaration || declaration.statement.isIncluded ) {
hasSideEffect = true;
}
}
if ( node.type !== 'Identifier' ) return;
// if there's no replacement, or it's the same, there's nothing more to do
const name = names[ node.name ];
if ( !name || name === node.name ) return;
// shorthand properties (`obj = { foo }`) need to be expanded
if ( parent.type === 'Property' && parent.shorthand ) {
magicString.insert( node.end, `: ${name}` );
parent.key._skip = true;
parent.value._skip = true; // redundant, but defensive
return;
}
// property names etc can be disregarded
if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return;
if ( parent.type === 'Property' && node !== parent.value ) return;
if ( parent.type === 'MethodDefinition' && node === parent.key ) return;
if ( parent.type === 'FunctionExpression' ) return;
if ( /Function/.test( parent.type ) && ~parent.params.indexOf( node ) ) return;
// TODO others...?
// all other identifiers should be overwritten
magicString.overwrite( node.start, node.end, name, true );
},
leave ( node ) {
if ( /^Function/.test( node.type ) ) depth -= 1;
if ( node._scope ) {
names = replacementStack.pop();
}
if ( hasSideEffect ) this.skip();
}
});
return magicString;
if ( hasSideEffect ) statement.mark();
return hasSideEffect;
}
source () {

69
src/ast/Scope.js

@ -1,4 +1,4 @@
import { blank } from '../utils/object';
import { blank, keys } from '../utils/object';
const extractors = {
Identifier ( names, param ) {
@ -33,35 +33,67 @@ function extractNames ( param ) {
return names;
}
class Declaration {
constructor () {
this.statement = null;
this.name = null;
this.isReassigned = false;
this.aliases = [];
}
addAlias ( declaration ) {
this.aliases.push( declaration );
}
addReference ( reference ) {
reference.declaration = this;
this.name = reference.name; // TODO handle differences of opinion
if ( reference.isReassignment ) this.isReassigned = true;
}
render ( es6 ) {
if ( es6 ) return this.name;
if ( !this.isReassigned || !this.isExported ) return this.name;
return `exports.${this.name}`;
}
use () {
this.isUsed = true;
if ( this.statement ) this.statement.mark();
this.aliases.forEach( alias => alias.use() );
}
}
export default class Scope {
constructor ( options ) {
options = options || {};
this.parent = options.parent;
this.depth = this.parent ? this.parent.depth + 1 : 0;
this.declarations = blank();
this.isBlockScope = !!options.block;
this.varDeclarations = [];
this.declarations = blank();
if ( options.params ) {
options.params.forEach( param => {
extractNames( param ).forEach( name => {
this.declarations[ name ] = true;
this.declarations[ name ] = new Declaration( name );
});
});
}
}
addDeclaration ( declaration, isBlockDeclaration, isVar ) {
addDeclaration ( node, isBlockDeclaration, isVar ) {
if ( !isBlockDeclaration && this.isBlockScope ) {
// it's a `var` or function node, and this
// is a block scope, so we need to go up
this.parent.addDeclaration( declaration, isBlockDeclaration, isVar );
this.parent.addDeclaration( node, isBlockDeclaration, isVar );
} else {
extractNames( declaration.id ).forEach( name => {
this.declarations[ name ] = true;
if ( isVar ) this.varDeclarations.push( name );
extractNames( node.id ).forEach( name => {
this.declarations[ name ] = new Declaration( name );
});
}
}
@ -71,15 +103,14 @@ export default class Scope {
( this.parent ? this.parent.contains( name ) : false );
}
findDefiningScope ( name ) {
if ( this.declarations[ name ] ) {
return this;
}
if ( this.parent ) {
return this.parent.findDefiningScope( name );
}
eachDeclaration ( fn ) {
keys( this.declarations ).forEach( key => {
fn( key, this.declarations[ key ] );
});
}
return null;
findDeclaration ( name ) {
return this.declarations[ name ] ||
( this.parent && this.parent.findDeclaration( name ) );
}
}

76
src/ast/attachScopes.js

@ -0,0 +1,76 @@
import { walk } from 'estree-walker';
import Scope from './Scope';
const blockDeclarations = {
'const': true,
'let': true
};
export default function attachScopes ( statement ) {
let { node, scope } = statement;
walk( node, {
enter ( node, parent ) {
// function foo () {...}
// class Foo {...}
if ( /(Function|Class)Declaration/.test( node.type ) ) {
scope.addDeclaration( node, false, false );
}
// var foo = 1
if ( node.type === 'VariableDeclaration' ) {
const isBlockDeclaration = blockDeclarations[ node.kind ];
// only one declarator per block, because we split them up already
scope.addDeclaration( node.declarations[0], isBlockDeclaration, true );
}
let newScope;
// create new function scope
if ( /Function/.test( node.type ) ) {
newScope = new Scope({
parent: scope,
block: false,
params: node.params
});
// named function expressions - the name is considered
// part of the function's scope
if ( node.type === 'FunctionExpression' && node.id ) {
newScope.addDeclaration( node, false, false );
}
}
// create new block scope
if ( node.type === 'BlockStatement' && !/Function/.test( parent.type ) ) {
newScope = new Scope({
parent: scope,
block: true
});
}
// catch clause has its own block scope
if ( node.type === 'CatchClause' ) {
newScope = new Scope({
parent: scope,
params: [ node.param ],
block: true
});
}
if ( newScope ) {
Object.defineProperty( node, '_scope', {
value: newScope,
configurable: true
});
scope = newScope;
}
},
leave ( node ) {
if ( node._scope ) {
scope = scope.parent;
}
}
});
}

59
src/ast/walk.js

@ -1,59 +0,0 @@
import { blank } from '../utils/object';
let shouldSkip;
let shouldAbort;
export default function walk ( ast, { enter, leave }) {
shouldAbort = false;
visit( ast, null, enter, leave );
}
let context = {
skip: () => shouldSkip = true,
abort: () => shouldAbort = true
};
let childKeys = blank();
let toString = Object.prototype.toString;
function isArray ( thing ) {
return toString.call( thing ) === '[object Array]';
}
function visit ( node, parent, enter, leave ) {
if ( !node || shouldAbort ) return;
if ( enter ) {
shouldSkip = false;
enter.call( context, node, parent );
if ( shouldSkip || shouldAbort ) return;
}
let keys = childKeys[ node.type ] || (
childKeys[ node.type ] = Object.keys( node ).filter( key => typeof node[ key ] === 'object' )
);
let key, value, i, j;
i = keys.length;
while ( i-- ) {
key = keys[i];
value = node[ key ];
if ( isArray( value ) ) {
j = value.length;
while ( j-- ) {
visit( value[j], node, enter, leave );
}
}
else if ( value && value.type ) {
visit( value, node, enter, leave );
}
}
if ( leave && !shouldAbort ) {
leave( node, parent );
}
}

2
src/finalisers/amd.js

@ -22,7 +22,7 @@ export default function amd ( bundle, magicString, { exportMode, indentString },
const interopBlock = getInteropBlock( bundle );
if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' );
const exportBlock = getExportBlock( bundle, exportMode );
const exportBlock = getExportBlock( bundle.entryModule, exportMode );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
return magicString

20
src/finalisers/cjs.js

@ -1,19 +1,21 @@
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
let importBlock = bundle.externalModules
.map( module => `var ${module.name} = require('${module.id}');`)
.join('\n');
const importBlock = bundle.externalModules
.map( module => {
let requireStatement = `var ${module.name} = require('${module.id}');`;
const interopBlock = getInteropBlock( bundle );
if ( module.declarations.default ) {
requireStatement += '\n' + ( module.exportsNames ? `var ${module.name}__default = ` : `${module.name} = ` ) +
`'default' in ${module.name} ? ${module.name}['default'] : ${module.name};`;
}
if ( interopBlock ) {
importBlock += '\n' + interopBlock;
}
return requireStatement;
})
.join( '\n' );
if ( importBlock ) {
intro += importBlock + '\n\n';
@ -21,7 +23,7 @@ export default function cjs ( bundle, magicString, { exportMode }, options ) {
magicString.prepend( intro );
const exportBlock = getExportBlock( bundle, exportMode, 'module.exports =' );
const exportBlock = getExportBlock( bundle.entryModule, exportMode, 'module.exports =' );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
return magicString;

39
src/finalisers/es6.js

@ -1,16 +1,5 @@
import { keys } from '../utils/object';
function specifiersFor ( externalModule ) {
return keys( externalModule.importedByBundle )
.filter( notDefault )
.sort()
.map( name => {
const id = externalModule.exports.lookup( name );
return name !== id.name ? `${name} as ${id.name}` : name;
});
}
function notDefault ( name ) {
return name !== 'default';
}
@ -19,19 +8,19 @@ export default function es6 ( bundle, magicString ) {
const importBlock = bundle.externalModules
.map( module => {
const specifiers = [];
const importedNames = keys( module.declarations )
.filter( name => name !== '*' && name !== 'default' );
const id = module.exports.lookup( 'default' );
if ( id ) {
specifiers.push( id.name );
if ( module.declarations.default ) {
specifiers.push( module.name );
}
if ( module.needsAll ) {
specifiers.push( '* as ' + module.name );
if ( module.declarations['*'] ) {
specifiers.push( `* as ${module.name}` );
}
if ( module.needsNamed ) {
specifiers.push( '{ ' + specifiersFor( module ).join( ', ' ) + ' }' );
if ( importedNames.length ) {
specifiers.push( `{ ${importedNames.join( ', ' )} }` );
}
return specifiers.length ?
@ -46,19 +35,19 @@ export default function es6 ( bundle, magicString ) {
const module = bundle.entryModule;
const specifiers = bundle.toExport.filter( notDefault ).map( name => {
const id = bundle.exports.lookup( name );
const specifiers = module.getExports().filter( notDefault ).map( name => {
const declaration = module.traceExport( name );
return id.name === name ?
return declaration.name === name ?
name :
`${id.name} as ${name}`;
`${declaration.name} as ${name}`;
});
let exportBlock = specifiers.length ? `export { ${specifiers.join(', ')} };` : '';
const defaultExport = module.exports.lookup( 'default' );
const defaultExport = module.exports.default || module.reexports.default;
if ( defaultExport ) {
exportBlock += `\nexport default ${ defaultExport.name };`;
exportBlock += `export default ${module.traceExport( 'default' ).name};`;
}
if ( exportBlock ) {

2
src/finalisers/iife.js

@ -33,7 +33,7 @@ export default function iife ( bundle, magicString, { exportMode, indentString }
const interopBlock = getInteropBlock( bundle );
if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' );
const exportBlock = getExportBlock( bundle, exportMode );
const exportBlock = getExportBlock( bundle.entryModule, exportMode );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
return magicString

29
src/finalisers/shared/getExportBlock.js

@ -1,24 +1,21 @@
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' ) {
export default function getExportBlock ( entryModule, exportMode, mechanism = 'return' ) {
if ( exportMode === 'default' ) {
const id = bundle.exports.lookup( 'default' );
return `${mechanism} ${wrapAccess( id )};`;
return `${mechanism} ${entryModule.declarations.default.render( false )};`;
}
return bundle.toExport
return entryModule.getExports()
.map( name => {
const id = bundle.exports.lookup( name );
const prop = name === 'default' ? `['default']` : `.${name}`;
const declaration = entryModule.traceExport( name );
const lhs = `exports${prop}`;
const rhs = declaration.render( false );
// prevent `exports.count = exports.count`
if ( lhs === rhs ) return null;
return `exports${propertyAccess( name )} = ${wrapAccess( id )};`;
return `${lhs} = ${rhs};`;
})
.filter( Boolean )
.join( '\n' );
}

11
src/finalisers/shared/getInteropBlock.js

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

2
src/finalisers/umd.js

@ -48,7 +48,7 @@ export default function umd ( bundle, magicString, { exportMode, indentString },
const interopBlock = getInteropBlock( bundle );
if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' );
const exportBlock = getExportBlock( bundle, exportMode );
const exportBlock = getExportBlock( bundle.entryModule, exportMode );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
return magicString

114
src/optimise/namespace-lookup.js

@ -1,114 +0,0 @@
import walk from '../ast/walk.js';
import getLocation from '../utils/getLocation.js';
// Extract the property access from a MemberExpression.
function property ( node ) {
return node.name ? `.${node.name}` : `[${node.value}]`;
}
// Recursively traverse the chain of member expressions from `node`,
// returning the access, e.g. `foo.bar[17]`
function chainedMemberExpression ( node ) {
if ( node.object.type === 'MemberExpression' ) {
return chainedMemberExpression( node.object ) + property( node.property );
}
return node.object.name + property( node.property );
}
export default function ( statement ) {
let localName; // The local name of the top-most imported namespace.
let topNode = null; // The top-node of the member expression.
let namespace = null; // An instance of `Module`.
walk( statement.node, {
leave ( node, parent ) {
// Optimize namespace lookups, which manifest as MemberExpressions.
if ( node.type === 'MemberExpression' && ( !topNode || node.object === topNode ) ) {
// Ignore anything that doesn't begin with an identifier.
if ( !topNode && node.object.type !== 'Identifier') return;
topNode = node;
// If we don't already have a namespace,
// we aren't currently exploring any chain of member expressions.
if ( !namespace ) {
localName = node.object.name;
// At first, we don't have a namespace, so we'll try to look one up.
const id = statement.module.locals.lookup( localName );
// It only counts if it exists, is a module, and isn't external.
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 = statement.module.id;
err.loc = getLocation( statement.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 mark the whole namespace for inclusion in the bundle.
//
// // 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.mark();
namespace = null;
topNode = null;
return;
}
const id = namespace.exports.lookup( name );
// If the namespace doesn't export the given name,
// we can throw an error (even for nested namespaces).
if ( !id ) {
throw new Error( `Module "${namespace.id}" doesn't export "${name}"!` );
}
// We can't resolve deeper. Replace the member chain.
if ( parent.type !== 'MemberExpression' || !( id.isModule && !id.isExternal ) ) {
if ( !~statement.dependantIds.indexOf( id ) ) {
statement.dependantIds.push( id );
}
// FIXME: do this better
// If an earlier stage detected that we depend on this name...
if ( statement.dependsOn[ localName ] ) {
// ... decrement the count...
if ( !--statement.dependsOn[ localName ] ) {
// ... and remove it if the count is 0.
delete statement.dependsOn[ localName ];
}
}
statement.namespaceReplacements.push( [ topNode, id ] );
namespace = null;
topNode = null;
return;
}
namespace = id;
}
}
});
}

4
src/utils/getExportMode.js

@ -5,7 +5,9 @@ function badExports ( option, keys ) {
}
export default function getExportMode ( bundle, exportMode ) {
const exportKeys = keys( bundle.entryModule.exports.names );
const exportKeys = keys( bundle.entryModule.exports )
.concat( keys( bundle.entryModule.reexports ) )
.concat( bundle.entryModule.exportAllSources ); // not keys, but makes our job easier this way
if ( exportMode === 'default' ) {
if ( exportKeys.length !== 1 || exportKeys[0] !== 'default' ) {

3
test/form/exports-at-end-if-possible/_config.js

@ -2,6 +2,5 @@ module.exports = {
description: 'exports variables at end, if possible',
options: {
moduleName: 'myBundle'
},
// solo: true
}
};

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__default, { a } from 'alphabet';
import alphabet, { a } from 'alphabet';
factory( null );
foo( bar );
containers.forEach( console.log, console );
console.log( a );
console.log( alphabet__default.length );
console.log( alphabet.length );

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

@ -1,15 +1,15 @@
define(function () { 'use strict';
var bar = 42;
var bar$1 = 42;
function foo () {
return bar;
return bar$1;
}
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$1 = 42;
function foo () {
return bar;
return bar$1;
}
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$1 = 42;
function foo () {
return bar;
return bar$1;
}
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$1 = 42;
function foo () {
return bar;
return bar$1;
}
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$1 = 42;
function foo () {
return bar;
return bar$1;
}
function _bar () {
function bar () {
alert( foo() );
}
_bar();
bar();
}));

0
test/function/shorthand-properties/_config.js → test/form/shorthand-properties/_config.js

25
test/form/shorthand-properties/_expected/amd.js

@ -0,0 +1,25 @@
define(function () { 'use strict';
function x () {
return 'foo';
}
var foo = { x };
function x$1 () {
return 'bar';
}
var bar = { x: x$1 };
function x$2 () {
return 'baz';
}
var baz = { x: x$2 };
assert.equal( foo.x(), 'foo' );
assert.equal( bar.x(), 'bar' );
assert.equal( baz.x(), 'baz' );
});

23
test/form/shorthand-properties/_expected/cjs.js

@ -0,0 +1,23 @@
'use strict';
function x () {
return 'foo';
}
var foo = { x };
function x$1 () {
return 'bar';
}
var bar = { x: x$1 };
function x$2 () {
return 'baz';
}
var baz = { x: x$2 };
assert.equal( foo.x(), 'foo' );
assert.equal( bar.x(), 'bar' );
assert.equal( baz.x(), 'baz' );

21
test/form/shorthand-properties/_expected/es6.js

@ -0,0 +1,21 @@
function x () {
return 'foo';
}
var foo = { x };
function x$1 () {
return 'bar';
}
var bar = { x: x$1 };
function x$2 () {
return 'baz';
}
var baz = { x: x$2 };
assert.equal( foo.x(), 'foo' );
assert.equal( bar.x(), 'bar' );
assert.equal( baz.x(), 'baz' );

25
test/form/shorthand-properties/_expected/iife.js

@ -0,0 +1,25 @@
(function () { 'use strict';
function x () {
return 'foo';
}
var foo = { x };
function x$1 () {
return 'bar';
}
var bar = { x: x$1 };
function x$2 () {
return 'baz';
}
var baz = { x: x$2 };
assert.equal( foo.x(), 'foo' );
assert.equal( bar.x(), 'bar' );
assert.equal( baz.x(), 'baz' );
})();

29
test/form/shorthand-properties/_expected/umd.js

@ -0,0 +1,29 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
factory();
}(this, function () { 'use strict';
function x () {
return 'foo';
}
var foo = { x };
function x$1 () {
return 'bar';
}
var bar = { x: x$1 };
function x$2 () {
return 'baz';
}
var baz = { x: x$2 };
assert.equal( foo.x(), 'foo' );
assert.equal( bar.x(), 'bar' );
assert.equal( baz.x(), 'baz' );
}));

7
test/form/shorthand-properties/bar.js

@ -0,0 +1,7 @@
function x () {
return 'bar';
}
var bar = { x };
export { bar };

7
test/form/shorthand-properties/baz.js

@ -0,0 +1,7 @@
function x () {
return 'baz';
}
var baz = { x };
export { baz };

7
test/form/shorthand-properties/foo.js

@ -0,0 +1,7 @@
function x () {
return 'foo';
}
var foo = { x };
export { foo };

7
test/form/shorthand-properties/main.js

@ -0,0 +1,7 @@
import { foo } from './foo';
import { bar } from './bar';
import { baz } from './baz';
assert.equal( foo.x(), 'foo' );
assert.equal( bar.x(), 'bar' );
assert.equal( baz.x(), 'baz' );

3
test/function/assignment-to-exports/_config.js

@ -6,6 +6,5 @@ module.exports = {
assert.equal( exports.count, 0 );
exports.incr();
assert.equal( exports.count, 1 );
},
// solo: true
}
};

2
test/function/consistent-renaming-c/_config.js

@ -1,3 +1,3 @@
module.exports = {
description: 'consistent renaming test c'
};
};

3
test/function/consistent-renaming-f/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'consistent renaming test f'
};

3
test/function/consistent-renaming-f/bar.js

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

9
test/function/consistent-renaming-f/main.js

@ -0,0 +1,9 @@
import bar from './bar';
export default function foo () {}
foo.prototype.a = function ( foo ) {
return bar();
};
assert.equal( new foo().a(), 'consistent' );

2
test/function/export-from-no-local-binding/_config.js

@ -1,3 +1,5 @@
var assert = require( 'assert' );
module.exports = {
description: 'export from does not create a local binding'
};

4
test/function/import-of-unexported-fails/_config.js

@ -2,9 +2,7 @@ var assert = require( 'assert' );
module.exports = {
description: 'marking an imported, but unexported, identifier should throw',
error: function ( err ) {
assert.equal( err.message.slice( 0, 50 ), 'The imported name "default" is never exported by "' );
assert.equal( err.message.slice( -10 ), 'empty.js".' );
assert.ok( /Module .+empty\.js does not export default \(imported by .+main\.js\)/.test( err.message ) );
}
};

3
test/function/module-sort-order/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'module sorter is not confused by top-level call expressions'
};

20
test/function/module-sort-order/a.js

@ -0,0 +1,20 @@
import { b } from './b';
import z from './z';
z();
var p = {
q: function () {
b.nope();
}
};
(function () {
var p = {
q: function () {
b.nope();
}
};
})();
export default 42;

1
test/function/module-sort-order/b.js

@ -0,0 +1 @@
export var b = function () {};

3
test/function/module-sort-order/c.js

@ -0,0 +1,3 @@
import { b } from './b';
export var c = function () {};

4
test/function/module-sort-order/main.js

@ -0,0 +1,4 @@
import a from './a';
import z from './z';
z();

5
test/function/module-sort-order/z.js

@ -0,0 +1,5 @@
import { c } from './c';
export default function () {
c();
}

5
test/function/namespace-optimisation-before-exports/_config.js

@ -1,5 +0,0 @@
module.exports = {
description: 'namespace optimisation must be done after all exports are defined'
};
// See: https://github.com/rollup/rollup/issues/148

1
test/function/namespace-optimisation-before-exports/bar.js

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

1
test/function/namespace-optimisation-before-exports/foo.js

@ -1 +0,0 @@
export { default as bar } from './bar.js';

6
test/function/namespace-optimisation-before-exports/main.js

@ -1,6 +0,0 @@
import * as foo from './foo';
import './zoo';
export default {
foo: foo
};

7
test/function/namespace-optimisation-before-exports/zoo.js

@ -1,7 +0,0 @@
import * as foo from './foo';
function wah () {
foo.bar();
}
wah();

3
test/function/pass-namespace-to-function/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'allows a namespace to be passed to a function'
};

1
test/function/pass-namespace-to-function/bar.js

@ -0,0 +1 @@
// this space left intentionally blank

7
test/function/pass-namespace-to-function/foo.js

@ -0,0 +1,7 @@
import * as bar from './bar';
export default function foo () {}
foo.x = function () {
doSomethingWith( bar );
};

5
test/function/pass-namespace-to-function/main.js

@ -0,0 +1,5 @@
import foo from './foo';
export default function () {
foo();
}

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

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

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

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

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

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

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

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

23
test/sourcemaps/names/_config.js

@ -8,21 +8,20 @@ module.exports = {
moduleName: 'myModule'
},
test: function ( code, map ) {
var match = /Object\.create\( ([^\.]+)\.prototype/.exec( code );
var smc = new SourceMapConsumer( map );
var deconflictedName = match[1];
if ( deconflictedName !== 'Foo' ) throw new Error( 'Need to update this test!' );
var pattern = /Object\.create\( ([\w\$\d]+)\.prototype \)/;
var match = pattern.exec( code );
var smc = new SourceMapConsumer( map );
var generatedLoc = getLocation( code, match.index + 'Object.create ( '.length );
var original = smc.originalPositionFor( generatedLoc );
assert.equal( original.name, 'Bar' );
var index = code.indexOf( deconflictedName );
var generatedLoc = getLocation( code, index );
var originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.name, null );
pattern = /function Foo([\w\$\d]+)/;
match = pattern.exec( code );
index = code.indexOf( deconflictedName, index + 1 );
generatedLoc = getLocation( code, index );
originalLoc = smc.originalPositionFor( generatedLoc );
assert.equal( originalLoc.name, 'Foo' );
generatedLoc = getLocation( code, match.index + 'function '.length );
original = smc.originalPositionFor( generatedLoc );
assert.equal( original.name, 'Foo' );
}
};

26
test/test.js

@ -75,6 +75,28 @@ describe( 'rollup', function () {
}, /must supply options\.dest/ );
});
});
it( 'expects options.moduleName for IIFE and UMD bundles', function () {
return rollup.rollup({
entry: 'x',
resolveId: function () { return 'test'; },
load: function () {
return 'export var foo = 42;';
}
}).then( function ( bundle ) {
assert.throws( function () {
bundle.generate({
format: 'umd'
});
}, /You must supply options\.moduleName for UMD bundles/ );
assert.throws( function () {
bundle.generate({
format: 'iife'
});
}, /You must supply options\.moduleName for IIFE bundles/ );
});
});
});
describe( 'function', function () {
@ -228,6 +250,10 @@ describe( 'rollup', function () {
expectedMap.sourcesContent = expectedMap.sourcesContent.map( normaliseOutput );
} catch ( err ) {}
if ( config.show ) {
console.log( actualCode + '\n\n\n' );
}
assert.equal( actualCode, expectedCode );
assert.deepEqual( actualMap, expectedMap );
});

114
test/testScope.js

@ -1,114 +0,0 @@
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( 'cannot reference undefined names', function () {
var real = new Scope();
var external = real.virtual(),
locals = real.virtual(),
exports = real.virtual();
external.define( 'Component' );
locals.bind( 'Comp', external.reference( 'Component' ) );
assert.throws( function () {
exports.bind( 'default', locals.reference( 'Foo' ) );
}, 'Cannot reference undefined identifier "Foo"' );
locals.define( 'Foo' );
exports.bind( 'default', locals.reference( 'Foo' ) );
});
});
Loading…
Cancel
Save