Browse Source

very incomplete start on #156 reboot. basically just broke a bunch of things

declarations-and-references
Rich Harris 9 years ago
parent
commit
1bf522388d
  1. 187
      src/Bundle.js
  2. 304
      src/Module.js
  3. 427
      src/Statement.js
  4. 26
      src/ast/Scope.js
  5. 79
      src/ast/attachScopes.js
  6. 2
      test/form/self-contained-bundle/_config.js
  7. 12
      test/test.js

187
src/Bundle.js

@ -47,7 +47,12 @@ export default class Bundle {
return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) )
.then( id => this.fetchModule( id ) )
.then( entryModule => {
entryModule.bindImportSpecifiers();
this.modules.forEach( module => {
module.bindImportSpecifiers();
module.bindReferences();
module.markAllSideEffects();
});
const defaultExport = entryModule.exports.default;
@ -84,12 +89,6 @@ export default class Bundle {
entryModule.markAllStatements( true );
this.markAllModifierStatements();
// Include all side-effects
// TODO does this obviate the need for markAllStatements throughout?
this.modules.forEach( module => {
module.markAllSideEffects();
});
this.orderedModules = this.sort();
});
}
@ -103,79 +102,6 @@ export default class Bundle {
let allReplacements = blank();
// Assign names to external modules
this.externalModules.forEach( module => {
// while we're here...
allReplacements[ module.id ] = blank();
// TODO is this necessary in the ES6 case?
let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id );
module.name = getSafeName( name );
});
// Discover conflicts (i.e. two statements in separate modules both define `foo`)
let i = this.orderedModules.length;
while ( i-- ) {
const module = this.orderedModules[i];
// while we're here...
allReplacements[ module.id ] = blank();
keys( module.definitions ).forEach( name => {
const safeName = getSafeName( name );
if ( safeName !== name ) {
module.rename( name, safeName );
allReplacements[ module.id ][ name ] = safeName;
}
});
}
// Assign non-conflicting names to internal default/namespace export
this.orderedModules.forEach( module => {
if ( !module.needsDefault && !module.needsAll ) return;
if ( module.needsAll ) {
const namespaceName = getSafeName( module.suggestedNames[ '*' ] );
module.replacements[ '*' ] = namespaceName;
}
if ( module.needsDefault || module.needsAll && module.exports.default ) {
const defaultExport = module.exports.default;
// only create a new name if either
// a) it's an expression (`export default 42`) or
// b) it's a name that is reassigned to (`export var a = 1; a = 2`)
if ( defaultExport && defaultExport.identifier && !defaultExport.isModified ) return; // TODO encapsulate check for whether we need synthetic default name
const defaultName = getSafeName( module.suggestedNames.default );
module.replacements.default = defaultName;
}
});
this.orderedModules.forEach( module => {
keys( module.imports ).forEach( localName => {
if ( !module.imports[ localName ].isUsed ) return;
const bundleName = this.trace( module, localName, es6 );
if ( bundleName !== localName ) {
allReplacements[ module.id ][ localName ] = bundleName;
}
});
});
function getSafeName ( name ) {
if ( name in nameCount ) {
nameCount[ name ] += 1;
name = `${name}$${nameCount[ name ]}`;
while ( name in nameCount ) name = `_${name}`; // just to avoid any crazy edge cases
return name;
}
nameCount[ name ] = 0;
return name;
}
return allReplacements;
}
@ -241,41 +167,7 @@ export default class Bundle {
this.modules.forEach( module => {
module.statements.forEach( statement => {
if ( statement.isIncluded ) return;
keys( statement.modifies ).forEach( name => {
const definingStatement = module.definitions[ name ];
const exportDeclaration = module.exports[ name ] || module.reexports[ name ] || (
module.exports.default && module.exports.default.identifier === name && module.exports.default
);
const shouldMark = ( definingStatement && definingStatement.isIncluded ) ||
( exportDeclaration && exportDeclaration.isUsed );
if ( shouldMark ) {
settled = false;
statement.mark();
return;
}
// special case - https://github.com/rollup/rollup/pull/40
// TODO refactor this? it's a bit confusing
const importDeclaration = module.imports[ name ];
if ( !importDeclaration || importDeclaration.module.isExternal ) return;
if ( importDeclaration.name === '*' ) {
importDeclaration.module.markAllExportStatements();
} else {
const otherExportDeclaration = importDeclaration.module.exports[ importDeclaration.name ];
// TODO things like `export default a + b` don't apply here... right?
const otherDefiningStatement = module.findDefiningStatement( otherExportDeclaration.localName );
if ( !otherDefiningStatement ) return;
statement.mark();
}
settled = false;
});
// TODO...
});
});
@ -289,61 +181,10 @@ 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.replacements[ name ] || name ] = true;
});
});
if ( format !== 'es6' && exportMode === 'named' ) {
keys( this.entryModule.exports )
.concat( keys( this.entryModule.reexports ) )
.forEach( name => {
const canonicalName = this.traceExport( this.entryModule, 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.entryModule.getExports()
.filter( key => !varExports[ key ] );
let magicString = new MagicString.Bundle({ separator: '\n\n' });
this.orderedModules.forEach( module => {
const source = module.render( allBundleExports, allReplacements[ module.id ], format );
const source = module.render();
if ( source.toString().length ) {
magicString.addSource( source );
}
@ -366,21 +207,13 @@ export default class Bundle {
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( ', ' )}` );
}
this.toExport = []; // TODO
magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options );
if ( options.banner ) magicString.prepend( options.banner + '\n' );
@ -460,7 +293,7 @@ export default class Bundle {
ordered.push( module );
}
visit( this.entryModule );
this.modules.forEach( visit );
if ( hasCycles ) {
let unordered = ordered;

304
src/Module.js

@ -2,29 +2,13 @@ import { parse } from 'acorn';
import MagicString from 'magic-string';
import Statement from './Statement';
import walk from './ast/walk';
import Scope from './ast/Scope';
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 deconflict ( name, names ) {
while ( name in names ) {
name = `_${name}`;
}
return name;
}
function isEmptyExportedVarDeclaration ( node, allBundleExports, moduleReplacements ) {
if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false;
const name = node.declarations[0].id.name;
const canonicalName = moduleReplacements[ name ] || name;
return canonicalName in allBundleExports;
}
export default class Module {
constructor ({ id, source, ast, bundle }) {
this.source = source;
@ -45,7 +29,6 @@ export default class Module {
this.magicString.remove( match.index, match.index + match[0].length );
}
this.suggestedNames = blank();
this.comments = [];
this.statements = this.parse( ast );
@ -53,7 +36,6 @@ export default class Module {
// all dependencies
this.dependencies = [];
this.resolvedIds = blank();
this.boundImportSpecifiers = false;
// imports and exports, indexed by local name
this.imports = blank();
@ -63,15 +45,8 @@ export default class Module {
this.exportAlls = [];
this.replacements = blank();
this.reassignments = [];
this.marked = blank();
this.scope = new Scope();
this.definitions = blank();
this.definitionPromises = blank();
this.modifications = blank();
this.analyse();
}
@ -203,44 +178,9 @@ export default class Module {
statement.analyse();
// consolidate names that are defined/modified in this module
keys( statement.defines ).forEach( name => {
this.definitions[ name ] = statement;
});
keys( statement.modifies ).forEach( name => {
( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement );
});
});
// 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;
});
});
// 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;
// while we're here, mark reassignments
statement.scope.varDeclarations.forEach( name => {
if ( reassigned[ name ] && !~this.reassignments.indexOf( name ) ) {
this.reassignments.push( name );
}
});
keys( statement.dependsOn ).forEach( name => {
if ( !this.definitions[ name ] && !this.imports[ name ] ) {
this.bundle.assumedGlobals[ name ] = true;
}
keys( statement.scope.declarations ).forEach( name => {
const declaration = statement.scope.declarations[ name ];
this.definitions[ name ] = { statement, declaration };
});
});
}
@ -250,9 +190,6 @@ export default class Module {
}
bindImportSpecifiers () {
if ( this.boundImportSpecifiers ) return;
this.boundImportSpecifiers = true;
[ this.imports, this.reexports ].forEach( specifiers => {
keys( specifiers ).forEach( name => {
const specifier = specifiers[ name ];
@ -268,83 +205,59 @@ export default class Module {
const id = this.resolvedIds[ delegate.source ];
delegate.module = this.bundle.moduleById[ id ];
});
this.dependencies.forEach( source => {
const id = this.resolvedIds[ source ];
const module = this.bundle.moduleById[ id ];
if ( !module.isExternal ) module.bindImportSpecifiers();
});
}
consolidateDependencies () {
let strongDependencies = blank();
bindReferences () {
this.statements.forEach( ( statement, i ) => {
statement.references.forEach( reference => {
let declaration;
function addDependency ( dependencies, declaration ) {
if ( declaration && declaration.module && !declaration.module.isExternal ) {
dependencies[ declaration.module.id ] = declaration.module;
return true;
}
}
// find in local scope...
declaration = reference.scope.findDeclaration( reference.name );
this.statements.forEach( statement => {
if ( statement.isImportDeclaration && !statement.node.specifiers.length ) {
// include module for its side-effects
const id = this.resolvedIds[ statement.node.source.value ];
const module = this.bundle.moduleById[ id ];
if ( declaration ) {
reference.declaration = declaration;
reference.definingStatement = statement;
return;
}
if ( !module.isExternal ) strongDependencies[ module.id ] = module;
}
let definition;
else if ( statement.isReexportDeclaration ) {
if ( statement.node.specifiers ) {
statement.node.specifiers.forEach( specifier => {
let reexport;
let module = this;
let name = specifier.exported.name;
while ( !module.isExternal && module.reexports[ name ] && module.reexports[ name ].isUsed ) {
reexport = module.reexports[ name ];
module = reexport.module;
name = reexport.localName;
}
addDependency( strongDependencies, reexport );
});
}
}
// ...or in module...
definition = this.definitions[ reference.name ];
else {
keys( statement.stronglyDependsOn ).forEach( name => {
if ( statement.defines[ name ] ) return;
// ...or from import
if ( !definition ) {
const importDeclaration = this.imports[ reference.name ];
if ( importDeclaration ) {
definition = importDeclaration.module.traceExport( importDeclaration.name );
}
}
addDependency( strongDependencies, this.exportDelegates[ name ] ) ||
addDependency( strongDependencies, this.imports[ name ] );
});
}
if ( definition ) {
reference.declaration = definition.declaration;
reference.definingStatement = definition.statement;
definition.declaration.references.push( reference );
} else {
//console.log( 'TODO no declaration. global?' );
}
});
});
}
consolidateDependencies () {
let strongDependencies = blank();
let weakDependencies = blank();
this.statements.forEach( statement => {
keys( statement.dependsOn ).forEach( name => {
if ( statement.defines[ name ] ) return;
addDependency( weakDependencies, this.exportDelegates[ name ] ) ||
addDependency( weakDependencies, this.imports[ name ] );
statement.references.forEach( reference => {
if ( reference.definingStatement ) {
const module = reference.definingStatement.module;
weakDependencies[ module.id ] = module;
}
});
});
// special case – `export { ... } from './other'` in entry module
if ( this.exportAlls.length ) {
this.exportAlls.forEach( ({ source }) => {
const resolved = this.resolvedIds[ source ];
const otherModule = this.bundle.moduleById[ resolved ];
strongDependencies[ otherModule.id ] = otherModule;
});
}
return { strongDependencies, weakDependencies };
}
@ -627,125 +540,50 @@ export default class Module {
return statements;
}
rename ( name, replacement ) {
this.replacements[ name ] = replacement;
}
render ( allBundleExports, moduleReplacements ) {
render () {
let magicString = this.magicString.clone();
this.statements.forEach( statement => {
if ( !statement.isIncluded ) {
magicString.remove( statement.start, statement.next );
return;
}
// skip `export { foo, bar, baz }`
if ( statement.node.type === 'ExportNamedDeclaration' ) {
// skip `export { foo, bar, baz }`
if ( statement.node.specifiers.length ) {
magicString.remove( statement.start, statement.next );
return;
}
// skip `export var foo;` if foo is exported
if ( isEmptyExportedVarDeclaration( statement.node.declaration, allBundleExports, moduleReplacements ) ) {
magicString.remove( statement.start, statement.next );
return;
}
}
// skip empty var declarations for exported bindings
// (otherwise we're left with `exports.foo;`, which is useless)
if ( isEmptyExportedVarDeclaration( statement.node, allBundleExports, moduleReplacements ) ) {
magicString.remove( statement.start, statement.next );
return;
}
// split up/remove var declarations as necessary
if ( statement.node.isSynthetic ) {
// insert `var/let/const` if necessary
if ( !allBundleExports[ statement.node.declarations[0].id.name ] ) {
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();
keys( statement.dependsOn )
.concat( keys( statement.defines ) )
.forEach( name => {
const bundleName = moduleReplacements[ name ] || name;
if ( allBundleExports[ bundleName ] ) {
bundleExports[ name ] = replacements[ name ] = allBundleExports[ bundleName ];
} else if ( bundleName !== name ) { // TODO weird structure
replacements[ name ] = bundleName;
}
});
statement.replaceIdentifiers( magicString, replacements, bundleExports );
// modify exports as necessary
if ( statement.isReexportDeclaration ) {
// remove `export { foo } from './other'` and `export * from './other'`
magicString.remove( statement.start, statement.next );
}
});
else 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 );
}
// remove `export` from `export class Foo {...}` or `export default Foo`
// TODO default exports need different treatment
else if ( statement.node.declaration.id ) {
magicString.remove( statement.node.start, statement.node.declaration.start );
}
return magicString.trim();
}
else if ( statement.node.type === 'ExportDefaultDeclaration' ) {
const canonicalName = this.defaultName();
trace ( name ) {
if ( name in this.definitions ) return this.definitions[ name ];
if ( name in this.imports ) {
const otherModule = this.imports[ name ].module;
return otherModule.traceExport( name );
}
if ( statement.node.declaration.type === 'Identifier' && canonicalName === ( moduleReplacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) {
magicString.remove( statement.start, statement.next );
return;
}
return null;
}
// prevent `var undefined = sideEffectyDefault(foo)`
if ( canonicalName === undefined ) {
magicString.remove( statement.start, statement.node.declaration.start );
return;
}
traceExport ( name ) {
// export { foo } from './other'
const reexportDeclaration = this.reexports[ name ];
if ( reexportDeclaration ) {
return reexportDeclaration.module.traceExport( reexportDeclaration.name );
}
// anonymous functions should be converted into declarations
if ( statement.node.declaration.type === 'FunctionExpression' ) {
magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${canonicalName}` );
} else {
magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${canonicalName} = ` );
}
const exportDeclaration = this.exports[ name ];
if ( exportDeclaration ) {
// TODO defaults should not be live...
if ( name === 'default' ) {
if ( exportDeclaration.identifier ) {
return this.trace( exportDeclaration.identifier );
}
else {
throw new Error( 'Unhandled export' );
}
throw new Error( 'TODO default expression exports' );
}
});
return magicString.trim();
}
suggestName ( defaultOrBatch, suggestion ) {
// deconflict anonymous default exports with this module's definitions
const shouldDeconflict = this.exports.default && this.exports.default.isAnonymous;
if ( shouldDeconflict ) suggestion = deconflict( suggestion, this.definitions );
if ( !this.suggestedNames[ defaultOrBatch ] ) {
this.suggestedNames[ defaultOrBatch ] = makeLegalIdentifier( suggestion );
return this.trace( exportDeclaration.name );
}
// TODO export *
throw new Error( 'could not trace export', name );
}
}

427
src/Statement.js

@ -1,12 +1,6 @@
import { blank, keys } from './utils/object';
import getLocation from './utils/getLocation';
import walk from './ast/walk';
import Scope from './ast/Scope';
const blockDeclarations = {
'const': true,
'let': true
};
import attachScopes from './ast/attachScopes';
const modifierNodes = {
AssignmentExpression: 'left',
@ -17,12 +11,43 @@ 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;
}
if ( node.type === 'Identifier' ) {
// 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;
// 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
// `var foo = function () {}` - same thing for present purposes
if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true;
this.parts = [];
let root = node;
while ( root.type === 'MemberExpression' ) {
this.parts.unshift( root.property.name );
root = root.object;
}
this.parts.unshift( root.name );
this.name = root.name;
}
}
export default class Statement {
@ -34,14 +59,9 @@ export default class Statement {
this.next = null; // filled in later
this.scope = new Scope();
this.defines = blank();
this.modifies = blank();
this.dependsOn = blank();
this.stronglyDependsOn = blank();
this.reassigns = blank();
this.references = [];
this.hasSideEffects = false;
this.isIncluded = false;
this.isImportDeclaration = node.type === 'ImportDeclaration';
@ -52,255 +72,39 @@ export default class Statement {
analyse () {
if ( this.isImportDeclaration ) return; // nothing to analyse
// attach scopes
attachScopes( this );
let references = this.references;
// find references
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;
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 ( node._scope ) scope = node._scope;
if ( newScope ) {
Object.defineProperty( node, '_scope', {
value: newScope,
configurable: true
});
if ( isReference( node, parent ) ) {
const reference = new Reference( node, scope );
references.push( reference );
scope = newScope;
this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
}
},
leave ( node ) {
if ( node._scope ) {
scope = scope.parent;
}
leave: ( node ) => {
if ( node._scope ) scope = scope.parent;
}
});
// 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
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 );
if ( !definingScope || definingScope.depth === 0 ) {
this.dependsOn[ node.name ] = true;
if ( strong ) this.stronglyDependsOn[ node.name ] = true;
}
}
}
checkForWrites ( scope, node, writeDepth ) {
const addNode = ( node, isAssignment ) => {
let depth = 0; // determine whether we're illegally modifying a binding or namespace
while ( node.type === 'MemberExpression' ) {
node = node.object;
depth += 1;
}
// disallow assignments/updates to imported bindings and namespaces
if ( isAssignment ) {
const importSpecifier = this.module.imports[ node.name ];
if ( importSpecifier && !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
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;
}
}
// special case = `export default foo; foo += 1;` - we'll
// need to assign a new variable so that the exported
// value is not updated by the second statement
if ( this.module.exports.default && depth === 0 && this.module.exports.default.identifier === node.name ) {
// but only if this is a) inside a function body or
// b) after the export declaration
if ( !!scope.parent || node.start > this.module.exports.default.statement.node.start ) {
this.module.exports.default.isModified = true;
}
}
// 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' ) {
this.modifies[ node.name ] = true;
}
};
if ( node.type === 'AssignmentExpression' ) {
addNode( node.left, true );
}
else if ( node.type === 'UpdateExpression' ) {
addNode( node.argument, true );
}
else if ( node.type === 'CallExpression' ) {
node.arguments.forEach( arg => addNode( arg, false ) );
// `foo.bar()` is assumed to mutate foo
if ( node.callee.type === 'MemberExpression' ) {
addNode( node.callee );
}
}
}
mark () {
if ( this.isIncluded ) return; // prevent infinite loops
this.isIncluded = true;
// `export { name } from './other'` is a special case
if ( this.isReexportDeclaration ) {
const id = this.module.resolvedIds[ this.node.source.value ];
const otherModule = this.module.bundle.moduleById[ id ];
if ( this.node.specifiers ) {
this.node.specifiers.forEach( specifier => {
const reexport = this.module.reexports[ specifier.exported.name ];
reexport.isUsed = true;
reexport.module = otherModule; // TODO still necessary?
if ( !otherModule.isExternal ) otherModule.markExport( specifier.local.name, specifier.exported.name, this.module );
});
} else {
otherModule.needsAll = true;
otherModule.getExports().forEach( name => {
if ( name !== 'default' ) otherModule.markExport( name, name, this.module );
});
this.references.forEach( reference => {
if ( reference.definingStatement ) {
reference.definingStatement.mark();
}
return;
}
Object.keys( this.dependsOn ).forEach( name => {
if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place?
this.module.mark( name );
});
}
@ -330,128 +134,7 @@ export default class Statement {
});
}
replaceIdentifiers ( magicString, names, bundleExports ) {
const replacementStack = [ names ];
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 name = node.declarations[0].id.name;
if ( node.declarations.length === 1 && bundleExports[ name ] ) {
magicString.overwrite( node.start, node.declarations[0].id.end, bundleExports[ name ], true );
node.declarations[0].id._skip = true;
}
// otherwise, we insert the `exports.foo = foo` after the declaration
else {
const exportInitialisers = node.declarations
.map( declarator => declarator.id.name )
.filter( name => !!bundleExports[ name ] )
.map( name => `\n${bundleExports[name]} = ${name};` )
.join( '' );
if ( exportInitialisers ) {
// TODO clean this up
try {
magicString.insert( node.end, exportInitialisers );
} catch ( err ) {
magicString.append( exportInitialisers );
}
}
}
}
}
const scope = node._scope;
if ( scope ) {
topLevel = false;
let newNames = blank();
let hasReplacements;
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 !== '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 ) {
replacementStack.pop();
names = replacementStack[ replacementStack.length - 1 ];
}
}
});
replaceIdentifiers ( magicString ) {
return magicString;
}

26
src/ast/Scope.js

@ -33,35 +33,38 @@ function extractNames ( param ) {
return names;
}
class Declaration {
constructor ( node ) {
this.references = [];
}
}
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( param );
});
});
}
}
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( node );
});
}
}
@ -71,6 +74,11 @@ export default class Scope {
( this.parent ? this.parent.contains( name ) : false );
}
findDeclaration ( name ) {
return this.declarations[ name ] ||
( this.parent && this.parent.findDeclaration( name ) );
}
findDefiningScope ( name ) {
if ( this.declarations[ name ] ) {
return this;

79
src/ast/attachScopes.js

@ -0,0 +1,79 @@
import walk from './walk';
import Scope from './Scope';
const blockDeclarations = {
'const': true,
'let': true
};
export default function attachScopes ( statement ) {
let { node, scope } = statement;
walk( 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;
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;
}
}
});
}

2
test/form/self-contained-bundle/_config.js

@ -1,3 +1,5 @@
module.exports = {
solo: true,
show: true,
description: 'self-contained bundle'
};

12
test/test.js

@ -15,11 +15,11 @@ var SOURCEMAPS = path.resolve( __dirname, 'sourcemaps' );
var CLI = path.resolve( __dirname, 'cli' );
var PROFILES = [
{ format: 'amd' },
// { format: 'amd' },
{ format: 'cjs' },
{ format: 'es6' },
{ format: 'iife' },
{ format: 'umd' }
// { format: 'es6' },
// { format: 'iife' },
// { format: 'umd' }
];
function extend ( target ) {
@ -228,6 +228,10 @@ describe( 'rollup', function () {
expectedMap.sourcesContent = expectedMap.sourcesContent.map( normaliseOutput );
} catch ( err ) {}
if ( config.show || unintendedError ) {
console.log( actualCode + '\n\n\n' );
}
assert.equal( actualCode, expectedCode );
assert.deepEqual( actualMap, expectedMap );
});

Loading…
Cancel
Save