Browse Source

Merge pull request #902 from rollup/rewrite

[WIP] Rewrite
gh-953
Rich Harris 8 years ago
committed by GitHub
parent
commit
0c73791ae6
  1. 11
      .eslintrc
  2. 1
      .travis.yml
  3. 1
      appveyor.yml
  4. 10
      package.json
  5. 1
      rollup.config.browser.js
  6. 9
      rollup.config.js
  7. 71
      src/Bundle.js
  8. 242
      src/Declaration.js
  9. 496
      src/Module.js
  10. 30
      src/Reference.js
  11. 160
      src/Statement.js
  12. 92
      src/ast/Node.js
  13. 52
      src/ast/Scope.js
  14. 78
      src/ast/attachScopes.js
  15. 38
      src/ast/conditions.js
  16. 7
      src/ast/create.js
  17. 63
      src/ast/enhance.js
  18. 6
      src/ast/isFunctionDeclaration.js
  19. 4
      src/ast/keys.js
  20. 19
      src/ast/modifierNodes.js
  21. 8
      src/ast/nodes/ArrayExpression.js
  22. 35
      src/ast/nodes/ArrowFunctionExpression.js
  23. 45
      src/ast/nodes/AssignmentExpression.js
  24. 38
      src/ast/nodes/BinaryExpression.js
  25. 71
      src/ast/nodes/BlockStatement.js
  26. 40
      src/ast/nodes/CallExpression.js
  27. 40
      src/ast/nodes/ClassDeclaration.js
  28. 26
      src/ast/nodes/ClassExpression.js
  29. 65
      src/ast/nodes/ConditionalExpression.js
  30. 11
      src/ast/nodes/ExportAllDeclaration.js
  31. 96
      src/ast/nodes/ExportDefaultDeclaration.js
  32. 25
      src/ast/nodes/ExportNamedDeclaration.js
  33. 16
      src/ast/nodes/ExpressionStatement.js
  34. 53
      src/ast/nodes/FunctionDeclaration.js
  35. 21
      src/ast/nodes/FunctionExpression.js
  36. 35
      src/ast/nodes/Identifier.js
  37. 55
      src/ast/nodes/IfStatement.js
  38. 16
      src/ast/nodes/ImportDeclaration.js
  39. 17
      src/ast/nodes/Literal.js
  40. 74
      src/ast/nodes/MemberExpression.js
  41. 8
      src/ast/nodes/NewExpression.js
  42. 8
      src/ast/nodes/ObjectExpression.js
  43. 11
      src/ast/nodes/ParenthesizedExpression.js
  44. 7
      src/ast/nodes/ReturnStatement.js
  45. 7
      src/ast/nodes/TemplateLiteral.js
  46. 20
      src/ast/nodes/ThisExpression.js
  47. 34
      src/ast/nodes/UnaryExpression.js
  48. 38
      src/ast/nodes/UpdateExpression.js
  49. 100
      src/ast/nodes/VariableDeclaration.js
  50. 91
      src/ast/nodes/VariableDeclarator.js
  51. 63
      src/ast/nodes/index.js
  52. 67
      src/ast/nodes/shared/callHasEffects.js
  53. 28
      src/ast/nodes/shared/disallowIllegalReassignment.js
  54. 40
      src/ast/nodes/shared/isUsedByBundle.js
  55. 0
      src/ast/nodes/shared/pureFunctions.js
  56. 40
      src/ast/scopes/BundleScope.js
  57. 47
      src/ast/scopes/ModuleScope.js
  58. 98
      src/ast/scopes/Scope.js
  59. 0
      src/ast/utils/extractNames.js
  60. 0
      src/ast/utils/flatten.js
  61. 0
      src/ast/utils/isReference.js
  62. 8
      src/ast/values.js
  63. 8
      src/finalisers/es.js
  64. 6
      src/finalisers/shared/getExportBlock.js
  65. 21
      src/utils/object.js
  66. 2
      src/utils/path.js
  67. 119
      src/utils/run.js
  68. 2
      test/form/assignment-to-exports-class-declaration/_config.js
  69. 3
      test/form/duplicated-var-declarations/_config.js
  70. 17
      test/form/duplicated-var-declarations/_expected/amd.js
  71. 15
      test/form/duplicated-var-declarations/_expected/cjs.js
  72. 13
      test/form/duplicated-var-declarations/_expected/es.js
  73. 18
      test/form/duplicated-var-declarations/_expected/iife.js
  74. 21
      test/form/duplicated-var-declarations/_expected/umd.js
  75. 10
      test/form/duplicated-var-declarations/main.js
  76. 4
      test/form/external-imports/_expected/es.js
  77. 5
      test/form/import-external-namespace-and-default/_expected/es.js
  78. 3
      test/form/includes-all-namespace-declarations/_config.js
  79. 5
      test/form/includes-all-namespace-declarations/_expected/amd.js
  80. 2
      test/form/includes-all-namespace-declarations/_expected/cjs.js
  81. 0
      test/form/includes-all-namespace-declarations/_expected/es.js
  82. 6
      test/form/includes-all-namespace-declarations/_expected/iife.js
  83. 9
      test/form/includes-all-namespace-declarations/_expected/umd.js
  84. 7
      test/form/includes-all-namespace-declarations/indirection.js
  85. 1
      test/form/includes-all-namespace-declarations/main.js
  86. 3
      test/form/includes-all-namespace-declarations/unused.js
  87. 2
      test/form/namespace-optimization/_expected/amd.js
  88. 2
      test/form/namespace-optimization/_expected/cjs.js
  89. 2
      test/form/namespace-optimization/_expected/es.js
  90. 2
      test/form/namespace-optimization/_expected/iife.js
  91. 4
      test/form/namespace-optimization/_expected/umd.js
  92. 2
      test/form/namespace-optimization/main.js
  93. 3
      test/form/no-treeshake/_expected/es.js
  94. 2
      test/form/removes-existing-sourcemap-comments/_expected/amd.js
  95. 2
      test/form/removes-existing-sourcemap-comments/_expected/cjs.js
  96. 2
      test/form/removes-existing-sourcemap-comments/_expected/es.js
  97. 2
      test/form/removes-existing-sourcemap-comments/_expected/iife.js
  98. 4
      test/form/removes-existing-sourcemap-comments/_expected/umd.js
  99. 3
      test/form/self-calling-function-with-effects/_config.js
  100. 20
      test/form/self-calling-function-with-effects/_expected/amd.js

11
.eslintrc

@ -27,9 +27,18 @@
"browser": true,
"node": true
},
"extends": "eslint:recommended",
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"settings": {
"import/ignore": [ 0, [
"\\.path.js$"
] ]
}
}

1
.travis.yml

@ -1,7 +1,6 @@
sudo: false
language: node_js
node_js:
- "0.12"
- "4"
- "6"
env:

1
appveyor.yml

@ -10,7 +10,6 @@ init:
environment:
matrix:
# node.js
- nodejs_version: 0.12
- nodejs_version: 4
- nodejs_version: 6

10
package.json

@ -11,14 +11,17 @@
"scripts": {
"pretest": "npm run build && npm run build:cli",
"test": "mocha",
"test:quick": "rollup -c && mocha",
"pretest-coverage": "npm run build",
"test-coverage": "rm -rf coverage/* && istanbul cover --report json node_modules/.bin/_mocha -- -u exports -R spec test/test.js",
"posttest-coverage": "remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped.json -b dist && remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped.lcov -t lcovonly -b dist && remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped -t html -b dist",
"ci": "npm run test-coverage && codecov < coverage/coverage-remapped.lcov",
"build": "git rev-parse HEAD > .commithash && rollup -c",
"watch": "rollup -c -w",
"build:cli": "rollup -c rollup.config.cli.js",
"build:browser": "git rev-parse HEAD > .commithash && rollup -c rollup.config.browser.js -o dist/rollup.browser.js",
"build:browser": "git rev-parse HEAD > .commithash && rollup -c rollup.config.browser.js",
"watch": "rollup -c -w",
"watch:browser": "rollup -c rollup.config.browser.js -w",
"watch:cli": "rollup -c rollup.config.cli.js -w",
"prepublish": "npm run lint && npm test && npm run build:browser",
"lint": "eslint src browser test/test.js test/utils test/**/_config.js"
},
@ -48,8 +51,9 @@
"buble": "^0.12.5",
"chalk": "^1.1.3",
"codecov.io": "^0.1.6",
"console-group": "^0.2.1",
"console-group": "^0.3.1",
"eslint": "^2.13.0",
"eslint-plugin-import": "^1.14.0",
"estree-walker": "^0.2.1",
"istanbul": "^0.4.3",
"magic-string": "^0.15.2",

1
rollup.config.browser.js

@ -9,5 +9,6 @@ config.plugins.push({
});
config.format = 'umd';
config.dest = 'dist/rollup.browser.js';
export default config;

9
rollup.config.js

@ -22,7 +22,14 @@ export default {
entry: 'src/rollup.js',
plugins: [
buble({
include: [ 'src/**', 'node_modules/acorn/**' ]
include: [ 'node_modules/acorn/**' ]
}),
buble({
include: [ 'src/**' ],
target: {
node: 4
}
}),
nodeResolve({

71
src/Bundle.js

@ -17,6 +17,7 @@ import collapseSourcemaps from './utils/collapseSourcemaps.js';
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
import callIfFunction from './utils/callIfFunction.js';
import { dirname, isRelative, isAbsolute, normalize, relative, resolve } from './utils/path.js';
import BundleScope from './ast/scopes/BundleScope.js';
export default class Bundle {
constructor ( options ) {
@ -59,14 +60,17 @@ export default class Bundle {
( id => options.paths.hasOwnProperty( id ) ? options.paths[ id ] : this.getPathRelativeToEntryDirname( id ) ) :
id => this.getPathRelativeToEntryDirname( id );
this.scope = new BundleScope();
// TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
[ 'module', 'exports', '_interopDefault' ].forEach( name => {
this.scope.findDeclaration( name ); // creates global declaration as side-effect
});
this.moduleById = new Map();
this.modules = [];
this.externalModules = [];
this.internalNamespaces = [];
this.context = String( options.context );
this.assumedGlobals = blank();
if ( typeof options.external === 'function' ) {
this.isExternal = options.external;
@ -77,11 +81,10 @@ export default class Bundle {
this.onwarn = options.onwarn || makeOnwarn();
// TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
[ 'module', 'exports', '_interopDefault' ].forEach( global => this.assumedGlobals[ global ] = true );
this.varOrConst = options.preferConst ? 'const' : 'var';
this.acornOptions = options.acorn || {};
this.dependentExpressions = [];
}
build () {
@ -100,7 +103,6 @@ export default class Bundle {
// Phase 2 – binding. We link references to their declarations
// to generate a complete picture of the bundle
this.modules.forEach( module => module.bindImportSpecifiers() );
this.modules.forEach( module => module.bindAliases() );
this.modules.forEach( module => module.bindReferences() );
// Phase 3 – marking. We 'run' each statement to see which ones
@ -109,19 +111,39 @@ export default class Bundle {
// mark all export statements
entryModule.getExports().forEach( name => {
const declaration = entryModule.traceExport( name );
declaration.exportName = name;
declaration.activate();
declaration.use();
if ( declaration.isNamespace ) {
declaration.needsNamespaceBlock = true;
}
});
// mark statements that should appear in the bundle
let settled = false;
while ( !settled ) {
settled = true;
if ( this.treeshake ) {
this.modules.forEach( module => {
if ( module.run( this.treeshake ) ) settled = false;
module.run();
});
let settled = false;
while ( !settled ) {
settled = true;
let i = this.dependentExpressions.length;
while ( i-- ) {
const expression = this.dependentExpressions[i];
const statement = expression.findParent( /ExpressionStatement/ );
if ( !statement || statement.ran ) {
this.dependentExpressions.splice( i, 1 );
} else if ( expression.isUsedByBundle() ) {
settled = false;
statement.run( statement.findScope() );
this.dependentExpressions.splice( i, 1 );
}
}
}
}
// Phase 4 – final preparation. We order the modules with an
@ -136,7 +158,7 @@ export default class Bundle {
const used = blank();
// ensure no conflicts with globals
keys( this.assumedGlobals ).forEach( name => used[ name ] = 1 );
keys( this.scope.declarations ).forEach( name => used[ name ] = 1 );
function getSafeName ( name ) {
while ( used[ name ] ) {
@ -147,27 +169,33 @@ export default class Bundle {
return name;
}
const toDeshadow = new Map();
this.externalModules.forEach( module => {
module.name = getSafeName( module.name );
const safeName = getSafeName( module.name );
toDeshadow.set( safeName, true );
module.name = safeName;
// ensure we don't shadow named external imports, if
// we're creating an ES6 bundle
forOwn( module.declarations, ( declaration, name ) => {
declaration.setSafeName( getSafeName( name ) );
const safeName = getSafeName( name );
toDeshadow.set( safeName, true );
declaration.setSafeName( safeName );
});
});
this.modules.forEach( module => {
forOwn( module.declarations, ( declaration, originalName ) => {
if ( declaration.isGlobal ) return;
if ( originalName === 'default' ) {
if ( declaration.original && !declaration.original.isReassigned ) return;
forOwn( module.scope.declarations, ( declaration ) => {
if ( declaration.isDefault && declaration.declaration.id ) {
return;
}
declaration.name = getSafeName( declaration.name );
});
});
this.scope.deshadow( toDeshadow );
}
fetchModule ( id, importer ) {
@ -306,6 +334,7 @@ export default class Bundle {
this.orderedModules.forEach( module => {
const source = module.render( format === 'es' );
if ( source.toString().length ) {
magicString.addSource( source );
usedModules.push( module );

242
src/Declaration.js

@ -1,35 +1,24 @@
import { blank, forOwn, keys } from './utils/object.js';
import makeLegalIdentifier from './utils/makeLegalIdentifier.js';
import run from './utils/run.js';
import { SyntheticReference } from './Reference.js';
const use = alias => alias.use();
import { UNKNOWN } from './ast/values.js';
export default class Declaration {
constructor ( node, isParam, statement ) {
if ( node ) {
if ( node.type === 'FunctionDeclaration' ) {
this.isFunctionDeclaration = true;
this.functionNode = node;
} else if ( node.type === 'VariableDeclarator' && node.init && /FunctionExpression/.test( node.init.type ) ) {
this.isFunctionDeclaration = true;
this.functionNode = node.init;
}
}
constructor ( node, isParam ) {
this.node = node;
this.statement = statement;
this.name = node.id ? node.id.name : node.name;
this.exportName = null;
this.isParam = isParam;
this.isReassigned = false;
this.aliases = [];
this.isUsed = false;
}
addAlias ( declaration ) {
this.aliases.push( declaration );
activate () {
if ( this.activated ) return;
this.activated = true;
if ( this.isParam ) return;
this.node.activate();
}
addReference ( reference ) {
@ -48,142 +37,15 @@ export default class Declaration {
return `exports.${this.exportName}`;
}
run ( strongDependencies ) {
if ( this.tested ) return this.hasSideEffects;
if ( !this.functionNode ) {
this.hasSideEffects = true; // err on the side of caution. TODO handle unambiguous `var x; x = y => z` cases
} else {
if ( this.running ) return true; // short-circuit infinite loop
this.running = true;
this.hasSideEffects = run( this.functionNode.body, this.functionNode._scope, this.statement, strongDependencies, false );
this.running = false;
}
this.tested = true;
return this.hasSideEffects;
}
use () {
if ( this.isUsed ) return;
this.isUsed = true;
if ( this.statement ) this.statement.mark();
this.aliases.forEach( use );
}
}
export class SyntheticDefaultDeclaration {
constructor ( node, statement, name ) {
this.node = node;
this.statement = statement;
this.name = name;
this.original = null;
this.exportName = null;
this.aliases = [];
}
addAlias ( declaration ) {
this.aliases.push( declaration );
}
addReference ( reference ) {
// Bind the reference to `this` declaration.
reference.declaration = this;
// Don't change the name to `default`; it's not a valid identifier name.
if ( reference.name === 'default' ) return;
this.name = reference.name;
}
bind ( declaration ) {
this.original = declaration;
}
render () {
return !this.original || this.original.isReassigned ?
this.name :
this.original.render();
}
run ( strongDependencies ) {
if ( this.original ) {
return this.original.run( strongDependencies );
}
let declaration = this.node.declaration;
while ( declaration.type === 'ParenthesizedExpression' ) declaration = declaration.expression;
if ( /FunctionExpression/.test( declaration.type ) ) {
return run( declaration.body, this.statement.scope, this.statement, strongDependencies, false );
}
// otherwise assume the worst
return true;
}
use () {
this.isUsed = true;
this.statement.mark();
if ( this.original ) this.original.use();
this.aliases.forEach( use );
}
}
export class SyntheticGlobalDeclaration {
constructor ( name ) {
this.name = name;
this.isExternal = true;
this.isGlobal = true;
this.isReassigned = false;
this.aliases = [];
this.isUsed = false;
}
addAlias ( declaration ) {
this.aliases.push( declaration );
}
addReference ( reference ) {
reference.declaration = this;
if ( reference.isReassignment ) this.isReassigned = true;
}
render () {
return this.name;
}
run () {
return true;
}
use () {
if ( this.isUsed ) return;
this.isUsed = true;
this.aliases.forEach( use );
}
}
export class SyntheticNamespaceDeclaration {
constructor ( module ) {
this.isNamespace = true;
this.module = module;
this.name = null;
this.name = module.basename();
this.needsNamespaceBlock = false;
this.aliases = [];
this.originals = blank();
module.getExports().forEach( name => {
@ -191,70 +53,40 @@ export class SyntheticNamespaceDeclaration {
});
}
addAlias ( declaration ) {
this.aliases.push( declaration );
}
activate () {
this.needsNamespaceBlock = true;
addReference ( reference ) {
// if we have e.g. `foo.bar`, we can optimise
// the reference by pointing directly to `bar`
if ( reference.parts.length ) {
const ref = reference.parts.shift();
reference.name = ref.name;
reference.end = ref.end;
const original = this.originals[ reference.name ];
// throw with an informative error message if the reference doesn't exist.
if ( !original ) {
this.module.bundle.onwarn( `Export '${reference.name}' is not defined by '${this.module.id}'` );
reference.isUndefined = true;
return;
}
// add synthetic references, in case of chained
// namespace imports
forOwn( this.originals, original => {
original.activate();
});
}
original.addReference( reference );
return;
}
addReference ( node ) {
this.name = node.name;
}
// 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 );
// add synthetic references, in case of chained
// namespace imports
forOwn( this.originals, ( original, name ) => {
original.addReference( new SyntheticReference( name ) );
});
}
gatherPossibleValues ( values ) {
values.add( UNKNOWN );
}
reference.declaration = this;
this.name = reference.name;
getName () {
return this.name;
}
renderBlock ( indentString ) {
renderBlock ( es, indentString ) {
const members = keys( this.originals ).map( name => {
const original = this.originals[ name ];
if ( original.isReassigned ) {
return `${indentString}get ${name} () { return ${original.render()}; }`;
return `${indentString}get ${name} () { return ${original.getName( es )}; }`;
}
return `${indentString}${name}: ${original.render()}`;
return `${indentString}${name}: ${original.getName( es )}`;
});
return `${this.module.bundle.varOrConst} ${this.render()} = Object.freeze({\n${members.join( ',\n' )}\n});\n\n`;
}
render () {
return this.name;
}
use () {
forOwn( this.originals, use );
this.aliases.forEach( use );
return `${this.module.bundle.varOrConst} ${this.getName( es )} = Object.freeze({\n${members.join( ',\n' )}\n});\n\n`;
}
}
@ -265,10 +97,12 @@ export class ExternalDeclaration {
this.safeName = null;
this.isExternal = true;
this.activated = true;
this.isNamespace = name === '*';
}
addAlias () {
activate () {
// noop
}
@ -280,7 +114,7 @@ export class ExternalDeclaration {
}
}
render ( es ) {
getName ( es ) {
if ( this.name === '*' ) {
return this.module.name;
}
@ -294,15 +128,7 @@ export class ExternalDeclaration {
return es ? this.safeName : `${this.module.name}.${this.name}`;
}
run () {
return true;
}
setSafeName ( name ) {
this.safeName = name;
}
use () {
// noop?
}
}

496
src/Module.js

@ -1,20 +1,30 @@
import { parse } from 'acorn/src/index.js';
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import Statement from './Statement.js';
import { assign, blank, keys } from './utils/object.js';
import { assign, blank, deepClone, keys } from './utils/object.js';
import { basename, extname } from './utils/path.js';
import getLocation from './utils/getLocation.js';
import makeLegalIdentifier from './utils/makeLegalIdentifier.js';
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
import {
SyntheticDefaultDeclaration,
SyntheticGlobalDeclaration,
SyntheticNamespaceDeclaration
} from './Declaration.js';
import { isFalsy, isTruthy } from './ast/conditions.js';
import { emptyBlockStatement } from './ast/create.js';
import extractNames from './ast/extractNames.js';
import { SyntheticNamespaceDeclaration } from './Declaration.js';
import extractNames from './ast/utils/extractNames.js';
import enhance from './ast/enhance.js';
import ModuleScope from './ast/scopes/ModuleScope.js';
function tryParse ( code, comments, acornOptions, id ) {
try {
return parse( code, assign({
ecmaVersion: 7,
sourceType: 'module',
onComment: ( block, text, start, end ) => comments.push({ block, text, start, end }),
preserveParens: true
}, acornOptions ));
} catch ( err ) {
err.code = 'PARSE_ERROR';
err.file = id; // see above - not necessarily true, but true enough
err.message += ` in ${id}`;
throw err;
}
}
export default class Module {
constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) {
@ -23,6 +33,10 @@ export default class Module {
this.originalSourceMap = originalSourceMap;
this.sourceMapChain = sourceMapChain;
this.comments = [];
this.ast = ast || tryParse( code, this.comments, bundle.acornOptions, id ); // TODO what happens to comments if AST is provided?
this.astClone = deepClone( this.ast );
this.bundle = bundle;
this.id = id;
this.excludeFromSourcemap = /\0/.test( id );
@ -55,18 +69,15 @@ export default class Module {
this.magicString.remove( match.index, match.index + match[0].length );
}
this.comments = [];
this.ast = ast;
this.statements = this.parse();
this.declarations = blank();
this.type = 'Module'; // TODO only necessary so that Scope knows this should be treated as a function scope... messy
this.scope = new ModuleScope( this );
this.analyse();
this.strongDependencies = [];
}
addExport ( statement ) {
const node = statement.node;
addExport ( node ) {
const source = node.source && node.source.value;
// export { name } from './other.js'
@ -114,7 +125,7 @@ export default class Module {
};
// create a synthetic declaration
this.declarations.default = new SyntheticDefaultDeclaration( node, statement, identifier || this.basename() );
//this.declarations.default = new SyntheticDefaultDeclaration( node, identifier || this.basename() );
}
// export var { foo, bar } = ...
@ -156,8 +167,7 @@ export default class Module {
}
}
addImport ( statement ) {
const node = statement.node;
addImport ( node ) {
const source = node.source.value;
if ( !~this.sources.indexOf( source ) ) this.sources.push( source );
@ -181,17 +191,21 @@ export default class Module {
}
analyse () {
enhance( this.ast, this, this.comments );
// discover this module's imports and exports
this.statements.forEach( statement => {
if ( statement.isImportDeclaration ) this.addImport( statement );
else if ( statement.isExportDeclaration ) this.addExport( statement );
let lastNode;
statement.firstPass();
for ( const node of this.ast.body ) {
if ( node.isImportDeclaration ) {
this.addImport( node );
} else if ( node.isExportDeclaration ) {
this.addExport( node );
}
statement.scope.eachDeclaration( ( name, declaration ) => {
this.declarations[ name ] = declaration;
});
});
if ( lastNode ) lastNode.next = node.leadingCommentStart || node.start;
lastNode = node;
}
}
basename () {
@ -201,27 +215,6 @@ export default class Module {
return makeLegalIdentifier( ext ? base.slice( 0, -ext.length ) : base );
}
bindAliases () {
keys( this.declarations ).forEach( name => {
if ( name === '*' ) return;
const declaration = this.declarations[ name ];
const statement = declaration.statement;
if ( !statement || statement.node.type !== 'VariableDeclaration' ) return;
const init = statement.node.declarations[0].init;
if ( !init || init.type === 'FunctionExpression' ) return;
statement.references.forEach( reference => {
if ( reference.name === name ) return;
const otherDeclaration = this.trace( reference.name );
if ( otherDeclaration ) otherDeclaration.addAlias( declaration );
});
});
}
bindImportSpecifiers () {
[ this.imports, this.reexports ].forEach( specifiers => {
keys( specifiers ).forEach( name => {
@ -246,32 +239,25 @@ export default class Module {
}
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 );
}
for ( const node of this.ast.body ) {
node.bind( this.scope );
}
this.statements.forEach( statement => {
// 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;
}
// if ( this.declarations.default ) {
// if ( this.exports.default.identifier ) {
// const declaration = this.trace( this.exports.default.identifier );
// if ( declaration ) this.declarations.default.bind( declaration );
// }
// }
}
statement.references.forEach( reference => {
const declaration = reference.scope.findDeclaration( reference.name ) ||
this.trace( reference.name );
findParent () {
// TODO what does it mean if we're here?
return null;
}
if ( declaration ) {
declaration.addReference( reference );
} else {
// TODO handle globals
this.bundle.assumedGlobals[ reference.name ] = true;
}
});
});
findScope () {
return this.scope;
}
getExports () {
@ -286,6 +272,8 @@ export default class Module {
});
this.exportAllModules.forEach( module => {
if ( module.isExternal ) return; // TODO
module.getExports().forEach( name => {
if ( name !== 'default' ) exports[ name ] = true;
});
@ -302,359 +290,26 @@ export default class Module {
return this.declarations['*'];
}
parse () {
// The ast can be supplied programmatically (but usually won't be)
if ( !this.ast ) {
// Try to extract a list of top-level statements/declarations. If
// the parse fails, attach file info and abort
try {
this.ast = parse( this.code, assign({
ecmaVersion: 6,
sourceType: 'module',
onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end }),
preserveParens: true
}, this.bundle.acornOptions ));
} catch ( err ) {
err.code = 'PARSE_ERROR';
err.file = this.id; // see above - not necessarily true, but true enough
err.message += ` in ${this.id}`;
throw err;
}
}
walk( this.ast, {
enter: node => {
// eliminate dead branches early
if ( node.type === 'IfStatement' ) {
if ( isFalsy( node.test ) ) {
this.magicString.overwrite( node.consequent.start, node.consequent.end, '{}' );
node.consequent = emptyBlockStatement( node.consequent.start, node.consequent.end );
} else if ( node.alternate && isTruthy( node.test ) ) {
this.magicString.overwrite( node.alternate.start, node.alternate.end, '{}' );
node.alternate = emptyBlockStatement( node.alternate.start, node.alternate.end );
}
}
this.magicString.addSourcemapLocation( node.start );
this.magicString.addSourcemapLocation( node.end );
},
leave: ( node, parent, prop ) => {
// eliminate dead branches early
if ( node.type === 'ConditionalExpression' ) {
if ( isFalsy( node.test ) ) {
this.magicString.remove( node.start, node.alternate.start );
parent[prop] = node.alternate;
} else if ( isTruthy( node.test ) ) {
this.magicString.remove( node.start, node.consequent.start );
this.magicString.remove( node.consequent.end, node.end );
parent[prop] = node.consequent;
}
}
}
});
const statements = [];
let lastChar = 0;
let commentIndex = 0;
this.ast.body.forEach( node => {
if ( node.type === 'EmptyStatement' ) return;
if (
node.type === 'ExportNamedDeclaration' &&
node.declaration &&
node.declaration.type === 'VariableDeclaration' &&
node.declaration.declarations &&
node.declaration.declarations.length > 1
) {
// push a synthetic export declaration
const syntheticNode = {
type: 'ExportNamedDeclaration',
specifiers: node.declaration.declarations.map( declarator => {
const id = { name: declarator.id.name };
return {
local: id,
exported: id
};
}),
isSynthetic: true
};
const statement = new Statement( syntheticNode, this, node.start, node.start );
statements.push( statement );
this.magicString.remove( node.start, node.declaration.start );
node = node.declaration;
}
// special case - top-level var declarations with multiple declarators
// should be split up. Otherwise, we may end up including code we
// don't need, just because an unwanted declarator is included
if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) {
// remove the leading var/let/const... UNLESS the previous node
// was also a synthetic node, in which case it'll get removed anyway
const lastStatement = statements[ statements.length - 1 ];
if ( !lastStatement || !lastStatement.node.isSynthetic ) {
this.magicString.remove( node.start, node.declarations[0].start );
}
node.declarations.forEach( declarator => {
const { start, end } = declarator;
const syntheticNode = {
type: 'VariableDeclaration',
kind: node.kind,
start,
end,
declarations: [ declarator ],
isSynthetic: true
};
const statement = new Statement( syntheticNode, this, start, end );
statements.push( statement );
});
lastChar = node.end; // TODO account for trailing line comment
}
else {
let comment;
do {
comment = this.comments[ commentIndex ];
if ( !comment ) break;
if ( comment.start > node.start ) break;
commentIndex += 1;
} while ( comment.end < lastChar );
const start = comment ? Math.min( comment.start, node.start ) : node.start;
const end = node.end; // TODO account for trailing line comment
const statement = new Statement( node, this, start, end );
statements.push( statement );
lastChar = end;
}
});
let i = statements.length;
let next = this.code.length;
while ( i-- ) {
statements[i].next = next;
if ( !statements[i].isSynthetic ) next = statements[i].start;
}
return statements;
}
render ( es ) {
const magicString = this.magicString.clone();
this.statements.forEach( statement => {
if ( !statement.isIncluded ) {
if ( statement.node.type === 'ImportDeclaration' ) {
magicString.remove( statement.node.start, statement.next );
return;
}
magicString.remove( statement.start, statement.next );
return;
}
statement.stringLiteralRanges.forEach( range => magicString.indentExclusionRanges.push( range ) );
// skip `export { foo, bar, baz }`
if ( statement.node.type === 'ExportNamedDeclaration' ) {
if ( statement.node.isSynthetic ) return;
// skip `export { foo, bar, baz }`
if ( statement.node.declaration === null ) {
magicString.remove( statement.start, statement.next );
return;
}
}
// split up/remove var declarations as necessary
if ( statement.node.type === 'VariableDeclaration' ) {
const declarator = statement.node.declarations[0];
if ( declarator.id.type === 'Identifier' ) {
const declaration = this.declarations[ declarator.id.name ];
if ( declaration.exportName && declaration.isReassigned ) { // `var foo = ...` becomes `exports.foo = ...`
magicString.remove( statement.start, declarator.init ? declarator.start : statement.next );
if ( !declarator.init ) return;
}
}
else {
// we handle destructuring differently, because whereas we can rewrite
// `var foo = ...` as `exports.foo = ...`, in a case like `var { a, b } = c()`
// where `a` or `b` is exported and reassigned, we have to append
// `exports.a = a;` and `exports.b = b` instead
extractNames( declarator.id ).forEach( name => {
const declaration = this.declarations[ name ];
if ( declaration.exportName && declaration.isReassigned ) {
magicString.insertLeft( statement.end, `;\nexports.${name} = ${declaration.render( es )}` );
}
});
}
if ( statement.node.isSynthetic ) {
// insert `var/let/const` if necessary
magicString.insertRight( statement.start, `${statement.node.kind} ` );
magicString.insertLeft( statement.end, ';' );
magicString.overwrite( statement.end, statement.next, '\n' ); // TODO account for trailing newlines
}
}
const toDeshadow = blank();
statement.references.forEach( reference => {
const { start, end } = reference;
if ( reference.isUndefined ) {
magicString.overwrite( start, end, 'undefined', true );
}
const declaration = reference.declaration;
if ( declaration ) {
const name = declaration.render( es );
// 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 === end - start ) return;
reference.rewritten = true;
// prevent local variables from shadowing renamed references
const identifier = name.match( /[^\.]+/ )[0];
if ( reference.scope.contains( identifier ) ) {
toDeshadow[ identifier ] = `${identifier}$$`; // TODO more robust mechanism
}
if ( reference.isShorthandProperty ) {
magicString.insertLeft( end, `: ${name}` );
} else {
magicString.overwrite( start, end, name, true );
}
}
});
if ( keys( toDeshadow ).length ) {
statement.references.forEach( reference => {
if ( !reference.rewritten && reference.name in toDeshadow ) {
const replacement = toDeshadow[ reference.name ];
magicString.overwrite( reference.start, reference.end, reference.isShorthandProperty ? `${reference.name}: ${replacement}` : replacement, true );
}
});
}
// modify exports as necessary
if ( statement.isExportDeclaration ) {
// remove `export` from `export var foo = 42`
// TODO: can we do something simpler here?
// we just want to remove `export`, right?
if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.declaration.type === 'VariableDeclaration' ) {
const name = extractNames( statement.node.declaration.declarations[ 0 ].id )[ 0 ];
const declaration = this.declarations[ name ];
// TODO is this even possible?
if ( !declaration ) throw new Error( `Missing declaration for ${name}!` );
let end;
if ( es ) {
end = statement.node.declaration.start;
} else {
if ( declaration.exportName && declaration.isReassigned ) {
const declarator = statement.node.declaration.declarations[0];
end = declarator.init ? declarator.start : statement.next;
} else {
end = statement.node.declaration.start;
}
}
magicString.remove( statement.node.start, end );
}
else if ( statement.node.type === 'ExportAllDeclaration' ) {
// TODO: remove once `export * from 'external'` is supported.
magicString.remove( statement.start, statement.next );
}
// 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 );
}
else if ( statement.node.type === 'ExportDefaultDeclaration' ) {
const defaultDeclaration = this.declarations.default;
// 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 ( !defaultDeclaration.exportName && !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 ${defaultName}` );
} else {
magicString.overwrite( statement.node.start, statement.node.declaration.start, `${this.bundle.varOrConst} ${defaultName} = ` );
}
}
else {
throw new Error( 'Unhandled export' );
}
}
});
for ( const node of this.ast.body ) {
node.render( magicString, es );
}
// add namespace block if necessary
const namespace = this.declarations['*'];
if ( namespace && namespace.needsNamespaceBlock ) {
magicString.append( '\n\n' + namespace.renderBlock( magicString.getIndentString() ) );
if ( this.namespace().needsNamespaceBlock ) {
magicString.append( '\n\n' + this.namespace().renderBlock( es, '\t' ) ); // TODO use correct indentation
}
return magicString.trim();
}
/**
* Statically runs the module marking the top-level statements that must be
* included for the module to execute successfully.
*
* @param {boolean} treeshake - if we should tree-shake the module
* @return {boolean} marked - if any new statements were marked for inclusion
*/
run ( treeshake ) {
if ( !treeshake ) {
this.statements.forEach( statement => {
if ( statement.isImportDeclaration || ( statement.isExportDeclaration && statement.node.isSynthetic ) ) return;
statement.mark();
});
return false;
run () {
for ( const node of this.ast.body ) {
if ( node.hasEffects( this.scope ) ) {
node.run( this.scope );
}
}
let marked = false;
this.statements.forEach( statement => {
marked = statement.run( this.strongDependencies ) || marked;
});
return marked;
}
toJSON () {
@ -662,14 +317,18 @@ export default class Module {
id: this.id,
code: this.code,
originalCode: this.originalCode,
ast: this.ast,
ast: this.astClone,
sourceMapChain: this.sourceMapChain,
resolvedIds: this.resolvedIds
};
}
trace ( name ) {
if ( name in this.declarations ) return this.declarations[ name ];
// TODO this is slightly circular
if ( name in this.scope.declarations ) {
return this.scope.declarations[ name ];
}
if ( name in this.imports ) {
const importDeclaration = this.imports[ name ];
const otherModule = importDeclaration.module;
@ -708,10 +367,7 @@ export default class Module {
const name = exportDeclaration.localName;
const declaration = this.trace( name );
if ( declaration ) return declaration;
this.bundle.assumedGlobals[ name ] = true;
return ( this.declarations[ name ] = new SyntheticGlobalDeclaration( name ) );
return declaration || this.bundle.scope.findDeclaration( name );
}
for ( let i = 0; i < this.exportAllModules.length; i += 1 ) {

30
src/Reference.js

@ -1,30 +0,0 @@
export class Reference {
constructor ( node, scope, statement ) {
this.node = node;
this.scope = scope;
this.statement = statement;
this.declaration = null; // bound later
this.parts = [];
let root = node;
while ( root.type === 'MemberExpression' ) {
this.parts.unshift( root.property );
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
this.rewritten = false;
}
}
export class SyntheticReference {
constructor ( name ) {
this.name = name;
this.parts = [];
}
}

160
src/Statement.js

@ -1,160 +0,0 @@
import { walk } from 'estree-walker';
import Scope from './ast/Scope.js';
import attachScopes from './ast/attachScopes.js';
import modifierNodes, { isModifierNode } from './ast/modifierNodes.js';
import isFunctionDeclaration from './ast/isFunctionDeclaration.js';
import isReference from './ast/isReference.js';
import getLocation from './utils/getLocation.js';
import run from './utils/run.js';
import { Reference } from './Reference.js';
export default class Statement {
constructor ( node, module, start, end ) {
this.node = node;
this.module = module;
this.start = start;
this.end = end;
this.next = null; // filled in later
this.scope = new Scope({ statement: this });
this.references = [];
this.stringLiteralRanges = [];
this.isIncluded = false;
this.ran = false;
this.isImportDeclaration = node.type === 'ImportDeclaration';
this.isExportDeclaration = /^Export/.test( node.type );
this.isReexportDeclaration = this.isExportDeclaration && !!node.source;
this.isFunctionDeclaration = isFunctionDeclaration( node ) ||
this.isExportDeclaration && isFunctionDeclaration( node.declaration );
}
firstPass () {
if ( this.isImportDeclaration ) return; // nothing to analyse
// attach scopes
attachScopes( this );
// find references
const statement = this;
let { module, references, scope, stringLiteralRanges } = this;
let contextDepth = 0;
walk( this.node, {
enter ( node, parent, prop ) {
// warn about eval
if ( node.type === 'CallExpression' && node.callee.name === 'eval' && !scope.contains( 'eval' ) ) {
// TODO show location
module.bundle.onwarn( `Use of \`eval\` (in ${module.id}) is strongly discouraged, as it poses security risks and may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` );
}
// skip re-export declarations
if ( node.type === 'ExportNamedDeclaration' && node.source ) return this.skip();
if ( node.type === 'TemplateElement' ) stringLiteralRanges.push([ node.start, node.end ]);
if ( node.type === 'Literal' && typeof node.value === 'string' && /\n/.test( node.raw ) ) {
stringLiteralRanges.push([ node.start + 1, node.end - 1 ]);
}
if ( node.type === 'ThisExpression' && contextDepth === 0 ) {
module.magicString.overwrite( node.start, node.end, module.bundle.context );
if ( module.bundle.context === 'undefined' ) module.bundle.onwarn( 'The `this` keyword is equivalent to `undefined` at the top level of an ES module, and has been rewritten' );
}
if ( node._scope ) scope = node._scope;
if ( /^Function/.test( node.type ) ) contextDepth += 1;
let isReassignment;
if ( parent && isModifierNode( parent ) ) {
let subject = parent[ modifierNodes[ parent.type ] ];
if ( node === subject ) {
let depth = 0;
while ( subject.type === 'MemberExpression' ) {
subject = subject.object;
depth += 1;
}
const importDeclaration = module.imports[ subject.name ];
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
if ( depth < minDepth ) {
const err = new Error( `Illegal reassignment to import '${subject.name}'` );
err.file = module.id;
err.loc = getLocation( module.magicString.original, subject.start );
throw err;
}
}
isReassignment = !depth;
}
}
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;
const isShorthandProperty = parent.type === 'Property' && parent.shorthand;
// Since `node.key` can equal `node.value` for shorthand properties
// we must use the `prop` argument provided by `estree-walker` to determine
// if we're looking at the key or the value.
// If they are equal, we'll return to not create duplicate references.
if ( isShorthandProperty && parent.value === parent.key && prop === 'value' ) {
return;
}
const reference = new Reference( node, referenceScope, statement );
reference.isReassignment = isReassignment;
reference.isShorthandProperty = isShorthandProperty;
references.push( reference );
this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
}
},
leave ( node ) {
if ( node._scope ) scope = scope.parent;
if ( /^Function/.test( node.type ) ) contextDepth -= 1;
}
});
}
mark () {
if ( this.isIncluded ) return; // prevent infinite loops
this.isIncluded = true;
this.references.forEach( reference => {
if ( reference.declaration ) reference.declaration.use();
});
}
run ( strongDependencies ) {
if ( ( this.ran && this.isIncluded ) || this.isImportDeclaration || this.isFunctionDeclaration ) return;
this.ran = true;
if ( run( this.node, this.scope, this, strongDependencies, false ) ) {
this.mark();
return true;
}
}
source () {
return this.module.source.slice( this.start, this.end );
}
toString () {
return this.module.magicString.slice( this.start, this.end );
}
}

92
src/ast/Node.js

@ -0,0 +1,92 @@
import { UNKNOWN } from './values.js';
import getLocation from '../utils/getLocation.js';
export default class Node {
bind ( scope ) {
this.eachChild( child => child.bind( scope ) );
}
eachChild ( callback ) {
for ( const key of this.keys ) {
if ( this.shorthand && key === 'key' ) continue; // key and value are the same
const value = this[ key ];
if ( value ) {
if ( 'length' in value ) {
for ( const child of value ) {
if ( child ) callback( child );
}
} else if ( value ) {
callback( value );
}
}
}
}
findParent ( selector ) {
return selector.test( this.type ) ? this : this.parent.findParent( selector );
}
// TODO abolish findScope. if a node needs to store scope, store it
findScope ( functionScope ) {
return this.parent.findScope( functionScope );
}
gatherPossibleValues ( values ) {
//this.eachChild( child => child.gatherPossibleValues( values ) );
values.add( UNKNOWN );
}
getValue () {
return UNKNOWN;
}
hasEffects ( scope ) {
for ( const key of this.keys ) {
const value = this[ key ];
if ( value ) {
if ( 'length' in value ) {
for ( const child of value ) {
if ( child && child.hasEffects( scope ) ) {
return true;
}
}
} else if ( value && value.hasEffects( scope ) ) {
return true;
}
}
}
}
initialise ( scope ) {
this.eachChild( child => child.initialise( scope ) );
}
locate () {
// useful for debugging
const location = getLocation( this.module.code, this.start );
location.file = this.module.id;
location.toString = () => JSON.stringify( location );
return location;
}
render ( code, es ) {
this.eachChild( child => child.render( code, es ) );
}
run ( scope ) {
if ( this.ran ) return;
this.ran = true;
this.eachChild( child => {
child.run( scope );
});
}
toString () {
return this.module.code.slice( this.start, this.end );
}
}

52
src/ast/Scope.js

@ -1,52 +0,0 @@
import { blank, keys } from '../utils/object.js';
import Declaration from '../Declaration.js';
import extractNames from './extractNames.js';
export default class Scope {
constructor ( options ) {
options = options || {};
this.parent = options.parent;
this.statement = options.statement || this.parent.statement;
this.isBlockScope = !!options.block;
this.isTopLevel = !this.parent || ( this.parent.isTopLevel && this.isBlockScope );
this.declarations = blank();
if ( options.params ) {
options.params.forEach( param => {
extractNames( param ).forEach( name => {
this.declarations[ name ] = new Declaration( param, true, this.statement );
});
});
}
}
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( node, isBlockDeclaration, isVar );
} else {
extractNames( node.id ).forEach( name => {
this.declarations[ name ] = new Declaration( node, false, this.statement );
});
}
}
contains ( name ) {
return this.declarations[ name ] ||
( this.parent ? this.parent.contains( name ) : false );
}
eachDeclaration ( fn ) {
keys( this.declarations ).forEach( key => {
fn( key, this.declarations[ key ] );
});
}
findDeclaration ( name ) {
return this.declarations[ name ] ||
( this.parent && this.parent.findDeclaration( name ) );
}
}

78
src/ast/attachScopes.js

@ -1,78 +0,0 @@
import { walk } from 'estree-walker';
import Scope from './Scope.js';
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, bar = 2
if ( node.type === 'VariableDeclaration' ) {
const isBlockDeclaration = blockDeclarations[ node.kind ];
node.declarations.forEach( declarator => {
scope.addDeclaration( declarator, isBlockDeclaration, true );
});
}
let newScope;
// create new function scope
if ( /(Function|Class)/.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 ( /(Function|Class)Expression/.test( node.type ) && node.id ) {
newScope.addDeclaration( node, false, false );
}
}
// create new block scope
if ( node.type === 'BlockStatement' && ( !parent || !/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;
}
}
});
}

38
src/ast/conditions.js

@ -1,38 +0,0 @@
export function isTruthy ( node ) {
if ( node.type === 'Literal' ) return !!node.value;
if ( node.type === 'ParenthesizedExpression' ) return isTruthy( node.expression );
if ( node.operator in operators ) return operators[ node.operator ]( node );
}
export function isFalsy ( node ) {
return not( isTruthy( node ) );
}
function not ( value ) {
return value === undefined ? value : !value;
}
function equals ( a, b, strict ) {
if ( a.type !== b.type ) return undefined;
if ( a.type === 'Literal' ) return strict ? a.value === b.value : a.value == b.value;
}
const operators = {
'==': x => {
return equals( x.left, x.right, false );
},
'!=': x => not( operators['==']( x ) ),
'===': x => {
return equals( x.left, x.right, true );
},
'!==': x => not( operators['===']( x ) ),
'!': x => isFalsy( x.argument ),
'&&': x => isTruthy( x.left ) && isTruthy( x.right ),
'||': x => isTruthy( x.left ) || isTruthy( x.right )
};

7
src/ast/create.js

@ -1,7 +0,0 @@
export function emptyBlockStatement ( start, end ) {
return {
start, end,
type: 'BlockStatement',
body: []
};
}

63
src/ast/enhance.js

@ -0,0 +1,63 @@
import nodes from './nodes/index.js';
import Node from './Node.js';
import keys from './keys.js';
const newline = /\n/;
export default function enhance ( ast, module, comments ) {
enhanceNode( ast, module, module, module.magicString );
let comment = comments.shift();
for ( const node of ast.body ) {
if ( comment && ( comment.start < node.start ) ) {
node.leadingCommentStart = comment.start;
}
while ( comment && comment.end < node.end ) comment = comments.shift();
// if the next comment is on the same line as the end of the node,
// treat is as a trailing comment
if ( comment && !newline.test( module.code.slice( node.end, comment.start ) ) ) {
node.trailingCommentEnd = comment.end; // TODO is node.trailingCommentEnd used anywhere?
comment = comments.shift();
}
node.initialise( module.scope );
}
}
function enhanceNode ( raw, parent, module, code ) {
if ( !raw ) return;
if ( 'length' in raw ) {
for ( let i = 0; i < raw.length; i += 1 ) {
enhanceNode( raw[i], parent, module, code );
}
return;
}
// with e.g. shorthand properties, key and value are
// the same node. We don't want to enhance an object twice
if ( raw.__enhanced ) return;
raw.__enhanced = true;
if ( !keys[ raw.type ] ) {
keys[ raw.type ] = Object.keys( raw ).filter( key => typeof raw[ key ] === 'object' );
}
raw.parent = parent;
raw.module = module;
raw.keys = keys[ raw.type ];
code.addSourcemapLocation( raw.start );
code.addSourcemapLocation( raw.end );
for ( const key of keys[ raw.type ] ) {
enhanceNode( raw[ key ], raw, module, code );
}
const type = nodes[ raw.type ] || Node;
raw.__proto__ = type.prototype;
}

6
src/ast/isFunctionDeclaration.js

@ -1,6 +0,0 @@
export default function isFunctionDeclaration ( node ) {
if ( !node ) return false;
return node.type === 'FunctionDeclaration' ||
( node.type === 'VariableDeclaration' && node.init && /FunctionExpression/.test( node.init.type ) );
}

4
src/ast/keys.js

@ -0,0 +1,4 @@
export default {
Program: [ 'body' ],
Literal: []
};

19
src/ast/modifierNodes.js

@ -1,19 +0,0 @@
const modifierNodes = {
AssignmentExpression: 'left',
UpdateExpression: 'argument',
UnaryExpression: 'argument'
};
export default modifierNodes;
export function isModifierNode ( node ) {
if ( !( node.type in modifierNodes ) ) {
return false;
}
if ( node.type === 'UnaryExpression' ) {
return node.operator === 'delete';
}
return true;
}

8
src/ast/nodes/ArrayExpression.js

@ -0,0 +1,8 @@
import Node from '../Node.js';
import { ARRAY } from '../values.js';
export default class ArrayExpression extends Node {
gatherPossibleValues ( values ) {
values.add( ARRAY );
}
}

35
src/ast/nodes/ArrowFunctionExpression.js

@ -0,0 +1,35 @@
import Node from '../Node.js';
import Scope from '../scopes/Scope.js';
import extractNames from '../utils/extractNames.js';
export default class ArrowFunctionExpression extends Node {
initialise ( scope ) {
if ( this.body.type !== 'BlockStatement' ) {
this.scope = new Scope({
parent: scope,
isBlockScope: false,
isLexicalBoundary: false
});
for ( const param of this.params ) {
for ( const name of extractNames( param ) ) {
this.scope.addDeclaration( name, null, null, true ); // TODO ugh
}
}
}
super.initialise( scope );
}
bind ( scope ) {
super.bind( this.scope || scope );
}
findScope ( functionScope ) {
return this.scope || this.parent.findScope( functionScope );
}
hasEffects () {
return false;
}
}

45
src/ast/nodes/AssignmentExpression.js

@ -0,0 +1,45 @@
import Node from '../Node.js';
import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js';
import isUsedByBundle from './shared/isUsedByBundle.js';
import { NUMBER, STRING } from '../values.js';
export default class AssignmentExpression extends Node {
bind ( scope ) {
let subject = this.left;
while ( this.left.type === 'ParenthesizedExpression' ) subject = subject.expression;
this.subject = subject;
disallowIllegalReassignment( scope, subject );
if ( subject.type === 'Identifier' ) {
const declaration = scope.findDeclaration( subject.name );
declaration.isReassigned = true;
if ( declaration.possibleValues ) { // TODO this feels hacky
if ( this.operator === '=' ) {
declaration.possibleValues.add( this.right );
} else if ( this.operator === '+=' ) {
declaration.possibleValues.add( STRING ).add( NUMBER );
} else {
declaration.possibleValues.add( NUMBER );
}
}
}
super.bind( scope );
}
hasEffects ( scope ) {
const hasEffects = this.isUsedByBundle() || this.right.hasEffects( scope );
return hasEffects;
}
initialise ( scope ) {
this.module.bundle.dependentExpressions.push( this );
super.initialise( scope );
}
isUsedByBundle () {
return isUsedByBundle( this.findScope(), this.subject );
}
}

38
src/ast/nodes/BinaryExpression.js

@ -0,0 +1,38 @@
import Node from '../Node.js';
import { UNKNOWN } from '../values.js';
const operators = {
'==': ( left, right ) => left == right,
'!=': ( left, right ) => left != right,
'===': ( left, right ) => left === right,
'!==': ( left, right ) => left !== right,
'<': ( left, right ) => left < right,
'<=': ( left, right ) => left <= right,
'>': ( left, right ) => left > right,
'>=': ( left, right ) => left >= right,
'<<': ( left, right ) => left << right,
'>>': ( left, right ) => left >> right,
'>>>': ( left, right ) => left >>> right,
'+': ( left, right ) => left + right,
'-': ( left, right ) => left - right,
'*': ( left, right ) => left * right,
'/': ( left, right ) => left / right,
'%': ( left, right ) => left % right,
'|': ( left, right ) => left | right,
'^': ( left, right ) => left ^ right,
'&': ( left, right ) => left & right,
in: ( left, right ) => left in right,
instanceof: ( left, right ) => left instanceof right
};
export default class BinaryExpression extends Node {
getValue () {
const leftValue = this.left.getValue();
if ( leftValue === UNKNOWN ) return UNKNOWN;
const rightValue = this.right.getValue();
if ( rightValue === UNKNOWN ) return UNKNOWN;
return operators[ this.operator ]( leftValue, rightValue );
}
}

71
src/ast/nodes/BlockStatement.js

@ -0,0 +1,71 @@
import Node from '../Node.js';
import Scope from '../scopes/Scope.js';
import extractNames from '../utils/extractNames.js';
export default class BlockStatement extends Node {
bind () {
for ( const node of this.body ) {
node.bind( this.scope );
}
}
createScope ( parent ) {
this.parentIsFunction = /Function/.test( this.parent.type );
this.isFunctionBlock = this.parentIsFunction || this.parent.type === 'Module';
this.scope = new Scope({
isBlockScope: !this.isFunctionBlock,
isLexicalBoundary: this.isFunctionBlock && this.parent.type !== 'ArrowFunctionExpression',
parent: parent || this.parent.findScope( false ), // TODO always supply parent
owner: this // TODO is this used anywhere?
});
const params = this.parent.params || ( this.parent.type === 'CatchClause' && [ this.parent.param ] );
if ( params && params.length ) {
params.forEach( node => {
extractNames( node ).forEach( name => {
this.scope.addDeclaration( name, node, false, true );
});
});
}
}
findScope ( functionScope ) {
return functionScope && !this.isFunctionBlock ? this.parent.findScope( functionScope ) : this.scope;
}
hasEffects () {
for ( const node of this.body ) {
if ( node.hasEffects( this.scope ) ) return true;
}
}
initialise () {
if ( !this.scope ) this.createScope(); // scope can be created early in some cases, e.g for (let i... )
let lastNode;
for ( const node of this.body ) {
node.initialise( this.scope );
if ( lastNode ) lastNode.next = node.start;
lastNode = node;
}
}
render ( code, es ) {
for ( const node of this.body ) {
node.render( code, es );
}
}
run () {
if ( this.ran ) return;
this.ran = true;
for ( const node of this.body ) {
// TODO only include non-top-level statements if necessary
node.run( this.scope );
}
}
}

40
src/ast/nodes/CallExpression.js

@ -0,0 +1,40 @@
import getLocation from '../../utils/getLocation.js';
import error from '../../utils/error.js';
import Node from '../Node.js';
import callHasEffects from './shared/callHasEffects.js';
export default class CallExpression extends Node {
bind ( scope ) {
if ( this.callee.type === 'Identifier' ) {
const declaration = scope.findDeclaration( this.callee.name );
if ( declaration.isNamespace ) {
error({
message: `Cannot call a namespace ('${this.callee.name}')`,
file: this.module.id,
pos: this.start,
loc: getLocation( this.module.code, this.start )
});
}
if ( this.callee.name === 'eval' && declaration.isGlobal ) {
this.module.bundle.onwarn( `Use of \`eval\` (in ${this.module.id}) is strongly discouraged, as it poses security risks and may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` );
}
}
super.bind( scope );
}
hasEffects ( scope ) {
return callHasEffects( scope, this.callee );
}
initialise ( scope ) {
this.module.bundle.dependentExpressions.push( this );
super.initialise( scope );
}
isUsedByBundle () {
return this.hasEffects( this.findScope() );
}
}

40
src/ast/nodes/ClassDeclaration.js

@ -0,0 +1,40 @@
import Node from '../Node.js';
// TODO is this basically identical to FunctionDeclaration?
export default class ClassDeclaration extends Node {
activate () {
if ( this.activated ) return;
this.activated = true;
this.body.run();
}
addReference () {
/* noop? */
}
gatherPossibleValues ( values ) {
values.add( this );
}
getName () {
return this.id.name;
}
hasEffects () {
return false;
}
initialise ( scope ) {
scope.addDeclaration( this.id.name, this, false, false );
super.initialise( scope );
}
render ( code, es ) {
if ( this.activated ) {
super.render( code, es );
} else {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
}
}
}

26
src/ast/nodes/ClassExpression.js

@ -0,0 +1,26 @@
import Node from '../Node.js';
import Scope from '../scopes/Scope.js';
export default class ClassExpression extends Node {
bind () {
super.bind( this.scope );
}
findScope () {
return this.scope;
}
initialise () {
this.scope = new Scope({
isBlockScope: true,
parent: this.parent.findScope( false )
});
if ( this.id ) {
// function expression IDs belong to the child scope...
this.scope.addDeclaration( this.id.name, this, false, true );
}
super.initialise( this.scope );
}
}

65
src/ast/nodes/ConditionalExpression.js

@ -0,0 +1,65 @@
import Node from '../Node.js';
import { UNKNOWN } from '../values.js';
export default class ConditionalExpression extends Node {
initialise ( scope ) {
if ( this.module.bundle.treeshake ) {
this.testValue = this.test.getValue();
if ( this.testValue === UNKNOWN ) {
super.initialise( scope );
}
else if ( this.testValue ) {
this.consequent.initialise( scope );
this.alternate = null;
} else if ( this.alternate ) {
this.alternate.initialise( scope );
this.consequent = null;
}
}
else {
super.initialise( scope );
}
}
gatherPossibleValues ( values ) {
const testValue = this.test.getValue();
if ( testValue === UNKNOWN ) {
values.add( this.consequent ).add( this.alternate );
} else {
values.add( testValue ? this.consequent : this.alternate );
}
}
getValue () {
const testValue = this.test.getValue();
if ( testValue === UNKNOWN ) return UNKNOWN;
return testValue ? this.consequent.getValue() : this.alternate.getValue();
}
render ( code, es ) {
if ( !this.module.bundle.treeshake ) {
super.render( code, es );
}
else {
if ( this.testValue === UNKNOWN ) {
super.render( code, es );
}
else if ( this.testValue ) {
code.remove( this.start, this.consequent.start );
code.remove( this.consequent.end, this.end );
this.consequent.render( code, es );
} else {
code.remove( this.start, this.alternate.start );
code.remove( this.alternate.end, this.end );
this.alternate.render( code, es );
}
}
}
}

11
src/ast/nodes/ExportAllDeclaration.js

@ -0,0 +1,11 @@
import Node from '../Node.js';
export default class ExportAllDeclaration extends Node {
initialise () {
this.isExportDeclaration = true;
}
render ( code ) {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
}
}

96
src/ast/nodes/ExportDefaultDeclaration.js

@ -0,0 +1,96 @@
import Node from '../Node.js';
const functionOrClassDeclaration = /^(?:Function|Class)Declaration/;
export default class ExportDefaultDeclaration extends Node {
initialise ( scope ) {
this.isExportDeclaration = true;
this.isDefault = true;
this.name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name || this.module.basename();
scope.declarations.default = this;
this.declaration.initialise( scope );
}
activate () {
if ( this.activated ) return;
this.activated = true;
this.run();
}
addReference ( reference ) {
this.name = reference.name;
if ( this.original ) this.original.addReference( reference );
}
bind ( scope ) {
const name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name;
if ( name ) this.original = scope.findDeclaration( name );
this.declaration.bind( scope );
}
gatherPossibleValues ( values ) {
this.declaration.gatherPossibleValues( values );
}
getName ( es ) {
if ( this.original && !this.original.isReassigned ) {
return this.original.getName( es );
}
return this.name;
}
// TODO this is total chaos, tidy it up
render ( code, es ) {
const treeshake = this.module.bundle.treeshake;
const name = this.getName( es );
if ( this.shouldInclude ) {
if ( this.activated ) {
if ( functionOrClassDeclaration.test( this.declaration.type ) ) {
if ( this.declaration.id ) {
code.remove( this.start, this.declaration.start );
} else {
throw new Error( 'TODO anonymous class/function declaration' );
}
}
else {
if ( this.original && this.original.getName( es ) === name ) {
// prevent `var foo = foo`
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
return; // don't render children. TODO this seems like a bit of a hack
} else {
code.overwrite( this.start, this.declaration.start, `${this.module.bundle.varOrConst} ${name} = ` );
}
}
} else {
// remove `var foo` from `var foo = bar()`, if `foo` is unused
code.remove( this.start, this.declaration.start );
}
super.render( code, es );
} else {
if ( treeshake ) {
if ( functionOrClassDeclaration.test( this.declaration.type ) && !this.declaration.activated ) {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
} else {
const hasEffects = this.declaration.hasEffects( this.module.scope );
code.remove( this.start, hasEffects ? this.declaration.start : this.next || this.end );
}
} else {
code.overwrite( this.start, this.declaration.start, `${this.module.bundle.varOrConst} ${name} = ` );
}
// code.remove( this.start, this.next || this.end );
}
}
run ( scope ) {
this.shouldInclude = true;
super.run( scope );
}
}

25
src/ast/nodes/ExportNamedDeclaration.js

@ -0,0 +1,25 @@
import Node from '../Node.js';
export default class ExportNamedDeclaration extends Node {
initialise ( scope ) {
this.isExportDeclaration = true;
if ( this.declaration ) {
this.declaration.initialise( scope );
}
}
bind ( scope ) {
if ( this.declaration ) {
this.declaration.bind( scope );
}
}
render ( code, es ) {
if ( this.declaration ) {
code.remove( this.start, this.declaration.start );
this.declaration.render( code, es );
} else {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
}
}
}

16
src/ast/nodes/ExpressionStatement.js

@ -0,0 +1,16 @@
import Node from '../Node.js';
export default class ExpressionStatement extends Node {
render ( code, es ) {
if ( !this.module.bundle.treeshake || this.shouldInclude ) {
super.render( code, es );
} else {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
}
}
run ( scope ) {
this.shouldInclude = true;
super.run( scope );
}
}

53
src/ast/nodes/FunctionDeclaration.js

@ -0,0 +1,53 @@
import Node from '../Node.js';
export default class FunctionDeclaration extends Node {
activate () {
if ( this.activated ) return;
this.activated = true;
const scope = this.body.scope;
this.params.forEach( param => param.run( scope ) ); // in case of assignment patterns
this.body.run();
}
addReference () {
/* noop? */
}
bind ( scope ) {
this.id.bind( scope );
this.params.forEach( param => param.bind( this.body.scope ) );
this.body.bind( scope );
}
gatherPossibleValues ( values ) {
values.add( this );
}
getName () {
return this.name;
}
hasEffects () {
return false;
}
initialise ( scope ) {
this.name = this.id.name; // may be overridden by bundle.deconflict
scope.addDeclaration( this.name, this, false, false );
this.body.createScope();
this.id.initialise( scope );
this.params.forEach( param => param.initialise( this.body.scope ) );
this.body.initialise( scope );
}
render ( code, es ) {
if ( !this.module.bundle.treeshake || this.activated ) {
super.render( code, es );
} else {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
}
}
}

21
src/ast/nodes/FunctionExpression.js

@ -0,0 +1,21 @@
import Node from '../Node.js';
export default class FunctionExpression extends Node {
bind () {
if ( this.id ) this.id.bind( this.body.scope );
this.params.forEach( param => param.bind( this.body.scope ) );
this.body.bind();
}
hasEffects () {
return false;
}
initialise () {
this.body.createScope(); // TODO we'll also need to do this for For[Of|In]Statement
if ( this.id ) this.id.initialise( this.body.scope );
this.params.forEach( param => param.initialise( this.body.scope ) );
this.body.initialise();
}
}

35
src/ast/nodes/Identifier.js

@ -0,0 +1,35 @@
import Node from '../Node.js';
import isReference from '../utils/isReference.js';
export default class Identifier extends Node {
bind ( scope ) {
if ( isReference( this, this.parent ) ) {
this.declaration = scope.findDeclaration( this.name );
this.declaration.addReference( this ); // TODO necessary?
}
}
gatherPossibleValues ( values ) {
if ( isReference( this, this.parent ) ) {
values.add( this );
}
}
render ( code, es ) {
if ( this.declaration ) {
const name = this.declaration.getName( es );
if ( name !== this.name ) {
code.overwrite( this.start, this.end, name, true );
// special case
if ( this.parent.type === 'Property' && this.parent.shorthand ) {
code.insertLeft( this.start, `${this.name}: ` );
}
}
}
}
run () {
if ( this.declaration ) this.declaration.activate();
}
}

55
src/ast/nodes/IfStatement.js

@ -0,0 +1,55 @@
import Node from '../Node.js';
import { UNKNOWN } from '../values.js';
// TODO DRY this out
export default class IfStatement extends Node {
initialise ( scope ) {
this.testValue = this.test.getValue();
if ( this.module.bundle.treeshake ) {
if ( this.testValue === UNKNOWN ) {
super.initialise( scope );
}
else if ( this.testValue ) {
this.consequent.initialise( scope );
this.alternate = null;
} else {
if ( this.alternate ) this.alternate.initialise( scope );
this.consequent = null;
}
}
else {
super.initialise( scope );
}
}
render ( code, es ) {
if ( this.module.bundle.treeshake ) {
if ( this.testValue === UNKNOWN ) {
super.render( code, es );
}
else {
code.overwrite( this.test.start, this.test.end, JSON.stringify( this.testValue ) );
// TODO if no block-scoped declarations, remove enclosing
// curlies and dedent block (if there is a block)
if ( this.testValue ) {
code.remove( this.start, this.consequent.start );
code.remove( this.consequent.end, this.end );
this.consequent.render( code, es );
} else {
code.remove( this.start, this.alternate ? this.alternate.start : this.next || this.end );
if ( this.alternate ) this.alternate.render( code, es );
}
}
}
else {
super.render( code, es );
}
}
}

16
src/ast/nodes/ImportDeclaration.js

@ -0,0 +1,16 @@
import Node from '../Node.js';
export default class ImportDeclaration extends Node {
bind () {
// noop
// TODO do the inter-module binding setup here?
}
initialise () {
this.isImportDeclaration = true;
}
render ( code ) {
code.remove( this.start, this.next || this.end );
}
}

17
src/ast/nodes/Literal.js

@ -0,0 +1,17 @@
import Node from '../Node.js';
export default class Literal extends Node {
getValue () {
return this.value;
}
gatherPossibleValues ( values ) {
values.add( this );
}
render ( code ) {
if ( typeof this.value === 'string' ) {
code.indentExclusionRanges.push([ this.start + 1, this.end - 1 ]);
}
}
}

74
src/ast/nodes/MemberExpression.js

@ -0,0 +1,74 @@
import isReference from '../utils/isReference.js';
import Node from '../Node.js';
import { UNKNOWN } from '../values.js';
class Keypath {
constructor ( node ) {
this.parts = [];
while ( node.type === 'MemberExpression' ) {
this.parts.unshift( node.property );
node = node.object;
}
this.root = node;
}
}
export default class MemberExpression extends Node {
bind ( scope ) {
// if this resolves to a namespaced declaration, prepare
// to replace it
// TODO this code is a bit inefficient
if ( isReference( this ) ) { // TODO optimise namespace access like `foo['bar']` as well
const keypath = new Keypath( this );
let declaration = scope.findDeclaration( keypath.root.name );
while ( declaration.isNamespace && keypath.parts.length ) {
const part = keypath.parts[0];
declaration = declaration.module.traceExport( part.name );
if ( !declaration ) {
this.module.bundle.onwarn( `Export '${part.name}' is not defined by '${this.module.id}'` );
break;
}
keypath.parts.shift();
}
if ( keypath.parts.length ) {
super.bind( scope );
return; // not a namespaced declaration
}
this.declaration = declaration;
if ( declaration.isExternal ) {
declaration.module.suggestName( keypath.root.name );
}
}
else {
super.bind( scope );
}
}
gatherPossibleValues ( values ) {
values.add( UNKNOWN ); // TODO
}
render ( code, es ) {
if ( this.declaration ) {
const name = this.declaration.getName( es );
if ( name !== this.name ) code.overwrite( this.start, this.end, name, true );
}
super.render( code, es );
}
run ( scope ) {
if ( this.declaration ) this.declaration.activate();
super.run( scope );
}
}

8
src/ast/nodes/NewExpression.js

@ -0,0 +1,8 @@
import Node from '../Node.js';
import callHasEffects from './shared/callHasEffects.js';
export default class NewExpression extends Node {
hasEffects ( scope ) {
return callHasEffects( scope, this.callee );
}
}

8
src/ast/nodes/ObjectExpression.js

@ -0,0 +1,8 @@
import Node from '../Node.js';
import { OBJECT } from '../values.js';
export default class ObjectExpression extends Node {
gatherPossibleValues ( values ) {
values.add( OBJECT );
}
}

11
src/ast/nodes/ParenthesizedExpression.js

@ -0,0 +1,11 @@
import Node from '../Node.js';
export default class ParenthesizedExpression extends Node {
getPossibleValues ( values ) {
return this.expression.getPossibleValues( values );
}
getValue () {
return this.expression.getValue();
}
}

7
src/ast/nodes/ReturnStatement.js

@ -0,0 +1,7 @@
import Node from '../Node.js';
export default class ReturnStatement extends Node {
// hasEffects () {
// return true;
// }
}

7
src/ast/nodes/TemplateLiteral.js

@ -0,0 +1,7 @@
import Node from '../Node.js';
export default class TemplateLiteral extends Node {
render ( code ) {
code.indentExclusionRanges.push([ this.start, this.end ]);
}
}

20
src/ast/nodes/ThisExpression.js

@ -0,0 +1,20 @@
import Node from '../Node.js';
export default class ThisExpression extends Node {
initialise ( scope ) {
const lexicalBoundary = scope.findLexicalBoundary();
if ( lexicalBoundary.isModuleScope ) {
this.alias = this.module.bundle.context;
if ( this.alias === 'undefined' ) {
this.module.bundle.onwarn( 'The `this` keyword is equivalent to `undefined` at the top level of an ES module, and has been rewritten' );
}
}
}
render ( code ) {
if ( this.alias ) {
code.overwrite( this.start, this.end, this.alias, true );
}
}
}

34
src/ast/nodes/UnaryExpression.js

@ -0,0 +1,34 @@
import Node from '../Node.js';
import { UNKNOWN } from '../values.js';
const operators = {
"-": value => -value,
"+": value => +value,
"!": value => !value,
"~": value => ~value,
typeof: value => typeof value,
void: () => undefined,
delete: () => UNKNOWN
};
export default class UnaryExpression extends Node {
bind ( scope ) {
if ( this.value === UNKNOWN ) super.bind( scope );
}
getValue () {
const argumentValue = this.argument.getValue();
if ( argumentValue === UNKNOWN ) return UNKNOWN;
return operators[ this.operator ]( argumentValue );
}
hasEffects ( scope ) {
return this.operator === 'delete' || this.argument.hasEffects( scope );
}
initialise ( scope ) {
this.value = this.getValue();
if ( this.value === UNKNOWN ) super.initialise( scope );
}
}

38
src/ast/nodes/UpdateExpression.js

@ -0,0 +1,38 @@
import Node from '../Node.js';
import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js';
import isUsedByBundle from './shared/isUsedByBundle.js';
import { NUMBER } from '../values.js';
export default class UpdateExpression extends Node {
bind ( scope ) {
let subject = this.argument;
while ( this.argument.type === 'ParenthesizedExpression' ) subject = subject.expression;
this.subject = subject;
disallowIllegalReassignment( scope, this.argument );
if ( subject.type === 'Identifier' ) {
const declaration = scope.findDeclaration( subject.name );
declaration.isReassigned = true;
if ( declaration.possibleValues ) {
declaration.possibleValues.add( NUMBER );
}
}
super.bind( scope );
}
hasEffects ( scope ) {
return isUsedByBundle( scope, this.subject );
}
initialise ( scope ) {
this.module.bundle.dependentExpressions.push( this );
super.initialise( scope );
}
isUsedByBundle () {
return isUsedByBundle( this.findScope(), this.subject );
}
}

100
src/ast/nodes/VariableDeclaration.js

@ -0,0 +1,100 @@
import Node from '../Node.js';
import extractNames from '../utils/extractNames.js';
function getSeparator ( code, start ) {
let c = start;
while ( c > 0 && code[ c - 1 ] !== '\n' ) {
c -= 1;
if ( code[c] === ';' || code[c] === '{' ) return '; ';
}
const lineStart = code.slice( c, start ).match( /^\s*/ )[0];
return `;\n${lineStart}`;
}
const forStatement = /^For(?:Of|In)Statement/;
export default class VariableDeclaration extends Node {
initialise ( scope ) {
this.scope = scope;
super.initialise( scope );
}
render ( code, es ) {
const treeshake = this.module.bundle.treeshake;
let shouldSeparate = false;
let separator;
if ( this.scope.isModuleScope && !forStatement.test( this.parent.type ) ) {
shouldSeparate = true;
separator = getSeparator( this.module.code, this.start );
}
let c = this.start;
let empty = true;
for ( let i = 0; i < this.declarations.length; i += 1 ) {
const declarator = this.declarations[i];
const prefix = empty ? '' : separator; // TODO indentation
if ( declarator.id.type === 'Identifier' ) {
const proxy = declarator.proxies.get( declarator.id.name );
const isExportedAndReassigned = !es && proxy.exportName && proxy.isReassigned;
if ( isExportedAndReassigned ) {
if ( declarator.init ) {
if ( shouldSeparate ) code.overwrite( c, declarator.start, prefix );
c = declarator.end;
empty = false;
}
} else if ( !treeshake || proxy.activated ) {
if ( shouldSeparate ) code.overwrite( c, declarator.start, `${prefix}${this.kind} ` ); // TODO indentation
c = declarator.end;
empty = false;
}
}
else {
const exportAssignments = [];
let activated = false;
extractNames( declarator.id ).forEach( name => {
const proxy = declarator.proxies.get( name );
const isExportedAndReassigned = !es && proxy.exportName && proxy.isReassigned;
if ( isExportedAndReassigned ) {
// code.overwrite( c, declarator.start, prefix );
// c = declarator.end;
// empty = false;
exportAssignments.push( 'TODO' );
} else if ( declarator.activated ) {
activated = true;
}
});
if ( !treeshake || activated ) {
if ( shouldSeparate ) code.overwrite( c, declarator.start, `${prefix}${this.kind} ` ); // TODO indentation
c = declarator.end;
empty = false;
}
if ( exportAssignments.length ) {
throw new Error( 'TODO' );
}
}
declarator.render( code, es );
}
if ( treeshake && empty ) {
code.remove( this.leadingCommentStart || this.start, this.next || this.end );
} else if ( this.end > c ) {
const hasSemicolon = code.original[ this.end - 1 ] === ';';
code.overwrite( c, this.end, hasSemicolon ? ';' : '' );
}
}
}

91
src/ast/nodes/VariableDeclarator.js

@ -0,0 +1,91 @@
import Node from '../Node.js';
import extractNames from '../utils/extractNames.js';
import { UNKNOWN } from '../values.js';
class DeclaratorProxy {
constructor ( name, declarator, isTopLevel, init ) {
this.name = name;
this.declarator = declarator;
this.activated = false;
this.isReassigned = false;
this.exportName = null;
this.duplicates = [];
this.possibleValues = new Set( init ? [ init ] : null );
}
activate () {
this.activated = true;
this.declarator.activate();
this.duplicates.forEach( dupe => dupe.activate() );
}
addReference () {
/* noop? */
}
gatherPossibleValues ( values ) {
this.possibleValues.forEach( value => values.add( value ) );
}
getName ( es ) {
// TODO desctructuring...
if ( es ) return this.name;
if ( !this.isReassigned || !this.exportName ) return this.name;
return `exports.${this.exportName}`;
}
toString () {
return this.name;
}
}
export default class VariableDeclarator extends Node {
activate () {
if ( this.activated ) return;
this.activated = true;
this.run( this.findScope() );
}
hasEffects ( scope ) {
return this.init && this.init.hasEffects( scope );
}
initialise ( scope ) {
this.proxies = new Map();
const lexicalBoundary = scope.findLexicalBoundary();
const init = this.init ?
( this.id.type === 'Identifier' ? this.init : UNKNOWN ) : // TODO maybe UNKNOWN is unnecessary
null;
extractNames( this.id ).forEach( name => {
const proxy = new DeclaratorProxy( name, this, lexicalBoundary.isModuleScope, init );
this.proxies.set( name, proxy );
scope.addDeclaration( name, proxy, this.parent.kind === 'var' );
});
super.initialise( scope );
}
render ( code, es ) {
extractNames( this.id ).forEach( name => {
const declaration = this.proxies.get( name );
if ( !es && declaration.exportName && declaration.isReassigned ) {
if ( this.init ) {
code.overwrite( this.start, this.id.end, declaration.getName( es ) );
} else if ( this.module.bundle.treeshake ) {
code.remove( this.start, this.end );
}
}
});
super.render( code, es );
}
}

63
src/ast/nodes/index.js

@ -0,0 +1,63 @@
import ArrayExpression from './ArrayExpression.js';
import ArrowFunctionExpression from './ArrowFunctionExpression.js';
import AssignmentExpression from './AssignmentExpression.js';
import BinaryExpression from './BinaryExpression.js';
import BlockStatement from './BlockStatement.js';
import CallExpression from './CallExpression.js';
import ClassDeclaration from './ClassDeclaration.js';
import ClassExpression from './ClassExpression.js';
import ConditionalExpression from './ConditionalExpression.js';
import ExportAllDeclaration from './ExportAllDeclaration.js';
import ExportDefaultDeclaration from './ExportDefaultDeclaration.js';
import ExportNamedDeclaration from './ExportNamedDeclaration.js';
import ExpressionStatement from './ExpressionStatement.js';
import FunctionDeclaration from './FunctionDeclaration.js';
import FunctionExpression from './FunctionExpression.js';
import Identifier from './Identifier.js';
import IfStatement from './IfStatement.js';
import ImportDeclaration from './ImportDeclaration.js';
import Literal from './Literal.js';
import MemberExpression from './MemberExpression.js';
import NewExpression from './NewExpression.js';
import ObjectExpression from './ObjectExpression.js';
import ParenthesizedExpression from './ParenthesizedExpression.js';
import ReturnStatement from './ReturnStatement.js';
import TemplateLiteral from './TemplateLiteral.js';
import ThisExpression from './ThisExpression.js';
import UnaryExpression from './UnaryExpression.js';
import UpdateExpression from './UpdateExpression.js';
import VariableDeclarator from './VariableDeclarator.js';
import VariableDeclaration from './VariableDeclaration.js';
export default {
ArrayExpression,
ArrowFunctionExpression,
AssignmentExpression,
BinaryExpression,
BlockStatement,
CallExpression,
ClassDeclaration,
ClassExpression,
ConditionalExpression,
ExportAllDeclaration,
ExportDefaultDeclaration,
ExportNamedDeclaration,
ExpressionStatement,
FunctionDeclaration,
FunctionExpression,
Identifier,
IfStatement,
ImportDeclaration,
Literal,
MemberExpression,
NewExpression,
ObjectExpression,
ParenthesizedExpression,
ReturnStatement,
TemplateLiteral,
ThisExpression,
UnaryExpression,
UpdateExpression,
VariableDeclarator,
VariableDeclaration
};

67
src/ast/nodes/shared/callHasEffects.js

@ -0,0 +1,67 @@
import flatten from '../../utils/flatten.js';
import isReference from '../../utils/isReference.js';
import pureFunctions from './pureFunctions.js';
import { UNKNOWN } from '../../values.js';
const currentlyCalling = new Set();
function fnHasEffects ( fn ) {
if ( currentlyCalling.has( fn ) ) return false; // prevent infinite loops... TODO there must be a better way
currentlyCalling.add( fn );
// handle body-less arrow functions
const scope = fn.body.scope || fn.scope;
const body = fn.body.body || [ fn.body ];
for ( const node of body ) {
if ( node.hasEffects( scope ) ) {
currentlyCalling.delete( fn );
return true;
}
}
currentlyCalling.delete( fn );
return false;
}
export default function callHasEffects ( scope, callee ) {
const values = new Set([ callee ]);
for ( const node of values ) {
if ( node === UNKNOWN ) return true; // err on side of caution
if ( /Function/.test( node.type ) ) {
if ( fnHasEffects( node ) ) return true;
}
else if ( isReference( node ) ) {
const flattened = flatten( node );
const declaration = scope.findDeclaration( flattened.name );
if ( declaration.isGlobal ) {
if ( !pureFunctions[ flattened.keypath ] ) return true;
}
else if ( declaration.isExternal ) {
return true; // TODO make this configurable? e.g. `path.[whatever]`
}
else {
if ( node.declaration ) {
node.declaration.gatherPossibleValues( values );
} else {
return true;
}
}
}
else {
if ( !node.gatherPossibleValues ) {
throw new Error( 'TODO' );
}
node.gatherPossibleValues( values );
}
}
return false;
}

28
src/ast/nodes/shared/disallowIllegalReassignment.js

@ -0,0 +1,28 @@
import getLocation from '../../../utils/getLocation.js';
import error from '../../../utils/error.js';
// TODO tidy this up a bit (e.g. they can both use node.module.imports)
export default function disallowIllegalReassignment ( scope, node ) {
if ( node.type === 'MemberExpression' && node.object.type === 'Identifier' ) {
const declaration = scope.findDeclaration( node.object.name );
if ( declaration.isNamespace ) {
error({
message: `Illegal reassignment to import '${node.object.name}'`,
file: node.module.id,
pos: node.start,
loc: getLocation( node.module.code, node.start )
});
}
}
else if ( node.type === 'Identifier' ) {
if ( node.module.imports[ node.name ] && !scope.contains( node.name ) ) {
error({
message: `Illegal reassignment to import '${node.name}'`,
file: node.module.id,
pos: node.start,
loc: getLocation( node.module.code, node.start )
});
}
}
}

40
src/ast/nodes/shared/isUsedByBundle.js

@ -0,0 +1,40 @@
import { UNKNOWN } from '../../values.js';
export default function isUsedByBundle ( scope, node ) {
while ( node.type === 'ParenthesizedExpression' ) node = node.expression;
// const expression = node;
while ( node.type === 'MemberExpression' ) node = node.object;
const declaration = scope.findDeclaration( node.name );
if ( declaration.isParam ) {
return true;
// TODO if we mutate a parameter, assume the worst
// return node !== expression;
}
if ( declaration.activated ) return true;
const values = new Set();
declaration.gatherPossibleValues( values );
for ( const value of values ) {
if ( value === UNKNOWN ) {
return true;
}
if ( value.type === 'Identifier' ) {
if ( value.declaration.activated ) {
return true;
}
value.declaration.gatherPossibleValues( values );
}
else if ( value.gatherPossibleValues ) {
value.gatherPossibleValues( values );
}
}
return false;
}

0
src/utils/pureFunctions.js → src/ast/nodes/shared/pureFunctions.js

40
src/ast/scopes/BundleScope.js

@ -0,0 +1,40 @@
import Scope from './Scope.js';
import { UNKNOWN } from '../values';
class SyntheticGlobalDeclaration {
constructor ( name ) {
this.name = name;
this.isExternal = true;
this.isGlobal = true;
this.isReassigned = false;
this.activated = true;
}
activate () {
/* noop */
}
addReference ( reference ) {
reference.declaration = this;
if ( reference.isReassignment ) this.isReassigned = true;
}
gatherPossibleValues ( values ) {
values.add( UNKNOWN );
}
getName () {
return this.name;
}
}
export default class BundleScope extends Scope {
findDeclaration ( name ) {
if ( !this.declarations[ name ] ) {
this.declarations[ name ] = new SyntheticGlobalDeclaration( name );
}
return this.declarations[ name ];
}
}

47
src/ast/scopes/ModuleScope.js

@ -0,0 +1,47 @@
import { forOwn } from '../../utils/object.js';
import Scope from './Scope.js';
export default class ModuleScope extends Scope {
constructor ( module ) {
super({
isBlockScope: false,
isLexicalBoundary: true,
isModuleScope: true,
parent: module.bundle.scope
});
this.module = module;
}
deshadow ( names ) {
names = new Map( names );
forOwn( this.module.imports, specifier => {
if ( specifier.module.isExternal ) return;
if ( specifier.name === '*' ) {
specifier.module.getExports().forEach( name => {
names.set( name, true );
});
} else {
const declaration = specifier.module.traceExport( specifier.name );
const name = declaration.getName( true );
if ( name !== specifier.name ) names.set( declaration.getName( true ) );
}
});
super.deshadow( names );
}
findDeclaration ( name ) {
if ( this.declarations[ name ] ) {
return this.declarations[ name ];
}
return this.module.trace( name ) || this.parent.findDeclaration( name );
}
findLexicalBoundary () {
return this;
}
}

98
src/ast/scopes/Scope.js

@ -0,0 +1,98 @@
import { blank, keys } from '../../utils/object.js';
import { UNKNOWN } from '../values.js';
class Parameter {
constructor ( name ) {
this.name = name;
this.isParam = true;
this.activated = true;
}
activate () {
// noop
}
addReference () {
// noop?
}
gatherPossibleValues ( values ) {
values.add( UNKNOWN ); // TODO populate this at call time
}
getName () {
return this.name;
}
}
export default class Scope {
constructor ( options ) {
options = options || {};
this.parent = options.parent;
this.isBlockScope = !!options.isBlockScope;
this.isLexicalBoundary = !!options.isLexicalBoundary;
this.isModuleScope = !!options.isModuleScope;
this.children = [];
if ( this.parent ) this.parent.children.push( this );
this.declarations = blank();
if ( this.isLexicalBoundary && !this.isModuleScope ) {
this.declarations.arguments = new Parameter( 'arguments' );
}
}
addDeclaration ( name, declaration, isVar, isParam ) {
if ( isVar && this.isBlockScope ) {
this.parent.addDeclaration( name, declaration, isVar, isParam );
} else {
const existingDeclaration = this.declarations[ name ];
if ( existingDeclaration && existingDeclaration.duplicates ) {
// TODO warn/throw on duplicates?
existingDeclaration.duplicates.push( declaration );
} else {
this.declarations[ name ] = isParam ? new Parameter( name ) : declaration;
}
}
}
contains ( name ) {
return !!this.declarations[ name ] ||
( this.parent ? this.parent.contains( name ) : false );
}
deshadow ( names ) {
keys( this.declarations ).forEach( key => {
const declaration = this.declarations[ key ];
// we can disregard exports.foo etc
if ( declaration.exportName && declaration.isReassigned ) return;
const name = declaration.getName( true );
let deshadowed = name;
let i = 1;
while ( names.has( deshadowed ) ) {
deshadowed = `${name}$$${i++}`;
}
declaration.name = deshadowed;
});
this.children.forEach( scope => scope.deshadow( names ) );
}
findDeclaration ( name ) {
return this.declarations[ name ] ||
( this.parent && this.parent.findDeclaration( name ) );
}
findLexicalBoundary () {
return this.isLexicalBoundary ? this : this.parent.findLexicalBoundary();
}
}

0
src/ast/extractNames.js → src/ast/utils/extractNames.js

0
src/ast/flatten.js → src/ast/utils/flatten.js

0
src/ast/isReference.js → src/ast/utils/isReference.js

8
src/ast/values.js

@ -0,0 +1,8 @@
// properties are for debugging purposes only
export const ARRAY = { ARRAY: true, toString: () => '[[ARRAY]]' };
export const BOOLEAN = { BOOLEAN: true, toString: () => '[[BOOLEAN]]' };
export const FUNCTION = { FUNCTION: true, toString: () => '[[FUNCTION]]' };
export const NUMBER = { NUMBER: true, toString: () => '[[NUMBER]]' };
export const OBJECT = { OBJECT: true, toString: () => '[[OBJECT]]' };
export const STRING = { STRING: true, toString: () => '[[STRING]]' };
export const UNKNOWN = { UNKNOWN: true, toString: () => '[[UNKNOWN]]' };

8
src/finalisers/es.js

@ -26,8 +26,8 @@ export default function es ( bundle, magicString, { intro }, options ) {
}
}
const namespaceSpecifier = module.declarations['*'] ? `* as ${module.name}` : null;
const namedSpecifier = importedNames.length ? `{ ${importedNames.join( ', ' )} }` : null;
const namespaceSpecifier = module.declarations['*'] ? `* as ${module.name}` : null; // TODO prevent unnecessary namespace import, e.g form/external-imports
const namedSpecifier = importedNames.length ? `{ ${importedNames.sort().join( ', ' )} }` : null;
if ( namespaceSpecifier && namedSpecifier ) {
// Namespace and named specifiers cannot be combined.
@ -56,7 +56,7 @@ export default function es ( bundle, magicString, { intro }, options ) {
const specifiers = module.getExports().filter( notDefault ).map( name => {
const declaration = module.traceExport( name );
const rendered = declaration.render( true );
const rendered = declaration.getName( true );
return rendered === name ?
name :
@ -67,7 +67,7 @@ export default function es ( bundle, magicString, { intro }, options ) {
const defaultExport = module.exports.default || module.reexports.default;
if ( defaultExport ) {
exportBlock += `export default ${module.traceExport( 'default' ).render( true )};`;
exportBlock += `export default ${module.traceExport( 'default' ).getName( true )};`;
}
if ( exportBlock ) magicString.append( '\n\n' + exportBlock.trim() );

6
src/finalisers/shared/getExportBlock.js

@ -1,6 +1,6 @@
export default function getExportBlock ( entryModule, exportMode, mechanism = 'return' ) {
if ( exportMode === 'default' ) {
return `${mechanism} ${entryModule.traceExport( 'default' ).render( false )};`;
return `${mechanism} ${entryModule.traceExport( 'default' ).getName( false )};`;
}
return entryModule.getExports()
@ -9,7 +9,9 @@ export default function getExportBlock ( entryModule, exportMode, mechanism = 'r
const declaration = entryModule.traceExport( name );
const lhs = `exports${prop}`;
const rhs = declaration.render( false );
const rhs = declaration ?
declaration.getName( false ) :
name; // exporting a global
// prevent `exports.count = exports.count`
if ( lhs === rhs ) return null;

21
src/utils/object.js

@ -17,3 +17,24 @@ export function assign ( target, ...sources ) {
return target;
}
const isArray = Array.isArray;
// used for cloning ASTs. Not for use with cyclical structures!
export function deepClone ( obj ) {
if ( !obj ) return obj;
if ( typeof obj !== 'object' ) return obj;
if ( isArray( obj ) ) {
const clone = new Array( obj.length );
for ( let i = 0; i < obj.length; i += 1 ) clone[i] = deepClone( obj[i] );
return clone;
}
const clone = {};
for ( const key in obj ) {
clone[ key ] = deepClone( obj[ key ] );
}
return clone;
}

2
src/utils/path.js

@ -13,4 +13,4 @@ export function normalize ( path ) {
return path.replace( /\\/g, '/' );
}
export * from 'path';
export { basename, dirname, extname, relative, resolve } from 'path';

119
src/utils/run.js

@ -1,119 +0,0 @@
import { walk } from 'estree-walker';
import modifierNodes, { isModifierNode } from '../ast/modifierNodes.js';
import isReference from '../ast/isReference.js';
import flatten from '../ast/flatten';
import pureFunctions from './pureFunctions.js';
import getLocation from './getLocation.js';
import error from './error.js';
function call ( callee, scope, statement, strongDependencies ) {
while ( callee.type === 'ParenthesizedExpression' ) callee = callee.expression;
if ( callee.type === 'Identifier' ) {
const declaration = scope.findDeclaration( callee.name ) ||
statement.module.trace( callee.name );
if ( declaration ) {
if ( declaration.isNamespace ) {
error({
message: `Cannot call a namespace ('${callee.name}')`,
file: statement.module.id,
pos: callee.start,
loc: getLocation( statement.module.code, callee.start )
});
}
return declaration.run( strongDependencies );
}
return !pureFunctions[ callee.name ];
}
if ( /FunctionExpression/.test( callee.type ) ) {
return run( callee.body, scope, statement, strongDependencies );
}
if ( callee.type === 'MemberExpression' ) {
const flattened = flatten( callee );
if ( flattened ) {
// if we're calling e.g. Object.keys(thing), there are no side-effects
// TODO make pureFunctions configurable
const declaration = scope.findDeclaration( flattened.name ) || statement.module.trace( flattened.name );
return ( !!declaration || !pureFunctions[ flattened.keypath ] );
}
}
// complex case like `( a ? b : c )()` or foo[bar].baz()`
// – err on the side of caution
return true;
}
export default function run ( node, scope, statement, strongDependencies, force ) {
let hasSideEffect = false;
walk( node, {
enter ( node, parent ) {
if ( !force && /Function/.test( node.type ) ) return this.skip();
if ( node._scope ) scope = node._scope;
if ( isReference( node, parent ) ) {
const flattened = flatten( node );
if ( flattened.name === 'arguments' ) {
hasSideEffect = true;
}
else if ( !scope.contains( flattened.name ) ) {
const declaration = statement.module.trace( flattened.name );
if ( declaration && !declaration.isExternal ) {
const module = declaration.module || declaration.statement.module; // TODO is this right?
if ( !module.isExternal && !~strongDependencies.indexOf( module ) ) strongDependencies.push( module );
}
}
}
else if ( node.type === 'DebuggerStatement' ) {
hasSideEffect = true;
}
else if ( node.type === 'ThrowStatement' ) {
// we only care about errors thrown at the top level, otherwise
// any function with error checking gets included if called
if ( scope.isTopLevel ) hasSideEffect = true;
}
else if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) {
if ( call( node.callee, scope, statement, strongDependencies ) ) {
hasSideEffect = true;
}
}
else if ( isModifierNode( node ) ) {
let subject = node[ modifierNodes[ node.type ] ];
while ( subject.type === 'MemberExpression' ) subject = subject.object;
let declaration = scope.findDeclaration( subject.name );
if ( declaration ) {
if ( declaration.isParam ) hasSideEffect = true;
} else if ( !scope.isTopLevel ) {
hasSideEffect = true;
} else {
declaration = statement.module.trace( subject.name );
if ( !declaration || declaration.isExternal || declaration.isUsed || ( declaration.original && declaration.original.isUsed ) ) {
hasSideEffect = true;
}
}
}
},
leave ( node ) {
if ( node._scope ) scope = scope.parent;
}
});
return hasSideEffect;
}

2
test/form/assignment-to-exports-class-declaration/_config.js

@ -1,5 +1,5 @@
module.exports = {
description: 'does not rewrite class declaration IDs',
description: 'does not rewrite class expression IDs',
options: {
moduleName: 'myModule'
}

3
test/form/duplicated-var-declarations/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'does not remove duplicated var declarations (#716)'
};

17
test/form/duplicated-var-declarations/_expected/amd.js

@ -0,0 +1,17 @@
define(function () { 'use strict';
var a = 1;
var b = 2;
assert.equal( a, 1 );
assert.equal( b, 2 );
var a = 3;
var b = 4;
var c = 5;
assert.equal( a, 3 );
assert.equal( b, 4 );
assert.equal( c, 5 );
});

15
test/form/duplicated-var-declarations/_expected/cjs.js

@ -0,0 +1,15 @@
'use strict';
var a = 1;
var b = 2;
assert.equal( a, 1 );
assert.equal( b, 2 );
var a = 3;
var b = 4;
var c = 5;
assert.equal( a, 3 );
assert.equal( b, 4 );
assert.equal( c, 5 );

13
test/form/duplicated-var-declarations/_expected/es.js

@ -0,0 +1,13 @@
var a = 1;
var b = 2;
assert.equal( a, 1 );
assert.equal( b, 2 );
var a = 3;
var b = 4;
var c = 5;
assert.equal( a, 3 );
assert.equal( b, 4 );
assert.equal( c, 5 );

18
test/form/duplicated-var-declarations/_expected/iife.js

@ -0,0 +1,18 @@
(function () {
'use strict';
var a = 1;
var b = 2;
assert.equal( a, 1 );
assert.equal( b, 2 );
var a = 3;
var b = 4;
var c = 5;
assert.equal( a, 3 );
assert.equal( b, 4 );
assert.equal( c, 5 );
}());

21
test/form/duplicated-var-declarations/_expected/umd.js

@ -0,0 +1,21 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, (function () { 'use strict';
var a = 1;
var b = 2;
assert.equal( a, 1 );
assert.equal( b, 2 );
var a = 3;
var b = 4;
var c = 5;
assert.equal( a, 3 );
assert.equal( b, 4 );
assert.equal( c, 5 );
})));

10
test/form/duplicated-var-declarations/main.js

@ -0,0 +1,10 @@
var a = 1, b = 2;
assert.equal( a, 1 );
assert.equal( b, 2 );
var a = 3, b = 4, c = 5;
assert.equal( a, 3 );
assert.equal( b, 4 );
assert.equal( c, 5 );

4
test/form/external-imports/_expected/es.js

@ -1,11 +1,11 @@
import factory from 'factory';
import { bar, foo } from 'baz';
import { port } from 'shipping-port';
import { forEach, port } from 'shipping-port';
import * as containers from 'shipping-port';
import alphabet, { a } from 'alphabet';
factory( null );
foo( bar, port );
containers.forEach( console.log, console );
forEach( console.log, console );
console.log( a );
console.log( alphabet.length );

5
test/form/import-external-namespace-and-default/_expected/es.js

@ -1,6 +1,7 @@
import * as foo from 'foo';
import { bar } from 'foo';
import foo__default from 'foo';
import * as foo from 'foo';
console.log( foo.bar );
console.log( bar );
console.log( foo__default );

3
test/form/includes-all-namespace-declarations/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'includes all declarations referenced by reified namespaces'
}

5
test/form/includes-all-namespace-declarations/_expected/amd.js

@ -0,0 +1,5 @@
define(function () { 'use strict';
});

2
test/form/includes-all-namespace-declarations/_expected/cjs.js

@ -0,0 +1,2 @@
'use strict';

0
test/form/includes-all-namespace-declarations/_expected/es.js

6
test/form/includes-all-namespace-declarations/_expected/iife.js

@ -0,0 +1,6 @@
(function () {
'use strict';
}());

9
test/form/includes-all-namespace-declarations/_expected/umd.js

@ -0,0 +1,9 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, (function () { 'use strict';
})));

7
test/form/includes-all-namespace-declarations/indirection.js

@ -0,0 +1,7 @@
import * as unused from './unused.js';
var indirection = {
unused: unused
};
export { indirection };

1
test/form/includes-all-namespace-declarations/main.js

@ -0,0 +1 @@
import { indirection } from './indirection.js';

3
test/form/includes-all-namespace-declarations/unused.js

@ -0,0 +1,3 @@
function foo () {}
export { foo };

2
test/form/namespace-optimization/_expected/amd.js

@ -2,6 +2,6 @@ define(function () { 'use strict';
function a () {}
a();
console.log( a() );
});

2
test/form/namespace-optimization/_expected/cjs.js

@ -2,4 +2,4 @@
function a () {}
a();
console.log( a() );

2
test/form/namespace-optimization/_expected/es.js

@ -1,3 +1,3 @@
function a () {}
a();
console.log( a() );

2
test/form/namespace-optimization/_expected/iife.js

@ -3,6 +3,6 @@
function a () {}
a();
console.log( a() );
}());

4
test/form/namespace-optimization/_expected/umd.js

@ -6,6 +6,6 @@
function a () {}
a();
console.log( a() );
})));
})));

2
test/form/namespace-optimization/main.js

@ -1,3 +1,3 @@
import * as foo from './foo';
foo.bar.quux.a();
console.log( foo.bar.quux.a() );

3
test/form/no-treeshake/_expected/es.js

@ -1,3 +1,4 @@
import { value } from 'external';
import * as external from 'external';
var foo = 'unused';
@ -11,7 +12,7 @@ function bar () {
}
function baz () {
return 13 + external.value;
return 13 + value;
}
var create = Object.create;

2
test/form/removes-existing-sourcemap-comments/_expected/amd.js

@ -1,6 +1,6 @@
define(function () { 'use strict';
function foo () {
var foo = function () {
return 42;
}

2
test/form/removes-existing-sourcemap-comments/_expected/cjs.js

@ -1,6 +1,6 @@
'use strict';
function foo () {
var foo = function () {
return 42;
}

2
test/form/removes-existing-sourcemap-comments/_expected/es.js

@ -1,4 +1,4 @@
function foo () {
var foo = function () {
return 42;
}

2
test/form/removes-existing-sourcemap-comments/_expected/iife.js

@ -1,7 +1,7 @@
(function () {
'use strict';
function foo () {
var foo = function () {
return 42;
}

4
test/form/removes-existing-sourcemap-comments/_expected/umd.js

@ -4,10 +4,10 @@
(factory());
}(this, (function () { 'use strict';
function foo () {
var foo = function () {
return 42;
}
console.log( foo() );
})));
})));

3
test/form/self-calling-function-with-effects/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'discards a self-calling function with side-effects'
};

20
test/form/self-calling-function-with-effects/_expected/amd.js

@ -0,0 +1,20 @@
define(function () { 'use strict';
function foo ( x ) {
effect( x );
if ( x > 0 ) foo( x - 1 );
}
function bar ( x ) {
effect( x );
if ( x > 0 ) baz( x );
}
function baz ( x ) {
bar( x - 1 );
}
foo( 10 );
bar( 10 );
});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save