From ae084c04245c6fc5935a9fcaa7bb43789b1f9d66 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 17 Aug 2015 17:23:43 -0400 Subject: [PATCH] first stab at handling reexports separately --- src/Bundle.js | 50 ++++++++--- src/Module.js | 165 ++++++++++++++++++++++--------------- src/Statement.js | 16 ++++ src/utils/getExportMode.js | 2 +- 4 files changed, 151 insertions(+), 82 deletions(-) diff --git a/src/Bundle.js b/src/Bundle.js index 563c58b..d8e4741 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -224,7 +224,7 @@ export default class Bundle { keys( statement.modifies ).forEach( name => { const definingStatement = module.definitions[ name ]; - const exportDeclaration = module.exports[ name ] || ( + const exportDeclaration = module.exports[ name ] || module.reexports[ name ] || ( module.exports.default && module.exports.default.identifier === name && module.exports.default ); @@ -305,11 +305,25 @@ export default class Bundle { varExports[ key ] = true; } }); + + keys( this.entryModule.reexports ).forEach( key => { + const reexportDeclaration = this.entryModule.reexports[ key ]; + + const originalDeclaration = reexportDeclaration.module.findDeclaration( reexportDeclaration.importedName ); + + if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) { + const canonicalName = this.trace( reexportDeclaration.module, reexportDeclaration.importedName, false ); + + allBundleExports[ canonicalName ] = `exports.${key}`; + 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 ) + .concat( keys( this.entryModule.reexports ) ) .filter( key => !varExports[ key ] ); @@ -476,40 +490,50 @@ export default class Bundle { // defined elsewhere const otherModule = importDeclaration.module; + const name = importDeclaration.name; if ( otherModule.isExternal ) { - if ( importDeclaration.name === 'default' ) { + if ( name === 'default' ) { return otherModule.needsNamed && !es6 ? `${otherModule.name}__default` : otherModule.name; } - if ( importDeclaration.name === '*' ) { + if ( name === '*' ) { return otherModule.name; } return es6 ? - importDeclaration.name : - `${otherModule.name}.${importDeclaration.name}`; + name : + `${otherModule.name}.${name}`; } - if ( importDeclaration.name === '*' ) { + if ( name === '*' ) { return otherModule.replacements[ '*' ]; } - if ( importDeclaration.name === 'default' ) { + if ( name === 'default' ) { return otherModule.defaultName(); } - const exportDeclaration = otherModule.exports[ importDeclaration.name ]; - if ( exportDeclaration ) return this.trace( otherModule, exportDeclaration.localName ); + return this.traceExport( otherModule, name, es6 ); + } + + traceExport ( module, name, es6 ) { + const reexportDeclaration = module.reexports[ name ]; + if ( reexportDeclaration ) { + return this.traceExport( reexportDeclaration.module, reexportDeclaration.importedName ); + } + + const exportDeclaration = module.exports[ name ]; + if ( exportDeclaration ) return this.trace( module, exportDeclaration.localName ); - for ( let i = 0; i < otherModule.exportDelegates.length; i += 1 ) { - const delegate = otherModule.exportDelegates[i]; - const delegateExportDeclaration = delegate.module.exports[ importDeclaration.name ]; + for ( let i = 0; i < module.exportDelegates.length; i += 1 ) { + const delegate = module.exportDelegates[i]; + const delegateExportDeclaration = delegate.module.exports[ name ]; if ( delegateExportDeclaration ) { - return this.trace( delegate.module, delegateExportDeclaration.localName ); + return this.trace( delegate.module, delegateExportDeclaration.localName, es6 ); } } diff --git a/src/Module.js b/src/Module.js index f272a74..b69ee64 100644 --- a/src/Module.js +++ b/src/Module.js @@ -55,6 +55,7 @@ export default class Module { // imports and exports, indexed by ID this.imports = blank(); this.exports = blank(); + this.reexports = blank(); this.exportAlls = blank(); @@ -74,10 +75,32 @@ export default class Module { const node = statement.node; const source = node.source && node.source.value; + // export { name } from './other' + if ( source ) { + if ( node.type === 'ExportAllDeclaration' ) { + // Store `export * from '...'` statements in an array of delegates. + // When an unknown import is encountered, we see if one of them can satisfy it. + this.exportDelegates.push({ + statement, + source + }); + } + + else { + node.specifiers.forEach( specifier => { + this.reexports[ specifier.exported.name ] = { + source, + importedName: specifier.local.name, + module: null // filled in later + }; + }); + } + } + // export default function foo () {} // export default foo; // export default 42; - if ( node.type === 'ExportDefaultDeclaration' ) { + else if ( node.type === 'ExportDefaultDeclaration' ) { const isDeclaration = /Declaration$/.test( node.declaration.type ); const isAnonymous = /(?:Class|Function)Expression$/.test( node.declaration.type ); @@ -108,20 +131,10 @@ export default class Module { const localName = specifier.local.name; const exportedName = specifier.exported.name; - // export { foo } from './foo'; - if ( source ) { - this.imports[ localName ] = { - source, - localName: exportedName, - name: localName - }; - } - this.exports[ exportedName ] = { statement, localName, - exportedName, - linkedImport: source ? this.imports[ localName ] : null + exportedName }; }); } @@ -146,15 +159,6 @@ export default class Module { }; } } - - // Store `export * from '...'` statements in an array of delegates. - // When an unknown import is encountered, we see if one of them can satisfy it. - else { - this.exportDelegates.push({ - statement, - source - }); - } } addImport ( statement ) { @@ -215,6 +219,13 @@ export default class Module { consolidateDependencies () { let strongDependencies = blank(); + function addDependency ( dependencies, declaration ) { + if ( declaration && declaration.module && !declaration.module.isExternal ) { + dependencies[ declaration.module.id ] = declaration.module; + return true; + } + } + this.statements.forEach( statement => { if ( statement.isImportDeclaration && !statement.node.specifiers.length && !statement.module.isExternal ) { // include module for its side-effects @@ -224,18 +235,17 @@ export default class Module { keys( statement.stronglyDependsOn ).forEach( name => { if ( statement.defines[ name ] ) return; - const exportAllDeclaration = this.exportAlls[ name ]; - - if ( exportAllDeclaration && exportAllDeclaration.module && !exportAllDeclaration.module.isExternal ) { - strongDependencies[ exportAllDeclaration.module.id ] = exportAllDeclaration.module; - return; + let module = this; + let reexportDeclaration; + while ( module.reexports[ name ] ) { + reexportDeclaration = module.reexports[ name ]; + module = reexportDeclaration.module; + name = reexportDeclaration.importedName; } - const importDeclaration = this.imports[ name ]; - - if ( importDeclaration && importDeclaration.module && !importDeclaration.module.isExternal ) { - strongDependencies[ importDeclaration.module.id ] = importDeclaration.module; - } + addDependency( strongDependencies, reexportDeclaration ) || + addDependency( strongDependencies, this.exportAlls[ name ] ) || + addDependency( strongDependencies, this.imports[ name ] ); }); }); @@ -245,11 +255,17 @@ export default class Module { keys( statement.dependsOn ).forEach( name => { if ( statement.defines[ name ] ) return; - const importDeclaration = this.imports[ name ]; - - if ( importDeclaration && importDeclaration.module && !importDeclaration.module.isExternal ) { - weakDependencies[ importDeclaration.module.id ] = importDeclaration.module; + let module = this; + let reexportDeclaration; + while ( module.reexports[ name ] ) { + reexportDeclaration = module.reexports[ name ]; + module = reexportDeclaration.module; + name = reexportDeclaration.importedName; } + + addDependency( weakDependencies, reexportDeclaration ) || + addDependency( weakDependencies, this.exportAlls[ name ] ) || + addDependency( weakDependencies, this.imports[ name ] ); }); }); @@ -368,39 +384,7 @@ export default class Module { return module.markAllExportStatements(); } - const exportDeclaration = module.exports[ importDeclaration.name ]; - - if ( !exportDeclaration ) { - const noExport = new Error( `Module ${module.id} does not export ${importDeclaration.name} (imported by ${this.id})` ); - - // See if there exists an export delegate that defines `name`. - return first( module.exportDelegates, noExport, declaration => { - return module.bundle.fetchModule( declaration.source, module.id ).then( submodule => { - declaration.module = submodule; - - return submodule.mark( name ).then( result => { - if ( !result.length ) throw noExport; - - // It's found! This module exports `name` through declaration. - // It is however not imported into this scope. - module.exportAlls[ name ] = declaration; - - declaration.statement.dependsOn[ name ] = - declaration.statement.stronglyDependsOn[ name ] = result; - - return result; - }); - }); - }); - } - - exportDeclaration.isUsed = true; - - if ( importDeclaration.name === 'default' ) { - return exportDeclaration.statement.mark(); - } - - return module.mark( exportDeclaration.localName ); + return module.markExport( importDeclaration.name, this ); }); } @@ -458,6 +442,51 @@ export default class Module { }); } + markExport ( name, importer ) { + const reexportDeclaration = this.reexports[ name ]; + if ( reexportDeclaration ) { + reexportDeclaration.isUsed = true; + + return this.bundle.fetchModule( reexportDeclaration.source, this.id ) + .then( otherModule => { + reexportDeclaration.module = otherModule; + return otherModule.markExport( reexportDeclaration.importedName ); + }); + } + + const exportDeclaration = this.exports[ name ]; + if ( exportDeclaration ) { + exportDeclaration.isUsed = true; + if ( name === 'default' ) { + return exportDeclaration.statement.mark(); + } + + return this.mark( exportDeclaration.localName ); + } + + const noExport = new Error( `Module ${this.id} does not export ${name} (imported by ${importer.id})` ); + + // See if there exists an export delegate that defines `name`. + return first( this.exportDelegates, noExport, declaration => { + return this.bundle.fetchModule( declaration.source, this.id ).then( submodule => { + declaration.module = submodule; + + return submodule.mark( name ).then( result => { + if ( !result.length ) throw noExport; + + // It's found! This module exports `name` through declaration. + // It is however not imported into this scope. + this.exportAlls[ name ] = declaration; + + declaration.statement.dependsOn[ name ] = + declaration.statement.stronglyDependsOn[ name ] = result; + + return result; + }); + }); + }); + } + parse ( ast ) { // The ast can be supplied programmatically (but usually won't be) if ( !ast ) { diff --git a/src/Statement.js b/src/Statement.js index f8d612b..3259d2a 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -26,6 +26,7 @@ export default class Statement { this.isImportDeclaration = node.type === 'ImportDeclaration'; this.isExportDeclaration = /^Export/.test( node.type ); + this.isReexportDeclaration = this.isExportDeclaration && !!node.source; } analyse () { @@ -235,6 +236,17 @@ export default class Statement { if ( this.isIncluded ) return; // prevent infinite loops this.isIncluded = true; + // `export { name } from './other'` is a special case + if ( this.isReexportDeclaration ) { + return this.module.bundle.fetchModule( this.node.source.value, this.module.id ) + .then( otherModule => { + return sequence( this.node.specifiers, specifier => { + this.module.reexports[ specifier.exported.name ].module = otherModule; + return otherModule.markExport( specifier.local.name ); + }); + }); + } + const dependencies = Object.keys( this.dependsOn ); return sequence( dependencies, name => { @@ -371,6 +383,10 @@ export default class Statement { return magicString; } + source () { + return this.module.source.slice( this.start, this.end ); + } + toString () { return this.module.magicString.slice( this.start, this.end ); } diff --git a/src/utils/getExportMode.js b/src/utils/getExportMode.js index 0dbf07d..86ed4d4 100644 --- a/src/utils/getExportMode.js +++ b/src/utils/getExportMode.js @@ -5,7 +5,7 @@ function badExports ( option, keys ) { } export default function getExportMode ( bundle, exportMode ) { - const exportKeys = keys( bundle.entryModule.exports ); + const exportKeys = keys( bundle.entryModule.exports ).concat( keys( bundle.entryModule.reexports ) ); if ( exportMode === 'default' ) { if ( exportKeys.length !== 1 || exportKeys[0] !== 'default' ) {