diff --git a/src/Bundle.js b/src/Bundle.js index 0fd7656..aac7790 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -290,13 +290,13 @@ export default class Bundle { // // This doesn't apply if the bundle is exported as ES6! let allBundleExports = blank(); - let isVarDeclaration = blank(); + let isReassignedVarDeclaration = blank(); let varExports = blank(); let getterExports = []; this.orderedModules.forEach( module => { - module.varDeclarations.forEach( name => { - isVarDeclaration[ module.replacements[ name ] || name ] = true; + module.reassignments.forEach( name => { + isReassignedVarDeclaration[ module.replacements[ name ] || name ] = true; }); }); @@ -306,7 +306,7 @@ export default class Bundle { .forEach( name => { const canonicalName = this.traceExport( this.entryModule, name ); - if ( isVarDeclaration[ canonicalName ] ) { + if ( isReassignedVarDeclaration[ canonicalName ] ) { varExports[ name ] = true; // if the same binding is exported multiple ways, we need to diff --git a/src/Module.js b/src/Module.js index 9964ae2..461c20b 100644 --- a/src/Module.js +++ b/src/Module.js @@ -64,7 +64,7 @@ export default class Module { this.replacements = blank(); - this.varDeclarations = []; + this.reassignments = []; this.definitions = blank(); this.definitionPromises = blank(); @@ -202,20 +202,35 @@ export default class Module { this.definitions[ name ] = statement; }); - statement.scope.varDeclarations.forEach( name => { - this.varDeclarations.push( name ); - }); - 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; diff --git a/src/Statement.js b/src/Statement.js index d4117e1..ad66b15 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -8,6 +8,14 @@ function isIife ( node, parent ) { return parent && parent.type === 'CallExpression' && node === parent.callee; } +function isFunctionDeclaration ( node, parent ) { + // `function foo () {}` + if ( node.type === 'FunctionDeclaration' ) return true; + + // `var foo = function () {}` - same thing for present purposes + if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true; +} + export default class Statement { constructor ( node, module, start, end ) { this.node = node; @@ -22,6 +30,8 @@ export default class Statement { this.dependsOn = blank(); this.stronglyDependsOn = blank(); + this.reassigns = blank(); + this.isIncluded = false; this.isImportDeclaration = node.type === 'ImportDeclaration'; @@ -117,22 +127,34 @@ export default class Statement { // calledImmediately(); // // ...but it's better than nothing - let depth = 0; + 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 ( node._scope ) { - if ( !scope.isBlockScope && !isIife( node, parent ) ) depth += 1; + if ( !scope.isBlockScope ) { + if ( !isIife( node, parent ) ) readDepth += 1; + if ( isFunctionDeclaration( node, parent ) ) writeDepth += 1; + } + scope = node._scope; } - this.checkForReads( scope, node, parent, !depth ); - this.checkForWrites( scope, node ); + this.checkForReads( scope, node, parent, !readDepth ); + this.checkForWrites( scope, node, writeDepth ); }, leave: ( node, parent ) => { if ( node._scope ) { - if ( !scope.isBlockScope && !isIife( node, parent ) ) depth -= 1; + if ( !scope.isBlockScope ) { + if ( !isIife( node, parent ) ) readDepth -= 1; + if ( isFunctionDeclaration( node, parent ) ) writeDepth -= 1; + } + scope = scope.parent; } } @@ -171,7 +193,7 @@ export default class Statement { } } - checkForWrites ( scope, node ) { + checkForWrites ( scope, node, writeDepth ) { const addNode = ( node, isAssignment ) => { let depth = 0; // determine whether we're illegally modifying a binding or namespace @@ -207,9 +229,26 @@ export default class Statement { 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; + } } - if ( node.type === 'Identifier' ) { + // 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; } }; diff --git a/test/form/exclude-unnecessary-modifications/_config.js b/test/form/exclude-unnecessary-modifications/_config.js new file mode 100644 index 0000000..2b3de2b --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'statements that modify definitions within unused functions are excluded' +}; diff --git a/test/form/exclude-unnecessary-modifications/_expected/amd.js b/test/form/exclude-unnecessary-modifications/_expected/amd.js new file mode 100644 index 0000000..84befa4 --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/_expected/amd.js @@ -0,0 +1,28 @@ +define(function () { 'use strict'; + + var foo = {}; + + mutate1( foo ); + + // should be included + [ 'a', 'b', 'c' ].forEach( function ( letter, i ) { + foo[ letter ] = i; + }); + + [ 'd', 'e', 'f' ].forEach( ( letter, i ) => { + foo[ letter ] = i; + }); + + function mutate1 () { + foo.mutated = 1; + } + + ({ + mutate2: function () { + foo.mutated = 2; + } + }).mutate2(); + + console.log( foo ); + +}); diff --git a/test/form/exclude-unnecessary-modifications/_expected/cjs.js b/test/form/exclude-unnecessary-modifications/_expected/cjs.js new file mode 100644 index 0000000..596c910 --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/_expected/cjs.js @@ -0,0 +1,26 @@ +'use strict'; + +var foo = {}; + +mutate1( foo ); + +// should be included +[ 'a', 'b', 'c' ].forEach( function ( letter, i ) { + foo[ letter ] = i; +}); + +[ 'd', 'e', 'f' ].forEach( ( letter, i ) => { + foo[ letter ] = i; +}); + +function mutate1 () { + foo.mutated = 1; +} + +({ + mutate2: function () { + foo.mutated = 2; + } +}).mutate2(); + +console.log( foo ); diff --git a/test/form/exclude-unnecessary-modifications/_expected/es6.js b/test/form/exclude-unnecessary-modifications/_expected/es6.js new file mode 100644 index 0000000..a88d21e --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/_expected/es6.js @@ -0,0 +1,24 @@ +var foo = {}; + +mutate1( foo ); + +// should be included +[ 'a', 'b', 'c' ].forEach( function ( letter, i ) { + foo[ letter ] = i; +}); + +[ 'd', 'e', 'f' ].forEach( ( letter, i ) => { + foo[ letter ] = i; +}); + +function mutate1 () { + foo.mutated = 1; +} + +({ + mutate2: function () { + foo.mutated = 2; + } +}).mutate2(); + +console.log( foo ); diff --git a/test/form/exclude-unnecessary-modifications/_expected/iife.js b/test/form/exclude-unnecessary-modifications/_expected/iife.js new file mode 100644 index 0000000..0fd4c40 --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/_expected/iife.js @@ -0,0 +1,28 @@ +(function () { 'use strict'; + + var foo = {}; + + mutate1( foo ); + + // should be included + [ 'a', 'b', 'c' ].forEach( function ( letter, i ) { + foo[ letter ] = i; + }); + + [ 'd', 'e', 'f' ].forEach( ( letter, i ) => { + foo[ letter ] = i; + }); + + function mutate1 () { + foo.mutated = 1; + } + + ({ + mutate2: function () { + foo.mutated = 2; + } + }).mutate2(); + + console.log( foo ); + +})(); diff --git a/test/form/exclude-unnecessary-modifications/_expected/umd.js b/test/form/exclude-unnecessary-modifications/_expected/umd.js new file mode 100644 index 0000000..1c2bb78 --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/_expected/umd.js @@ -0,0 +1,32 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + factory(); +}(this, function () { 'use strict'; + + var foo = {}; + + mutate1( foo ); + + // should be included + [ 'a', 'b', 'c' ].forEach( function ( letter, i ) { + foo[ letter ] = i; + }); + + [ 'd', 'e', 'f' ].forEach( ( letter, i ) => { + foo[ letter ] = i; + }); + + function mutate1 () { + foo.mutated = 1; + } + + ({ + mutate2: function () { + foo.mutated = 2; + } + }).mutate2(); + + console.log( foo ); + +})); diff --git a/test/form/exclude-unnecessary-modifications/foo.js b/test/form/exclude-unnecessary-modifications/foo.js new file mode 100644 index 0000000..2280631 --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/foo.js @@ -0,0 +1,37 @@ +var foo = {}; + +mutate1( foo ); + +// should be included +[ 'a', 'b', 'c' ].forEach( function ( letter, i ) { + foo[ letter ] = i; +}); + +[ 'd', 'e', 'f' ].forEach( ( letter, i ) => { + foo[ letter ] = i; +}); + +function mutate1 () { + foo.mutated = 1; +} + +({ + mutate2: function () { + foo.mutated = 2; + } +}).mutate2(); + +// should be excluded +var mutate2 = function () { + foo.mutated = 2; +} + +function unused1 () { + foo.wat = 'nope'; +} + +function unused2 () { + mutate1( foo ); +} + +export default foo; diff --git a/test/form/exclude-unnecessary-modifications/main.js b/test/form/exclude-unnecessary-modifications/main.js new file mode 100644 index 0000000..03d6edf --- /dev/null +++ b/test/form/exclude-unnecessary-modifications/main.js @@ -0,0 +1,2 @@ +import foo from './foo'; +console.log( foo ); diff --git a/test/form/exported-empty-vars/_config.js b/test/form/exported-empty-vars/_config.js deleted file mode 100644 index fa0287c..0000000 --- a/test/form/exported-empty-vars/_config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - description: 'removes empty var declarations that are exported', - options: { - moduleName: 'myBundle' - } -}; diff --git a/test/form/exported-empty-vars/_expected/amd.js b/test/form/exported-empty-vars/_expected/amd.js deleted file mode 100644 index c0f1472..0000000 --- a/test/form/exported-empty-vars/_expected/amd.js +++ /dev/null @@ -1,8 +0,0 @@ -define(['exports'], function (exports) { 'use strict'; - - exports.foo = 42; - - exports.bar = 43; - exports.baz = 44; - -}); diff --git a/test/form/exported-empty-vars/_expected/cjs.js b/test/form/exported-empty-vars/_expected/cjs.js deleted file mode 100644 index 4011bd7..0000000 --- a/test/form/exported-empty-vars/_expected/cjs.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -exports.foo = 42; - -exports.bar = 43; -exports.baz = 44; diff --git a/test/form/exported-empty-vars/_expected/es6.js b/test/form/exported-empty-vars/_expected/es6.js deleted file mode 100644 index 4b76753..0000000 --- a/test/form/exported-empty-vars/_expected/es6.js +++ /dev/null @@ -1,9 +0,0 @@ -var foo; -foo = 42; - -var bar; -var baz; -bar = 43; -baz = 44; - -export { foo, bar, baz }; diff --git a/test/form/exported-empty-vars/_expected/iife.js b/test/form/exported-empty-vars/_expected/iife.js deleted file mode 100644 index 63c56b2..0000000 --- a/test/form/exported-empty-vars/_expected/iife.js +++ /dev/null @@ -1,8 +0,0 @@ -(function (exports) { 'use strict'; - - exports.foo = 42; - - exports.bar = 43; - exports.baz = 44; - -})((this.myBundle = {})); diff --git a/test/form/exported-empty-vars/bar.js b/test/form/exported-empty-vars/bar.js deleted file mode 100644 index b8818da..0000000 --- a/test/form/exported-empty-vars/bar.js +++ /dev/null @@ -1,6 +0,0 @@ -var bar, baz; - -bar = 43; -baz = 44; - -export { bar, baz }; diff --git a/test/form/exported-empty-vars/foo.js b/test/form/exported-empty-vars/foo.js deleted file mode 100644 index 2c71d30..0000000 --- a/test/form/exported-empty-vars/foo.js +++ /dev/null @@ -1,2 +0,0 @@ -export var foo; -foo = 42; diff --git a/test/form/exported-empty-vars/main.js b/test/form/exported-empty-vars/main.js deleted file mode 100644 index 21ee721..0000000 --- a/test/form/exported-empty-vars/main.js +++ /dev/null @@ -1,2 +0,0 @@ -export { foo } from './foo'; -export { bar, baz } from './bar'; diff --git a/test/form/exports-at-end-if-possible/_config.js b/test/form/exports-at-end-if-possible/_config.js new file mode 100644 index 0000000..a898bc7 --- /dev/null +++ b/test/form/exports-at-end-if-possible/_config.js @@ -0,0 +1,7 @@ +module.exports = { + description: 'exports variables at end, if possible', + options: { + moduleName: 'myBundle' + }, + // solo: true +}; diff --git a/test/form/exports-at-end-if-possible/_expected/amd.js b/test/form/exports-at-end-if-possible/_expected/amd.js new file mode 100644 index 0000000..2612901 --- /dev/null +++ b/test/form/exports-at-end-if-possible/_expected/amd.js @@ -0,0 +1,11 @@ +define(['exports'], function (exports) { 'use strict'; + + var FOO = 'foo'; + + console.log( FOO ); + console.log( FOO ); + console.log( FOO ); + + exports.FOO = FOO; + +}); diff --git a/test/form/exports-at-end-if-possible/_expected/cjs.js b/test/form/exports-at-end-if-possible/_expected/cjs.js new file mode 100644 index 0000000..57d55d3 --- /dev/null +++ b/test/form/exports-at-end-if-possible/_expected/cjs.js @@ -0,0 +1,9 @@ +'use strict'; + +var FOO = 'foo'; + +console.log( FOO ); +console.log( FOO ); +console.log( FOO ); + +exports.FOO = FOO; diff --git a/test/form/exports-at-end-if-possible/_expected/es6.js b/test/form/exports-at-end-if-possible/_expected/es6.js new file mode 100644 index 0000000..b42b4e4 --- /dev/null +++ b/test/form/exports-at-end-if-possible/_expected/es6.js @@ -0,0 +1,7 @@ +var FOO = 'foo'; + +console.log( FOO ); +console.log( FOO ); +console.log( FOO ); + +export { FOO }; diff --git a/test/form/exports-at-end-if-possible/_expected/iife.js b/test/form/exports-at-end-if-possible/_expected/iife.js new file mode 100644 index 0000000..d1b29c8 --- /dev/null +++ b/test/form/exports-at-end-if-possible/_expected/iife.js @@ -0,0 +1,11 @@ +(function (exports) { 'use strict'; + + var FOO = 'foo'; + + console.log( FOO ); + console.log( FOO ); + console.log( FOO ); + + exports.FOO = FOO; + +})((this.myBundle = {})); diff --git a/test/form/exported-empty-vars/_expected/umd.js b/test/form/exports-at-end-if-possible/_expected/umd.js similarity index 72% rename from test/form/exported-empty-vars/_expected/umd.js rename to test/form/exports-at-end-if-possible/_expected/umd.js index 9cde4bd..f30f365 100644 --- a/test/form/exported-empty-vars/_expected/umd.js +++ b/test/form/exports-at-end-if-possible/_expected/umd.js @@ -4,9 +4,12 @@ factory((global.myBundle = {})); }(this, function (exports) { 'use strict'; - exports.foo = 42; + var FOO = 'foo'; - exports.bar = 43; - exports.baz = 44; + console.log( FOO ); + console.log( FOO ); + console.log( FOO ); + + exports.FOO = FOO; })); diff --git a/test/form/exports-at-end-if-possible/main.js b/test/form/exports-at-end-if-possible/main.js new file mode 100644 index 0000000..ee6a75a --- /dev/null +++ b/test/form/exports-at-end-if-possible/main.js @@ -0,0 +1,5 @@ +export var FOO = 'foo'; + +console.log( FOO ); +console.log( FOO ); +console.log( FOO ); diff --git a/test/form/multiple-exports/_expected/amd.js b/test/form/multiple-exports/_expected/amd.js index 5460f4f..2ebff3b 100644 --- a/test/form/multiple-exports/_expected/amd.js +++ b/test/form/multiple-exports/_expected/amd.js @@ -1,6 +1,9 @@ define(['exports'], function (exports) { 'use strict'; - exports.foo = 1; - exports.bar = 2; + var foo = 1; + var bar = 2; + + exports.foo = foo; + exports.bar = bar; }); diff --git a/test/form/multiple-exports/_expected/cjs.js b/test/form/multiple-exports/_expected/cjs.js index 0bbfe60..1968b8b 100644 --- a/test/form/multiple-exports/_expected/cjs.js +++ b/test/form/multiple-exports/_expected/cjs.js @@ -1,4 +1,7 @@ 'use strict'; -exports.foo = 1; -exports.bar = 2; +var foo = 1; +var bar = 2; + +exports.foo = foo; +exports.bar = bar; diff --git a/test/form/multiple-exports/_expected/iife.js b/test/form/multiple-exports/_expected/iife.js index 09461cb..8ed8290 100644 --- a/test/form/multiple-exports/_expected/iife.js +++ b/test/form/multiple-exports/_expected/iife.js @@ -1,6 +1,9 @@ (function (exports) { 'use strict'; - exports.foo = 1; - exports.bar = 2; + var foo = 1; + var bar = 2; + + exports.foo = foo; + exports.bar = bar; })((this.myBundle = {})); diff --git a/test/form/multiple-exports/_expected/umd.js b/test/form/multiple-exports/_expected/umd.js index 2fd0eb7..687b75c 100644 --- a/test/form/multiple-exports/_expected/umd.js +++ b/test/form/multiple-exports/_expected/umd.js @@ -4,7 +4,10 @@ factory((global.myBundle = {})); }(this, function (exports) { 'use strict'; - exports.foo = 1; - exports.bar = 2; + var foo = 1; + var bar = 2; + + exports.foo = foo; + exports.bar = bar; })); diff --git a/test/form/preserves-comments-after-imports/_expected/amd.js b/test/form/preserves-comments-after-imports/_expected/amd.js index fd4591b..82a0a52 100644 --- a/test/form/preserves-comments-after-imports/_expected/amd.js +++ b/test/form/preserves-comments-after-imports/_expected/amd.js @@ -4,6 +4,8 @@ define(['exports'], function (exports) { 'use strict'; var number = 5; /** A comment for obj */ - exports.obj = { number }; + var obj = { number }; + + exports.obj = obj; }); diff --git a/test/form/preserves-comments-after-imports/_expected/cjs.js b/test/form/preserves-comments-after-imports/_expected/cjs.js index 86e64c1..8b15cd1 100644 --- a/test/form/preserves-comments-after-imports/_expected/cjs.js +++ b/test/form/preserves-comments-after-imports/_expected/cjs.js @@ -4,4 +4,6 @@ var number = 5; /** A comment for obj */ -exports.obj = { number }; +var obj = { number }; + +exports.obj = obj; diff --git a/test/form/preserves-comments-after-imports/_expected/iife.js b/test/form/preserves-comments-after-imports/_expected/iife.js index 02f5255..9da8252 100644 --- a/test/form/preserves-comments-after-imports/_expected/iife.js +++ b/test/form/preserves-comments-after-imports/_expected/iife.js @@ -4,6 +4,8 @@ var number = 5; /** A comment for obj */ - exports.obj = { number }; + var obj = { number }; + + exports.obj = obj; })((this.myBundle = {})); diff --git a/test/form/preserves-comments-after-imports/_expected/umd.js b/test/form/preserves-comments-after-imports/_expected/umd.js index 6993070..96fec25 100644 --- a/test/form/preserves-comments-after-imports/_expected/umd.js +++ b/test/form/preserves-comments-after-imports/_expected/umd.js @@ -8,6 +8,8 @@ var number = 5; /** A comment for obj */ - exports.obj = { number }; + var obj = { number }; + + exports.obj = obj; })); diff --git a/test/function/assignment-to-exports/_config.js b/test/function/assignment-to-exports/_config.js index c958adb..fac0cb3 100644 --- a/test/function/assignment-to-exports/_config.js +++ b/test/function/assignment-to-exports/_config.js @@ -6,5 +6,6 @@ module.exports = { assert.equal( exports.count, 0 ); exports.incr(); assert.equal( exports.count, 1 ); - } + }, + // solo: true };