Browse Source

handle multiple exports of a single binding

contingency-plan
Rich Harris 9 years ago
parent
commit
975528d034
  1. 60
      src/Bundle.js
  2. 32
      src/Module.js
  3. 13
      src/Statement.js
  4. 16
      src/ast/Scope.js
  5. 10
      test/function/export-two-ways/_config.js
  6. 2
      test/function/export-two-ways/foo.js
  7. 5
      test/function/export-two-ways/main.js

60
src/Bundle.js

@ -290,35 +290,34 @@ export default class Bundle {
// //
// This doesn't apply if the bundle is exported as ES6! // This doesn't apply if the bundle is exported as ES6!
let allBundleExports = blank(); let allBundleExports = blank();
let varDeclarations = blank();
let varExports = blank(); let varExports = blank();
let getterExports = [];
if ( format !== 'es6' && exportMode === 'named' ) { this.orderedModules.forEach( module => {
keys( this.entryModule.exports ).forEach( key => { keys( module.varDeclarations ).forEach( name => {
const exportDeclaration = this.entryModule.exports[ key ]; varDeclarations[ module.replacements[ name ] || name ] = true;
const originalDeclaration = this.entryModule.findDeclaration( exportDeclaration.localName );
if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) {
const canonicalName = this.trace( this.entryModule, exportDeclaration.localName, false );
allBundleExports[ canonicalName ] = `exports.${key}`;
varExports[ key ] = true;
}
}); });
});
keys( this.entryModule.reexports ).forEach( key => { if ( format !== 'es6' && exportMode === 'named' ) {
const reexportDeclaration = this.entryModule.reexports[ key ]; keys( this.entryModule.exports )
.concat( keys( this.entryModule.reexports ) )
if ( reexportDeclaration.module.isExternal ) return; .forEach( name => {
const originalDeclaration = reexportDeclaration.module.findDeclaration( reexportDeclaration.localName ); const canonicalName = this.traceExport( this.entryModule, name );
if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) { if ( varDeclarations[ canonicalName ] ) {
const canonicalName = this.trace( reexportDeclaration.module, reexportDeclaration.localName, false ); varExports[ name ] = true;
allBundleExports[ canonicalName ] = `exports.${key}`; // if the same binding is exported multiple ways, we need to
varExports[ key ] = true; // 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 // since we're rewriting variable exports, we want to
@ -327,7 +326,6 @@ export default class Bundle {
.concat( keys( this.entryModule.reexports ) ) .concat( keys( this.entryModule.reexports ) )
.filter( key => !varExports[ key ] ); .filter( key => !varExports[ key ] );
let magicString = new MagicString.Bundle({ separator: '\n\n' }); let magicString = new MagicString.Bundle({ separator: '\n\n' });
this.orderedModules.forEach( module => { this.orderedModules.forEach( module => {
@ -370,6 +368,16 @@ export default class Bundle {
magicString.prepend( namespaceBlock ); magicString.prepend( namespaceBlock );
if ( getterExports.length ) {
// TODO offer ES3-safe (but not spec-compliant) alternative?
const indent = magicString.getIndentString();
const getterExportsBlock = `Object.defineProperties(exports, {\n` +
getterExports.map( ({ key, value }) => indent + `${key}: { get: function () { return ${value}; } }` ).join( ',\n' ) +
`\n});`;
magicString.append( '\n\n' + getterExportsBlock );
}
const finalise = finalisers[ format ]; const finalise = finalisers[ format ];
if ( !finalise ) { if ( !finalise ) {

32
src/Module.js

@ -65,6 +65,7 @@ export default class Module {
this.replacements = blank(); this.replacements = blank();
this.definitions = blank(); this.definitions = blank();
this.varDeclarations = blank();
this.definitionPromises = blank(); this.definitionPromises = blank();
this.modifications = blank(); this.modifications = blank();
@ -200,6 +201,10 @@ export default class Module {
this.definitions[ name ] = statement; this.definitions[ name ] = statement;
}); });
keys( statement.declaresVar ).forEach( name => {
this.varDeclarations[ name ] = statement;
});
keys( statement.modifies ).forEach( name => { keys( statement.modifies ).forEach( name => {
( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement ); ( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement );
}); });
@ -303,33 +308,6 @@ export default class Module {
}); });
} }
findDeclaration ( localName ) {
const importDeclaration = this.imports[ localName ];
// name was defined by another module
if ( importDeclaration ) {
const module = importDeclaration.module;
if ( module.isExternal ) return null;
if ( importDeclaration.name === '*' ) return null;
if ( importDeclaration.name === 'default' ) return null;
const exportDeclaration = module.exports[ importDeclaration.name ];
return module.findDeclaration( exportDeclaration.localName );
}
// name was defined by this module, if any
let i = this.statements.length;
while ( i-- ) {
const declaration = this.statements[i].scope.declarations[ localName ];
if ( declaration ) {
return declaration;
}
}
return null;
}
mark ( name ) { mark ( name ) {
// shortcut cycles // shortcut cycles
if ( this.definitionPromises[ name ] ) { if ( this.definitionPromises[ name ] ) {

13
src/Statement.js

@ -18,6 +18,7 @@ export default class Statement {
this.scope = new Scope(); this.scope = new Scope();
this.defines = blank(); this.defines = blank();
this.declaresVar = blank();
this.modifies = blank(); this.modifies = blank();
this.dependsOn = blank(); this.dependsOn = blank();
this.stronglyDependsOn = blank(); this.stronglyDependsOn = blank();
@ -43,7 +44,7 @@ export default class Statement {
case 'FunctionDeclaration': case 'FunctionDeclaration':
case 'ArrowFunctionExpression': case 'ArrowFunctionExpression':
if ( node.type === 'FunctionDeclaration' ) { if ( node.type === 'FunctionDeclaration' ) {
scope.addDeclaration( node.id.name, node ); scope.addDeclaration( node.id.name, node, false );
} }
newScope = new Scope({ newScope = new Scope({
@ -55,7 +56,7 @@ export default class Statement {
// named function expressions - the name is considered // named function expressions - the name is considered
// part of the function's scope // part of the function's scope
if ( node.type === 'FunctionExpression' && node.id ) { if ( node.type === 'FunctionExpression' && node.id ) {
newScope.addDeclaration( node.id.name, node ); newScope.addDeclaration( node.id.name, node, false );
} }
break; break;
@ -81,12 +82,12 @@ export default class Statement {
case 'VariableDeclaration': case 'VariableDeclaration':
node.declarations.forEach( declarator => { node.declarations.forEach( declarator => {
scope.addDeclaration( declarator.id.name, node ); scope.addDeclaration( declarator.id.name, node, true );
}); });
break; break;
case 'ClassDeclaration': case 'ClassDeclaration':
scope.addDeclaration( node.id.name, node ); scope.addDeclaration( node.id.name, node, false );
break; break;
} }
@ -142,6 +143,10 @@ export default class Statement {
keys( scope.declarations ).forEach( name => { keys( scope.declarations ).forEach( name => {
this.defines[ name ] = true; this.defines[ name ] = true;
}); });
keys( scope.varDeclarations ).forEach( name => {
this.declaresVar[ name ] = true;
});
} }
checkForReads ( scope, node, parent, strong ) { checkForReads ( scope, node, parent, strong ) {

16
src/ast/Scope.js

@ -12,6 +12,7 @@ export default class Scope {
this.parent = options.parent; this.parent = options.parent;
this.depth = this.parent ? this.parent.depth + 1 : 0; this.depth = this.parent ? this.parent.depth + 1 : 0;
this.declarations = blank(); this.declarations = blank();
this.varDeclarations = blank();
this.isBlockScope = !!options.block; this.isBlockScope = !!options.block;
if ( options.params ) { if ( options.params ) {
@ -21,25 +22,16 @@ export default class Scope {
} }
} }
// add ( name, isBlockDeclaration ) { addDeclaration ( name, declaration, isVar ) {
// if ( !isBlockDeclaration && this.isBlockScope ) {
// // it's a `var` or function declaration, and this
// // is a block scope, so we need to go up
// this.parent.add( name, isBlockDeclaration );
// } else {
// this.names.push( name );
// }
// }
addDeclaration ( name, declaration ) {
const isBlockDeclaration = declaration.type === 'VariableDeclaration' && blockDeclarations[ declaration.kind ]; const isBlockDeclaration = declaration.type === 'VariableDeclaration' && blockDeclarations[ declaration.kind ];
if ( !isBlockDeclaration && this.isBlockScope ) { if ( !isBlockDeclaration && this.isBlockScope ) {
// it's a `var` or function declaration, and this // it's a `var` or function declaration, and this
// is a block scope, so we need to go up // is a block scope, so we need to go up
this.parent.addDeclaration( name, declaration ); this.parent.addDeclaration( name, declaration, isVar );
} else { } else {
this.declarations[ name ] = declaration; this.declarations[ name ] = declaration;
if ( isVar ) this.varDeclarations[ name ] = true;
} }
} }

10
test/function/export-two-ways/_config.js

@ -0,0 +1,10 @@
var assert = require( 'assert' );
module.exports = {
description: 'exports the same binding more than one way',
exports: function ( exports ) {
assert.equal( exports.a, 2 );
assert.equal( exports.b, 2 );
assert.equal( exports.c, 2 );
}
};

2
test/function/export-two-ways/foo.js

@ -0,0 +1,2 @@
export var foo = 1;
foo = 2;

5
test/function/export-two-ways/main.js

@ -0,0 +1,5 @@
import { foo } from './foo';
export { foo as a };
export { foo as b };
export { foo as c };
Loading…
Cancel
Save