diff --git a/src/Bundle.js b/src/Bundle.js index fa64522..c62b138 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -87,12 +87,18 @@ export default class Bundle { }); } + // TODO would be better to deconflict once, rather than per-render deconflict ( es6 ) { let definers = blank(); let conflicts = blank(); + let allReplacements = blank(); + // Assign names to external modules this.externalModules.forEach( module => { + // while we're here... + allReplacements[ module.id ] = blank(); + let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id ); while ( definers[ name ] ) { @@ -107,6 +113,9 @@ export default class Bundle { // Discover conflicts (i.e. two statements in separate modules both define `foo`) this.orderedModules.forEach( module => { + // while we're here... + allReplacements[ module.id ] = blank(); + module.statements.forEach( statement => { if ( !statement.isIncluded ) return; @@ -145,9 +154,61 @@ export default class Bundle { modules.forEach( module => { const replacement = getSafeName( name ); module.rename( name, replacement ); + allReplacements[ module.id ][ name ] = replacement; }); }); + // Assign non-conflicting names to internal default/namespace export + this.orderedModules.forEach( module => { + if ( !module.needsDefault && !module.needsAll ) return; + + if ( module.needsDefault ) { + const defaultExport = module.exports.default; + + // only create a new name if either + // a) it's an expression (`export default 42`) or + // b) it's a name that is reassigned to (`export var a = 1; a = 2`) + if ( defaultExport.declaredName && !defaultExport.isModified ) return; // TODO encapsulate check for whether we need synthetic default name + + const defaultName = getSafeName( module.suggestedNames.default ); + + module.replacements.default = defaultName; + } + + // TODO namespace + }); + + this.orderedModules.forEach( module => { + keys( module.imports ).forEach( localName => { + if ( !module.imports[ localName ].isUsed ) return; + + const bundleName = trace( module, localName ); + if ( bundleName !== localName ) { + allReplacements[ module.id ][ localName ] = bundleName; + } + }); + }); + + function trace ( module, localName ) { + const importDeclaration = module.imports[ localName ]; + + // defined in this module + if ( !importDeclaration ) { + return module.replacements[ localName ] || localName; + } + + // defined elsewhere + const otherModule = importDeclaration.module; + + if ( otherModule.isExternal ) { + // TODO ES6 + return `${otherModule.name}.${importDeclaration.name}`; + } + + const exportDeclaration = otherModule.exports[ importDeclaration.name ]; + return trace( otherModule, exportDeclaration.localName ); + } + // TODO assign names to default/namespace exports, based on suggestions // TODO trace bindings and rename within modules here (rather than later // with getCanonicalName) @@ -160,6 +221,8 @@ export default class Bundle { conflicts[ name ] = true; return name; } + + return allReplacements; } fetchModule ( importee, importer ) { @@ -262,7 +325,7 @@ export default class Bundle { render ( options = {} ) { const format = options.format || 'es6'; - this.deconflict( format === 'es6' ); + const allReplacements = this.deconflict( format === 'es6' ); // If we have named exports from the bundle, and those exports // are assigned to *within* the bundle, we may need to rewrite e.g. @@ -288,7 +351,7 @@ export default class Bundle { const originalDeclaration = this.entryModule.findDeclaration( exportDeclaration.localName ); if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) { - const canonicalName = this.entryModule.getCanonicalName( exportDeclaration.localName, false ); + const canonicalName = this.entryModule.replacements[ exportDeclaration.localName ] || exportDeclaration.localName; allBundleExports[ canonicalName ] = `exports.${key}`; this.varExports[ key ] = true; @@ -305,7 +368,7 @@ export default class Bundle { let magicString = new MagicString.Bundle({ separator: '\n\n' }); this.orderedModules.forEach( module => { - const source = module.render( allBundleExports, format ); + const source = module.render( allBundleExports, allReplacements[ module.id ], format ); if ( source.toString().length ) { magicString.addSource( source ); } @@ -316,10 +379,10 @@ export default class Bundle { const namespaceBlock = this.internalNamespaceModules.map( module => { const exportKeys = keys( module.exports ); - return `var ${module.getCanonicalName('*', format === 'es6')} = {\n` + + return `var ${module.namespaceName} = {\n` + exportKeys.map( key => { const localName = module.exports[ key ].localName; - return `${indentString}get ${key} () { return ${module.getCanonicalName(localName, format === 'es6')}; }`; + return `${indentString}get ${key} () { return ${localName}; }`; // TODO... }).join( ',\n' ) + `\n};\n\n`; }).join( '' ); diff --git a/src/ExternalModule.js b/src/ExternalModule.js index 1406832..e24d42c 100644 --- a/src/ExternalModule.js +++ b/src/ExternalModule.js @@ -26,18 +26,6 @@ export default class ExternalModule { return null; } - getCanonicalName ( name, es6 ) { - if ( name === 'default' ) { - return this.needsNamed && !es6 ? `${this.name}__default` : this.name; - } - - if ( name === '*' ) { - return this.name; // TODO is this correct in ES6? - } - - return es6 ? ( this.canonicalNames[ name ] || name ) : `${this.name}.${name}`; - } - rename ( name, replacement ) { this.canonicalNames[ name ] = replacement; } diff --git a/src/Module.js b/src/Module.js index 92308d0..d231e56 100644 --- a/src/Module.js +++ b/src/Module.js @@ -19,11 +19,11 @@ function deconflict ( name, names ) { return name; } -function isEmptyExportedVarDeclaration ( node, module, allBundleExports, es6 ) { +function isEmptyExportedVarDeclaration ( node, module, allBundleExports, moduleReplacements, es6 ) { if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false; const name = node.declarations[0].id.name; - const canonicalName = module.getCanonicalName( name, es6 ); + const canonicalName = moduleReplacements[ name ]; return canonicalName in allBundleExports; } @@ -62,7 +62,8 @@ export default class Module { // array of all-export sources this.exportDelegates = []; - this.canonicalNames = blank(); + this.canonicalNames = blank(); // TODO still necessary? + this.replacements = blank(); this.definitions = blank(); this.definitionPromises = blank(); @@ -293,58 +294,6 @@ export default class Module { return null; } - getCanonicalName ( localName, es6 ) { - // Special case - if ( localName === 'default' && ( this.exports.default.isModified || !this.suggestedNames.default ) ) { - let canonicalName = makeLegalIdentifier( this.id.replace( dirname( this.bundle.entryModule.id ) + '/', '' ).replace( /\.js$/, '' ) ); - return deconflict( canonicalName, this.definitions ); - } - - if ( this.suggestedNames[ localName ] ) { - localName = this.suggestedNames[ localName ]; - } - - const id = localName + ( es6 ? '-es6' : '' ); // TODO ugh this seems like a terrible hack - - if ( !this.canonicalNames[ id ] ) { - let canonicalName; - - if ( this.imports[ localName ] ) { - const importDeclaration = this.imports[ localName ]; - const module = importDeclaration.module; - - if ( importDeclaration.name === '*' ) { - canonicalName = module.suggestedNames[ '*' ]; - } else { - let exporterLocalName; - - if ( module.isExternal ) { - exporterLocalName = importDeclaration.name; - } else { - const exportDeclaration = module.exports[ importDeclaration.name ]; - - // The export declaration of the particular name is known. - if (exportDeclaration) { - exporterLocalName = exportDeclaration.localName; - } else { // export * from '...' - exporterLocalName = importDeclaration.name; - } - } - - canonicalName = module.getCanonicalName( exporterLocalName, es6 ); - } - } - - else { - canonicalName = localName; - } - - this.canonicalNames[ id ] = canonicalName; - } - - return this.canonicalNames[ id ]; - } - mark ( name ) { // shortcut cycles. TODO this won't work everywhere... if ( this.definitionPromises[ name ] ) { @@ -356,6 +305,7 @@ export default class Module { // The definition for this name is in a different module if ( this.imports[ name ] ) { const importDeclaration = this.imports[ name ]; + importDeclaration.isUsed = true; promise = this.bundle.fetchModule( importDeclaration.source, this.id ) .then( module => { @@ -380,15 +330,15 @@ export default class Module { module.suggestName( 'default', `${suggestion}__default` ); } - if ( module.isExternal ) { - if ( importDeclaration.name === 'default' ) { - module.needsDefault = true; - } else if ( importDeclaration.name === '*' ) { - module.needsAll = true; - } else { - module.needsNamed = true; - } + if ( importDeclaration.name === 'default' ) { + module.needsDefault = true; + } else if ( importDeclaration.name === '*' ) { + module.needsAll = true; + } else { + module.needsNamed = true; + } + if ( module.isExternal ) { module.importedByBundle.push( importDeclaration ); return emptyArrayPromise; } @@ -601,11 +551,10 @@ export default class Module { } rename ( name, replacement ) { - // TODO again, hacky... - this.canonicalNames[ name ] = this.canonicalNames[ name + '-es6' ] = replacement; + this.replacements[ name ] = replacement; } - render ( allBundleExports, format ) { + render ( allBundleExports, moduleReplacements, format ) { let magicString = this.magicString.clone(); this.statements.forEach( statement => { @@ -623,7 +572,7 @@ export default class Module { } // skip `export var foo;` if foo is exported - if ( isEmptyExportedVarDeclaration( statement.node.declaration, this, allBundleExports, format === 'es6' ) ) { + if ( isEmptyExportedVarDeclaration( statement.node.declaration, this, allBundleExports, moduleReplacements, format === 'es6' ) ) { magicString.remove( statement.start, statement.next ); return; } @@ -631,7 +580,7 @@ export default class Module { // skip empty var declarations for exported bindings // (otherwise we're left with `exports.foo;`, which is useless) - if ( isEmptyExportedVarDeclaration( statement.node, this, allBundleExports, format === 'es6' ) ) { + if ( isEmptyExportedVarDeclaration( statement.node, this, allBundleExports, moduleReplacements, format === 'es6' ) ) { magicString.remove( statement.start, statement.next ); return; } @@ -652,12 +601,12 @@ export default class Module { keys( statement.dependsOn ) .concat( keys( statement.defines ) ) .forEach( name => { - const canonicalName = this.getCanonicalName( name, format === 'es6' ); + const bundleName = moduleReplacements[ name ] || name; - if ( allBundleExports[ canonicalName ] ) { - bundleExports[ name ] = replacements[ name ] = allBundleExports[ canonicalName ]; - } else if ( name !== canonicalName ) { - replacements[ name ] = canonicalName; + if ( allBundleExports[ bundleName ] ) { + bundleExports[ name ] = replacements[ name ] = allBundleExports[ bundleName ]; + } else if ( bundleName !== name ) { // TODO weird structure + replacements[ name ] = bundleName; } }); @@ -677,9 +626,16 @@ export default class Module { } else if ( statement.node.type === 'ExportDefaultDeclaration' ) { - const canonicalName = this.getCanonicalName( 'default', format === 'es6' ); + //const canonicalName = this.getCanonicalName( 'default', format === 'es6' ); + let canonicalName; + if ( this.replacements.default ) { + canonicalName = this.replacements.default; + } else { + canonicalName = statement.node.declaration.name; // TODO declaredName? + canonicalName = this.replacements[ canonicalName ] || canonicalName; + } - if ( statement.node.declaration.type === 'Identifier' && canonicalName === this.getCanonicalName( statement.node.declaration.name, format === 'es6' ) ) { + if ( statement.node.declaration.type === 'Identifier' && canonicalName === ( moduleReplacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) { magicString.remove( statement.start, statement.next ); return; } diff --git a/src/finalisers/es6.js b/src/finalisers/es6.js index 85c25ce..8daa5cf 100644 --- a/src/finalisers/es6.js +++ b/src/finalisers/es6.js @@ -37,7 +37,7 @@ export default function es6 ( bundle, magicString ) { const exportBlock = keys( exports ).map( exportedName => { const specifier = exports[ exportedName ]; - const canonicalName = bundle.entryModule.getCanonicalName( specifier.localName ); + const canonicalName = bundle.entryModule.replacements[ specifier.localName ] || specifier.localName; if ( exportedName === 'default' ) { return `export default ${canonicalName};`; diff --git a/src/finalisers/shared/getExportBlock.js b/src/finalisers/shared/getExportBlock.js index c02c727..ca91179 100644 --- a/src/finalisers/shared/getExportBlock.js +++ b/src/finalisers/shared/getExportBlock.js @@ -1,12 +1,13 @@ export default function getExportBlock ( bundle, exportMode, mechanism = 'return' ) { if ( exportMode === 'default' ) { - return `${mechanism} ${bundle.entryModule.getCanonicalName('default')};`; + return `${mechanism} ${bundle.entryModule.replacements.default};`; } return bundle.toExport .map( name => { const prop = name === 'default' ? `['default']` : `.${name}`; - return `exports${prop} = ${bundle.entryModule.getCanonicalName(name)};`; + name = bundle.entryModule.replacements[ name ] || name; + return `exports${prop} = ${name};`; }) .join( '\n' ); }