diff --git a/CHANGELOG.md b/CHANGELOG.md index cde4345..9beffb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # rollup changelog +## 0.17.4 + +* Allow imports from hidden directories (replay of [#133](https://github.com/rollup/rollup/issues/133)) + +## 0.17.3 + +* Handle parenthesised default exports ([#136](https://github.com/rollup/rollup/issues/136)) + +## 0.17.2 + +* Allow use of scoped npm packages ([#131](https://github.com/rollup/rollup/issues/131)) + +## 0.17.1 + +* Allow namespaces to be passed to a function ([#149](https://github.com/rollup/rollup/issues/149)) + +## 0.17.0 + +* Roll back to 0.15.0 and reapply subsequent fixes pending resolution of ([#132](https://github.com/rollup/rollup/issues/132)) and related issues + ## 0.16.4 * Fix import paths with `.` ([#133](https://github.com/rollup/rollup/issues/133)) diff --git a/appveyor.yml b/appveyor.yml index 588638a..cd3fab4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,11 @@ init: environment: matrix: - - nodejs_version: 4 + # node.js + - nodejs_version: 0.10 + - nodejs_version: 0.12 + # io.js + - nodejs_version: 1 install: - ps: Install-Product node $env:nodejs_version diff --git a/package.json b/package.json index 8ed39a7..3849a5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "0.16.4", + "version": "0.17.4", "description": "Next-generation ES6 module bundler", "main": "dist/rollup.js", "jsnext:main": "src/rollup.js", @@ -59,6 +59,7 @@ "dependencies": { "acorn": "^2.3.0", "chalk": "^1.0.0", + "estree-walker": "^0.1.3", "magic-string": "^0.7.0", "minimist": "^1.1.1", "sander": "^0.3.3", diff --git a/src/Bundle.js b/src/Bundle.js index 96eeaa5..65a381b 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -10,9 +10,6 @@ import { defaultLoader } from './utils/load'; import getExportMode from './utils/getExportMode'; import getIndentString from './utils/getIndentString'; import { unixizePath } from './utils/normalizePlatform.js'; -import Scope from './Scope'; - -import optimiseNamespaceLookups from './optimise/namespace-lookup.js'; export default class Bundle { constructor ( options ) { @@ -31,69 +28,81 @@ export default class Bundle { transform: ensureArray( options.transform ) }; - // The global scope, and the bundle's internal scope. - this.globals = new Scope(); - this.scope = new Scope( this.globals ); - - // Strictly speaking, these globals only apply to non-ES6, non-default-only bundles. - // However, the deconfliction logic is greatly simplified by being the same for all formats. - // * CommonJS needs `module` and `exports` ( and `require`? ) to be in scope. - // * SystemJS needs a reference to a function for its `exports`, - // and another one for any `module` it imports. These global names can be reused! - [ 'exports', 'module' ] - .forEach( name => { - this.globals.define( name ); - this.scope.bind( name, this.globals.reference( name ) ); - }); - - // Alias for entryModule.exports. - this.exports = null; - - this.toExport = null; - this.pending = blank(); this.moduleById = blank(); this.modules = []; - this.statements = null; this.externalModules = []; + this.internalNamespaces = []; + + this.assumedGlobals = blank(); + + // TODO strictly speaking, this only applies with non-ES6, non-default-only bundles + [ 'module', 'exports' ].forEach( global => this.assumedGlobals[ global ] = true ); } build () { return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) ) .then( id => this.fetchModule( id ) ) .then( entryModule => { - this.modules.forEach( module => { - module.statements.forEach( optimiseNamespaceLookups ); - }); - this.entryModule = entryModule; - this.exports = entryModule.exports; - entryModule.markAllStatements( true ); - entryModule.markAllExports(); + this.modules.forEach( module => module.bindImportSpecifiers() ); + this.modules.forEach( module => module.bindAliases() ); + this.modules.forEach( module => module.bindReferences() ); - // Include all side-effects - this.modules.forEach( module => { - module.markAllSideEffects(); + // mark all export statements + entryModule.getExports().forEach( name => { + const declaration = entryModule.traceExport( name ); + declaration.isExported = true; + + if ( declaration.statement ) declaration.use(); }); - // Sort the modules. + let settled = false; + while ( !settled ) { + settled = true; + + this.modules.forEach( module => { + if ( module.markAllSideEffects() ) settled = false; + }); + } + this.orderedModules = this.sort(); + this.deconflict(); + }); + } - // As a last step, deconflict all identifier names, once. - this.scope.deconflict(); + deconflict () { + let used = blank(); - // Alias the default import to the external module named - // for external modules that don't need named imports. - this.externalModules.forEach( module => { - const externalDefault = module.exports.lookup( 'default' ); + // ensure no conflicts with globals + keys( this.assumedGlobals ).forEach( name => used[ name ] = 1 ); - if ( externalDefault && !( module.needsNamed || module.needsAll ) ) { - externalDefault.name = module.name; - } - }); + function getSafeName ( name ) { + if ( used[ name ] ) { + return `${name}$${used[name]++}`; + } + + used[ name ] = 1; + return name; + } + + this.externalModules.forEach( module => { + module.name = getSafeName( module.name ); + }); + + this.modules.forEach( module => { + keys( module.declarations ).forEach( originalName => { + const declaration = module.declarations[ originalName ]; + + if ( originalName === 'default' ) { + if ( declaration.original && !declaration.original.isReassigned ) return; + } + + declaration.name = getSafeName( declaration.name ); }); + }); } fetchModule ( id ) { @@ -110,23 +119,12 @@ export default class Bundle { source = source.code; } - const module = new Module({ - id, - source, - ast, - bundle: this - }); + const module = new Module({ id, source, ast, bundle: this }); this.modules.push( module ); this.moduleById[ id ] = module; - return this.fetchAllDependencies( module ).then( () => { - // Analyze the module once all its dependencies have been resolved. - // This means that any dependencies of a module has already been - // analysed when it's time for the module itself. - module.analyse(); - return module; - }); + return this.fetchAllDependencies( module ).then( () => module ); }); } @@ -139,7 +137,7 @@ export default class Bundle { // external module if ( !resolvedId ) { if ( !this.moduleById[ source ] ) { - const module = new ExternalModule( { id: source, bundle: this } ); + const module = new ExternalModule( source ); this.externalModules.push( module ); this.moduleById[ source ] = module; } @@ -164,95 +162,19 @@ export default class Bundle { // Determine export mode - 'default', 'named', 'none' const exportMode = getExportMode( this, options.exports ); - // 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; - // - // This doesn't apply if the bundle is exported as ES6! - let allBundleExports = blank(); - let isReassignedVarDeclaration = blank(); - let varExports = blank(); - let getterExports = []; - - this.orderedModules.forEach( module => { - module.reassignments.forEach( name => { - isReassignedVarDeclaration[ module.locals.lookup( name ).name ] = true; - }); - }); - - if ( format !== 'es6' && exportMode === 'named' ) { - this.exports.getNames() - .forEach( name => { - const canonicalName = this.exports.lookup( name ).name; - - if ( isReassignedVarDeclaration[ 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 - // ensure we don't try and export them again at the bottom - this.toExport = this.exports.getNames() - .filter( key => !varExports[ key ] ); - let magicString = new MagicString.Bundle({ separator: '\n\n' }); this.orderedModules.forEach( module => { - const source = module.render( allBundleExports, format === 'es6' ); + const source = module.render( format === 'es6' ); if ( source.toString().length ) { magicString.addSource( source ); } }); - // prepend bundle with internal namespaces const indentString = getIndentString( magicString, options ); - const namespaceBlock = this.modules.filter( module => module.needsDynamicAccess ).map( module => { - const exports = module.exports.getNames().map( name => { - const id = module.exports.lookup( name ); - return `${indentString}get ${name} () { return ${id.name}; }`; - }); - - return `var ${module.name} = {\n` + - exports.join( ',\n' ) + - `\n};\n\n`; - }).join( '' ); - - magicString.prepend( namespaceBlock ); - - if ( getterExports.length ) { - // TODO offer ES3-safe (but not spec-compliant) alternative? - const getterExportsBlock = `Object.defineProperties(exports, {\n` + - getterExports.map( ({ key, value }) => indentString + `${key}: { get: function () { return ${value}; } }` ).join( ',\n' ) + - `\n});`; - - magicString.append( '\n\n' + getterExportsBlock ); - } - const finalise = finalisers[ format ]; - - if ( !finalise ) { - throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` ); - } + if ( !finalise ) throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` ); magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options ); @@ -277,19 +199,15 @@ export default class Bundle { } sort () { - // Set of visited module ids. - let seen = blank(); - + let seen = {}; let ordered = []; let hasCycles; - // Map from module id to list of modules. - let strongDeps = blank(); - - // Map from module id to boolean. - let stronglyDependsOn = blank(); + let strongDeps = {}; + let stronglyDependsOn = {}; function visit ( module ) { + if ( seen[ module.id ] ) return; seen[ module.id ] = true; const { strongDependencies, weakDependencies } = module.consolidateDependencies(); @@ -338,7 +256,7 @@ export default class Bundle { ordered.push( module ); } - visit( this.entryModule ); + this.modules.forEach( visit ); if ( hasCycles ) { let unordered = ordered; diff --git a/src/ExternalModule.js b/src/ExternalModule.js index 4b49a2e..effdd5b 100644 --- a/src/ExternalModule.js +++ b/src/ExternalModule.js @@ -1,66 +1,75 @@ import { blank } from './utils/object'; import makeLegalIdentifier from './utils/makeLegalIdentifier'; -// An external identifier. -class Id { +class ExternalDeclaration { constructor ( module, name ) { - this.originalName = this.name = name; this.module = module; + this.name = name; + this.isExternal = true; + } - this.modifierStatements = []; + addAlias () { + // noop } - // Flags the identifier as imported by the bundle when marked. - mark () { - this.module.importedByBundle[ this.originalName ] = true; - this.modifierStatements.forEach( stmt => stmt.mark() ); + addReference ( reference ) { + reference.declaration = this; + + if ( this.name === 'default' || this.name === '*' ) { + this.module.suggestName( reference.name ); + } } -} -export default class ExternalModule { - constructor ( { id, bundle } ) { - this.id = id; + render ( es6 ) { + if ( this.name === '*' ) { + return this.module.name; + } - // Implement `Identifier` interface. - this.originalName = this.name = makeLegalIdentifier( id ); - this.module = this; - this.isModule = true; + if ( this.name === 'default' ) { + return !es6 && this.module.exportsNames ? + `${this.module.name}__default` : + this.module.name; + } - // Define the external module's name in the bundle scope. - bundle.scope.define( id, this ); + return es6 ? this.name : `${this.module.name}.${this.name}`; + } - this.isExternal = true; - this.importedByBundle = blank(); + use () { + // noop? + } +} - // Invariant: needsNamed and needsAll are never both true at once. - // Because an import with both a namespace and named import is invalid: - // - // import * as ns, { a } from '...' - // - this.needsNamed = false; - this.needsAll = false; +export default class ExternalModule { + constructor ( id ) { + this.id = id; + this.name = makeLegalIdentifier( id ); - this.exports = bundle.scope.virtual( false ); + this.nameSuggestions = blank(); + this.mostCommonSuggestion = 0; - const { reference } = this.exports; + this.isExternal = true; + this.declarations = blank(); - // Override reference. - this.exports.reference = name => { - if ( name !== 'default' ) { - this.needsNamed = true; - } + this.exportsNames = false; + } - if ( !this.exports.defines( name ) ) { - this.exports.define( name, new Id( this, name ) ); - } + suggestName ( name ) { + if ( !this.nameSuggestions[ name ] ) this.nameSuggestions[ name ] = 0; + this.nameSuggestions[ name ] += 1; - return reference.call( this.exports, name ); - }; + if ( this.nameSuggestions[ name ] > this.mostCommonSuggestion ) { + this.mostCommonSuggestion = this.nameSuggestions[ name ]; + this.name = name; + } } - // External modules are always marked for inclusion in the bundle. - // Marking an external module signals its use as a namespace. - mark () { - this.needsAll = true; + traceExport ( name ) { + if ( name !== 'default' && name !== '*' ) { + this.exportsNames = true; + } + + return this.declarations[ name ] || ( + this.declarations[ name ] = new ExternalDeclaration( this, name ) + ); } } diff --git a/src/Module.js b/src/Module.js index 1b2970e..442c349 100644 --- a/src/Module.js +++ b/src/Module.js @@ -1,135 +1,160 @@ -import { basename, extname } from './utils/path'; import { parse } from 'acorn'; import MagicString from 'magic-string'; +import { walk } from 'estree-walker'; import Statement from './Statement'; -import walk from './ast/walk'; import { blank, keys } from './utils/object'; +import { basename, extname } from './utils/path'; import getLocation from './utils/getLocation'; import makeLegalIdentifier from './utils/makeLegalIdentifier'; import SOURCEMAPPING_URL from './utils/sourceMappingURL'; -function removeSourceMappingURLComments ( source, magicString ) { - const SOURCEMAPPING_URL_PATTERN = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ); - let match; +class SyntheticDefaultDeclaration { + constructor ( node, statement, name ) { + this.node = node; + this.statement = statement; + this.name = name; - while ( match = SOURCEMAPPING_URL_PATTERN.exec( source ) ) { - magicString.remove( match.index, match.index + match[0].length ); + this.original = null; + this.isExported = false; + this.aliases = []; } -} -function assign ( target, source ) { - for ( let key in source ) target[ key ] = source[ key ]; -} + addAlias ( declaration ) { + this.aliases.push( declaration ); + } -class Id { - constructor ( module, name, statement ) { - this.originalName = this.name = name; - this.module = module; - this.statement = statement; + addReference ( reference ) { + reference.declaration = this; + this.name = reference.name; + } - this.modifierStatements = []; + bind ( declaration ) { + this.original = declaration; + } - // modifiers - this.isUsed = false; + render () { + return !this.original || this.original.isReassigned ? + this.name : + this.original.render(); } - mark () { + use () { this.isUsed = true; this.statement.mark(); - this.modifierStatements.forEach( stmt => stmt.mark() ); + + this.aliases.forEach( alias => alias.use() ); } } -class LateBoundIdPlaceholder { - constructor ( module, name ) { +class SyntheticNamespaceDeclaration { + constructor ( module ) { this.module = module; - this.name = name; - this.placeholder = true; + this.name = null; + + this.needsNamespaceBlock = false; + this.aliases = []; + + this.originals = blank(); + module.getExports().forEach( name => { + this.originals[ name ] = module.traceExport( name ); + }); } - mark () { - throw new Error(`The imported name "${this.name}" is never exported by "${this.module.id}".`); + addAlias ( declaration ) { + this.aliases.push( declaration ); } -} -export default class Module { - constructor ({ id, source, ast, bundle }) { - this.source = source; + addReference ( reference ) { + // if we have e.g. `foo.bar`, we can optimise + // the reference by pointing directly to `bar` + if ( reference.parts.length ) { + reference.name = reference.parts.shift(); - this.bundle = bundle; - this.id = id; - this.module = this; - this.isModule = true; + reference.end += reference.name.length + 1; // TODO this is brittle - // Implement Identifier interface. - this.name = makeLegalIdentifier( basename( id ).slice( 0, -extname( id ).length ) ); + const original = this.originals[ reference.name ]; - // HACK: If `id` isn't a path, the above code yields the empty string. - if ( !this.name ) { - this.name = makeLegalIdentifier( id ); + original.addReference( reference ); + return; } - // By default, `id` is the filename. Custom resolvers and loaders - // can change that, but it makes sense to use it for the source filename - this.magicString = new MagicString( source, { - filename: id - }); + // otherwise we're accessing the namespace directly, + // which means we need to mark all of this module's + // exports and render a namespace block in the bundle + if ( !this.needsNamespaceBlock ) { + this.needsNamespaceBlock = true; + this.module.bundle.internalNamespaces.push( this ); - removeSourceMappingURLComments( source, this.magicString ); + keys( this.originals ).forEach( name => { + const original = this.originals[ name ]; + original.use(); + }); + } - this.comments = []; + reference.declaration = this; + this.name = reference.name; + } - this.statements = this.parse( ast ); + renderBlock ( indentString ) { + const members = keys( this.originals ).map( name => { + const original = this.originals[ name ]; - // all dependencies - this.resolvedIds = blank(); + if ( original.isReassigned ) { + return `${indentString}get ${name} () { return ${original.render()}; }`; + } - // Virtual scopes for the local and exported names. - this.locals = bundle.scope.virtual( true ); - this.exports = bundle.scope.virtual( false ); + return `${indentString}${name}: ${original.render()}`; + }); - const { reference, inScope } = this.exports; + return `var ${this.render()} = {\n${members.join( ',\n' )}\n};\n\n`; + } - this.exports.reference = name => { - // If we have it, grab it. - if ( inScope.call( this.exports, name ) ) { - return reference.call( this.exports, name ); - } + render () { + return this.name; + } - // ... otherwise search allExportsFrom - for ( let i = 0; i < this.allExportsFrom.length; i += 1 ) { - const module = this.allExportsFrom[i]; - if ( module.exports.inScope( name ) ) { - return module.exports.reference( name ); - } - } + use () { + // noop? + this.aliases.forEach( alias => alias.use() ); + } +} - // throw new Error( `The name "${name}" is never exported (from ${this.id})!` ); - this.exports.define( name, new LateBoundIdPlaceholder( this, name ) ); - return reference.call( this.exports, name ); - }; +export default class Module { + constructor ({ id, source, ast, bundle }) { + this.source = source; + this.bundle = bundle; + this.id = id; - this.exports.inScope = name => { - if ( inScope.call( this.exports, name ) ) return true; + // all dependencies + this.dependencies = []; + this.resolvedIds = blank(); - return this.allExportsFrom.some( module => module.exports.inScope( name ) ); - }; + // imports and exports, indexed by local name + this.imports = blank(); + this.exports = blank(); + this.reexports = blank(); - // Create a unique virtual scope for references to the module. - // const unique = bundle.scope.virtual(); - // unique.define( this.name, this ); - // this.reference = unique.reference( this.name ); + this.exportAllSources = []; + this.exportAllModules = null; - // As far as we know, all our exported bindings have been resolved. - this.allExportsResolved = true; - this.allExportsFrom = []; + // By default, `id` is the filename. Custom resolvers and loaders + // can change that, but it makes sense to use it for the source filename + this.magicString = new MagicString( source, { + filename: id + }); - this.reassignments = []; + // remove existing sourceMappingURL comments + const pattern = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ); + let match; + while ( match = pattern.exec( source ) ) { + this.magicString.remove( match.index, match.index + match[0].length ); + } - // TODO: change to false, and detect when it's necessary. - this.needsDynamicAccess = false; + this.comments = []; + this.statements = this.parse( ast ); - this.dependencies = this.collectDependencies(); + this.declarations = blank(); + this.analyse(); } addExport ( statement ) { @@ -138,32 +163,21 @@ export default class Module { // export { name } from './other' if ( source ) { - const module = this.getModule( source ); + if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( 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. - - if ( module.isExternal ) { - let err = new Error( `Cannot trace 'export *' references through external modules.` ); - err.file = this.id; - err.loc = getLocation( this.source, node.start ); - throw err; - } - - // It seems like we must re-export all exports from another module... - this.allExportsResolved = false; - - if ( !~this.allExportsFrom.indexOf( module ) ) { - this.allExportsFrom.push( module ); - } + this.exportAllSources.push( source ); } else { node.specifiers.forEach( specifier => { - // Bind the export of this module, to the export of the other. - this.exports.bind( specifier.exported.name, - module.exports.reference( specifier.local.name ) ); + this.reexports[ specifier.exported.name ] = { + source, + localName: specifier.local.name, + module: null // filled in later + }; }); } } @@ -172,26 +186,15 @@ export default class Module { // export default foo; // export default 42; else if ( node.type === 'ExportDefaultDeclaration' ) { - const isDeclaration = /Declaration$/.test( node.declaration.type ); - const isAnonymous = /(?:Class|Function)Expression$/.test( node.declaration.type ); - - const identifier = isDeclaration ? - node.declaration.id.name : - node.declaration.type === 'Identifier' ? - node.declaration.name : - null; - const name = identifier || this.name; - - // Always define a new `Identifier` for the default export. - const id = new Id( this, name, statement ); - - // Keep the identifier name, if one exists. - // We can optimize the newly created default `Identifier` away, - // if it is never modified. - // in case of `export default foo; foo = somethingElse` - assign( id, { isDeclaration, isAnonymous, identifier } ); - - this.exports.define( 'default', id ); + const identifier = ( node.declaration.id && node.declaration.id.name ) || node.declaration.name; + + this.exports.default = { + localName: 'default', + identifier + }; + + // create a synthetic declaration + this.declarations.default = new SyntheticDefaultDeclaration( node, statement, identifier || this.basename() ); } // export { foo, bar, baz } @@ -204,7 +207,7 @@ export default class Module { const localName = specifier.local.name; const exportedName = specifier.exported.name; - this.exports.bind( exportedName, this.locals.reference( localName ) ); + this.exports[ exportedName ] = { localName }; }); } @@ -221,49 +224,32 @@ export default class Module { name = declaration.id.name; } - this.locals.define( name, new Id( this, name, statement ) ); - this.exports.bind( name, this.locals.reference( name ) ); + this.exports[ name ] = { localName: name }; } } } addImport ( statement ) { const node = statement.node; - const module = this.getModule( node.source.value ); + const source = node.source.value; - node.specifiers.forEach( specifier => { - const isDefault = specifier.type === 'ImportDefaultSpecifier'; - const isNamespace = specifier.type === 'ImportNamespaceSpecifier'; + if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source ); + node.specifiers.forEach( specifier => { const localName = specifier.local.name; - if ( this.locals.defines( localName ) ) { + if ( this.imports[ localName ] ) { const err = new Error( `Duplicated import '${localName}'` ); err.file = this.id; err.loc = getLocation( this.source, specifier.start ); throw err; } - if ( isNamespace ) { - // If it's a namespace import, we bind the localName to the module itself. - module.needsAll = true; - module.name = localName; - this.locals.bind( localName, module ); - } else { - const name = isDefault ? 'default' : specifier.imported.name; - - this.locals.bind( localName, module.exports.reference( name ) ); - - // For compliance with earlier Rollup versions. - // If the module is external, and we access the default. - // Rewrite the module name, and the default name to the - // `localName` we use for it. - if ( module.isExternal && isDefault ) { - const id = module.exports.lookup( name ); - module.name = id.name = localName; - id.name += '__default'; - } - } + const isDefault = specifier.type === 'ImportDefaultSpecifier'; + const isNamespace = specifier.type === 'ImportNamespaceSpecifier'; + + const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name; + this.imports[ localName ] = { source, name, module: null }; }); } @@ -275,215 +261,146 @@ export default class Module { statement.analyse(); - // consolidate names that are defined/modified in this module - keys( statement.defines ).forEach( name => { - this.locals.define( name, new Id( this, name, statement ) ); + statement.scope.eachDeclaration( ( name, declaration ) => { + this.declarations[ name ] = declaration; }); }); + } - // If all exports aren't resolved, but all our delegate modules are... - if ( !this.allExportsResolved && this.allExportsFrom.every( module => module.allExportsResolved )) { - // .. then all our exports should be as well. - this.allExportsResolved = true; - - // For all modules we export all from, iterate through its exported names. - // If we don't already define the binding 'name', - // bind the name to the other module's reference. - this.allExportsFrom.forEach( module => { - module.exports.getNames().forEach( name => { - if ( name !== 'default' && !this.exports.defines( name ) ) { - this.exports.bind( name, module.exports.reference( name ) ); - } - }); - }); - } - - // discover variables that are reassigned inside function - // bodies, so we can keep bindings live, e.g. - // - // export var count = 0; - // export function incr () { count += 1 } - let reassigned = blank(); - this.statements.forEach( statement => { - keys( statement.reassigns ).forEach( name => { - reassigned[ name ] = true; - }); - }); + basename () { + return makeLegalIdentifier( basename( this.id ).slice( 0, -extname( this.id ).length ) ); + } - // if names are referenced that are neither defined nor imported - // in this module, we assume that they're globals - this.statements.forEach( statement => { - if ( statement.isReexportDeclaration ) return; + bindAliases () { + keys( this.declarations ).forEach( name => { + const declaration = this.declarations[ name ]; + const statement = declaration.statement; + if ( statement.node.type !== 'VariableDeclaration' ) return; - // while we're here, mark reassignments - statement.scope.varDeclarations.forEach( name => { - if ( reassigned[ name ] && !~this.reassignments.indexOf( name ) ) { - this.reassignments.push( name ); - } - }); + statement.references.forEach( reference => { + if ( reference.name === name || !reference.isImmediatelyUsed ) return; - keys( statement.dependsOn ).forEach( name => { - // For each name we depend on that isn't in scope, - // add a new global and bind the local name to it. - if ( !this.locals.inScope( name ) ) { - this.bundle.globals.define( name, { - originalName: name, - name, - mark () {} - }); - this.locals.bind( name, this.bundle.globals.reference( name ) ); - } + const otherDeclaration = this.trace( reference.name ); + if ( otherDeclaration ) otherDeclaration.addAlias( declaration ); }); }); - - // OPTIMIZATION! - // If we have a default export and it's value is never modified, - // bind to it directly. - const def = this.exports.lookup( 'default' ); - if ( def && !def.isModified && def.identifier ) { - this.exports.bind( 'default', this.locals.reference( def.identifier ) ); - } } - // Returns the set of imported module ids by going through all import/exports statements. - collectDependencies () { - const importedModules = blank(); + bindImportSpecifiers () { + [ this.imports, this.reexports ].forEach( specifiers => { + keys( specifiers ).forEach( name => { + const specifier = specifiers[ name ]; - this.statements.forEach( statement => { - if ( statement.isImportDeclaration || ( statement.isExportDeclaration && statement.node.source ) ) { - importedModules[ statement.node.source.value ] = true; - } + const id = this.resolvedIds[ specifier.source ]; + specifier.module = this.bundle.moduleById[ id ]; + }); }); - return keys( importedModules ); + this.exportAllModules = this.exportAllSources.map( source => { + const id = this.resolvedIds[ source ]; + return this.bundle.moduleById[ id ]; + }); } - consolidateDependencies () { - let strongDependencies = blank(); - - function addDependency ( dependencies, declaration ) { - if ( declaration && declaration.module && !declaration.module.isExternal ) { - dependencies[ declaration.module.id ] = declaration.module; - return true; + bindReferences () { + if ( this.declarations.default ) { + if ( this.exports.default.identifier ) { + const declaration = this.trace( this.exports.default.identifier ); + if ( declaration ) this.declarations.default.bind( declaration ); } } this.statements.forEach( statement => { - if ( statement.isImportDeclaration && !statement.node.specifiers.length ) { - // include module for its side-effects - const module = this.getModule( statement.node.source.value ); - - if ( !module.isExternal ) strongDependencies[ module.id ] = module; + // skip `export { foo, bar, baz }`... + if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) { + // ...unless this is the entry module + if ( this !== this.bundle.entryModule ) return; } - else if ( statement.isReexportDeclaration ) { - if ( statement.node.specifiers ) { - statement.node.specifiers.forEach( specifier => { - let name = specifier.exported.name; - - let id = this.exports.lookup( name ); + statement.references.forEach( reference => { + const declaration = reference.scope.findDeclaration( reference.name ) || + this.trace( reference.name ); - addDependency( strongDependencies, id ); - }); + if ( declaration ) { + declaration.addReference( reference ); + } else { + // TODO handle globals + this.bundle.assumedGlobals[ reference.name ] = true; } - } - - else { - keys( statement.stronglyDependsOn ).forEach( name => { - if ( statement.defines[ name ] ) return; - - addDependency( strongDependencies, this.locals.lookup( name ) ); - }); - } + }); }); + } + consolidateDependencies () { + let strongDependencies = blank(); let weakDependencies = blank(); - this.statements.forEach( statement => { - keys( statement.dependsOn ).forEach( name => { - if ( statement.defines[ name ] ) return; + // treat all imports as weak dependencies + this.dependencies.forEach( source => { + const id = this.resolvedIds[ source ]; + const dependency = this.bundle.moduleById[ id ]; - addDependency( weakDependencies, this.locals.lookup( name ) ); - }); + if ( !dependency.isExternal ) { + weakDependencies[ dependency.id ] = dependency; + } }); - // Go through all our local and exported ids and make us depend on - // the defining modules as well as - this.exports.getIds().concat(this.locals.getIds()).forEach( id => { - if ( id.module && !id.module.isExternal ) { - weakDependencies[ id.module.id ] = id.module; - } + // identify strong dependencies to break ties in case of cycles + this.statements.forEach( statement => { + statement.references.forEach( reference => { + const declaration = reference.declaration; - if ( !id.modifierStatements ) return; + if ( declaration && declaration.statement ) { + const module = declaration.statement.module; + if ( module === this ) return; - id.modifierStatements.forEach( statement => { - const module = statement.module; - weakDependencies[ module.id ] = module; + // TODO disregard function declarations + if ( reference.isImmediatelyUsed ) { + strongDependencies[ module.id ] = module; + } + } }); }); - // `Bundle.sort` gets stuck in an infinite loop if a module has - // `strongDependencies` to itself. Make sure it doesn't happen. - delete strongDependencies[ this.id ]; - delete weakDependencies[ this.id ]; - return { strongDependencies, weakDependencies }; } - getModule ( source ) { - return this.bundle.moduleById[ this.resolvedIds[ source ] ]; - } - - // If a module is marked, enforce dynamic access of its properties. - mark () { - if ( this.needsDynamicAccess ) return; - this.needsDynamicAccess = true; - - this.markAllExports(); - } + getExports () { + let exports = blank(); - markAllSideEffects () { - this.statements.forEach( statement => { - statement.markSideEffect(); + keys( this.exports ).forEach( name => { + exports[ name ] = true; }); - } - markAllStatements ( isEntryModule ) { - this.statements.forEach( statement => { - if ( statement.isIncluded ) return; // TODO can this happen? probably not... - - // skip import declarations... - if ( statement.isImportDeclaration ) { - // ...unless they're empty, in which case assume we're importing them for the side-effects - // THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar - if ( !statement.node.specifiers.length ) { - const otherModule = this.getModule( statement.node.source.value ); + keys( this.reexports ).forEach( name => { + exports[ name ] = true; + }); - if ( !otherModule.isExternal ) otherModule.markAllStatements(); - } - } + this.exportAllModules.forEach( module => { + module.getExports().forEach( name => { + if ( name !== 'default' ) exports[ name ] = true; + }); + }); - // skip `export { foo, bar, baz }`... - else if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) { - // ...but ensure they are defined, if this is the entry module - if ( isEntryModule ) statement.mark(); - } + return keys( exports ); + } - // include everything else - else { - // Be sure to mark the default export for the entry module. - if ( isEntryModule && statement.node.type === 'ExportDefaultDeclaration' ) { - this.exports.lookup( 'default' ).mark(); - } + markAllSideEffects () { + let hasSideEffect = false; - statement.mark(); - } + this.statements.forEach( statement => { + if ( statement.markSideEffect() ) hasSideEffect = true; }); + + return hasSideEffect; } - // Marks all exported identifiers. - markAllExports () { - this.exports.getIds().forEach( id => id.mark() ); + namespace () { + if ( !this.declarations['*'] ) { + this.declarations['*'] = new SyntheticNamespaceDeclaration( this ); + } + + return this.declarations['*']; } parse ( ast ) { @@ -575,7 +492,7 @@ export default class Module { return statements; } - render ( toExport, direct ) { + render ( es6 ) { let magicString = this.magicString.clone(); this.statements.forEach( statement => { @@ -596,55 +513,61 @@ export default class Module { // split up/remove var declarations as necessary if ( statement.node.isSynthetic ) { // insert `var/let/const` if necessary - if ( !toExport[ statement.node.declarations[0].id.name ] ) { + const declaration = this.declarations[ statement.node.declarations[0].id.name ]; + if ( !( declaration.isExported && declaration.isReassigned ) ) { // TODO encapsulate this magicString.insert( statement.start, `${statement.node.kind} ` ); } magicString.overwrite( statement.end, statement.next, ';\n' ); // TODO account for trailing newlines } - let replacements = blank(); - let bundleExports = blank(); - - // Indirect identifier access. - if ( !direct ) { - keys( statement.dependsOn ) - .forEach( name => { - const id = this.locals.lookup( name ); - - // We shouldn't create a replacement for `id` if - // 1. `id` is a Global, in which case it has no module property - // 2. `id.module` isn't external, which means we have direct access - // 3. `id` is its own module, in the case of namespace imports - if ( id.module && id.module.isExternal && id.module !== id ) { - replacements[ name ] = id.originalName === 'default' ? - // default names are always directly accessed - id.name : - // other names are indirectly accessed - `${id.module.name}.${id.originalName}`; - } - }); - } + let toDeshadow = blank(); + + statement.references.forEach( reference => { + const declaration = reference.declaration; - keys( statement.dependsOn ) - .concat( keys( statement.defines ) ) - .forEach( name => { - const bundleName = this.locals.lookup( name ).name; + if ( declaration ) { + const { start, end } = reference; + const name = declaration.render( es6 ); - if ( toExport[ bundleName ] ) { - bundleExports[ name ] = replacements[ name ] = toExport[ bundleName ]; - } else if ( bundleName !== name && !replacements[ name ] ) { // TODO weird structure - replacements[ name ] = bundleName; + // the second part of this check is necessary because of + // namespace optimisation – name of `foo.bar` could be `bar` + if ( reference.name === name && name.length === reference.end - reference.start ) return; + + // prevent local variables from shadowing renamed references + const identifier = name.match( /[^\.]+/ )[0]; + if ( reference.scope.contains( identifier ) ) { + toDeshadow[ identifier ] = `${identifier}$$`; // TODO more robust mechanism } - }); - statement.replaceIdentifiers( magicString, replacements, bundleExports ); + if ( reference.isShorthandProperty ) { + magicString.insert( end, `: ${name}` ); + } else { + magicString.overwrite( start, end, name, true ); + } + } + }); + + if ( keys( toDeshadow ).length ) { + statement.references.forEach( reference => { + if ( reference.name in toDeshadow ) { + magicString.overwrite( reference.start, reference.end, toDeshadow[ reference.name ], true ); + } + }); + } // modify exports as necessary if ( statement.isExportDeclaration ) { // remove `export` from `export var foo = 42` if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.declaration.type === 'VariableDeclaration' ) { - magicString.remove( statement.node.start, statement.node.declaration.start ); + const name = statement.node.declaration.declarations[0].id.name; + const declaration = this.declarations[ name ]; + + const end = declaration.isExported && declaration.isReassigned ? + statement.node.declaration.declarations[0].start : + statement.node.declaration.start; + + magicString.remove( statement.node.start, end ); } else if ( statement.node.type === 'ExportAllDeclaration' ) { @@ -659,25 +582,27 @@ export default class Module { } else if ( statement.node.type === 'ExportDefaultDeclaration' ) { - const def = this.exports.lookup( 'default' ); + const defaultDeclaration = this.declarations.default; - // FIXME: dunno what to do here yet. - if ( statement.node.declaration.type === 'Identifier' && def.name === ( replacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) { + // prevent `var foo = foo` + if ( defaultDeclaration.original && !defaultDeclaration.original.isReassigned ) { magicString.remove( statement.start, statement.next ); return; } + const defaultName = defaultDeclaration.render(); + // prevent `var undefined = sideEffectyDefault(foo)` - if ( !def.isUsed ) { + if ( !defaultDeclaration.isExported && !defaultDeclaration.isUsed ) { magicString.remove( statement.start, statement.node.declaration.start ); return; } // anonymous functions should be converted into declarations if ( statement.node.declaration.type === 'FunctionExpression' ) { - magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${def.name}` ); + magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${defaultName}` ); } else { - magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${def.name} = ` ); + magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${defaultName} = ` ); } } @@ -687,6 +612,53 @@ export default class Module { } }); + // add namespace block if necessary + const namespace = this.declarations['*']; + if ( namespace && namespace.needsNamespaceBlock ) { + magicString.append( '\n\n' + namespace.renderBlock( magicString.getIndentString() ) ); + } + return magicString.trim(); } + + trace ( name ) { + if ( name in this.declarations ) return this.declarations[ name ]; + if ( name in this.imports ) { + const importDeclaration = this.imports[ name ]; + const otherModule = importDeclaration.module; + + if ( importDeclaration.name === '*' && !otherModule.isExternal ) { + return otherModule.namespace(); + } + + return otherModule.traceExport( importDeclaration.name, this ); + } + + return null; + } + + traceExport ( name, importer ) { + // export { foo } from './other' + const reexportDeclaration = this.reexports[ name ]; + if ( reexportDeclaration ) { + return reexportDeclaration.module.traceExport( reexportDeclaration.localName, this ); + } + + const exportDeclaration = this.exports[ name ]; + if ( exportDeclaration ) { + return this.trace( exportDeclaration.localName ); + } + + for ( let i = 0; i < this.exportAllModules.length; i += 1 ) { + const module = this.exportAllModules[i]; + const declaration = module.traceExport( name, this ); + + if ( declaration ) return declaration; + } + + let errorMessage = `Module ${this.id} does not export ${name}`; + if ( importer ) errorMessage += ` (imported by ${importer.id})`; + + throw new Error( errorMessage ); + } } diff --git a/src/Scope.js b/src/Scope.js deleted file mode 100644 index 00062c5..0000000 --- a/src/Scope.js +++ /dev/null @@ -1,175 +0,0 @@ -import { blank, keys } from './utils/object'; - -// A minimal `Identifier` implementation. Anything that has an `originalName`, -// and a mutable `name` property can be used as an `Identifier`. -class Identifier { - constructor ( name ) { - this.originalName = this.name = name; - } - - mark () { - // noop - } -} - -// A reference to an `Identifier`. -function Reference ( scope, index ) { - this.scope = scope; - this.index = index; -} - -// Dereferences a `Reference`. -function dereference ( ref ) { - return ref.scope.ids[ ref.index ]; -} - -function isntReference ( id ) { - return !( id instanceof Reference ); -} - -// Returns a function that will prefix its argument with '_' -// and append a number if called with the same argument more than once. -function underscorePrefix () { - function number ( x ) { - if ( !( x in map ) ) { - map[ x ] = 0; - return ''; - } - - return map[ x ]++; - } - - var map = blank(); - - return x => `_${x}${number( x )}`; -} - -// ## Scope -// A Scope is a mapping from string names to `Identifiers`. -export default class Scope { - constructor ( parent ) { - this.ids = []; - this.names = blank(); - - this.parent = parent || null; - this.used = blank(); - } - - // Binds the `name` to the given reference `ref`. - bind ( name, ref ) { - this.ids[ this.index( name ) ] = ref; - } - - // Deconflict all names within the scope, - // using the given renaming function. - // If no function is supplied, `underscorePrefix` is used. - deconflict ( rename = underscorePrefix() ) { - const names = this.used; - - this.ids.filter( ref => ref instanceof Reference ).forEach( ref => { - // Same scope. - if ( ref.scope.ids === this.ids ) return; - - // Another scope! - while ( ref instanceof Reference ) { - ref = dereference( ref ); - } - - names[ ref.name ] = ref; - }); - - this.ids.filter( isntReference ).forEach( id => { - // TODO: can this be removed? - if ( typeof id === 'string' ) { - throw new Error( `Required name "${id}" is undefined!` ); - } - - let name = id.name; - - while ( name in names && names[ name ] !== id ) { - name = rename( name ); - } - names[ name ] = id; - - id.name = name; - }); - } - - // Defines `name` in the scope to be `id`. - // If no `id` is supplied, a plain `Identifier` is created. - define ( name, id ) { - this.ids[ this.index( name ) ] = id || new Identifier( name ); - } - - // TODO: rename! Too similar to `define`. - defines ( name ) { - return name in this.names; - } - - // Return the names referenced to in the scope. - getNames () { - return keys( this.names ); - } - - // *private, don't use* - // - // Return `name`'s index in the `ids` array if it exists, - // otherwise returns the index to a new placeholder slot. - index ( name ) { - if ( !( name in this.names ) ) { - return this.names[ name ] = this.ids.push( name ) - 1; - } - - return this.names[ name ]; - } - - // Returns true if `name` is in Scope. - inScope ( name ) { - if ( name in this.names ) return true; - - return this.parent ? this.parent.inScope( name ) : false; - } - - // Returns a list of `[ name, identifier ]` tuples. - getIds () { - return keys( this.names ).map( name => this.lookup( name ) ); - } - - // Lookup the identifier referred to by `name`. - lookup ( name ) { - if ( !( name in this.names ) && this.parent ) { - return this.parent.lookup( name ); - } - - let id = this.ids[ this.names[ name ] ]; - - while ( id instanceof Reference ) { - id = dereference( id ); - } - - return id; - } - - // Get a reference to the identifier `name` in this scope. - reference ( name ) { - if ( !( name in this.names ) ) { - throw new Error( `Cannot reference undefined identifier "${name}"` ); - } - - return new Reference( this, this.names[ name ] ); - } - - // Return the used names of the scope. - // Names aren't considered used unless they're deconflicted. - usedNames () { - return keys( this.used ).sort(); - } - - // Create and return a virtual `Scope` instance, bound to - // the actual scope of `this`, optionally inherit the parent scope. - virtual ( inheritParent ) { - const scope = new Scope( inheritParent ? this.parent : null ); - scope.ids = this.ids; - return scope; - } -} diff --git a/src/Statement.js b/src/Statement.js index c8b8a8f..ae83957 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -1,12 +1,7 @@ -import { blank, keys } from './utils/object'; -import getLocation from './utils/getLocation'; -import walk from './ast/walk'; +import { walk } from 'estree-walker'; import Scope from './ast/Scope'; - -const blockDeclarations = { - 'const': true, - 'let': true -}; +import attachScopes from './ast/attachScopes'; +import getLocation from './utils/getLocation'; const modifierNodes = { AssignmentExpression: 'left', @@ -17,12 +12,48 @@ function isIife ( node, parent ) { return parent && parent.type === 'CallExpression' && node === parent.callee; } -function isFunctionDeclaration ( node, parent ) { - // `function foo () {}` - if ( node.type === 'FunctionDeclaration' ) return true; +function isReference ( node, parent ) { + if ( node.type === 'MemberExpression' ) { + return !node.computed && isReference( node.object, node ); + } + + if ( node.type === 'Identifier' ) { + // TODO is this right? + if ( parent.type === 'MemberExpression' ) return parent.computed || node === parent.object; + + // disregard the `bar` in { bar: foo } + if ( parent.type === 'Property' && node !== parent.value ) return false; + + // disregard the `bar` in `class Foo { bar () {...} }` + if ( parent.type === 'MethodDefinition' ) return false; - // `var foo = function () {}` - same thing for present purposes - if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true; + // disregard the `bar` in `export { foo as bar }` + if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return; + + return true; + } +} + +class Reference { + constructor ( node, scope ) { + this.node = node; + this.scope = scope; + + this.declaration = null; // bound later + + this.parts = []; + + let root = node; + while ( root.type === 'MemberExpression' ) { + this.parts.unshift( root.property.name ); + root = root.object; + } + + this.name = root.name; + + this.start = node.start; + this.end = node.start + this.name.length; // can be overridden in the case of namespace members + } } export default class Statement { @@ -34,15 +65,8 @@ export default class Statement { this.next = null; // filled in later this.scope = new Scope(); - this.defines = blank(); - this.dependsOn = blank(); - this.stronglyDependsOn = blank(); - this.reassigns = blank(); - - // TODO: make this more efficient - this.dependantIds = []; - this.namespaceReplacements = []; + this.references = []; this.isIncluded = false; @@ -54,260 +78,99 @@ export default class Statement { analyse () { if ( this.isImportDeclaration ) return; // nothing to analyse - // `export { name } from './other'` is a special case - if ( this.isReexportDeclaration ) { - this.node.specifiers && this.node.specifiers.forEach( specifier => { - const id = this.module.exports.lookup( specifier.exported.name ); - - if ( !~this.dependantIds.indexOf( id ) ) { - this.dependantIds.push( id ); - } - }); - - return; - } - - let scope = this.scope; - - walk( this.node, { - enter ( node, parent ) { - let newScope; - - switch ( node.type ) { - case 'FunctionDeclaration': - scope.addDeclaration( node, false, false ); - break; - - case 'BlockStatement': - if ( parent && /Function/.test( parent.type ) ) { - newScope = new Scope({ - parent: scope, - block: false, - params: parent.params - }); - - // named function expressions - the name is considered - // part of the function's scope - if ( parent.type === 'FunctionExpression' && parent.id ) { - newScope.addDeclaration( parent, false, false ); - } - } else { - newScope = new Scope({ - parent: scope, - block: true - }); - } - - break; + // attach scopes + attachScopes( this ); - case 'CatchClause': - newScope = new Scope({ - parent: scope, - params: [ node.param ], - block: true - }); - - break; - - case 'VariableDeclaration': - node.declarations.forEach( declarator => { - const isBlockDeclaration = node.type === 'VariableDeclaration' && blockDeclarations[ node.kind ]; - scope.addDeclaration( declarator, isBlockDeclaration, true ); - }); - break; - - case 'ClassDeclaration': - scope.addDeclaration( node, false, false ); - break; - } - - if ( newScope ) { - Object.defineProperty( node, '_scope', { - value: newScope, - configurable: true - }); - - scope = newScope; - } - }, - leave ( node ) { - if ( node._scope ) { - scope = scope.parent; - } - } + // attach statement to each top-level declaration, + // so we can mark statements easily + this.scope.eachDeclaration( ( name, declaration ) => { + declaration.statement = this; }); - // This allows us to track whether we're looking at code that will - // be executed immediately (either outside a function, or immediately - // inside an IIFE), for the purposes of determining whether dependencies - // are strong or weak. It's not bulletproof, since it wouldn't catch... - // - // var calledImmediately = function () { - // doSomethingWith( strongDependency ); - // } - // calledImmediately(); - // - // ...but it's better than nothing + // find references + let { module, references, scope } = this; let readDepth = 0; - // This allows us to track whether a modifying statement (i.e. assignment - // /update expressions) need to be captured - let writeDepth = 0; - - if ( !this.isImportDeclaration ) { - walk( this.node, { - enter: ( node, parent ) => { - if ( isFunctionDeclaration( node, parent ) ) writeDepth += 1; - if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1; - - if ( node._scope ) scope = node._scope; - - this.checkForReads( scope, node, parent, !readDepth ); - this.checkForWrites( scope, node, writeDepth ); - }, - leave: ( node, parent ) => { - if ( isFunctionDeclaration( node, parent ) ) writeDepth -= 1; - if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1; - - if ( node._scope ) scope = scope.parent; - } - }); - } - - keys( scope.declarations ).forEach( name => { - this.defines[ name ] = true; - }); - } - - checkForReads ( scope, node, parent, strong ) { - if ( node.type === 'Identifier' ) { - // disregard the `bar` in `foo.bar` - these appear as Identifier nodes - if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) { - return; - } - - // disregard the `bar` in { bar: foo } - if ( parent.type === 'Property' && node !== parent.value ) { - return; - } - - // disregard the `bar` in `class Foo { bar () {...} }` - if ( parent.type === 'MethodDefinition' ) return; - - // disregard the `bar` in `export { foo as bar }` - if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return; - - const definingScope = scope.findDefiningScope( node.name ); + walk( this.node, { + enter ( node, parent ) { + if ( node._scope ) scope = node._scope; + if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1; - if ( !definingScope || definingScope.depth === 0 ) { - if ( !( node.name in this.dependsOn ) ) { - this.dependsOn[ node.name ] = 0; + // special case – shorthand properties. because node.key === node.value, + // we can't differentiate once we've descended into the node + if ( node.type === 'Property' && node.shorthand ) { + const reference = new Reference( node.key, scope ); + reference.isShorthandProperty = true; // TODO feels a bit kludgy + references.push( reference ); + return this.skip(); } - this.dependsOn[ node.name ]++; - if ( strong ) this.stronglyDependsOn[ node.name ] = true; - } - } - } + let isReassignment; - checkForWrites ( scope, node, writeDepth ) { - const addNode = ( node, isAssignment ) => { - let depth = 0; // determine whether we're illegally modifying a binding or namespace + if ( parent && parent.type in modifierNodes ) { + let subject = parent[ modifierNodes[ parent.type ] ]; + let depth = 0; - while ( node.type === 'MemberExpression' ) { - node = node.object; - depth += 1; - } - - // disallow assignments/updates to imported bindings and namespaces - if ( isAssignment ) { - const importSpecifier = this.module.locals.lookup( node.name ); + while ( subject.type === 'MemberExpression' ) { + subject = subject.object; + depth += 1; + } - if ( importSpecifier && importSpecifier.module !== this.module && !scope.contains( node.name ) ) { - const minDepth = importSpecifier.name === '*' ? - 2 : // cannot do e.g. `namespace.foo = bar` - 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine + const importDeclaration = module.imports[ subject.name ]; - if ( depth < minDepth ) { - const err = new Error( `Illegal reassignment to import '${node.name}'` ); - err.file = this.module.id; - err.loc = getLocation( this.module.magicString.toString(), node.start ); - throw err; - } - } + if ( !scope.contains( subject.name ) && importDeclaration ) { + const minDepth = importDeclaration.name === '*' ? + 2 : // cannot do e.g. `namespace.foo = bar` + 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine - // special case = `export default foo; foo += 1;` - we'll - // need to assign a new variable so that the exported - // value is not updated by the second statement - const def = this.module.exports.lookup( 'default' ); - if ( def && depth === 0 && def.name === node.name ) { - // but only if this is a) inside a function body or - // b) after the export declaration - if ( !!scope.parent || node.start > def.statement.node.start ) { - def.isModified = true; + if ( depth < minDepth ) { + const err = new Error( `Illegal reassignment to import '${subject.name}'` ); + err.file = module.id; + err.loc = getLocation( module.magicString.toString(), subject.start ); + throw err; + } } - } - - // we track updates/reassignments to variables, to know whether we - // need to rewrite it later from `foo` to `exports.foo` to keep - // bindings live - if ( - depth === 0 && - writeDepth > 0 && - !scope.contains( node.name ) - ) { - this.reassigns[ node.name ] = true; - } - } - // we only care about writes that happen a) at the top level, - // or b) inside a function that could be immediately invoked. - // Writes inside named functions are only relevant if the - // function is called, in which case we don't need to do - // anything (but we still need to call checkForWrites to - // catch illegal reassignments to imported bindings) - if ( writeDepth === 0 && node.type === 'Identifier' ) { - const id = this.module.locals.lookup( node.name ); - - if ( id && id.modifierStatements && !~id.modifierStatements.indexOf( this ) ) { - id.modifierStatements.push( this ); + isReassignment = !depth; } - } - }; - if ( node.type === 'AssignmentExpression' ) { - addNode( node.left, true ); - } + if ( isReference( node, parent ) ) { + // function declaration IDs are a special case – they're associated + // with the parent scope + const referenceScope = parent.type === 'FunctionDeclaration' && node === parent.id ? + scope.parent : + scope; - else if ( node.type === 'UpdateExpression' ) { - addNode( node.argument, true ); - } + const reference = new Reference( node, referenceScope ); + references.push( reference ); - else if ( node.type === 'CallExpression' ) { - node.arguments.forEach( arg => addNode( arg, false ) ); + reference.isImmediatelyUsed = !readDepth; + reference.isReassignment = isReassignment; - // `foo.bar()` is assumed to mutate foo - if ( node.callee.type === 'MemberExpression' ) { - addNode( node.callee ); + this.skip(); // don't descend from `foo.bar.baz` into `foo.bar` + } + }, + leave ( node, parent ) { + if ( node._scope ) scope = scope.parent; + if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1; } - } + }); } mark () { if ( this.isIncluded ) return; // prevent infinite loops this.isIncluded = true; - this.dependantIds.forEach( id => id.mark() ); - - // TODO: perhaps these could also be added? - keys( this.dependsOn ).forEach( name => { - if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place? - this.module.locals.lookup( name ).mark(); + this.references.forEach( reference => { + if ( reference.declaration ) reference.declaration.use(); }); } markSideEffect () { + if ( this.isIncluded ) return; + const statement = this; + let hasSideEffect = false; walk( this.node, { enter ( node, parent ) { @@ -315,141 +178,27 @@ export default class Statement { // If this is a top-level call expression, or an assignment to a global, // this statement will need to be marked - if ( node.type === 'CallExpression' ) { - statement.mark(); + if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) { + hasSideEffect = true; } else if ( node.type in modifierNodes ) { let subject = node[ modifierNodes[ node.type ] ]; while ( subject.type === 'MemberExpression' ) subject = subject.object; - if ( statement.module.bundle.globals.defines( subject.name ) ) statement.mark(); - } - } - }); - } + const declaration = statement.module.trace( subject.name ); - replaceIdentifiers ( magicString, names, bundleExports ) { - const statement = this; - - const replacementStack = []; - const nameList = keys( names ); - - let deshadowList = []; - nameList.forEach( name => { - const replacement = names[ name ]; - deshadowList.push( replacement.split( '.' )[0] ); - }); - - let topLevel = true; - let depth = 0; - - walk( this.node, { - enter ( node, parent ) { - if ( node._skip ) return this.skip(); - - if ( /^Function/.test( node.type ) ) depth += 1; - - // `this` is undefined at the top level of ES6 modules - if ( node.type === 'ThisExpression' && depth === 0 ) { - magicString.overwrite( node.start, node.end, 'undefined', true ); - } - - // 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 id = node.declarations[0].id; - const name = id.name; - - if ( node.declarations.length === 1 && bundleExports[ name ] ) { - magicString.overwrite( node.start, id.end, bundleExports[ name ], true ); - id._skip = true; - } - } - } - - const scope = node._scope; - - if ( scope ) { - topLevel = false; - - let newNames = blank(); - // Consider a scope to have replacements if there are any namespaceReplacements. - let hasReplacements = statement.namespaceReplacements.length > 0; - - keys( names ).forEach( name => { - if ( !scope.declarations[ name ] ) { - newNames[ name ] = names[ name ]; - hasReplacements = true; - } - }); - - deshadowList.forEach( name => { - if ( scope.declarations[ name ] ) { - newNames[ name ] = name + '$$'; // TODO better mechanism - hasReplacements = true; - } - }); - - if ( !hasReplacements && depth > 0 ) { - return this.skip(); - } - - names = newNames; - replacementStack.push( newNames ); - } - - if ( node.type === 'MemberExpression' ) { - const replacements = statement.namespaceReplacements; - for ( let i = 0; i < replacements.length; i += 1 ) { - const [ top, id ] = replacements[ i ]; - - if ( node === top ) { - magicString.overwrite( node.start, node.end, id.name ); - return this.skip(); - } + if ( !declaration || declaration.statement.isIncluded ) { + hasSideEffect = true; } } - if ( node.type !== 'Identifier' ) return; - - // if there's no replacement, or it's the same, there's nothing more to do - const name = names[ node.name ]; - if ( !name || name === node.name ) return; - - // shorthand properties (`obj = { foo }`) need to be expanded - if ( parent.type === 'Property' && parent.shorthand ) { - magicString.insert( node.end, `: ${name}` ); - parent.key._skip = true; - parent.value._skip = true; // redundant, but defensive - return; - } - - // property names etc can be disregarded - if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return; - if ( parent.type === 'Property' && node !== parent.value ) return; - if ( parent.type === 'MethodDefinition' && node === parent.key ) return; - if ( parent.type === 'FunctionExpression' ) return; - if ( /Function/.test( parent.type ) && ~parent.params.indexOf( node ) ) return; - // TODO others...? - - // all other identifiers should be overwritten - magicString.overwrite( node.start, node.end, name, true ); - }, - - leave ( node ) { - if ( /^Function/.test( node.type ) ) depth -= 1; - - if ( node._scope ) { - names = replacementStack.pop(); - } + if ( hasSideEffect ) this.skip(); } }); - return magicString; + if ( hasSideEffect ) statement.mark(); + return hasSideEffect; } source () { diff --git a/src/ast/Scope.js b/src/ast/Scope.js index 7bd79bf..df082e9 100644 --- a/src/ast/Scope.js +++ b/src/ast/Scope.js @@ -1,4 +1,4 @@ -import { blank } from '../utils/object'; +import { blank, keys } from '../utils/object'; const extractors = { Identifier ( names, param ) { @@ -33,35 +33,67 @@ function extractNames ( param ) { return names; } +class Declaration { + constructor () { + this.statement = null; + this.name = null; + + this.isReassigned = false; + this.aliases = []; + } + + addAlias ( declaration ) { + this.aliases.push( declaration ); + } + + addReference ( reference ) { + reference.declaration = this; + this.name = reference.name; // TODO handle differences of opinion + + if ( reference.isReassignment ) this.isReassigned = true; + } + + render ( es6 ) { + if ( es6 ) return this.name; + if ( !this.isReassigned || !this.isExported ) return this.name; + + return `exports.${this.name}`; + } + + use () { + this.isUsed = true; + if ( this.statement ) this.statement.mark(); + + this.aliases.forEach( alias => alias.use() ); + } +} + export default class Scope { constructor ( options ) { options = options || {}; this.parent = options.parent; - this.depth = this.parent ? this.parent.depth + 1 : 0; - this.declarations = blank(); this.isBlockScope = !!options.block; - this.varDeclarations = []; + this.declarations = blank(); if ( options.params ) { options.params.forEach( param => { extractNames( param ).forEach( name => { - this.declarations[ name ] = true; + this.declarations[ name ] = new Declaration( name ); }); }); } } - addDeclaration ( declaration, isBlockDeclaration, isVar ) { + addDeclaration ( node, isBlockDeclaration, isVar ) { if ( !isBlockDeclaration && this.isBlockScope ) { // it's a `var` or function node, and this // is a block scope, so we need to go up - this.parent.addDeclaration( declaration, isBlockDeclaration, isVar ); + this.parent.addDeclaration( node, isBlockDeclaration, isVar ); } else { - extractNames( declaration.id ).forEach( name => { - this.declarations[ name ] = true; - if ( isVar ) this.varDeclarations.push( name ); + extractNames( node.id ).forEach( name => { + this.declarations[ name ] = new Declaration( name ); }); } } @@ -71,15 +103,14 @@ export default class Scope { ( this.parent ? this.parent.contains( name ) : false ); } - findDefiningScope ( name ) { - if ( this.declarations[ name ] ) { - return this; - } - - if ( this.parent ) { - return this.parent.findDefiningScope( name ); - } + eachDeclaration ( fn ) { + keys( this.declarations ).forEach( key => { + fn( key, this.declarations[ key ] ); + }); + } - return null; + findDeclaration ( name ) { + return this.declarations[ name ] || + ( this.parent && this.parent.findDeclaration( name ) ); } } diff --git a/src/ast/attachScopes.js b/src/ast/attachScopes.js new file mode 100644 index 0000000..888324a --- /dev/null +++ b/src/ast/attachScopes.js @@ -0,0 +1,76 @@ +import { walk } from 'estree-walker'; +import Scope from './Scope'; + +const blockDeclarations = { + 'const': true, + 'let': true +}; + +export default function attachScopes ( statement ) { + let { node, scope } = statement; + + walk( node, { + enter ( node, parent ) { + // function foo () {...} + // class Foo {...} + if ( /(Function|Class)Declaration/.test( node.type ) ) { + scope.addDeclaration( node, false, false ); + } + + // var foo = 1 + if ( node.type === 'VariableDeclaration' ) { + const isBlockDeclaration = blockDeclarations[ node.kind ]; + // only one declarator per block, because we split them up already + scope.addDeclaration( node.declarations[0], isBlockDeclaration, true ); + } + + let newScope; + + // create new function scope + if ( /Function/.test( node.type ) ) { + newScope = new Scope({ + parent: scope, + block: false, + params: node.params + }); + + // named function expressions - the name is considered + // part of the function's scope + if ( node.type === 'FunctionExpression' && node.id ) { + newScope.addDeclaration( node, false, false ); + } + } + + // create new block scope + if ( node.type === 'BlockStatement' && !/Function/.test( parent.type ) ) { + newScope = new Scope({ + parent: scope, + block: true + }); + } + + // catch clause has its own block scope + if ( node.type === 'CatchClause' ) { + newScope = new Scope({ + parent: scope, + params: [ node.param ], + block: true + }); + } + + if ( newScope ) { + Object.defineProperty( node, '_scope', { + value: newScope, + configurable: true + }); + + scope = newScope; + } + }, + leave ( node ) { + if ( node._scope ) { + scope = scope.parent; + } + } + }); +} diff --git a/src/ast/walk.js b/src/ast/walk.js deleted file mode 100644 index b91c68b..0000000 --- a/src/ast/walk.js +++ /dev/null @@ -1,59 +0,0 @@ -import { blank } from '../utils/object'; - -let shouldSkip; -let shouldAbort; - -export default function walk ( ast, { enter, leave }) { - shouldAbort = false; - visit( ast, null, enter, leave ); -} - -let context = { - skip: () => shouldSkip = true, - abort: () => shouldAbort = true -}; - -let childKeys = blank(); - -let toString = Object.prototype.toString; - -function isArray ( thing ) { - return toString.call( thing ) === '[object Array]'; -} - -function visit ( node, parent, enter, leave ) { - if ( !node || shouldAbort ) return; - - if ( enter ) { - shouldSkip = false; - enter.call( context, node, parent ); - if ( shouldSkip || shouldAbort ) return; - } - - let keys = childKeys[ node.type ] || ( - childKeys[ node.type ] = Object.keys( node ).filter( key => typeof node[ key ] === 'object' ) - ); - - let key, value, i, j; - - i = keys.length; - while ( i-- ) { - key = keys[i]; - value = node[ key ]; - - if ( isArray( value ) ) { - j = value.length; - while ( j-- ) { - visit( value[j], node, enter, leave ); - } - } - - else if ( value && value.type ) { - visit( value, node, enter, leave ); - } - } - - if ( leave && !shouldAbort ) { - leave( node, parent ); - } -} diff --git a/src/finalisers/amd.js b/src/finalisers/amd.js index 831de3a..757c7f4 100644 --- a/src/finalisers/amd.js +++ b/src/finalisers/amd.js @@ -22,7 +22,7 @@ export default function amd ( bundle, magicString, { exportMode, indentString }, const interopBlock = getInteropBlock( bundle ); if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' ); - const exportBlock = getExportBlock( bundle, exportMode ); + const exportBlock = getExportBlock( bundle.entryModule, exportMode ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); return magicString diff --git a/src/finalisers/cjs.js b/src/finalisers/cjs.js index 8e065d2..ecb30e0 100644 --- a/src/finalisers/cjs.js +++ b/src/finalisers/cjs.js @@ -1,19 +1,21 @@ -import getInteropBlock from './shared/getInteropBlock'; import getExportBlock from './shared/getExportBlock'; export default function cjs ( bundle, magicString, { exportMode }, options ) { let intro = options.useStrict === false ? `` : `'use strict';\n\n`; // TODO handle empty imports, once they're supported - let importBlock = bundle.externalModules - .map( module => `var ${module.name} = require('${module.id}');`) - .join('\n'); + const importBlock = bundle.externalModules + .map( module => { + let requireStatement = `var ${module.name} = require('${module.id}');`; - const interopBlock = getInteropBlock( bundle ); + if ( module.declarations.default ) { + requireStatement += '\n' + ( module.exportsNames ? `var ${module.name}__default = ` : `${module.name} = ` ) + + `'default' in ${module.name} ? ${module.name}['default'] : ${module.name};`; + } - if ( interopBlock ) { - importBlock += '\n' + interopBlock; - } + return requireStatement; + }) + .join( '\n' ); if ( importBlock ) { intro += importBlock + '\n\n'; @@ -21,7 +23,7 @@ export default function cjs ( bundle, magicString, { exportMode }, options ) { magicString.prepend( intro ); - const exportBlock = getExportBlock( bundle, exportMode, 'module.exports =' ); + const exportBlock = getExportBlock( bundle.entryModule, exportMode, 'module.exports =' ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); return magicString; diff --git a/src/finalisers/es6.js b/src/finalisers/es6.js index 3cdd71c..0f6d864 100644 --- a/src/finalisers/es6.js +++ b/src/finalisers/es6.js @@ -1,16 +1,5 @@ import { keys } from '../utils/object'; -function specifiersFor ( externalModule ) { - return keys( externalModule.importedByBundle ) - .filter( notDefault ) - .sort() - .map( name => { - const id = externalModule.exports.lookup( name ); - - return name !== id.name ? `${name} as ${id.name}` : name; - }); -} - function notDefault ( name ) { return name !== 'default'; } @@ -19,19 +8,19 @@ export default function es6 ( bundle, magicString ) { const importBlock = bundle.externalModules .map( module => { const specifiers = []; + const importedNames = keys( module.declarations ) + .filter( name => name !== '*' && name !== 'default' ); - const id = module.exports.lookup( 'default' ); - - if ( id ) { - specifiers.push( id.name ); + if ( module.declarations.default ) { + specifiers.push( module.name ); } - if ( module.needsAll ) { - specifiers.push( '* as ' + module.name ); + if ( module.declarations['*'] ) { + specifiers.push( `* as ${module.name}` ); } - if ( module.needsNamed ) { - specifiers.push( '{ ' + specifiersFor( module ).join( ', ' ) + ' }' ); + if ( importedNames.length ) { + specifiers.push( `{ ${importedNames.join( ', ' )} }` ); } return specifiers.length ? @@ -46,19 +35,19 @@ export default function es6 ( bundle, magicString ) { const module = bundle.entryModule; - const specifiers = bundle.toExport.filter( notDefault ).map( name => { - const id = bundle.exports.lookup( name ); + const specifiers = module.getExports().filter( notDefault ).map( name => { + const declaration = module.traceExport( name ); - return id.name === name ? + return declaration.name === name ? name : - `${id.name} as ${name}`; + `${declaration.name} as ${name}`; }); let exportBlock = specifiers.length ? `export { ${specifiers.join(', ')} };` : ''; - const defaultExport = module.exports.lookup( 'default' ); + const defaultExport = module.exports.default || module.reexports.default; if ( defaultExport ) { - exportBlock += `\nexport default ${ defaultExport.name };`; + exportBlock += `export default ${module.traceExport( 'default' ).name};`; } if ( exportBlock ) { diff --git a/src/finalisers/iife.js b/src/finalisers/iife.js index fef3978..997f2e7 100644 --- a/src/finalisers/iife.js +++ b/src/finalisers/iife.js @@ -33,7 +33,7 @@ export default function iife ( bundle, magicString, { exportMode, indentString } const interopBlock = getInteropBlock( bundle ); if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' ); - const exportBlock = getExportBlock( bundle, exportMode ); + const exportBlock = getExportBlock( bundle.entryModule, exportMode ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); return magicString diff --git a/src/finalisers/shared/getExportBlock.js b/src/finalisers/shared/getExportBlock.js index 6c53a3d..a6de6af 100644 --- a/src/finalisers/shared/getExportBlock.js +++ b/src/finalisers/shared/getExportBlock.js @@ -1,24 +1,21 @@ -function wrapAccess ( id ) { - return ( id.originalName !== 'default' && id.module && id.module.isExternal ) ? - id.module.name + propertyAccess( id.originalName ) : id.name; -} - -function propertyAccess ( name ) { - return name === 'default' ? `['default']` : `.${name}`; -} - -export default function getExportBlock ( bundle, exportMode, mechanism = 'return' ) { +export default function getExportBlock ( entryModule, exportMode, mechanism = 'return' ) { if ( exportMode === 'default' ) { - const id = bundle.exports.lookup( 'default' ); - - return `${mechanism} ${wrapAccess( id )};`; + return `${mechanism} ${entryModule.declarations.default.render( false )};`; } - return bundle.toExport + return entryModule.getExports() .map( name => { - const id = bundle.exports.lookup( name ); + const prop = name === 'default' ? `['default']` : `.${name}`; + const declaration = entryModule.traceExport( name ); + + const lhs = `exports${prop}`; + const rhs = declaration.render( false ); + + // prevent `exports.count = exports.count` + if ( lhs === rhs ) return null; - return `exports${propertyAccess( name )} = ${wrapAccess( id )};`; + return `${lhs} = ${rhs};`; }) + .filter( Boolean ) .join( '\n' ); } diff --git a/src/finalisers/shared/getInteropBlock.js b/src/finalisers/shared/getInteropBlock.js index 684af72..e0e2617 100644 --- a/src/finalisers/shared/getInteropBlock.js +++ b/src/finalisers/shared/getInteropBlock.js @@ -1,12 +1,11 @@ export default function getInteropBlock ( bundle ) { return bundle.externalModules .map( module => { - const def = module.exports.lookup( 'default' ); - - if ( !def ) return; - - return ( module.needsNamed ? 'var ' : '' ) + - `${def.name} = 'default' in ${module.name} ? ${module.name}['default'] : ${module.name};`; + return module.declarations.default ? + ( module.exportsNames ? + `var ${module.name}__default = 'default' in ${module.name} ? ${module.name}['default'] : ${module.name};` : + `${module.name} = 'default' in ${module.name} ? ${module.name}['default'] : ${module.name};` ) : + null; }) .filter( Boolean ) .join( '\n' ); diff --git a/src/finalisers/umd.js b/src/finalisers/umd.js index d2f01db..2b6ce84 100644 --- a/src/finalisers/umd.js +++ b/src/finalisers/umd.js @@ -48,7 +48,7 @@ export default function umd ( bundle, magicString, { exportMode, indentString }, const interopBlock = getInteropBlock( bundle ); if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' ); - const exportBlock = getExportBlock( bundle, exportMode ); + const exportBlock = getExportBlock( bundle.entryModule, exportMode ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); return magicString diff --git a/src/optimise/namespace-lookup.js b/src/optimise/namespace-lookup.js deleted file mode 100644 index ca03fde..0000000 --- a/src/optimise/namespace-lookup.js +++ /dev/null @@ -1,114 +0,0 @@ -import walk from '../ast/walk.js'; -import getLocation from '../utils/getLocation.js'; - -// Extract the property access from a MemberExpression. -function property ( node ) { - return node.name ? `.${node.name}` : `[${node.value}]`; -} - -// Recursively traverse the chain of member expressions from `node`, -// returning the access, e.g. `foo.bar[17]` -function chainedMemberExpression ( node ) { - if ( node.object.type === 'MemberExpression' ) { - return chainedMemberExpression( node.object ) + property( node.property ); - } - - return node.object.name + property( node.property ); -} - -export default function ( statement ) { - let localName; // The local name of the top-most imported namespace. - let topNode = null; // The top-node of the member expression. - let namespace = null; // An instance of `Module`. - - walk( statement.node, { - leave ( node, parent ) { - // Optimize namespace lookups, which manifest as MemberExpressions. - if ( node.type === 'MemberExpression' && ( !topNode || node.object === topNode ) ) { - // Ignore anything that doesn't begin with an identifier. - if ( !topNode && node.object.type !== 'Identifier') return; - - topNode = node; - - // If we don't already have a namespace, - // we aren't currently exploring any chain of member expressions. - if ( !namespace ) { - localName = node.object.name; - - // At first, we don't have a namespace, so we'll try to look one up. - const id = statement.module.locals.lookup( localName ); - - // It only counts if it exists, is a module, and isn't external. - if ( !id || !id.isModule || id.isExternal ) return; - - namespace = id; - } - - // If a namespace is the left hand side of an assignment, throw an error. - if ( parent.type === 'AssignmentExpression' && parent.left === node || - parent.type === 'UpdateExpression' && parent.argument === node ) { - const err = new Error( `Illegal reassignment to import '${chainedMemberExpression( node )}'` ); - err.file = statement.module.id; - err.loc = getLocation( statement.module.magicString.toString(), node.start ); - throw err; - } - - // Extract the name of the accessed property, from and Identifier or Literal. - // Any eventual Literal value is converted to a string. - const name = !node.computed ? node.property.name : - ( node.property.type === 'Literal' ? String( node.property.value ) : null ); - - // If we can't resolve the name being accessed statically, - // we mark the whole namespace for inclusion in the bundle. - // - // // resolvable - // console.log( javascript.keywords.for ) - // console.log( javascript.keywords[ 'for' ] ) - // console.log( javascript.keywords[ 6 ] ) - // - // // unresolvable - // console.log( javascript.keywords[ index ] ) - // console.log( javascript.keywords[ 1 + 5 ] ) - if ( name === null ) { - namespace.mark(); - - namespace = null; - topNode = null; - return; - } - - const id = namespace.exports.lookup( name ); - - // If the namespace doesn't export the given name, - // we can throw an error (even for nested namespaces). - if ( !id ) { - throw new Error( `Module "${namespace.id}" doesn't export "${name}"!` ); - } - - // We can't resolve deeper. Replace the member chain. - if ( parent.type !== 'MemberExpression' || !( id.isModule && !id.isExternal ) ) { - if ( !~statement.dependantIds.indexOf( id ) ) { - statement.dependantIds.push( id ); - } - - // FIXME: do this better - // If an earlier stage detected that we depend on this name... - if ( statement.dependsOn[ localName ] ) { - // ... decrement the count... - if ( !--statement.dependsOn[ localName ] ) { - // ... and remove it if the count is 0. - delete statement.dependsOn[ localName ]; - } - } - - statement.namespaceReplacements.push( [ topNode, id ] ); - namespace = null; - topNode = null; - return; - } - - namespace = id; - } - } - }); -} diff --git a/src/utils/getExportMode.js b/src/utils/getExportMode.js index 439c33c..b5c9fc4 100644 --- a/src/utils/getExportMode.js +++ b/src/utils/getExportMode.js @@ -5,7 +5,9 @@ function badExports ( option, keys ) { } export default function getExportMode ( bundle, exportMode ) { - const exportKeys = keys( bundle.entryModule.exports.names ); + const exportKeys = keys( bundle.entryModule.exports ) + .concat( keys( bundle.entryModule.reexports ) ) + .concat( bundle.entryModule.exportAllSources ); // not keys, but makes our job easier this way if ( exportMode === 'default' ) { if ( exportKeys.length !== 1 || exportKeys[0] !== 'default' ) { diff --git a/test/form/exports-at-end-if-possible/_config.js b/test/form/exports-at-end-if-possible/_config.js index a898bc7..30d375e 100644 --- a/test/form/exports-at-end-if-possible/_config.js +++ b/test/form/exports-at-end-if-possible/_config.js @@ -2,6 +2,5 @@ module.exports = { description: 'exports variables at end, if possible', options: { moduleName: 'myBundle' - }, - // solo: true + } }; diff --git a/test/form/external-imports/_expected/cjs.js b/test/form/external-imports/_expected/cjs.js index b6f5d31..ac9f79d 100644 --- a/test/form/external-imports/_expected/cjs.js +++ b/test/form/external-imports/_expected/cjs.js @@ -1,10 +1,10 @@ 'use strict'; var factory = require('factory'); +factory = 'default' in factory ? factory['default'] : factory; var baz = require('baz'); var containers = require('shipping-port'); var alphabet = require('alphabet'); -factory = 'default' in factory ? factory['default'] : factory; var alphabet__default = 'default' in alphabet ? alphabet['default'] : alphabet; factory( null ); diff --git a/test/form/external-imports/_expected/es6.js b/test/form/external-imports/_expected/es6.js index 4d05b46..cd41111 100644 --- a/test/form/external-imports/_expected/es6.js +++ b/test/form/external-imports/_expected/es6.js @@ -1,10 +1,10 @@ import factory from 'factory'; import { bar, foo } from 'baz'; import * as containers from 'shipping-port'; -import alphabet__default, { a } from 'alphabet'; +import alphabet, { a } from 'alphabet'; factory( null ); foo( bar ); containers.forEach( console.log, console ); console.log( a ); -console.log( alphabet__default.length ); +console.log( alphabet.length ); diff --git a/test/form/internal-conflict-resolution/_expected/amd.js b/test/form/internal-conflict-resolution/_expected/amd.js index 62ff65b..24d63fa 100644 --- a/test/form/internal-conflict-resolution/_expected/amd.js +++ b/test/form/internal-conflict-resolution/_expected/amd.js @@ -1,15 +1,15 @@ define(function () { 'use strict'; - var bar = 42; + var bar$1 = 42; function foo () { - return bar; + return bar$1; } - function _bar () { + function bar () { alert( foo() ); } - _bar(); + bar(); }); diff --git a/test/form/internal-conflict-resolution/_expected/cjs.js b/test/form/internal-conflict-resolution/_expected/cjs.js index cbc7af9..76c44a7 100644 --- a/test/form/internal-conflict-resolution/_expected/cjs.js +++ b/test/form/internal-conflict-resolution/_expected/cjs.js @@ -1,13 +1,13 @@ 'use strict'; -var bar = 42; +var bar$1 = 42; function foo () { - return bar; + return bar$1; } -function _bar () { +function bar () { alert( foo() ); } -_bar(); +bar(); diff --git a/test/form/internal-conflict-resolution/_expected/es6.js b/test/form/internal-conflict-resolution/_expected/es6.js index 7bb41e3..086f425 100644 --- a/test/form/internal-conflict-resolution/_expected/es6.js +++ b/test/form/internal-conflict-resolution/_expected/es6.js @@ -1,11 +1,11 @@ -var bar = 42; +var bar$1 = 42; function foo () { - return bar; + return bar$1; } -function _bar () { +function bar () { alert( foo() ); } -_bar(); +bar(); diff --git a/test/form/internal-conflict-resolution/_expected/iife.js b/test/form/internal-conflict-resolution/_expected/iife.js index 76687ff..b32c809 100644 --- a/test/form/internal-conflict-resolution/_expected/iife.js +++ b/test/form/internal-conflict-resolution/_expected/iife.js @@ -1,15 +1,15 @@ (function () { 'use strict'; - var bar = 42; + var bar$1 = 42; function foo () { - return bar; + return bar$1; } - function _bar () { + function bar () { alert( foo() ); } - _bar(); + bar(); })(); diff --git a/test/form/internal-conflict-resolution/_expected/umd.js b/test/form/internal-conflict-resolution/_expected/umd.js index d9d801b..c879634 100644 --- a/test/form/internal-conflict-resolution/_expected/umd.js +++ b/test/form/internal-conflict-resolution/_expected/umd.js @@ -4,16 +4,16 @@ factory(); }(this, function () { 'use strict'; - var bar = 42; + var bar$1 = 42; function foo () { - return bar; + return bar$1; } - function _bar () { + function bar () { alert( foo() ); } - _bar(); + bar(); })); diff --git a/test/function/shorthand-properties/_config.js b/test/form/shorthand-properties/_config.js similarity index 100% rename from test/function/shorthand-properties/_config.js rename to test/form/shorthand-properties/_config.js diff --git a/test/form/shorthand-properties/_expected/amd.js b/test/form/shorthand-properties/_expected/amd.js new file mode 100644 index 0000000..73e20c0 --- /dev/null +++ b/test/form/shorthand-properties/_expected/amd.js @@ -0,0 +1,25 @@ +define(function () { 'use strict'; + + function x () { + return 'foo'; + } + + var foo = { x }; + + function x$1 () { + return 'bar'; + } + + var bar = { x: x$1 }; + + function x$2 () { + return 'baz'; + } + + var baz = { x: x$2 }; + + assert.equal( foo.x(), 'foo' ); + assert.equal( bar.x(), 'bar' ); + assert.equal( baz.x(), 'baz' ); + +}); diff --git a/test/form/shorthand-properties/_expected/cjs.js b/test/form/shorthand-properties/_expected/cjs.js new file mode 100644 index 0000000..ec18995 --- /dev/null +++ b/test/form/shorthand-properties/_expected/cjs.js @@ -0,0 +1,23 @@ +'use strict'; + +function x () { + return 'foo'; +} + +var foo = { x }; + +function x$1 () { + return 'bar'; +} + +var bar = { x: x$1 }; + +function x$2 () { + return 'baz'; +} + +var baz = { x: x$2 }; + +assert.equal( foo.x(), 'foo' ); +assert.equal( bar.x(), 'bar' ); +assert.equal( baz.x(), 'baz' ); diff --git a/test/form/shorthand-properties/_expected/es6.js b/test/form/shorthand-properties/_expected/es6.js new file mode 100644 index 0000000..f6d31d4 --- /dev/null +++ b/test/form/shorthand-properties/_expected/es6.js @@ -0,0 +1,21 @@ +function x () { + return 'foo'; +} + +var foo = { x }; + +function x$1 () { + return 'bar'; +} + +var bar = { x: x$1 }; + +function x$2 () { + return 'baz'; +} + +var baz = { x: x$2 }; + +assert.equal( foo.x(), 'foo' ); +assert.equal( bar.x(), 'bar' ); +assert.equal( baz.x(), 'baz' ); diff --git a/test/form/shorthand-properties/_expected/iife.js b/test/form/shorthand-properties/_expected/iife.js new file mode 100644 index 0000000..d5ba3c7 --- /dev/null +++ b/test/form/shorthand-properties/_expected/iife.js @@ -0,0 +1,25 @@ +(function () { 'use strict'; + + function x () { + return 'foo'; + } + + var foo = { x }; + + function x$1 () { + return 'bar'; + } + + var bar = { x: x$1 }; + + function x$2 () { + return 'baz'; + } + + var baz = { x: x$2 }; + + assert.equal( foo.x(), 'foo' ); + assert.equal( bar.x(), 'bar' ); + assert.equal( baz.x(), 'baz' ); + +})(); diff --git a/test/form/shorthand-properties/_expected/umd.js b/test/form/shorthand-properties/_expected/umd.js new file mode 100644 index 0000000..b027a94 --- /dev/null +++ b/test/form/shorthand-properties/_expected/umd.js @@ -0,0 +1,29 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + factory(); +}(this, function () { 'use strict'; + + function x () { + return 'foo'; + } + + var foo = { x }; + + function x$1 () { + return 'bar'; + } + + var bar = { x: x$1 }; + + function x$2 () { + return 'baz'; + } + + var baz = { x: x$2 }; + + assert.equal( foo.x(), 'foo' ); + assert.equal( bar.x(), 'bar' ); + assert.equal( baz.x(), 'baz' ); + +})); diff --git a/test/form/shorthand-properties/bar.js b/test/form/shorthand-properties/bar.js new file mode 100644 index 0000000..f8f3758 --- /dev/null +++ b/test/form/shorthand-properties/bar.js @@ -0,0 +1,7 @@ +function x () { + return 'bar'; +} + +var bar = { x }; + +export { bar }; diff --git a/test/form/shorthand-properties/baz.js b/test/form/shorthand-properties/baz.js new file mode 100644 index 0000000..ee1d362 --- /dev/null +++ b/test/form/shorthand-properties/baz.js @@ -0,0 +1,7 @@ +function x () { + return 'baz'; +} + +var baz = { x }; + +export { baz }; diff --git a/test/form/shorthand-properties/foo.js b/test/form/shorthand-properties/foo.js new file mode 100644 index 0000000..1fa770d --- /dev/null +++ b/test/form/shorthand-properties/foo.js @@ -0,0 +1,7 @@ +function x () { + return 'foo'; +} + +var foo = { x }; + +export { foo }; diff --git a/test/form/shorthand-properties/main.js b/test/form/shorthand-properties/main.js new file mode 100644 index 0000000..6774a8e --- /dev/null +++ b/test/form/shorthand-properties/main.js @@ -0,0 +1,7 @@ +import { foo } from './foo'; +import { bar } from './bar'; +import { baz } from './baz'; + +assert.equal( foo.x(), 'foo' ); +assert.equal( bar.x(), 'bar' ); +assert.equal( baz.x(), 'baz' ); diff --git a/test/function/assignment-to-exports/_config.js b/test/function/assignment-to-exports/_config.js index fac0cb3..c958adb 100644 --- a/test/function/assignment-to-exports/_config.js +++ b/test/function/assignment-to-exports/_config.js @@ -6,6 +6,5 @@ module.exports = { assert.equal( exports.count, 0 ); exports.incr(); assert.equal( exports.count, 1 ); - }, - // solo: true + } }; diff --git a/test/function/consistent-renaming-c/_config.js b/test/function/consistent-renaming-c/_config.js index ad1b3a9..4b30d56 100644 --- a/test/function/consistent-renaming-c/_config.js +++ b/test/function/consistent-renaming-c/_config.js @@ -1,3 +1,3 @@ module.exports = { description: 'consistent renaming test c' -}; \ No newline at end of file +}; diff --git a/test/function/consistent-renaming-f/_config.js b/test/function/consistent-renaming-f/_config.js new file mode 100644 index 0000000..873e296 --- /dev/null +++ b/test/function/consistent-renaming-f/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'consistent renaming test f' +}; diff --git a/test/function/consistent-renaming-f/bar.js b/test/function/consistent-renaming-f/bar.js new file mode 100644 index 0000000..afe602c --- /dev/null +++ b/test/function/consistent-renaming-f/bar.js @@ -0,0 +1,3 @@ +export default function foo() { + return 'consistent'; +} diff --git a/test/function/consistent-renaming-f/main.js b/test/function/consistent-renaming-f/main.js new file mode 100644 index 0000000..d1e53f9 --- /dev/null +++ b/test/function/consistent-renaming-f/main.js @@ -0,0 +1,9 @@ +import bar from './bar'; + +export default function foo () {} + +foo.prototype.a = function ( foo ) { + return bar(); +}; + +assert.equal( new foo().a(), 'consistent' ); diff --git a/test/function/export-from-no-local-binding/_config.js b/test/function/export-from-no-local-binding/_config.js index 7500be3..333c68b 100644 --- a/test/function/export-from-no-local-binding/_config.js +++ b/test/function/export-from-no-local-binding/_config.js @@ -1,3 +1,5 @@ +var assert = require( 'assert' ); + module.exports = { description: 'export from does not create a local binding' }; diff --git a/test/function/import-of-unexported-fails/_config.js b/test/function/import-of-unexported-fails/_config.js index 75e1586..3027ba9 100644 --- a/test/function/import-of-unexported-fails/_config.js +++ b/test/function/import-of-unexported-fails/_config.js @@ -2,9 +2,7 @@ var assert = require( 'assert' ); module.exports = { description: 'marking an imported, but unexported, identifier should throw', - error: function ( err ) { - assert.equal( err.message.slice( 0, 50 ), 'The imported name "default" is never exported by "' ); - assert.equal( err.message.slice( -10 ), 'empty.js".' ); + assert.ok( /Module .+empty\.js does not export default \(imported by .+main\.js\)/.test( err.message ) ); } }; diff --git a/test/function/module-sort-order/_config.js b/test/function/module-sort-order/_config.js new file mode 100644 index 0000000..9b7f002 --- /dev/null +++ b/test/function/module-sort-order/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'module sorter is not confused by top-level call expressions' +}; diff --git a/test/function/module-sort-order/a.js b/test/function/module-sort-order/a.js new file mode 100644 index 0000000..c84b679 --- /dev/null +++ b/test/function/module-sort-order/a.js @@ -0,0 +1,20 @@ +import { b } from './b'; +import z from './z'; + +z(); + +var p = { + q: function () { + b.nope(); + } +}; + +(function () { + var p = { + q: function () { + b.nope(); + } + }; +})(); + +export default 42; diff --git a/test/function/module-sort-order/b.js b/test/function/module-sort-order/b.js new file mode 100644 index 0000000..1d6eae8 --- /dev/null +++ b/test/function/module-sort-order/b.js @@ -0,0 +1 @@ +export var b = function () {}; diff --git a/test/function/module-sort-order/c.js b/test/function/module-sort-order/c.js new file mode 100644 index 0000000..5280bb0 --- /dev/null +++ b/test/function/module-sort-order/c.js @@ -0,0 +1,3 @@ +import { b } from './b'; + +export var c = function () {}; diff --git a/test/function/module-sort-order/main.js b/test/function/module-sort-order/main.js new file mode 100644 index 0000000..dcd42cb --- /dev/null +++ b/test/function/module-sort-order/main.js @@ -0,0 +1,4 @@ +import a from './a'; +import z from './z'; + +z(); diff --git a/test/function/module-sort-order/z.js b/test/function/module-sort-order/z.js new file mode 100644 index 0000000..8b67172 --- /dev/null +++ b/test/function/module-sort-order/z.js @@ -0,0 +1,5 @@ +import { c } from './c'; + +export default function () { + c(); +} diff --git a/test/function/namespace-optimisation-before-exports/_config.js b/test/function/namespace-optimisation-before-exports/_config.js deleted file mode 100644 index e730b5a..0000000 --- a/test/function/namespace-optimisation-before-exports/_config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - description: 'namespace optimisation must be done after all exports are defined' -}; - -// See: https://github.com/rollup/rollup/issues/148 diff --git a/test/function/namespace-optimisation-before-exports/bar.js b/test/function/namespace-optimisation-before-exports/bar.js deleted file mode 100644 index c2b6db8..0000000 --- a/test/function/namespace-optimisation-before-exports/bar.js +++ /dev/null @@ -1 +0,0 @@ -export default function bar () {} diff --git a/test/function/namespace-optimisation-before-exports/foo.js b/test/function/namespace-optimisation-before-exports/foo.js deleted file mode 100644 index d4e76e2..0000000 --- a/test/function/namespace-optimisation-before-exports/foo.js +++ /dev/null @@ -1 +0,0 @@ -export { default as bar } from './bar.js'; diff --git a/test/function/namespace-optimisation-before-exports/main.js b/test/function/namespace-optimisation-before-exports/main.js deleted file mode 100644 index 85eac4c..0000000 --- a/test/function/namespace-optimisation-before-exports/main.js +++ /dev/null @@ -1,6 +0,0 @@ -import * as foo from './foo'; -import './zoo'; - -export default { - foo: foo -}; diff --git a/test/function/namespace-optimisation-before-exports/zoo.js b/test/function/namespace-optimisation-before-exports/zoo.js deleted file mode 100644 index 82a9c9d..0000000 --- a/test/function/namespace-optimisation-before-exports/zoo.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as foo from './foo'; - -function wah () { - foo.bar(); -} - -wah(); diff --git a/test/function/pass-namespace-to-function/_config.js b/test/function/pass-namespace-to-function/_config.js new file mode 100644 index 0000000..d93fddb --- /dev/null +++ b/test/function/pass-namespace-to-function/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'allows a namespace to be passed to a function' +}; diff --git a/test/function/pass-namespace-to-function/bar.js b/test/function/pass-namespace-to-function/bar.js new file mode 100644 index 0000000..8d5a2a2 --- /dev/null +++ b/test/function/pass-namespace-to-function/bar.js @@ -0,0 +1 @@ +// this space left intentionally blank diff --git a/test/function/pass-namespace-to-function/foo.js b/test/function/pass-namespace-to-function/foo.js new file mode 100644 index 0000000..29e1da1 --- /dev/null +++ b/test/function/pass-namespace-to-function/foo.js @@ -0,0 +1,7 @@ +import * as bar from './bar'; + +export default function foo () {} + +foo.x = function () { + doSomethingWith( bar ); +}; diff --git a/test/function/pass-namespace-to-function/main.js b/test/function/pass-namespace-to-function/main.js new file mode 100644 index 0000000..505e20b --- /dev/null +++ b/test/function/pass-namespace-to-function/main.js @@ -0,0 +1,5 @@ +import foo from './foo'; + +export default function () { + foo(); +} diff --git a/test/function/shorthand-properties/baz.js b/test/function/shorthand-properties/baz.js deleted file mode 100644 index d826bfa..0000000 --- a/test/function/shorthand-properties/baz.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function bar () { - return 'main-bar'; -} diff --git a/test/function/shorthand-properties/foo.js b/test/function/shorthand-properties/foo.js deleted file mode 100644 index 831360e..0000000 --- a/test/function/shorthand-properties/foo.js +++ /dev/null @@ -1,10 +0,0 @@ -import baz from './baz.js'; - -function bar () { - return 'foo-bar'; -} - -export var foo = { - bar, - baz -}; diff --git a/test/function/shorthand-properties/main.js b/test/function/shorthand-properties/main.js deleted file mode 100644 index 754fba8..0000000 --- a/test/function/shorthand-properties/main.js +++ /dev/null @@ -1,5 +0,0 @@ -import bar from './baz.js'; -import { foo } from './foo'; - -assert.equal( bar(), 'main-bar' ); -assert.equal( foo.bar(), 'foo-bar' ); diff --git a/test/function/tracks-alias-mutations/_config.js b/test/function/tracks-alias-mutations/_config.js index c3a0619..f2faaad 100644 --- a/test/function/tracks-alias-mutations/_config.js +++ b/test/function/tracks-alias-mutations/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'tracks mutations of aliased objects', - skip: true + description: 'tracks mutations of aliased objects' }; diff --git a/test/sourcemaps/names/_config.js b/test/sourcemaps/names/_config.js index 36523ef..ce0bccb 100644 --- a/test/sourcemaps/names/_config.js +++ b/test/sourcemaps/names/_config.js @@ -8,21 +8,20 @@ module.exports = { moduleName: 'myModule' }, test: function ( code, map ) { - var match = /Object\.create\( ([^\.]+)\.prototype/.exec( code ); + var smc = new SourceMapConsumer( map ); - var deconflictedName = match[1]; - if ( deconflictedName !== 'Foo' ) throw new Error( 'Need to update this test!' ); + var pattern = /Object\.create\( ([\w\$\d]+)\.prototype \)/; + var match = pattern.exec( code ); - var smc = new SourceMapConsumer( map ); + var generatedLoc = getLocation( code, match.index + 'Object.create ( '.length ); + var original = smc.originalPositionFor( generatedLoc ); + assert.equal( original.name, 'Bar' ); - var index = code.indexOf( deconflictedName ); - var generatedLoc = getLocation( code, index ); - var originalLoc = smc.originalPositionFor( generatedLoc ); - assert.equal( originalLoc.name, null ); + pattern = /function Foo([\w\$\d]+)/; + match = pattern.exec( code ); - index = code.indexOf( deconflictedName, index + 1 ); - generatedLoc = getLocation( code, index ); - originalLoc = smc.originalPositionFor( generatedLoc ); - assert.equal( originalLoc.name, 'Foo' ); + generatedLoc = getLocation( code, match.index + 'function '.length ); + original = smc.originalPositionFor( generatedLoc ); + assert.equal( original.name, 'Foo' ); } }; diff --git a/test/test.js b/test/test.js index 520330a..dfdcac7 100644 --- a/test/test.js +++ b/test/test.js @@ -75,6 +75,28 @@ describe( 'rollup', function () { }, /must supply options\.dest/ ); }); }); + + it( 'expects options.moduleName for IIFE and UMD bundles', function () { + return rollup.rollup({ + entry: 'x', + resolveId: function () { return 'test'; }, + load: function () { + return 'export var foo = 42;'; + } + }).then( function ( bundle ) { + assert.throws( function () { + bundle.generate({ + format: 'umd' + }); + }, /You must supply options\.moduleName for UMD bundles/ ); + + assert.throws( function () { + bundle.generate({ + format: 'iife' + }); + }, /You must supply options\.moduleName for IIFE bundles/ ); + }); + }); }); describe( 'function', function () { @@ -228,6 +250,10 @@ describe( 'rollup', function () { expectedMap.sourcesContent = expectedMap.sourcesContent.map( normaliseOutput ); } catch ( err ) {} + if ( config.show ) { + console.log( actualCode + '\n\n\n' ); + } + assert.equal( actualCode, expectedCode ); assert.deepEqual( actualMap, expectedMap ); }); diff --git a/test/testScope.js b/test/testScope.js deleted file mode 100644 index 67e443d..0000000 --- a/test/testScope.js +++ /dev/null @@ -1,114 +0,0 @@ -require('babel/register'); -var assert = require( 'assert' ); - -var Scope = require( '../src/Scope' ); - -describe( 'Scope', function () { - it( 'can define and bind names', function () { - const scope = new Scope(); - - // If I define 'a'... - scope.define( 'a' ); - - // ... and bind 'b' to a reference to 'a'... - scope.bind( 'b', scope.reference( 'a' ) ); - - // ... lookups for 'a' and 'b' should both - // resolve to the same identifier. - assert.equal( scope.lookup( 'b' ), scope.lookup( 'a' ) ); - }); - - describe( 'parent:', function () { - var parent = new Scope(), - child = new Scope( parent ); - - it( 'allows children access to its names', function () { - parent.define( 'a' ); - - assert.equal( child.lookup( 'a' ), parent.lookup( 'a' ) ); - }); - - it( 'names in the child scope shadows the parent', function () { - child.define( 'a' ); - - assert.notEqual( child.lookup( 'a' ), parent.lookup( 'a' ) ); - - child.define( 'b' ); - - assert.equal( parent.lookup( 'b' ), undefined ); - }); - }); - - describe( 'virtual scope:', function () { - var real, a, b; - - beforeEach(function () { - real = new Scope(); - a = real.virtual(); - b = real.virtual(); - }); - - it( 'is created within another scope', function () { - // The actual ids are the same. - assert.equal( real.ids, a.ids ); - assert.equal( real.ids, b.ids ); - }); - - it( 'lookups different identifiers', function () { - // If I define 'a' in both scopes... - a.define( 'a' ); - b.define( 'a' ); - - // ... the name 'a' should lookup different identifiers. - assert.notEqual( a.lookup( 'a' ), b.lookup( 'b' ) ); - }); - - it( 'can deconflict names', function () { - a.define( 'a' ); - b.define( 'a' ); - - // Deconflicting the actual scope should make all identifiers unique. - real.deconflict(); - - assert.deepEqual( real.usedNames(), [ '_a', 'a' ] ); - }); - - it( 'deconflicts with a custom function, if provided', function () { - for (var i = 0; i < 26; i++) { - // Create 26 scopes, all of which define 'a'. - real.virtual().define( 'a' ); - } - - // Custom deconfliction function which ignores the current name. - var num = 10; - real.deconflict( function () { - return (num++).toString(36); - }); - - assert.deepEqual( real.usedNames(), 'abcdefghijklmnopqrstuvwxyz'.split('') ); - - // Deconflicting twice has no additional effect. - real.deconflict(); - assert.deepEqual( real.usedNames(), 'abcdefghijklmnopqrstuvwxyz'.split('') ); - }); - }); - - it( 'cannot reference undefined names', function () { - var real = new Scope(); - - var external = real.virtual(), - locals = real.virtual(), - exports = real.virtual(); - - external.define( 'Component' ); - - locals.bind( 'Comp', external.reference( 'Component' ) ); - - assert.throws( function () { - exports.bind( 'default', locals.reference( 'Foo' ) ); - }, 'Cannot reference undefined identifier "Foo"' ); - - locals.define( 'Foo' ); - exports.bind( 'default', locals.reference( 'Foo' ) ); - }); -});