Browse Source

rewrite exported vars, to keep exports live

contingency-plan
Rich-Harris 10 years ago
parent
commit
4b26a86d1d
  1. 46
      src/Bundle.js
  2. 4
      src/ExternalModule.js
  3. 26
      src/Module.js
  4. 92
      src/Statement.js
  5. 42
      src/ast/Scope.js
  6. 2
      src/finalisers/amd.js
  7. 2
      src/finalisers/cjs.js
  8. 2
      src/finalisers/iife.js
  9. 2
      src/finalisers/umd.js
  10. 10
      test/function/assignment-to-exports/_config.js
  11. 5
      test/function/assignment-to-exports/main.js

46
src/Bundle.js

@ -32,6 +32,10 @@ export default class Bundle {
}; };
this.entryModule = null; this.entryModule = null;
this.varExports = blank();
this.toExport = null;
this.modulePromises = blank(); this.modulePromises = blank();
this.statements = []; this.statements = [];
this.externalModules = []; this.externalModules = [];
@ -179,21 +183,59 @@ export default class Bundle {
let previousMargin = 0; let previousMargin = 0;
// 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;
//
// TODO This doesn't apply if the bundle is exported as ES6!
let allBundleExports = blank();
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.entryModule.getCanonicalName( exportDeclaration.localName );
allBundleExports[ canonicalName ] = `exports.${key}`;
this.varExports[ key ] = true;
}
});
// since we're rewriting variable exports, we want to
// ensure we don't try and export them again at the bottom
this.toExport = keys( this.entryModule.exports )
.filter( key => !this.varExports[ key ] );
// Apply new names and add to the output bundle // Apply new names and add to the output bundle
this.statements.forEach( statement => { this.statements.forEach( statement => {
let replacements = blank(); let replacements = blank();
let bundleExports = blank();
keys( statement.dependsOn ) keys( statement.dependsOn )
.concat( keys( statement.defines ) ) .concat( keys( statement.defines ) )
.forEach( name => { .forEach( name => {
const canonicalName = statement.module.getCanonicalName( name ); const canonicalName = statement.module.getCanonicalName( name );
if ( name !== canonicalName ) { if ( allBundleExports[ canonicalName ] ) {
bundleExports[ name ] = replacements[ name ] = allBundleExports[ canonicalName ];
} else if ( name !== canonicalName ) {
replacements[ name ] = canonicalName; replacements[ name ] = canonicalName;
} }
}); });
const source = statement.replaceIdentifiers( replacements ); const source = statement.replaceIdentifiers( replacements, bundleExports );
// modify exports as necessary // modify exports as necessary
if ( statement.isExportDeclaration ) { if ( statement.isExportDeclaration ) {

4
src/ExternalModule.js

@ -37,4 +37,8 @@ export default class ExternalModule {
this.suggestedNames[ exportName ] = suggestion; this.suggestedNames[ exportName ] = suggestion;
} }
} }
findDefiningStatement () {
return null;
}
} }

26
src/Module.js

@ -178,6 +178,32 @@ 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;
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 statement = this.statements[i];
const declaration = this.statements[i].scope.declarations[ localName ];
if ( declaration ) {
return declaration;
}
}
return null;
}
getCanonicalName ( localName ) { getCanonicalName ( localName ) {
if ( this.suggestedNames[ localName ] ) { if ( this.suggestedNames[ localName ] ) {
localName = this.suggestedNames[ localName ]; localName = this.suggestedNames[ localName ];

92
src/Statement.js

@ -37,24 +37,6 @@ export default class Statement {
let scope = this.scope; let scope = this.scope;
function addToScope ( declarator ) {
var name = declarator.id.name;
scope.add( name, false );
if ( !scope.parent ) {
statement.defines[ name ] = true;
}
}
function addToBlockScope ( declarator ) {
var name = declarator.id.name;
scope.add( name, true );
if ( !scope.parent ) {
statement.defines[ name ] = true;
}
}
walk( this.node, { walk( this.node, {
enter ( node ) { enter ( node ) {
let newScope; let newScope;
@ -65,20 +47,22 @@ export default class Statement {
case 'FunctionExpression': case 'FunctionExpression':
case 'FunctionDeclaration': case 'FunctionDeclaration':
case 'ArrowFunctionExpression': case 'ArrowFunctionExpression':
let names = node.params.map( getName );
if ( node.type === 'FunctionDeclaration' ) { if ( node.type === 'FunctionDeclaration' ) {
addToScope( node ); scope.addDeclaration( node.id.name, node );
} else if ( node.type === 'FunctionExpression' && node.id ) {
names.push( node.id.name );
} }
newScope = new Scope({ newScope = new Scope({
parent: scope, parent: scope,
params: names, // TODO rest params? params: node.params, // TODO rest params?
block: false block: false
}); });
// 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 );
}
break; break;
case 'BlockStatement': case 'BlockStatement':
@ -92,18 +76,20 @@ export default class Statement {
case 'CatchClause': case 'CatchClause':
newScope = new Scope({ newScope = new Scope({
parent: scope, parent: scope,
params: [ node.param.name ], params: [ node.param ],
block: true block: true
}); });
break; break;
case 'VariableDeclaration': case 'VariableDeclaration':
node.declarations.forEach( node.kind === 'let' ? addToBlockScope : addToScope ); // TODO const? node.declarations.forEach( declarator => {
scope.addDeclaration( declarator.id.name, node );
});
break; break;
case 'ClassDeclaration': case 'ClassDeclaration':
addToScope( node ); scope.addDeclaration( node.id.name, node );
break; break;
} }
@ -132,6 +118,10 @@ export default class Statement {
} }
}); });
} }
keys( scope.declarations ).forEach( name => {
statement.defines[ name ] = true;
});
} }
checkForReads ( scope, node, parent ) { checkForReads ( scope, node, parent ) {
@ -247,7 +237,9 @@ export default class Statement {
}); });
} }
replaceIdentifiers ( names ) { replaceIdentifiers ( names, bundleExports ) {
const module = this.module;
const magicString = this.magicString.clone(); const magicString = this.magicString.clone();
const replacementStack = [ names ]; const replacementStack = [ names ];
const nameList = keys( names ); const nameList = keys( names );
@ -258,24 +250,60 @@ export default class Statement {
deshadowList.push( replacement.split( '.' )[0] ); deshadowList.push( replacement.split( '.' )[0] );
}); });
if ( nameList.length > 0 ) { if ( nameList.length > 0 || keys( bundleExports ).length ) {
let topLevel = true;
walk( this.node, { walk( this.node, {
enter ( node, parent ) { enter ( node, parent ) {
if ( node._skip ) return this.skip();
// 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 ] );
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( '' );
// TODO clean this up
try {
magicString.insert( node.end, exportInitialisers );
} catch ( err ) {
magicString.append( exportInitialisers );
}
}
}
}
const scope = node._scope; const scope = node._scope;
if ( scope ) { if ( scope ) {
topLevel = false;
let newNames = blank(); let newNames = blank();
let hasReplacements; let hasReplacements;
keys( names ).forEach( key => { keys( names ).forEach( key => {
if ( !~scope.names.indexOf( key ) ) { if ( !scope.declarations[ key ] ) {
newNames[ key ] = names[ key ]; newNames[ key ] = names[ key ];
hasReplacements = true; hasReplacements = true;
} }
}); });
deshadowList.forEach( name => { deshadowList.forEach( name => {
if ( ~scope.names.indexOf( name ) ) { if ( ~scope.declarations[ name ] ) {
newNames[ name ] = name + '$$'; // TODO better mechanism newNames[ name ] = name + '$$'; // TODO better mechanism
hasReplacements = true; hasReplacements = true;
} }
@ -289,7 +317,7 @@ export default class Statement {
replacementStack.push( newNames ); replacementStack.push( newNames );
} }
// We want to rewrite identifiers (that aren't property names) // We want to rewrite identifiers (that aren't property names etc)
if ( node.type !== 'Identifier' ) return; if ( node.type !== 'Identifier' ) return;
if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return; if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return;
if ( parent.type === 'Property' && node !== parent.value ) return; if ( parent.type === 'Property' && node !== parent.value ) return;

42
src/ast/Scope.js

@ -1,29 +1,59 @@
import { blank } from '../utils/object';
const blockDeclarations = {
'const': true,
'let': true
};
export default class Scope { export default class Scope {
constructor ( options ) { constructor ( options ) {
options = options || {}; options = options || {};
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.names = options.params || []; this.declarations = blank();
this.isBlockScope = !!options.block; this.isBlockScope = !!options.block;
if ( options.params ) {
options.params.forEach( param => {
this.declarations[ param.name ] = param;
});
}
} }
add ( name, isBlockDeclaration ) { // 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 ) {
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.add( name, isBlockDeclaration ); this.parent.addDeclaration( name, declaration );
} else { } else {
this.names.push( name ); this.declarations[ name ] = declaration;
} }
} }
getDeclaration ( name ) {
return this.declarations[ name ] ||
this.parent && this.parent.getDeclaration( name );
}
contains ( name ) { contains ( name ) {
return !!this.findDefiningScope( name ); return !!this.getDeclaration( name );
} }
findDefiningScope ( name ) { findDefiningScope ( name ) {
if ( ~this.names.indexOf( name ) ) { if ( !!this.declarations[ name ] ) {
return this; return this;
} }

2
src/finalisers/amd.js

@ -22,7 +22,7 @@ export default function amd ( bundle, magicString, exportMode, options ) {
if ( exportMode === 'default' ) { if ( exportMode === 'default' ) {
exportBlock = `return ${bundle.entryModule.getCanonicalName('default')};`; exportBlock = `return ${bundle.entryModule.getCanonicalName('default')};`;
} else { } else {
exportBlock = Object.keys( exports ).map( name => { exportBlock = bundle.toExport.map( name => {
return `exports.${name} = ${exports[name].localName};`; return `exports.${name} = ${exports[name].localName};`;
}).join( '\n' ); }).join( '\n' );
} }

2
src/finalisers/cjs.js

@ -27,7 +27,7 @@ export default function cjs ( bundle, magicString, exportMode ) {
if ( exportMode === 'default' && bundle.entryModule.exports.default ) { if ( exportMode === 'default' && bundle.entryModule.exports.default ) {
exportBlock = `module.exports = ${bundle.entryModule.getCanonicalName('default')};`; exportBlock = `module.exports = ${bundle.entryModule.getCanonicalName('default')};`;
} else if ( exportMode === 'named' ) { } else if ( exportMode === 'named' ) {
exportBlock = keys( bundle.entryModule.exports ) exportBlock = bundle.toExport
.map( key => { .map( key => {
const specifier = bundle.entryModule.exports[ key ]; const specifier = bundle.entryModule.exports[ key ];
const name = bundle.entryModule.getCanonicalName( specifier.localName ); const name = bundle.entryModule.getCanonicalName( specifier.localName );

2
src/finalisers/iife.js

@ -27,6 +27,8 @@ export default function iife ( bundle, magicString, exportMode, options ) {
magicString.append( `\n\nreturn ${bundle.entryModule.getCanonicalName('default')};` ); magicString.append( `\n\nreturn ${bundle.entryModule.getCanonicalName('default')};` );
} }
// TODO named exports
return magicString return magicString
.indent() .indent()
.prepend( intro ) .prepend( intro )

2
src/finalisers/umd.js

@ -50,7 +50,7 @@ export default function umd ( bundle, magicString, exportMode, options ) {
const canonicalName = bundle.entryModule.getCanonicalName( 'default' ); const canonicalName = bundle.entryModule.getCanonicalName( 'default' );
exportBlock = `return ${canonicalName};`; exportBlock = `return ${canonicalName};`;
} else { } else {
exportBlock = Object.keys( exports ).map( name => { exportBlock = bundle.toExport.map( name => {
const canonicalName = bundle.entryModule.getCanonicalName( exports[ name ].localName ); const canonicalName = bundle.entryModule.getCanonicalName( exports[ name ].localName );
return `exports.${name} = ${canonicalName};`; return `exports.${name} = ${canonicalName};`;
}).join( '\n' ); }).join( '\n' );

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

@ -0,0 +1,10 @@
var assert = require( 'assert' );
module.exports = {
description: 'exports are kept up-to-date',
exports: function ( exports ) {
assert.equal( exports.count, 0 );
exports.incr();
assert.equal( exports.count, 1 );
}
};

5
test/function/assignment-to-exports/main.js

@ -0,0 +1,5 @@
export var count = 0;
export function incr () {
count += 1;
}
Loading…
Cancel
Save