From 1bf522388d4eaeb0cb226da5c56362b32752b019 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Oct 2015 18:25:26 -0400 Subject: [PATCH] very incomplete start on #156 reboot. basically just broke a bunch of things --- src/Bundle.js | 187 +-------- src/Module.js | 304 ++++----------- src/Statement.js | 427 +++------------------ src/ast/Scope.js | 26 +- src/ast/attachScopes.js | 79 ++++ test/form/self-contained-bundle/_config.js | 2 + test/test.js | 12 +- 7 files changed, 242 insertions(+), 795 deletions(-) create mode 100644 src/ast/attachScopes.js diff --git a/src/Bundle.js b/src/Bundle.js index 889e68e..1f6413b 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -47,7 +47,12 @@ export default class Bundle { return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) ) .then( id => this.fetchModule( id ) ) .then( entryModule => { - entryModule.bindImportSpecifiers(); + this.modules.forEach( module => { + module.bindImportSpecifiers(); + module.bindReferences(); + module.markAllSideEffects(); + }); + const defaultExport = entryModule.exports.default; @@ -84,12 +89,6 @@ export default class Bundle { entryModule.markAllStatements( true ); this.markAllModifierStatements(); - // Include all side-effects - // TODO does this obviate the need for markAllStatements throughout? - this.modules.forEach( module => { - module.markAllSideEffects(); - }); - this.orderedModules = this.sort(); }); } @@ -103,79 +102,6 @@ export default class Bundle { let allReplacements = blank(); - // Assign names to external modules - this.externalModules.forEach( module => { - // while we're here... - allReplacements[ module.id ] = blank(); - - // TODO is this necessary in the ES6 case? - let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id ); - module.name = getSafeName( name ); - }); - - // Discover conflicts (i.e. two statements in separate modules both define `foo`) - let i = this.orderedModules.length; - while ( i-- ) { - const module = this.orderedModules[i]; - - // while we're here... - allReplacements[ module.id ] = blank(); - - keys( module.definitions ).forEach( name => { - const safeName = getSafeName( name ); - if ( safeName !== name ) { - module.rename( name, safeName ); - allReplacements[ module.id ][ name ] = safeName; - } - }); - } - - // Assign non-conflicting names to internal default/namespace export - this.orderedModules.forEach( module => { - if ( !module.needsDefault && !module.needsAll ) return; - - if ( module.needsAll ) { - const namespaceName = getSafeName( module.suggestedNames[ '*' ] ); - module.replacements[ '*' ] = namespaceName; - } - - if ( module.needsDefault || module.needsAll && module.exports.default ) { - const defaultExport = module.exports.default; - - // only create a new name if either - // a) it's an expression (`export default 42`) or - // b) it's a name that is reassigned to (`export var a = 1; a = 2`) - if ( defaultExport && defaultExport.identifier && !defaultExport.isModified ) return; // TODO encapsulate check for whether we need synthetic default name - - const defaultName = getSafeName( module.suggestedNames.default ); - module.replacements.default = defaultName; - } - }); - - this.orderedModules.forEach( module => { - keys( module.imports ).forEach( localName => { - if ( !module.imports[ localName ].isUsed ) return; - - const bundleName = this.trace( module, localName, es6 ); - if ( bundleName !== localName ) { - allReplacements[ module.id ][ localName ] = bundleName; - } - }); - }); - - function getSafeName ( name ) { - if ( name in nameCount ) { - nameCount[ name ] += 1; - name = `${name}$${nameCount[ name ]}`; - - while ( name in nameCount ) name = `_${name}`; // just to avoid any crazy edge cases - return name; - } - - nameCount[ name ] = 0; - return name; - } - return allReplacements; } @@ -241,41 +167,7 @@ export default class Bundle { this.modules.forEach( module => { module.statements.forEach( statement => { if ( statement.isIncluded ) return; - - keys( statement.modifies ).forEach( name => { - const definingStatement = module.definitions[ name ]; - const exportDeclaration = module.exports[ name ] || module.reexports[ name ] || ( - module.exports.default && module.exports.default.identifier === name && module.exports.default - ); - - const shouldMark = ( definingStatement && definingStatement.isIncluded ) || - ( exportDeclaration && exportDeclaration.isUsed ); - - if ( shouldMark ) { - settled = false; - statement.mark(); - return; - } - - // special case - https://github.com/rollup/rollup/pull/40 - // TODO refactor this? it's a bit confusing - const importDeclaration = module.imports[ name ]; - if ( !importDeclaration || importDeclaration.module.isExternal ) return; - - if ( importDeclaration.name === '*' ) { - importDeclaration.module.markAllExportStatements(); - } else { - const otherExportDeclaration = importDeclaration.module.exports[ importDeclaration.name ]; - // TODO things like `export default a + b` don't apply here... right? - const otherDefiningStatement = module.findDefiningStatement( otherExportDeclaration.localName ); - - if ( !otherDefiningStatement ) return; - - statement.mark(); - } - - settled = false; - }); + // TODO... }); }); @@ -289,61 +181,10 @@ 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.replacements[ name ] || name ] = true; - }); - }); - - if ( format !== 'es6' && exportMode === 'named' ) { - keys( this.entryModule.exports ) - .concat( keys( this.entryModule.reexports ) ) - .forEach( name => { - const canonicalName = this.traceExport( this.entryModule, 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.entryModule.getExports() - .filter( key => !varExports[ key ] ); - let magicString = new MagicString.Bundle({ separator: '\n\n' }); this.orderedModules.forEach( module => { - const source = module.render( allBundleExports, allReplacements[ module.id ], format ); + const source = module.render(); if ( source.toString().length ) { magicString.addSource( source ); } @@ -366,21 +207,13 @@ export default class Bundle { 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( ', ' )}` ); } + this.toExport = []; // TODO magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options ); if ( options.banner ) magicString.prepend( options.banner + '\n' ); @@ -460,7 +293,7 @@ export default class Bundle { ordered.push( module ); } - visit( this.entryModule ); + this.modules.forEach( visit ); if ( hasCycles ) { let unordered = ordered; diff --git a/src/Module.js b/src/Module.js index 2066203..d42d32b 100644 --- a/src/Module.js +++ b/src/Module.js @@ -2,29 +2,13 @@ import { parse } from 'acorn'; import MagicString from 'magic-string'; import Statement from './Statement'; import walk from './ast/walk'; +import Scope from './ast/Scope'; 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 deconflict ( name, names ) { - while ( name in names ) { - name = `_${name}`; - } - - return name; -} - -function isEmptyExportedVarDeclaration ( node, allBundleExports, moduleReplacements ) { - if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false; - - const name = node.declarations[0].id.name; - const canonicalName = moduleReplacements[ name ] || name; - - return canonicalName in allBundleExports; -} - export default class Module { constructor ({ id, source, ast, bundle }) { this.source = source; @@ -45,7 +29,6 @@ export default class Module { this.magicString.remove( match.index, match.index + match[0].length ); } - this.suggestedNames = blank(); this.comments = []; this.statements = this.parse( ast ); @@ -53,7 +36,6 @@ export default class Module { // all dependencies this.dependencies = []; this.resolvedIds = blank(); - this.boundImportSpecifiers = false; // imports and exports, indexed by local name this.imports = blank(); @@ -63,15 +45,8 @@ export default class Module { this.exportAlls = []; - this.replacements = blank(); - - this.reassignments = []; - - this.marked = blank(); + this.scope = new Scope(); this.definitions = blank(); - this.definitionPromises = blank(); - this.modifications = blank(); - this.analyse(); } @@ -203,44 +178,9 @@ export default class Module { statement.analyse(); - // consolidate names that are defined/modified in this module - keys( statement.defines ).forEach( name => { - this.definitions[ name ] = statement; - }); - - keys( statement.modifies ).forEach( name => { - ( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement ); - }); - }); - - // 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; - }); - }); - - // 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; - - // while we're here, mark reassignments - statement.scope.varDeclarations.forEach( name => { - if ( reassigned[ name ] && !~this.reassignments.indexOf( name ) ) { - this.reassignments.push( name ); - } - }); - - keys( statement.dependsOn ).forEach( name => { - if ( !this.definitions[ name ] && !this.imports[ name ] ) { - this.bundle.assumedGlobals[ name ] = true; - } + keys( statement.scope.declarations ).forEach( name => { + const declaration = statement.scope.declarations[ name ]; + this.definitions[ name ] = { statement, declaration }; }); }); } @@ -250,9 +190,6 @@ export default class Module { } bindImportSpecifiers () { - if ( this.boundImportSpecifiers ) return; - this.boundImportSpecifiers = true; - [ this.imports, this.reexports ].forEach( specifiers => { keys( specifiers ).forEach( name => { const specifier = specifiers[ name ]; @@ -268,83 +205,59 @@ export default class Module { const id = this.resolvedIds[ delegate.source ]; delegate.module = this.bundle.moduleById[ id ]; }); - - this.dependencies.forEach( source => { - const id = this.resolvedIds[ source ]; - const module = this.bundle.moduleById[ id ]; - - if ( !module.isExternal ) module.bindImportSpecifiers(); - }); } - consolidateDependencies () { - let strongDependencies = blank(); + bindReferences () { + this.statements.forEach( ( statement, i ) => { + statement.references.forEach( reference => { + let declaration; - function addDependency ( dependencies, declaration ) { - if ( declaration && declaration.module && !declaration.module.isExternal ) { - dependencies[ declaration.module.id ] = declaration.module; - return true; - } - } + // find in local scope... + declaration = reference.scope.findDeclaration( reference.name ); - this.statements.forEach( statement => { - if ( statement.isImportDeclaration && !statement.node.specifiers.length ) { - // include module for its side-effects - const id = this.resolvedIds[ statement.node.source.value ]; - const module = this.bundle.moduleById[ id ]; + if ( declaration ) { + reference.declaration = declaration; + reference.definingStatement = statement; + return; + } - if ( !module.isExternal ) strongDependencies[ module.id ] = module; - } + let definition; - else if ( statement.isReexportDeclaration ) { - if ( statement.node.specifiers ) { - statement.node.specifiers.forEach( specifier => { - let reexport; - - let module = this; - let name = specifier.exported.name; - while ( !module.isExternal && module.reexports[ name ] && module.reexports[ name ].isUsed ) { - reexport = module.reexports[ name ]; - module = reexport.module; - name = reexport.localName; - } - - addDependency( strongDependencies, reexport ); - }); - } - } + // ...or in module... + definition = this.definitions[ reference.name ]; - else { - keys( statement.stronglyDependsOn ).forEach( name => { - if ( statement.defines[ name ] ) return; + // ...or from import + if ( !definition ) { + const importDeclaration = this.imports[ reference.name ]; + if ( importDeclaration ) { + definition = importDeclaration.module.traceExport( importDeclaration.name ); + } + } - addDependency( strongDependencies, this.exportDelegates[ name ] ) || - addDependency( strongDependencies, this.imports[ name ] ); - }); - } + if ( definition ) { + reference.declaration = definition.declaration; + reference.definingStatement = definition.statement; + definition.declaration.references.push( reference ); + } else { + //console.log( 'TODO no declaration. global?' ); + } + }); }); + } + consolidateDependencies () { + let strongDependencies = blank(); let weakDependencies = blank(); this.statements.forEach( statement => { - keys( statement.dependsOn ).forEach( name => { - if ( statement.defines[ name ] ) return; - - addDependency( weakDependencies, this.exportDelegates[ name ] ) || - addDependency( weakDependencies, this.imports[ name ] ); + statement.references.forEach( reference => { + if ( reference.definingStatement ) { + const module = reference.definingStatement.module; + weakDependencies[ module.id ] = module; + } }); }); - // special case – `export { ... } from './other'` in entry module - if ( this.exportAlls.length ) { - this.exportAlls.forEach( ({ source }) => { - const resolved = this.resolvedIds[ source ]; - const otherModule = this.bundle.moduleById[ resolved ]; - - strongDependencies[ otherModule.id ] = otherModule; - }); - } - return { strongDependencies, weakDependencies }; } @@ -627,125 +540,50 @@ export default class Module { return statements; } - rename ( name, replacement ) { - this.replacements[ name ] = replacement; - } - - render ( allBundleExports, moduleReplacements ) { + render () { let magicString = this.magicString.clone(); this.statements.forEach( statement => { if ( !statement.isIncluded ) { magicString.remove( statement.start, statement.next ); - return; - } - - // skip `export { foo, bar, baz }` - if ( statement.node.type === 'ExportNamedDeclaration' ) { - // skip `export { foo, bar, baz }` - if ( statement.node.specifiers.length ) { - magicString.remove( statement.start, statement.next ); - return; - } - - // skip `export var foo;` if foo is exported - if ( isEmptyExportedVarDeclaration( statement.node.declaration, allBundleExports, moduleReplacements ) ) { - magicString.remove( statement.start, statement.next ); - return; - } - } - - // skip empty var declarations for exported bindings - // (otherwise we're left with `exports.foo;`, which is useless) - if ( isEmptyExportedVarDeclaration( statement.node, allBundleExports, moduleReplacements ) ) { - magicString.remove( statement.start, statement.next ); - return; - } - - // split up/remove var declarations as necessary - if ( statement.node.isSynthetic ) { - // insert `var/let/const` if necessary - if ( !allBundleExports[ statement.node.declarations[0].id.name ] ) { - 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(); - - keys( statement.dependsOn ) - .concat( keys( statement.defines ) ) - .forEach( name => { - const bundleName = moduleReplacements[ name ] || name; - - if ( allBundleExports[ bundleName ] ) { - bundleExports[ name ] = replacements[ name ] = allBundleExports[ bundleName ]; - } else if ( bundleName !== name ) { // TODO weird structure - replacements[ name ] = bundleName; - } - }); - - statement.replaceIdentifiers( magicString, replacements, bundleExports ); - - // modify exports as necessary - if ( statement.isReexportDeclaration ) { - // remove `export { foo } from './other'` and `export * from './other'` - magicString.remove( statement.start, statement.next ); } + }); - else 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 ); - } - - // remove `export` from `export class Foo {...}` or `export default Foo` - // TODO default exports need different treatment - else if ( statement.node.declaration.id ) { - magicString.remove( statement.node.start, statement.node.declaration.start ); - } + return magicString.trim(); + } - else if ( statement.node.type === 'ExportDefaultDeclaration' ) { - const canonicalName = this.defaultName(); + trace ( name ) { + if ( name in this.definitions ) return this.definitions[ name ]; + if ( name in this.imports ) { + const otherModule = this.imports[ name ].module; + return otherModule.traceExport( name ); + } - if ( statement.node.declaration.type === 'Identifier' && canonicalName === ( moduleReplacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) { - magicString.remove( statement.start, statement.next ); - return; - } + return null; + } - // prevent `var undefined = sideEffectyDefault(foo)` - if ( canonicalName === undefined ) { - magicString.remove( statement.start, statement.node.declaration.start ); - return; - } + traceExport ( name ) { + // export { foo } from './other' + const reexportDeclaration = this.reexports[ name ]; + if ( reexportDeclaration ) { + return reexportDeclaration.module.traceExport( reexportDeclaration.name ); + } - // anonymous functions should be converted into declarations - if ( statement.node.declaration.type === 'FunctionExpression' ) { - magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${canonicalName}` ); - } else { - magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${canonicalName} = ` ); - } + const exportDeclaration = this.exports[ name ]; + if ( exportDeclaration ) { + // TODO defaults should not be live... + if ( name === 'default' ) { + if ( exportDeclaration.identifier ) { + return this.trace( exportDeclaration.identifier ); } - else { - throw new Error( 'Unhandled export' ); - } + throw new Error( 'TODO default expression exports' ); } - }); - return magicString.trim(); - } - - suggestName ( defaultOrBatch, suggestion ) { - // deconflict anonymous default exports with this module's definitions - const shouldDeconflict = this.exports.default && this.exports.default.isAnonymous; - - if ( shouldDeconflict ) suggestion = deconflict( suggestion, this.definitions ); - - if ( !this.suggestedNames[ defaultOrBatch ] ) { - this.suggestedNames[ defaultOrBatch ] = makeLegalIdentifier( suggestion ); + return this.trace( exportDeclaration.name ); } + + // TODO export * + throw new Error( 'could not trace export', name ); } } diff --git a/src/Statement.js b/src/Statement.js index 5cc62cf..6cd00bd 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -1,12 +1,6 @@ -import { blank, keys } from './utils/object'; -import getLocation from './utils/getLocation'; import walk from './ast/walk'; import Scope from './ast/Scope'; - -const blockDeclarations = { - 'const': true, - 'let': true -}; +import attachScopes from './ast/attachScopes'; const modifierNodes = { AssignmentExpression: 'left', @@ -17,12 +11,43 @@ 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; + } + + if ( node.type === 'Identifier' ) { + // 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; + + // 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 - // `var foo = function () {}` - same thing for present purposes - if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true; + this.parts = []; + + let root = node; + while ( root.type === 'MemberExpression' ) { + this.parts.unshift( root.property.name ); + root = root.object; + } + + this.parts.unshift( root.name ); + this.name = root.name; + } } export default class Statement { @@ -34,14 +59,9 @@ export default class Statement { this.next = null; // filled in later this.scope = new Scope(); - this.defines = blank(); - this.modifies = blank(); - this.dependsOn = blank(); - this.stronglyDependsOn = blank(); - this.reassigns = blank(); + this.references = []; - this.hasSideEffects = false; this.isIncluded = false; this.isImportDeclaration = node.type === 'ImportDeclaration'; @@ -52,255 +72,39 @@ export default class Statement { analyse () { if ( this.isImportDeclaration ) return; // nothing to analyse + // attach scopes + attachScopes( this ); + + let references = this.references; + + // find references 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; - - 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 ( node._scope ) scope = node._scope; - if ( newScope ) { - Object.defineProperty( node, '_scope', { - value: newScope, - configurable: true - }); + if ( isReference( node, parent ) ) { + const reference = new Reference( node, scope ); + references.push( reference ); - scope = newScope; + this.skip(); // don't descend from `foo.bar.baz` into `foo.bar` } }, - leave ( node ) { - if ( node._scope ) { - scope = scope.parent; - } + leave: ( node ) => { + if ( node._scope ) scope = scope.parent; } }); - - // 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 - 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 ); - - if ( !definingScope || definingScope.depth === 0 ) { - this.dependsOn[ node.name ] = true; - if ( strong ) this.stronglyDependsOn[ node.name ] = true; - } - } - } - - checkForWrites ( scope, node, writeDepth ) { - const addNode = ( node, isAssignment ) => { - let depth = 0; // determine whether we're illegally modifying a binding or namespace - - while ( node.type === 'MemberExpression' ) { - node = node.object; - depth += 1; - } - - // disallow assignments/updates to imported bindings and namespaces - if ( isAssignment ) { - const importSpecifier = this.module.imports[ node.name ]; - - if ( importSpecifier && !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 - - 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; - } - } - - // 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 - if ( this.module.exports.default && depth === 0 && this.module.exports.default.identifier === node.name ) { - // but only if this is a) inside a function body or - // b) after the export declaration - if ( !!scope.parent || node.start > this.module.exports.default.statement.node.start ) { - this.module.exports.default.isModified = true; - } - } - - // 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' ) { - this.modifies[ node.name ] = true; - } - }; - - if ( node.type === 'AssignmentExpression' ) { - addNode( node.left, true ); - } - - else if ( node.type === 'UpdateExpression' ) { - addNode( node.argument, true ); - } - - else if ( node.type === 'CallExpression' ) { - node.arguments.forEach( arg => addNode( arg, false ) ); - - // `foo.bar()` is assumed to mutate foo - if ( node.callee.type === 'MemberExpression' ) { - addNode( node.callee ); - } - } } mark () { if ( this.isIncluded ) return; // prevent infinite loops this.isIncluded = true; - // `export { name } from './other'` is a special case - if ( this.isReexportDeclaration ) { - const id = this.module.resolvedIds[ this.node.source.value ]; - const otherModule = this.module.bundle.moduleById[ id ]; - - if ( this.node.specifiers ) { - this.node.specifiers.forEach( specifier => { - const reexport = this.module.reexports[ specifier.exported.name ]; - - reexport.isUsed = true; - reexport.module = otherModule; // TODO still necessary? - - if ( !otherModule.isExternal ) otherModule.markExport( specifier.local.name, specifier.exported.name, this.module ); - }); - } else { - otherModule.needsAll = true; - - otherModule.getExports().forEach( name => { - if ( name !== 'default' ) otherModule.markExport( name, name, this.module ); - }); + this.references.forEach( reference => { + if ( reference.definingStatement ) { + reference.definingStatement.mark(); } - - return; - } - - Object.keys( this.dependsOn ).forEach( name => { - if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place? - this.module.mark( name ); }); } @@ -330,128 +134,7 @@ export default class Statement { }); } - replaceIdentifiers ( magicString, names, bundleExports ) { - const replacementStack = [ names ]; - 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 name = node.declarations[0].id.name; - if ( node.declarations.length === 1 && bundleExports[ name ] ) { - magicString.overwrite( node.start, node.declarations[0].id.end, bundleExports[ name ], true ); - node.declarations[0].id._skip = true; - } - - // otherwise, we insert the `exports.foo = foo` after the declaration - else { - const exportInitialisers = node.declarations - .map( declarator => declarator.id.name ) - .filter( name => !!bundleExports[ name ] ) - .map( name => `\n${bundleExports[name]} = ${name};` ) - .join( '' ); - - if ( exportInitialisers ) { - // TODO clean this up - try { - magicString.insert( node.end, exportInitialisers ); - } catch ( err ) { - magicString.append( exportInitialisers ); - } - } - } - } - } - - const scope = node._scope; - - if ( scope ) { - topLevel = false; - - let newNames = blank(); - let hasReplacements; - - 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 !== '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 ) { - replacementStack.pop(); - names = replacementStack[ replacementStack.length - 1 ]; - } - } - }); - + replaceIdentifiers ( magicString ) { return magicString; } diff --git a/src/ast/Scope.js b/src/ast/Scope.js index 7bd79bf..b6306b5 100644 --- a/src/ast/Scope.js +++ b/src/ast/Scope.js @@ -33,35 +33,38 @@ function extractNames ( param ) { return names; } +class Declaration { + constructor ( node ) { + this.references = []; + } +} + 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( param ); }); }); } } - 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( node ); }); } } @@ -71,6 +74,11 @@ export default class Scope { ( this.parent ? this.parent.contains( name ) : false ); } + findDeclaration ( name ) { + return this.declarations[ name ] || + ( this.parent && this.parent.findDeclaration( name ) ); + } + findDefiningScope ( name ) { if ( this.declarations[ name ] ) { return this; diff --git a/src/ast/attachScopes.js b/src/ast/attachScopes.js new file mode 100644 index 0000000..49716a4 --- /dev/null +++ b/src/ast/attachScopes.js @@ -0,0 +1,79 @@ +import walk from './walk'; +import Scope from './Scope'; + +const blockDeclarations = { + 'const': true, + 'let': true +}; + +export default function attachScopes ( statement ) { + let { node, scope } = statement; + + walk( 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; + + 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; + } + } + }); +} diff --git a/test/form/self-contained-bundle/_config.js b/test/form/self-contained-bundle/_config.js index 29f06f0..81ba2de 100644 --- a/test/form/self-contained-bundle/_config.js +++ b/test/form/self-contained-bundle/_config.js @@ -1,3 +1,5 @@ module.exports = { + solo: true, + show: true, description: 'self-contained bundle' }; diff --git a/test/test.js b/test/test.js index 520330a..f4e049a 100644 --- a/test/test.js +++ b/test/test.js @@ -15,11 +15,11 @@ var SOURCEMAPS = path.resolve( __dirname, 'sourcemaps' ); var CLI = path.resolve( __dirname, 'cli' ); var PROFILES = [ - { format: 'amd' }, + // { format: 'amd' }, { format: 'cjs' }, - { format: 'es6' }, - { format: 'iife' }, - { format: 'umd' } + // { format: 'es6' }, + // { format: 'iife' }, + // { format: 'umd' } ]; function extend ( target ) { @@ -228,6 +228,10 @@ describe( 'rollup', function () { expectedMap.sourcesContent = expectedMap.sourcesContent.map( normaliseOutput ); } catch ( err ) {} + if ( config.show || unintendedError ) { + console.log( actualCode + '\n\n\n' ); + } + assert.equal( actualCode, expectedCode ); assert.deepEqual( actualMap, expectedMap ); });