From e36a57527b490a748a6e13e7952c8a8d382fa774 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sun, 18 Sep 2016 17:45:09 -0400 Subject: [PATCH] prevent {export foo as default} from creating a live binding (#860) --- src/Module.js | 8 ++- src/ast/nodes/ExportNamedDeclaration.js | 67 ++++++++++++++++++++-- test/function/export-as-default/_config.js | 1 - 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/Module.js b/src/Module.js index 6e58131..3abdd4c 100644 --- a/src/Module.js +++ b/src/Module.js @@ -170,7 +170,13 @@ export default class Module { throw new Error( `A module cannot have multiple exports with the same name ('${exportedName}')` ); } - this.exports[ exportedName ] = { localName }; + // `export { default as foo }` – special case. We want importers + // to use the UnboundDefaultExport proxy, not the original declaration + if ( exportedName === 'default' ) { + this.exports[ exportedName ] = { localName: 'default' }; + } else { + this.exports[ exportedName ] = { localName }; + } }); } else { this.bundle.onwarn( `Module ${this.id} has an empty export declaration` ); diff --git a/src/ast/nodes/ExportNamedDeclaration.js b/src/ast/nodes/ExportNamedDeclaration.js index b815dd6..3f9ba4e 100644 --- a/src/ast/nodes/ExportNamedDeclaration.js +++ b/src/ast/nodes/ExportNamedDeclaration.js @@ -1,17 +1,59 @@ +import { find } from '../../utils/array.js'; import Node from '../Node.js'; +class UnboundDefaultExport { + constructor ( original ) { + this.original = original; + this.name = original.name; + } + + activate () { + if ( this.activated ) return; + this.activated = true; + + this.original.activate(); + } + + addReference ( reference ) { + this.name = reference.name; + this.original.addReference( reference ); + } + + bind ( scope ) { + this.original.bind( scope ); + } + + gatherPossibleValues ( values ) { + this.original.gatherPossibleValues( values ); + } + + getName ( es ) { + if ( this.original && !this.original.isReassigned ) { + return this.original.getName( es ); + } + + return this.name; + } +} + export default class ExportNamedDeclaration extends Node { initialise ( scope ) { + this.scope = scope; this.isExportDeclaration = true; - if ( this.declaration ) { - this.declaration.initialise( scope ); + + // special case – `export { foo as default }` should not create a live binding + const defaultExport = find( this.specifiers, specifier => specifier.exported.name === 'default' ); + if ( defaultExport ) { + const declaration = this.scope.findDeclaration( defaultExport.local.name ); + this.defaultExport = new UnboundDefaultExport( declaration ); + scope.declarations.default = this.defaultExport; } + + if ( this.declaration ) this.declaration.initialise( scope ); } bind ( scope ) { - if ( this.declaration ) { - this.declaration.bind( scope ); - } + if ( this.declaration ) this.declaration.bind( scope ); } render ( code, es ) { @@ -19,7 +61,20 @@ export default class ExportNamedDeclaration extends Node { code.remove( this.start, this.declaration.start ); this.declaration.render( code, es ); } else { - code.remove( this.leadingCommentStart || this.start, this.next || this.end ); + const start = this.leadingCommentStart || this.start; + const end = this.next || this.end; + + if ( this.defaultExport ) { + const name = this.defaultExport.getName( es ); + const originalName = this.defaultExport.original.getName( es ); + + if ( name !== originalName ) { + code.overwrite( start, end, `var ${name} = ${originalName};` ); + return; + } + } + + code.remove( start, end ); } } } diff --git a/test/function/export-as-default/_config.js b/test/function/export-as-default/_config.js index 0f4773a..d5de425 100644 --- a/test/function/export-as-default/_config.js +++ b/test/function/export-as-default/_config.js @@ -1,6 +1,5 @@ var assert = require( 'assert' ); module.exports = { - solo: true, description: 'export { foo as default } does not create a live binding' };