From 975528d034465f2a7d17de42198b60b4f57069a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Aug 2015 10:08:31 -0400 Subject: [PATCH] handle multiple exports of a single binding --- src/Bundle.js | 60 ++++++++++++++---------- src/Module.js | 32 ++----------- src/Statement.js | 13 +++-- src/ast/Scope.js | 16 ++----- test/function/export-two-ways/_config.js | 10 ++++ test/function/export-two-ways/foo.js | 2 + test/function/export-two-ways/main.js | 5 ++ 7 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 test/function/export-two-ways/_config.js create mode 100644 test/function/export-two-ways/foo.js create mode 100644 test/function/export-two-ways/main.js diff --git a/src/Bundle.js b/src/Bundle.js index 070eabe..0c95c0c 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -290,35 +290,34 @@ export default class Bundle { // // This doesn't apply if the bundle is exported as ES6! let allBundleExports = blank(); + let varDeclarations = blank(); let varExports = blank(); + let getterExports = []; - if ( format !== 'es6' && exportMode === 'named' ) { - keys( this.entryModule.exports ).forEach( key => { - const exportDeclaration = this.entryModule.exports[ key ]; - - 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; - } + this.orderedModules.forEach( module => { + keys( module.varDeclarations ).forEach( name => { + varDeclarations[ module.replacements[ name ] || name ] = true; }); + }); - keys( this.entryModule.reexports ).forEach( key => { - const reexportDeclaration = this.entryModule.reexports[ key ]; - - if ( reexportDeclaration.module.isExternal ) return; - const originalDeclaration = reexportDeclaration.module.findDeclaration( reexportDeclaration.localName ); - - if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) { - const canonicalName = this.trace( reexportDeclaration.module, reexportDeclaration.localName, false ); - - allBundleExports[ canonicalName ] = `exports.${key}`; - varExports[ key ] = 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 ( varDeclarations[ 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 @@ -327,7 +326,6 @@ export default class Bundle { .concat( keys( this.entryModule.reexports ) ) .filter( key => !varExports[ key ] ); - let magicString = new MagicString.Bundle({ separator: '\n\n' }); this.orderedModules.forEach( module => { @@ -370,6 +368,16 @@ export default class Bundle { 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 ]; if ( !finalise ) { diff --git a/src/Module.js b/src/Module.js index 7e8a87c..a3ad6c8 100644 --- a/src/Module.js +++ b/src/Module.js @@ -65,6 +65,7 @@ export default class Module { this.replacements = blank(); this.definitions = blank(); + this.varDeclarations = blank(); this.definitionPromises = blank(); this.modifications = blank(); @@ -200,6 +201,10 @@ export default class Module { this.definitions[ name ] = statement; }); + keys( statement.declaresVar ).forEach( name => { + this.varDeclarations[ name ] = statement; + }); + keys( statement.modifies ).forEach( name => { ( 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 ) { // shortcut cycles if ( this.definitionPromises[ name ] ) { diff --git a/src/Statement.js b/src/Statement.js index e11567c..228e5f1 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -18,6 +18,7 @@ export default class Statement { this.scope = new Scope(); this.defines = blank(); + this.declaresVar = blank(); this.modifies = blank(); this.dependsOn = blank(); this.stronglyDependsOn = blank(); @@ -43,7 +44,7 @@ export default class Statement { case 'FunctionDeclaration': case 'ArrowFunctionExpression': if ( node.type === 'FunctionDeclaration' ) { - scope.addDeclaration( node.id.name, node ); + scope.addDeclaration( node.id.name, node, false ); } newScope = new Scope({ @@ -55,7 +56,7 @@ export default class Statement { // named function expressions - the name is considered // part of the function's scope if ( node.type === 'FunctionExpression' && node.id ) { - newScope.addDeclaration( node.id.name, node ); + newScope.addDeclaration( node.id.name, node, false ); } break; @@ -81,12 +82,12 @@ export default class Statement { case 'VariableDeclaration': node.declarations.forEach( declarator => { - scope.addDeclaration( declarator.id.name, node ); + scope.addDeclaration( declarator.id.name, node, true ); }); break; case 'ClassDeclaration': - scope.addDeclaration( node.id.name, node ); + scope.addDeclaration( node.id.name, node, false ); break; } @@ -142,6 +143,10 @@ export default class Statement { keys( scope.declarations ).forEach( name => { this.defines[ name ] = true; }); + + keys( scope.varDeclarations ).forEach( name => { + this.declaresVar[ name ] = true; + }); } checkForReads ( scope, node, parent, strong ) { diff --git a/src/ast/Scope.js b/src/ast/Scope.js index d9bfae8..4c88967 100644 --- a/src/ast/Scope.js +++ b/src/ast/Scope.js @@ -12,6 +12,7 @@ export default class Scope { this.parent = options.parent; this.depth = this.parent ? this.parent.depth + 1 : 0; this.declarations = blank(); + this.varDeclarations = blank(); this.isBlockScope = !!options.block; if ( options.params ) { @@ -21,25 +22,16 @@ export default class Scope { } } - // add ( name, isBlockDeclaration ) { - // 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 ) { + addDeclaration ( name, declaration, isVar ) { const isBlockDeclaration = declaration.type === 'VariableDeclaration' && blockDeclarations[ declaration.kind ]; 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.addDeclaration( name, declaration ); + this.parent.addDeclaration( name, declaration, isVar ); } else { this.declarations[ name ] = declaration; + if ( isVar ) this.varDeclarations[ name ] = true; } } diff --git a/test/function/export-two-ways/_config.js b/test/function/export-two-ways/_config.js new file mode 100644 index 0000000..9dc72d3 --- /dev/null +++ b/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 ); + } +}; diff --git a/test/function/export-two-ways/foo.js b/test/function/export-two-ways/foo.js new file mode 100644 index 0000000..2ce0ac6 --- /dev/null +++ b/test/function/export-two-ways/foo.js @@ -0,0 +1,2 @@ +export var foo = 1; +foo = 2; diff --git a/test/function/export-two-ways/main.js b/test/function/export-two-ways/main.js new file mode 100644 index 0000000..589038f --- /dev/null +++ b/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 };