Browse Source

Merge branch 'master' into gh-860

legacy-quote-reserved-properties
Rich-Harris 8 years ago
parent
commit
ec69703171
  1. 11
      .eslintrc
  2. 86
      CHANGELOG.md
  3. 2
      LICENSE.md
  4. 2
      README.md
  5. 2
      bin/src/help.md
  6. 3
      bin/src/runRollup.js
  7. 16
      package.json
  8. 1
      rollup.config.browser.js
  9. 10
      rollup.config.cli.js
  10. 5
      rollup.config.js
  11. 115
      src/Bundle.js
  12. 242
      src/Declaration.js
  13. 510
      src/Module.js
  14. 30
      src/Reference.js
  15. 160
      src/Statement.js
  16. 94
      src/ast/Node.js
  17. 52
      src/ast/Scope.js
  18. 78
      src/ast/attachScopes.js
  19. 38
      src/ast/conditions.js
  20. 7
      src/ast/create.js
  21. 63
      src/ast/enhance.js
  22. 6
      src/ast/isFunctionDeclaration.js
  23. 4
      src/ast/keys.js
  24. 19
      src/ast/modifierNodes.js
  25. 8
      src/ast/nodes/ArrayExpression.js
  26. 38
      src/ast/nodes/ArrowFunctionExpression.js
  27. 47
      src/ast/nodes/AssignmentExpression.js
  28. 38
      src/ast/nodes/BinaryExpression.js
  29. 49
      src/ast/nodes/BlockStatement.js
  30. 40
      src/ast/nodes/CallExpression.js
  31. 45
      src/ast/nodes/ClassDeclaration.js
  32. 26
      src/ast/nodes/ClassExpression.js
  33. 65
      src/ast/nodes/ConditionalExpression.js
  34. 9
      src/ast/nodes/EmptyStatement.js
  35. 11
      src/ast/nodes/ExportAllDeclaration.js
  36. 96
      src/ast/nodes/ExportDefaultDeclaration.js
  37. 25
      src/ast/nodes/ExportNamedDeclaration.js
  38. 5
      src/ast/nodes/ExpressionStatement.js
  39. 22
      src/ast/nodes/ForInStatement.js
  40. 22
      src/ast/nodes/ForOfStatement.js
  41. 23
      src/ast/nodes/ForStatement.js
  42. 53
      src/ast/nodes/FunctionDeclaration.js
  43. 21
      src/ast/nodes/FunctionExpression.js
  44. 35
      src/ast/nodes/Identifier.js
  45. 55
      src/ast/nodes/IfStatement.js
  46. 16
      src/ast/nodes/ImportDeclaration.js
  47. 17
      src/ast/nodes/Literal.js
  48. 74
      src/ast/nodes/MemberExpression.js
  49. 8
      src/ast/nodes/NewExpression.js
  50. 8
      src/ast/nodes/ObjectExpression.js
  51. 11
      src/ast/nodes/ParenthesizedExpression.js
  52. 7
      src/ast/nodes/ReturnStatement.js
  53. 8
      src/ast/nodes/TemplateLiteral.js
  54. 20
      src/ast/nodes/ThisExpression.js
  55. 7
      src/ast/nodes/ThrowStatement.js
  56. 34
      src/ast/nodes/UnaryExpression.js
  57. 40
      src/ast/nodes/UpdateExpression.js
  58. 100
      src/ast/nodes/VariableDeclaration.js
  59. 91
      src/ast/nodes/VariableDeclarator.js
  60. 78
      src/ast/nodes/index.js
  61. 16
      src/ast/nodes/shared/Statement.js
  62. 29
      src/ast/nodes/shared/assignTo.js
  63. 67
      src/ast/nodes/shared/callHasEffects.js
  64. 28
      src/ast/nodes/shared/disallowIllegalReassignment.js
  65. 40
      src/ast/nodes/shared/isUsedByBundle.js
  66. 0
      src/ast/nodes/shared/pureFunctions.js
  67. 40
      src/ast/scopes/BundleScope.js
  68. 53
      src/ast/scopes/ModuleScope.js
  69. 98
      src/ast/scopes/Scope.js
  70. 0
      src/ast/utils/extractNames.js
  71. 0
      src/ast/utils/flatten.js
  72. 0
      src/ast/utils/isReference.js
  73. 8
      src/ast/values.js
  74. 10
      src/finalisers/amd.js
  75. 9
      src/finalisers/cjs.js
  76. 15
      src/finalisers/es.js
  77. 22
      src/finalisers/iife.js
  78. 6
      src/finalisers/shared/getExportBlock.js
  79. 4
      src/finalisers/shared/getInteropBlock.js
  80. 14
      src/finalisers/umd.js
  81. 14
      src/rollup.js
  82. 17
      src/utils/defaults.js
  83. 55
      src/utils/flushTime.js
  84. 21
      src/utils/object.js
  85. 2
      src/utils/path.js
  86. 119
      src/utils/run.js
  87. 4
      src/utils/transformBundle.js
  88. 2
      test/form/assignment-to-exports-class-declaration/_config.js
  89. 3
      test/form/body-less-for-loops/_config.js
  90. 16
      test/form/body-less-for-loops/_expected/amd.js
  91. 14
      test/form/body-less-for-loops/_expected/cjs.js
  92. 12
      test/form/body-less-for-loops/_expected/es.js
  93. 17
      test/form/body-less-for-loops/_expected/iife.js
  94. 20
      test/form/body-less-for-loops/_expected/umd.js
  95. 12
      test/form/body-less-for-loops/main.js
  96. 3
      test/form/duplicated-var-declarations/_config.js
  97. 17
      test/form/duplicated-var-declarations/_expected/amd.js
  98. 15
      test/form/duplicated-var-declarations/_expected/cjs.js
  99. 13
      test/form/duplicated-var-declarations/_expected/es.js
  100. 18
      test/form/duplicated-var-declarations/_expected/iife.js

11
.eslintrc

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

86
CHANGELOG.md

@ -1,5 +1,91 @@
# rollup changelog # rollup changelog
## 0.35.15
* Warn on missing unused imports in deshadowing phase ([#928](https://github.com/rollup/rollup/issues/928))
* Always add a newline to the end of bundles ([#958](https://github.com/rollup/rollup/issues/958))
## 0.35.14
* Include all parent statements of expression with effects, up to function boundary ([#930](https://github.com/rollup/rollup/issues/930))
## 0.35.13
* Include superclasses when including their subclasses ([#932](https://github.com/rollup/rollup/issues/932))
## 0.35.12
* Add `interop: false` option to disable unwrapping of external imports ([#939](https://github.com/rollup/rollup/issues/939))
## 0.35.11
* Deconflict reified namespaces with other declarations ([#910](https://github.com/rollup/rollup/issues/910))
## 0.35.10
* Only remove EmptyStatement nodes directly inside blocks ([#913](https://github.com/rollup/rollup/issues/931))
## 0.35.9
* Support Node 0.12 ([#909](https://github.com/rollup/rollup/issues/909))
## 0.35.8
* Correctly deshadow re-assigned module functions ([#910](https://github.com/rollup/rollup/issues/910))
## 0.35.7
* Refactor `flushTime.js` ([#922](https://github.com/rollup/rollup/pull/922))
## 0.35.6
* Fix browser build
## 0.35.5
* Allow empty for loop heads ([#919](https://github.com/rollup/rollup/issues/919))
## 0.35.4
* Preserve effects in for-of and for-in loops ([#870](https://github.com/rollup/rollup/issues/870))
* Remove empty statements ([#918](https://github.com/rollup/rollup/pull/918))
## 0.35.3
* Render identifiers inside template literals
## 0.35.2
* Fix broken build caused by out of date locally installed dependencies
## 0.35.1
* Rewrite deconflicted class identifiers ([#915](https://github.com/rollup/rollup/pull/915))
* Include `dependencies` in `bundle.modules` objects ([#903](https://github.com/rollup/rollup/issues/903))
* Update to Acorn 4 ([#914](https://github.com/rollup/rollup/pull/914))
## 0.35.0
* Rewrite analysis/tree-shaking code ([#902](https://github.com/rollup/rollup/pull/902))
* Include conditional mutations of global objects ([#901](https://github.com/rollup/rollup/issues/901))
* Only reify namespaces if necessary ([#898](https://github.com/rollup/rollup/issues/898))
* Track mutations of aliased globals ([#893](https://github.com/rollup/rollup/issues/893))
* Include duplicated var declarations ([#716](https://github.com/rollup/rollup/issues/716))
## 0.34.13
* Pass `{ format }` through to `transformBundle` ([#867](https://github.com/rollup/rollup/issues/867))
## 0.34.12
* Fix `rollup --watch` ([#887](https://github.com/rollup/rollup/issues/887))
* Case-sensitive paths ([#862](https://github.com/rollup/rollup/issues/862))
## 0.34.11
* Prevent leaky state when `bundle` is reused ([#875](https://github.com/rollup/rollup/issues/875))
* Ensure `intro` appears before interop block ([#880](https://github.com/rollup/rollup/issues/880))
## 0.34.10 ## 0.34.10
* Allow custom `options.context` to replace top-level `this` ([#851](https://github.com/rollup/rollup/issues/851)) * Allow custom `options.context` to replace top-level `this` ([#851](https://github.com/rollup/rollup/issues/851))

2
LICENSE.md

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015 [these people](https://github.com/rollup/rollup/graphs/contributors) Copyright (c) 2016 [these people](https://github.com/rollup/rollup/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

2
README.md

@ -18,7 +18,7 @@
alt="dependency status"> alt="dependency status">
</a> </a>
<a href="https://codecov.io/github/rollup/rollup?branch=master"> <a href="https://codecov.io/github/rollup/rollup?branch=master">
<img src="https://codecov.io/github/rollup/rollup/coverage.svg?branch=master" alt="Coverage via Codecov" /> <img src="https://codecov.io/gh/rollup/rollup/branch/master/graph/badge.svg" alt="Coverage via Codecov" />
</a> </a>
<a href='https://gitter.im/rollup/rollup?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge'> <a href='https://gitter.im/rollup/rollup?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge'>
<img src='https://badges.gitter.im/rollup/rollup.svg' <img src='https://badges.gitter.im/rollup/rollup.svg'

2
bin/src/help.md

@ -12,7 +12,7 @@ Basic options:
-w, --watch Watch files in bundle and rebuild on changes -w, --watch Watch files in bundle and rebuild on changes
-i, --input Input (alternative to <entry file>) -i, --input Input (alternative to <entry file>)
-o, --output <output> Output (if absent, prints to stdout) -o, --output <output> Output (if absent, prints to stdout)
-f, --format [es6] Type of output (amd, cjs, es6, iife, umd) -f, --format [es] Type of output (amd, cjs, es, iife, umd)
-e, --external Comma-separate list of module IDs to exclude -e, --external Comma-separate list of module IDs to exclude
-g, --globals Comma-separate list of `module ID:Global` pairs -g, --globals Comma-separate list of `module ID:Global` pairs
Any module IDs defined here are added to external Any module IDs defined here are added to external

3
bin/src/runRollup.js

@ -1,10 +1,9 @@
import { realpathSync } from 'fs'; import { realpathSync } from 'fs';
import * as rollup from 'rollup';
import relative from 'require-relative'; import relative from 'require-relative';
import handleError from './handleError'; import handleError from './handleError';
import SOURCEMAPPING_URL from './sourceMappingUrl.js'; import SOURCEMAPPING_URL from './sourceMappingUrl.js';
const rollup = require( '../dist/rollup.js' ); // TODO make this an import, somehow
import { install as installSourcemapSupport } from 'source-map-support'; import { install as installSourcemapSupport } from 'source-map-support';
installSourcemapSupport(); installSourcemapSupport();

16
package.json

@ -1,6 +1,6 @@
{ {
"name": "rollup", "name": "rollup",
"version": "0.34.10", "version": "0.35.15",
"description": "Next-generation ES6 module bundler", "description": "Next-generation ES6 module bundler",
"main": "dist/rollup.js", "main": "dist/rollup.js",
"module": "dist/rollup.es.js", "module": "dist/rollup.es.js",
@ -11,14 +11,17 @@
"scripts": { "scripts": {
"pretest": "npm run build && npm run build:cli", "pretest": "npm run build && npm run build:cli",
"test": "mocha", "test": "mocha",
"test:quick": "rollup -c && mocha",
"pretest-coverage": "npm run build", "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", "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", "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", "ci": "npm run test-coverage && codecov < coverage/coverage-remapped.lcov",
"build": "git rev-parse HEAD > .commithash && rollup -c", "build": "git rev-parse HEAD > .commithash && rollup -c",
"watch": "rollup -c -w",
"build:cli": "rollup -c rollup.config.cli.js", "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", "prepublish": "npm run lint && npm test && npm run build:browser",
"lint": "eslint src browser test/test.js test/utils test/**/_config.js" "lint": "eslint src browser test/test.js test/utils test/**/_config.js"
}, },
@ -44,12 +47,13 @@
}, },
"homepage": "https://github.com/rollup/rollup", "homepage": "https://github.com/rollup/rollup",
"devDependencies": { "devDependencies": {
"acorn": "^3.2.0", "acorn": "^4.0.1",
"buble": "^0.12.5", "buble": "^0.12.5",
"chalk": "^1.1.3", "chalk": "^1.1.3",
"codecov.io": "^0.1.6", "codecov.io": "^0.1.6",
"console-group": "^0.2.1", "console-group": "^0.3.1",
"eslint": "^2.13.0", "eslint": "^2.13.0",
"eslint-plugin-import": "^1.14.0",
"estree-walker": "^0.2.1", "estree-walker": "^0.2.1",
"istanbul": "^0.4.3", "istanbul": "^0.4.3",
"magic-string": "^0.15.2", "magic-string": "^0.15.2",
@ -57,7 +61,7 @@
"mocha": "^3.0.0", "mocha": "^3.0.0",
"remap-istanbul": "^0.6.4", "remap-istanbul": "^0.6.4",
"require-relative": "^0.8.7", "require-relative": "^0.8.7",
"rollup": "^0.34.2", "rollup": "^0.34.0",
"rollup-plugin-buble": "^0.12.1", "rollup-plugin-buble": "^0.12.1",
"rollup-plugin-commonjs": "^3.0.0", "rollup-plugin-commonjs": "^3.0.0",
"rollup-plugin-json": "^2.0.0", "rollup-plugin-json": "^2.0.0",

1
rollup.config.browser.js

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

10
rollup.config.cli.js

@ -15,7 +15,7 @@ export default {
buble(), buble(),
commonjs({ commonjs({
include: 'node_modules/**', include: 'node_modules/**',
namedExports: { 'chalk': [ 'red', 'cyan', 'grey' ] } namedExports: { chalk: [ 'red', 'cyan', 'grey' ] }
}), }),
nodeResolve({ nodeResolve({
main: true main: true
@ -25,6 +25,10 @@ export default {
'fs', 'fs',
'path', 'path',
'module', 'module',
'source-map-support' 'source-map-support',
] 'rollup'
],
paths: {
rollup: '../dist/rollup.js'
}
}; };

5
rollup.config.js

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

115
src/Bundle.js

@ -1,3 +1,4 @@
import { timeStart, timeEnd } from './utils/flushTime.js';
import { decode } from 'sourcemap-codec'; import { decode } from 'sourcemap-codec';
import { Bundle as MagicStringBundle } from 'magic-string'; import { Bundle as MagicStringBundle } from 'magic-string';
import first from './utils/first.js'; import first from './utils/first.js';
@ -17,6 +18,7 @@ import collapseSourcemaps from './utils/collapseSourcemaps.js';
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js'; import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
import callIfFunction from './utils/callIfFunction.js'; import callIfFunction from './utils/callIfFunction.js';
import { dirname, isRelative, isAbsolute, normalize, relative, resolve } from './utils/path.js'; import { dirname, isRelative, isAbsolute, normalize, relative, resolve } from './utils/path.js';
import BundleScope from './ast/scopes/BundleScope.js';
export default class Bundle { export default class Bundle {
constructor ( options ) { constructor ( options ) {
@ -59,14 +61,17 @@ export default class Bundle {
( id => options.paths.hasOwnProperty( id ) ? options.paths[ id ] : this.getPathRelativeToEntryDirname( id ) ) : ( id => options.paths.hasOwnProperty( id ) ? options.paths[ id ] : this.getPathRelativeToEntryDirname( id ) ) :
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.moduleById = new Map();
this.modules = []; this.modules = [];
this.externalModules = []; this.externalModules = [];
this.internalNamespaces = [];
this.context = String( options.context ); this.context = String( options.context );
this.assumedGlobals = blank();
if ( typeof options.external === 'function' ) { if ( typeof options.external === 'function' ) {
this.isExternal = options.external; this.isExternal = options.external;
@ -77,11 +82,10 @@ export default class Bundle {
this.onwarn = options.onwarn || makeOnwarn(); 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.varOrConst = options.preferConst ? 'const' : 'var';
this.acornOptions = options.acorn || {}; this.acornOptions = options.acorn || {};
this.dependentExpressions = [];
} }
build () { build () {
@ -99,36 +103,71 @@ export default class Bundle {
// Phase 2 – binding. We link references to their declarations // Phase 2 – binding. We link references to their declarations
// to generate a complete picture of the bundle // to generate a complete picture of the bundle
timeStart( 'phase 2' );
this.modules.forEach( module => module.bindImportSpecifiers() ); this.modules.forEach( module => module.bindImportSpecifiers() );
this.modules.forEach( module => module.bindAliases() );
this.modules.forEach( module => module.bindReferences() ); this.modules.forEach( module => module.bindReferences() );
timeEnd( 'phase 2' );
// Phase 3 – marking. We 'run' each statement to see which ones // Phase 3 – marking. We 'run' each statement to see which ones
// need to be included in the generated bundle // need to be included in the generated bundle
timeStart( 'phase 3' );
// mark all export statements // mark all export statements
entryModule.getExports().forEach( name => { entryModule.getExports().forEach( name => {
const declaration = entryModule.traceExport( name ); const declaration = entryModule.traceExport( name );
declaration.exportName = name; declaration.exportName = name;
declaration.activate();
declaration.use(); if ( declaration.isNamespace ) {
declaration.needsNamespaceBlock = true;
}
}); });
// mark statements that should appear in the bundle // mark statements that should appear in the bundle
let settled = false; if ( this.treeshake ) {
while ( !settled ) {
settled = true;
this.modules.forEach( module => { 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];
let statement = expression;
while ( statement.parent && !/Function/.test( statement.parent.type ) ) statement = statement.parent;
if ( !statement || statement.ran ) {
this.dependentExpressions.splice( i, 1 );
} else if ( expression.isUsedByBundle() ) {
settled = false;
statement.run( statement.findScope() );
this.dependentExpressions.splice( i, 1 );
}
}
}
} }
timeEnd( 'phase 3' );
// Phase 4 – final preparation. We order the modules with an // Phase 4 – final preparation. We order the modules with an
// enhanced topological sort that accounts for cycles, then // enhanced topological sort that accounts for cycles, then
// ensure that names are deconflicted throughout the bundle // ensure that names are deconflicted throughout the bundle
timeStart( 'phase 4' );
this.orderedModules = this.sort(); this.orderedModules = this.sort();
this.deconflict(); this.deconflict();
timeEnd( 'phase 4' );
}); });
} }
@ -136,7 +175,7 @@ export default class Bundle {
const used = blank(); const used = blank();
// ensure no conflicts with globals // 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 ) { function getSafeName ( name ) {
while ( used[ name ] ) { while ( used[ name ] ) {
@ -147,27 +186,39 @@ export default class Bundle {
return name; return name;
} }
const toDeshadow = new Map();
this.externalModules.forEach( module => { 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 // ensure we don't shadow named external imports, if
// we're creating an ES6 bundle // we're creating an ES6 bundle
forOwn( module.declarations, ( declaration, name ) => { forOwn( module.declarations, ( declaration, name ) => {
declaration.setSafeName( getSafeName( name ) ); const safeName = getSafeName( name );
toDeshadow.set( safeName, true );
declaration.setSafeName( safeName );
}); });
}); });
this.modules.forEach( module => { this.modules.forEach( module => {
forOwn( module.declarations, ( declaration, originalName ) => { forOwn( module.scope.declarations, ( declaration ) => {
if ( declaration.isGlobal ) return; if ( declaration.isDefault && declaration.declaration.id ) {
return;
if ( originalName === 'default' ) {
if ( declaration.original && !declaration.original.isReassigned ) return;
} }
declaration.name = getSafeName( declaration.name ); declaration.name = getSafeName( declaration.name );
}); });
// deconflict reified namespaces
const namespace = module.namespace();
if ( namespace.needsNamespaceBlock ) {
namespace.name = getSafeName( namespace.name );
}
}); });
this.scope.deshadow( toDeshadow );
} }
fetchModule ( id, importer ) { fetchModule ( id, importer ) {
@ -304,29 +355,38 @@ export default class Bundle {
let magicString = new MagicStringBundle({ separator: '\n\n' }); let magicString = new MagicStringBundle({ separator: '\n\n' });
const usedModules = []; const usedModules = [];
timeStart( 'render modules' );
this.orderedModules.forEach( module => { this.orderedModules.forEach( module => {
const source = module.render( format === 'es' ); const source = module.render( format === 'es' );
if ( source.toString().length ) { if ( source.toString().length ) {
magicString.addSource( source ); magicString.addSource( source );
usedModules.push( module ); usedModules.push( module );
} }
}); });
const intro = [ options.intro ] timeEnd( 'render modules' );
let intro = [ options.intro ]
.concat( .concat(
this.plugins.map( plugin => plugin.intro && plugin.intro() ) this.plugins.map( plugin => plugin.intro && plugin.intro() )
) )
.filter( Boolean ) .filter( Boolean )
.join( '\n\n' ); .join( '\n\n' );
if ( intro ) magicString.prepend( intro + '\n' ); if ( intro ) intro += '\n';
const indentString = getIndentString( magicString, options ); const indentString = getIndentString( magicString, options );
const finalise = finalisers[ format ]; const finalise = finalisers[ format ];
if ( !finalise ) throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` ); if ( !finalise ) throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options ); timeStart( 'render format' );
magicString = finalise( this, magicString.trim(), { exportMode, indentString, intro }, options );
timeEnd( 'render format' );
const banner = [ options.banner ] const banner = [ options.banner ]
.concat( this.plugins.map( plugin => plugin.banner ) ) .concat( this.plugins.map( plugin => plugin.banner ) )
@ -347,10 +407,12 @@ export default class Bundle {
let map = null; let map = null;
const bundleSourcemapChain = []; const bundleSourcemapChain = [];
code = transformBundle( code, this.plugins, bundleSourcemapChain ) code = transformBundle( code, this.plugins, bundleSourcemapChain, options )
.replace( new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ), '' ); .replace( new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ), '' );
if ( options.sourceMap ) { if ( options.sourceMap ) {
timeStart( 'sourceMap' );
let file = options.sourceMapFile || options.dest; let file = options.sourceMapFile || options.dest;
if ( file ) file = resolve( typeof process !== 'undefined' ? process.cwd() : '', file ); if ( file ) file = resolve( typeof process !== 'undefined' ? process.cwd() : '', file );
@ -365,8 +427,11 @@ export default class Bundle {
} }
map.sources = map.sources.map( normalize ); map.sources = map.sources.map( normalize );
timeEnd( 'sourceMap' );
} }
if ( code[ code.length - 1 ] !== '\n' ) code += '\n';
return { code, map }; return { code, map };
} }

242
src/Declaration.js

@ -1,35 +1,24 @@
import { blank, forOwn, keys } from './utils/object.js'; import { blank, forOwn, keys } from './utils/object.js';
import makeLegalIdentifier from './utils/makeLegalIdentifier.js'; import makeLegalIdentifier from './utils/makeLegalIdentifier.js';
import run from './utils/run.js'; import { UNKNOWN } from './ast/values.js';
import { SyntheticReference } from './Reference.js';
const use = alias => alias.use();
export default class Declaration { export default class Declaration {
constructor ( node, isParam, statement ) { constructor ( node, isParam ) {
if ( node ) { this.node = 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;
}
}
this.statement = statement;
this.name = node.id ? node.id.name : node.name; this.name = node.id ? node.id.name : node.name;
this.exportName = null; this.exportName = null;
this.isParam = isParam; this.isParam = isParam;
this.isReassigned = false; this.isReassigned = false;
this.aliases = [];
this.isUsed = false;
} }
addAlias ( declaration ) { activate () {
this.aliases.push( declaration ); if ( this.activated ) return;
this.activated = true;
if ( this.isParam ) return;
this.node.activate();
} }
addReference ( reference ) { addReference ( reference ) {
@ -48,142 +37,15 @@ export default class Declaration {
return `exports.${this.exportName}`; 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 { export class SyntheticNamespaceDeclaration {
constructor ( module ) { constructor ( module ) {
this.isNamespace = true; this.isNamespace = true;
this.module = module; this.module = module;
this.name = null; this.name = module.basename();
this.needsNamespaceBlock = false; this.needsNamespaceBlock = false;
this.aliases = [];
this.originals = blank(); this.originals = blank();
module.getExports().forEach( name => { module.getExports().forEach( name => {
@ -191,70 +53,40 @@ export class SyntheticNamespaceDeclaration {
}); });
} }
addAlias ( declaration ) { activate () {
this.aliases.push( declaration ); this.needsNamespaceBlock = true;
}
addReference ( reference ) { // add synthetic references, in case of chained
// if we have e.g. `foo.bar`, we can optimise // namespace imports
// the reference by pointing directly to `bar` forOwn( this.originals, original => {
if ( reference.parts.length ) { original.activate();
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;
}
original.addReference( reference ); addReference ( node ) {
return; this.name = node.name;
} }
// otherwise we're accessing the namespace directly, gatherPossibleValues ( values ) {
// which means we need to mark all of this module's values.add( UNKNOWN );
// 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 ) );
});
}
reference.declaration = this; getName () {
this.name = reference.name; return this.name;
} }
renderBlock ( indentString ) { renderBlock ( es, indentString ) {
const members = keys( this.originals ).map( name => { const members = keys( this.originals ).map( name => {
const original = this.originals[ name ]; const original = this.originals[ name ];
if ( original.isReassigned ) { 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`; return `${this.module.bundle.varOrConst} ${this.getName( es )} = Object.freeze({\n${members.join( ',\n' )}\n});\n\n`;
}
render () {
return this.name;
}
use () {
forOwn( this.originals, use );
this.aliases.forEach( use );
} }
} }
@ -265,10 +97,12 @@ export class ExternalDeclaration {
this.safeName = null; this.safeName = null;
this.isExternal = true; this.isExternal = true;
this.activated = true;
this.isNamespace = name === '*'; this.isNamespace = name === '*';
} }
addAlias () { activate () {
// noop // noop
} }
@ -280,7 +114,7 @@ export class ExternalDeclaration {
} }
} }
render ( es ) { getName ( es ) {
if ( this.name === '*' ) { if ( this.name === '*' ) {
return this.module.name; return this.module.name;
} }
@ -294,15 +128,7 @@ export class ExternalDeclaration {
return es ? this.safeName : `${this.module.name}.${this.name}`; return es ? this.safeName : `${this.module.name}.${this.name}`;
} }
run () {
return true;
}
setSafeName ( name ) { setSafeName ( name ) {
this.safeName = name; this.safeName = name;
} }
use () {
// noop?
}
} }

510
src/Module.js

@ -1,20 +1,31 @@
import { timeStart, timeEnd } from './utils/flushTime.js';
import { parse } from 'acorn/src/index.js'; import { parse } from 'acorn/src/index.js';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { walk } from 'estree-walker'; import { assign, blank, deepClone, keys } from './utils/object.js';
import Statement from './Statement.js';
import { assign, blank, keys } from './utils/object.js';
import { basename, extname } from './utils/path.js'; import { basename, extname } from './utils/path.js';
import getLocation from './utils/getLocation.js'; import getLocation from './utils/getLocation.js';
import makeLegalIdentifier from './utils/makeLegalIdentifier.js'; import makeLegalIdentifier from './utils/makeLegalIdentifier.js';
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js'; import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
import { import { SyntheticNamespaceDeclaration } from './Declaration.js';
SyntheticDefaultDeclaration, import extractNames from './ast/utils/extractNames.js';
SyntheticGlobalDeclaration, import enhance from './ast/enhance.js';
SyntheticNamespaceDeclaration import ModuleScope from './ast/scopes/ModuleScope.js';
} from './Declaration.js';
import { isFalsy, isTruthy } from './ast/conditions.js'; function tryParse ( code, comments, acornOptions, id ) {
import { emptyBlockStatement } from './ast/create.js'; try {
import extractNames from './ast/extractNames.js'; 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 { export default class Module {
constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) { constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) {
@ -23,6 +34,15 @@ export default class Module {
this.originalSourceMap = originalSourceMap; this.originalSourceMap = originalSourceMap;
this.sourceMapChain = sourceMapChain; this.sourceMapChain = sourceMapChain;
this.comments = [];
timeStart( 'ast' );
this.ast = ast || tryParse( code, this.comments, bundle.acornOptions, id ); // TODO what happens to comments if AST is provided?
this.astClone = deepClone( this.ast );
timeEnd( 'ast' );
this.bundle = bundle; this.bundle = bundle;
this.id = id; this.id = id;
this.excludeFromSourcemap = /\0/.test( id ); this.excludeFromSourcemap = /\0/.test( id );
@ -55,18 +75,20 @@ export default class Module {
this.magicString.remove( match.index, match.index + match[0].length ); this.magicString.remove( match.index, match.index + match[0].length );
} }
this.comments = [];
this.ast = ast;
this.statements = this.parse();
this.declarations = blank(); 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 );
timeStart( 'analyse' );
this.analyse(); this.analyse();
timeEnd( 'analyse' );
this.strongDependencies = []; this.strongDependencies = [];
} }
addExport ( statement ) { addExport ( node ) {
const node = statement.node;
const source = node.source && node.source.value; const source = node.source && node.source.value;
// export { name } from './other.js' // export { name } from './other.js'
@ -114,7 +136,7 @@ export default class Module {
}; };
// create a synthetic declaration // 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 } = ... // export var { foo, bar } = ...
@ -156,8 +178,7 @@ export default class Module {
} }
} }
addImport ( statement ) { addImport ( node ) {
const node = statement.node;
const source = node.source.value; const source = node.source.value;
if ( !~this.sources.indexOf( source ) ) this.sources.push( source ); if ( !~this.sources.indexOf( source ) ) this.sources.push( source );
@ -181,17 +202,21 @@ export default class Module {
} }
analyse () { analyse () {
enhance( this.ast, this, this.comments );
// discover this module's imports and exports // discover this module's imports and exports
this.statements.forEach( statement => { let lastNode;
if ( statement.isImportDeclaration ) this.addImport( statement );
else if ( statement.isExportDeclaration ) this.addExport( statement );
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 ) => { if ( lastNode ) lastNode.next = node.leadingCommentStart || node.start;
this.declarations[ name ] = declaration; lastNode = node;
}); }
});
} }
basename () { basename () {
@ -201,27 +226,6 @@ export default class Module {
return makeLegalIdentifier( ext ? base.slice( 0, -ext.length ) : base ); 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 () { bindImportSpecifiers () {
[ this.imports, this.reexports ].forEach( specifiers => { [ this.imports, this.reexports ].forEach( specifiers => {
keys( specifiers ).forEach( name => { keys( specifiers ).forEach( name => {
@ -246,32 +250,25 @@ export default class Module {
} }
bindReferences () { bindReferences () {
if ( this.declarations.default ) { for ( const node of this.ast.body ) {
if ( this.exports.default.identifier ) { node.bind( this.scope );
const declaration = this.trace( this.exports.default.identifier );
if ( declaration ) this.declarations.default.bind( declaration );
}
} }
this.statements.forEach( statement => { // if ( this.declarations.default ) {
// skip `export { foo, bar, baz }`... // if ( this.exports.default.identifier ) {
if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) { // const declaration = this.trace( this.exports.default.identifier );
// ...unless this is the entry module // if ( declaration ) this.declarations.default.bind( declaration );
if ( this !== this.bundle.entryModule ) return; // }
} // }
}
statement.references.forEach( reference => { findParent () {
const declaration = reference.scope.findDeclaration( reference.name ) || // TODO what does it mean if we're here?
this.trace( reference.name ); return null;
}
if ( declaration ) { findScope () {
declaration.addReference( reference ); return this.scope;
} else {
// TODO handle globals
this.bundle.assumedGlobals[ reference.name ] = true;
}
});
});
} }
getExports () { getExports () {
@ -286,6 +283,8 @@ export default class Module {
}); });
this.exportAllModules.forEach( module => { this.exportAllModules.forEach( module => {
if ( module.isExternal ) return; // TODO
module.getExports().forEach( name => { module.getExports().forEach( name => {
if ( name !== 'default' ) exports[ name ] = true; if ( name !== 'default' ) exports[ name ] = true;
}); });
@ -302,374 +301,46 @@ export default class Module {
return this.declarations['*']; 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 ) { render ( es ) {
const magicString = this.magicString; 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' ) { for ( const node of this.ast.body ) {
const defaultDeclaration = this.declarations.default; node.render( magicString, es );
}
// 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' );
}
}
});
// add namespace block if necessary if ( this.namespace().needsNamespaceBlock ) {
const namespace = this.declarations['*']; magicString.append( '\n\n' + this.namespace().renderBlock( es, '\t' ) ); // TODO use correct indentation
if ( namespace && namespace.needsNamespaceBlock ) {
magicString.append( '\n\n' + namespace.renderBlock( magicString.getIndentString() ) );
} }
return magicString.trim(); return magicString.trim();
} }
/** run () {
* Statically runs the module marking the top-level statements that must be for ( const node of this.ast.body ) {
* included for the module to execute successfully. if ( node.hasEffects( this.scope ) ) {
* node.run( this.scope );
* @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;
} }
let marked = false;
this.statements.forEach( statement => {
marked = statement.run( this.strongDependencies ) || marked;
});
return marked;
} }
toJSON () { toJSON () {
return { return {
id: this.id, id: this.id,
dependencies: this.dependencies.map( module => module.id ),
code: this.code, code: this.code,
originalCode: this.originalCode, originalCode: this.originalCode,
ast: this.ast, ast: this.astClone,
sourceMapChain: this.sourceMapChain, sourceMapChain: this.sourceMapChain,
resolvedIds: this.resolvedIds resolvedIds: this.resolvedIds
}; };
} }
trace ( name ) { 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 ) { if ( name in this.imports ) {
const importDeclaration = this.imports[ name ]; const importDeclaration = this.imports[ name ];
const otherModule = importDeclaration.module; const otherModule = importDeclaration.module;
@ -708,10 +379,7 @@ export default class Module {
const name = exportDeclaration.localName; const name = exportDeclaration.localName;
const declaration = this.trace( name ); const declaration = this.trace( name );
if ( declaration ) return declaration; return declaration || this.bundle.scope.findDeclaration( name );
this.bundle.assumedGlobals[ name ] = true;
return ( this.declarations[ name ] = new SyntheticGlobalDeclaration( name ) );
} }
for ( let i = 0; i < this.exportAllModules.length; i += 1 ) { 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 );
}
}

94
src/ast/Node.js

@ -0,0 +1,94 @@
import { UNKNOWN } from './values.js';
import getLocation from '../utils/getLocation.js';
export default class Node {
bind ( scope ) {
this.eachChild( child => child.bind( this.scope || 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 ) {
if ( this.scope ) scope = this.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( this.scope || 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( this.scope || 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 );
}
}

38
src/ast/nodes/ArrowFunctionExpression.js

@ -0,0 +1,38 @@
import Node from '../Node.js';
import Scope from '../scopes/Scope.js';
import extractNames from '../utils/extractNames.js';
export default class ArrowFunctionExpression extends Node {
bind ( scope ) {
super.bind( this.scope || scope );
}
findScope ( functionScope ) {
return this.scope || this.parent.findScope( functionScope );
}
hasEffects () {
return false;
}
initialise ( scope ) {
if ( this.body.type === 'BlockStatement' ) {
this.body.createScope( scope );
this.scope = this.body.scope;
} else {
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( this.scope );
}
}

47
src/ast/nodes/AssignmentExpression.js

@ -0,0 +1,47 @@
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.scope = scope;
this.module.bundle.dependentExpressions.push( this );
super.initialise( scope );
}
isUsedByBundle () {
return isUsedByBundle( this.scope, 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 );
}
}

49
src/ast/nodes/BlockStatement.js

@ -0,0 +1,49 @@
import Statement from './shared/Statement.js';
import Scope from '../scopes/Scope.js';
import extractNames from '../utils/extractNames.js';
export default class BlockStatement extends Statement {
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({
parent,
isBlockScope: !this.isFunctionBlock,
isLexicalBoundary: this.isFunctionBlock && this.parent.type !== 'ArrowFunctionExpression',
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;
}
initialise ( scope ) {
if ( !this.scope ) this.createScope( scope ); // 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;
}
}
}

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() );
}
}

45
src/ast/nodes/ClassDeclaration.js

@ -0,0 +1,45 @@
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;
if ( this.superClass ) this.superClass.run( this.scope );
this.body.run();
}
addReference () {
/* noop? */
}
gatherPossibleValues ( values ) {
values.add( this );
}
getName () {
return this.name;
}
hasEffects () {
return false;
}
initialise ( scope ) {
this.scope = scope;
this.name = this.id.name;
scope.addDeclaration( this.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 );
}
}
}
}

9
src/ast/nodes/EmptyStatement.js

@ -0,0 +1,9 @@
import Statement from './shared/Statement.js';
export default class EmptyStatement extends Statement {
render ( code ) {
if ( this.parent.type === 'BlockStatement' || this.parent.type === 'Program' ) {
code.remove( this.start, this.end );
}
}
}

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 );
}
}
}

5
src/ast/nodes/ExpressionStatement.js

@ -0,0 +1,5 @@
import Statement from './shared/Statement.js';
export default class ExpressionStatement extends Statement {
}

22
src/ast/nodes/ForInStatement.js

@ -0,0 +1,22 @@
import Statement from './shared/Statement.js';
import assignTo from './shared/assignTo.js';
import Scope from '../scopes/Scope.js';
import { STRING } from '../values.js';
export default class ForInStatement extends Statement {
initialise ( scope ) {
if ( this.body.type === 'BlockStatement' ) {
this.body.createScope( scope );
this.scope = this.body.scope;
} else {
this.scope = new Scope({
parent: scope,
isBlockScope: true,
isLexicalBoundary: false
});
}
super.initialise( this.scope );
assignTo( this.left, this.scope, STRING );
}
}

22
src/ast/nodes/ForOfStatement.js

@ -0,0 +1,22 @@
import Statement from './shared/Statement.js';
import assignTo from './shared/assignTo.js';
import Scope from '../scopes/Scope.js';
import { UNKNOWN } from '../values.js';
export default class ForOfStatement extends Statement {
initialise ( scope ) {
if ( this.body.type === 'BlockStatement' ) {
this.body.createScope( scope );
this.scope = this.body.scope;
} else {
this.scope = new Scope({
parent: scope,
isBlockScope: true,
isLexicalBoundary: false
});
}
super.initialise( this.scope );
assignTo( this.left, this.scope, UNKNOWN );
}
}

23
src/ast/nodes/ForStatement.js

@ -0,0 +1,23 @@
import Statement from './shared/Statement.js';
import Scope from '../scopes/Scope.js';
export default class ForStatement extends Statement {
initialise ( scope ) {
if ( this.body.type === 'BlockStatement' ) {
this.body.createScope( scope );
this.scope = this.body.scope;
} else {
this.scope = new Scope({
parent: scope,
isBlockScope: true,
isLexicalBoundary: false
});
}
// can't use super, because we need to control the order
if ( this.init ) this.init.initialise( this.scope );
if ( this.test ) this.test.initialise( this.scope );
if ( this.update ) this.update.initialise( this.scope );
this.body.initialise( this.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( scope );
this.id.initialise( scope );
this.params.forEach( param => param.initialise( this.body.scope ) );
this.body.initialise();
}
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 ( scope ) {
this.body.createScope( scope );
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 Statement from './shared/Statement.js';
import { UNKNOWN } from '../values.js';
// TODO DRY this out
export default class IfStatement extends Statement {
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;
// }
}

8
src/ast/nodes/TemplateLiteral.js

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

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 );
}
}
}

7
src/ast/nodes/ThrowStatement.js

@ -0,0 +1,7 @@
import Node from '../Node.js';
export default class ThrowStatement extends Node {
hasEffects ( scope ) {
return scope.findLexicalBoundary().isModuleScope; // TODO should this just be `true`? probably...
}
}

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 );
}
}

40
src/ast/nodes/UpdateExpression.js

@ -0,0 +1,40 @@
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.scope = scope;
this.module.bundle.dependentExpressions.push( this );
super.initialise( scope );
}
isUsedByBundle () {
return isUsedByBundle( this.scope, 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 );
}
}

78
src/ast/nodes/index.js

@ -0,0 +1,78 @@
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 EmptyStatement from './EmptyStatement.js';
import ExportAllDeclaration from './ExportAllDeclaration.js';
import ExportDefaultDeclaration from './ExportDefaultDeclaration.js';
import ExportNamedDeclaration from './ExportNamedDeclaration.js';
import ExpressionStatement from './ExpressionStatement.js';
import ForStatement from './ForStatement.js';
import ForInStatement from './ForInStatement.js';
import ForOfStatement from './ForOfStatement.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 Statement from './shared/Statement.js';
import TemplateLiteral from './TemplateLiteral.js';
import ThisExpression from './ThisExpression.js';
import ThrowStatement from './ThrowStatement.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,
DoWhileStatement: Statement,
EmptyStatement,
ExportAllDeclaration,
ExportDefaultDeclaration,
ExportNamedDeclaration,
ExpressionStatement,
ForStatement,
ForInStatement,
ForOfStatement,
FunctionDeclaration,
FunctionExpression,
Identifier,
IfStatement,
ImportDeclaration,
Literal,
MemberExpression,
NewExpression,
ObjectExpression,
ParenthesizedExpression,
ReturnStatement,
SwitchStatement: Statement,
TemplateLiteral,
ThisExpression,
ThrowStatement,
TryStatement: Statement,
UnaryExpression,
UpdateExpression,
VariableDeclarator,
VariableDeclaration,
WhileStatement: Statement
};

16
src/ast/nodes/shared/Statement.js

@ -0,0 +1,16 @@
import Node from '../../Node.js';
export default class Statement 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 );
}
}

29
src/ast/nodes/shared/assignTo.js

@ -0,0 +1,29 @@
import extractNames from '../../utils/extractNames.js';
export default function assignToForLoopLeft ( node, scope, value ) {
if ( node.type === 'VariableDeclaration' ) {
for ( const proxy of node.declarations[0].proxies.values() ) {
proxy.possibleValues.add( value );
}
}
else {
while ( node.type === 'ParenthesizedExpression' ) node = node.expression;
if ( node.type === 'MemberExpression' ) {
// apparently this is legal JavaScript? Though I don't know what
// kind of monster would write `for ( foo.bar of thing ) {...}`
// for now, do nothing, as I'm not sure anything needs to happen...
}
else {
for ( const name of extractNames( node ) ) {
const declaration = scope.findDeclaration( name );
if ( declaration.possibleValues ) {
declaration.possibleValues.add( value );
}
}
}
}
}

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 ];
}
}

53
src/ast/scopes/ModuleScope.js

@ -0,0 +1,53 @@
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;
specifier.module.getExports().forEach( name => {
names.set(name);
});
if ( specifier.name !== '*' ) {
const declaration = specifier.module.traceExport( specifier.name );
if ( !declaration ) {
this.module.bundle.onwarn( `Non-existent export '${specifier.name}' is imported from ${specifier.module.id} by ${this.module.id}` );
return;
}
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]]' };

10
src/finalisers/amd.js

@ -3,7 +3,7 @@ import getInteropBlock from './shared/getInteropBlock.js';
import getExportBlock from './shared/getExportBlock.js'; import getExportBlock from './shared/getExportBlock.js';
import esModuleExport from './shared/esModuleExport.js'; import esModuleExport from './shared/esModuleExport.js';
export default function amd ( bundle, magicString, { exportMode, indentString }, options ) { export default function amd ( bundle, magicString, { exportMode, indentString, intro }, options ) {
const deps = bundle.externalModules.map( quotePath ); const deps = bundle.externalModules.map( quotePath );
const args = bundle.externalModules.map( getName ); const args = bundle.externalModules.map( getName );
@ -17,12 +17,14 @@ export default function amd ( bundle, magicString, { exportMode, indentString },
( deps.length ? `[${deps.join( ', ' )}], ` : `` ); ( deps.length ? `[${deps.join( ', ' )}], ` : `` );
const useStrict = options.useStrict !== false ? ` 'use strict';` : ``; const useStrict = options.useStrict !== false ? ` 'use strict';` : ``;
const intro = `define(${params}function (${args.join( ', ' )}) {${useStrict}\n\n`; const wrapperStart = `define(${params}function (${args.join( ', ' )}) {${useStrict}\n\n`;
// var foo__default = 'default' in foo ? foo['default'] : foo; // var foo__default = 'default' in foo ? foo['default'] : foo;
const interopBlock = getInteropBlock( bundle ); const interopBlock = getInteropBlock( bundle, options );
if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' ); if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' );
if ( intro ) magicString.prepend( intro );
const exportBlock = getExportBlock( bundle.entryModule, exportMode ); const exportBlock = getExportBlock( bundle.entryModule, exportMode );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
if ( exportMode === 'named' ) magicString.append( `\n\n${esModuleExport}` ); if ( exportMode === 'named' ) magicString.append( `\n\n${esModuleExport}` );
@ -31,5 +33,5 @@ export default function amd ( bundle, magicString, { exportMode, indentString },
return magicString return magicString
.indent( indentString ) .indent( indentString )
.append( '\n\n});' ) .append( '\n\n});' )
.prepend( intro ); .prepend( wrapperStart );
} }

9
src/finalisers/cjs.js

@ -1,18 +1,19 @@
import getExportBlock from './shared/getExportBlock.js'; import getExportBlock from './shared/getExportBlock.js';
import esModuleExport from './shared/esModuleExport.js'; import esModuleExport from './shared/esModuleExport.js';
export default function cjs ( bundle, magicString, { exportMode }, options ) { export default function cjs ( bundle, magicString, { exportMode, intro }, options ) {
let intro = ( options.useStrict === false ? `` : `'use strict';\n\n` ) + intro = ( options.useStrict === false ? intro : `'use strict';\n\n${intro}` ) +
( exportMode === 'named' ? `${esModuleExport}\n\n` : '' ); ( exportMode === 'named' ? `${esModuleExport}\n\n` : '' );
let needsInterop = false; let needsInterop = false;
const varOrConst = bundle.varOrConst; const varOrConst = bundle.varOrConst;
const interop = options.interop !== false;
// TODO handle empty imports, once they're supported // TODO handle empty imports, once they're supported
const importBlock = bundle.externalModules const importBlock = bundle.externalModules
.map( module => { .map( module => {
if ( module.declarations.default ) { if ( interop && module.declarations.default ) {
if ( module.exportsNamespace ) { if ( module.exportsNamespace ) {
return `${varOrConst} ${module.name} = require('${module.path}');` + return `${varOrConst} ${module.name} = require('${module.path}');` +
`\n${varOrConst} ${module.name}__default = ${module.name}['default'];`; `\n${varOrConst} ${module.name}__default = ${module.name}['default'];`;

15
src/finalisers/es.js

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

22
src/finalisers/iife.js

@ -16,7 +16,7 @@ function setupNamespace ( keypath ) {
.join( '\n' ) + '\n'; .join( '\n' ) + '\n';
} }
export default function iife ( bundle, magicString, { exportMode, indentString }, options ) { export default function iife ( bundle, magicString, { exportMode, indentString, intro }, options ) {
const globalNameMaker = getGlobalNameMaker( options.globals || blank(), bundle.onwarn ); const globalNameMaker = getGlobalNameMaker( options.globals || blank(), bundle.onwarn );
const name = options.moduleName; const name = options.moduleName;
@ -35,29 +35,31 @@ export default function iife ( bundle, magicString, { exportMode, indentString }
args.unshift( 'exports' ); args.unshift( 'exports' );
} }
const useStrict = options.useStrict !== false ? `'use strict';` : ``; const useStrict = options.useStrict !== false ? `${indentString}'use strict';\n\n` : ``;
let intro = `(function (${args}) {\n`; let wrapperIntro = `(function (${args}) {\n${useStrict}`;
const outro = `\n\n}(${dependencies}));`; const wrapperOutro = `\n\n}(${dependencies}));`;
if ( exportMode === 'default' ) { if ( exportMode === 'default' ) {
intro = ( isNamespaced ? `this.` : `${bundle.varOrConst} ` ) + `${name} = ${intro}`; wrapperIntro = ( isNamespaced ? `this.` : `${bundle.varOrConst} ` ) + `${name} = ${wrapperIntro}`;
} }
if ( isNamespaced ) { if ( isNamespaced ) {
intro = setupNamespace( name ) + intro; wrapperIntro = setupNamespace( name ) + wrapperIntro;
} }
// var foo__default = 'default' in foo ? foo['default'] : foo; // var foo__default = 'default' in foo ? foo['default'] : foo;
const interopBlock = getInteropBlock( bundle ); const interopBlock = getInteropBlock( bundle, options );
if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' ); if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' );
if ( useStrict ) magicString.prepend( useStrict + '\n\n' );
if ( intro ) magicString.prepend( intro );
const exportBlock = getExportBlock( bundle.entryModule, exportMode ); const exportBlock = getExportBlock( bundle.entryModule, exportMode );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
if ( options.outro ) magicString.append( `\n${options.outro}` ); if ( options.outro ) magicString.append( `\n${options.outro}` );
return magicString return magicString
.indent( indentString ) .indent( indentString )
.prepend( intro ) .prepend( wrapperIntro )
.append( outro ); .append( wrapperOutro );
} }

6
src/finalisers/shared/getExportBlock.js

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

4
src/finalisers/shared/getInteropBlock.js

@ -1,7 +1,7 @@
export default function getInteropBlock ( bundle ) { export default function getInteropBlock ( bundle, options ) {
return bundle.externalModules return bundle.externalModules
.map( module => { .map( module => {
if ( !module.declarations.default ) return null; if ( !module.declarations.default || options.interop === false ) return null;
if ( module.exportsNamespace ) { if ( module.exportsNamespace ) {
return `${bundle.varOrConst} ${module.name}__default = ${module.name}['default'];`; return `${bundle.varOrConst} ${module.name}__default = ${module.name}['default'];`;

14
src/finalisers/umd.js

@ -16,7 +16,9 @@ function setupNamespace ( name ) {
.join( ', ' ); .join( ', ' );
} }
export default function umd ( bundle, magicString, { exportMode, indentString }, options ) { const wrapperOutro = '\n\n})));';
export default function umd ( bundle, magicString, { exportMode, indentString, intro }, options ) {
if ( exportMode !== 'none' && !options.moduleName ) { if ( exportMode !== 'none' && !options.moduleName ) {
throw new Error( 'You must supply options.moduleName for UMD bundles' ); throw new Error( 'You must supply options.moduleName for UMD bundles' );
} }
@ -54,7 +56,7 @@ export default function umd ( bundle, magicString, { exportMode, indentString },
exports.noConflict = function() { global.${options.moduleName} = current; return exports; }; exports.noConflict = function() { global.${options.moduleName} = current; return exports; };
})()` : `(${defaultExport}factory(${globalDeps}))`; })()` : `(${defaultExport}factory(${globalDeps}))`;
const intro = const wrapperIntro =
`(function (global, factory) { `(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? ${cjsExport}factory(${cjsDeps.join( ', ' )}) : typeof exports === 'object' && typeof module !== 'undefined' ? ${cjsExport}factory(${cjsDeps.join( ', ' )}) :
typeof define === 'function' && define.amd ? define(${amdParams}factory) : typeof define === 'function' && define.amd ? define(${amdParams}factory) :
@ -64,9 +66,11 @@ export default function umd ( bundle, magicString, { exportMode, indentString },
`.replace( /^\t\t/gm, '' ).replace( /^\t/gm, magicString.getIndentString() ); `.replace( /^\t\t/gm, '' ).replace( /^\t/gm, magicString.getIndentString() );
// var foo__default = 'default' in foo ? foo['default'] : foo; // var foo__default = 'default' in foo ? foo['default'] : foo;
const interopBlock = getInteropBlock( bundle ); const interopBlock = getInteropBlock( bundle, options );
if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' ); if ( interopBlock ) magicString.prepend( interopBlock + '\n\n' );
if ( intro ) magicString.prepend( intro );
const exportBlock = getExportBlock( bundle.entryModule, exportMode ); const exportBlock = getExportBlock( bundle.entryModule, exportMode );
if ( exportBlock ) magicString.append( '\n\n' + exportBlock ); if ( exportBlock ) magicString.append( '\n\n' + exportBlock );
if ( exportMode === 'named' ) magicString.append( `\n\n${esModuleExport}` ); if ( exportMode === 'named' ) magicString.append( `\n\n${esModuleExport}` );
@ -75,6 +79,6 @@ export default function umd ( bundle, magicString, { exportMode, indentString },
return magicString return magicString
.trim() .trim()
.indent( indentString ) .indent( indentString )
.append( '\n\n})));' ) .append( wrapperOutro )
.prepend( intro ); .prepend( wrapperIntro );
} }

14
src/rollup.js

@ -1,3 +1,4 @@
import { timeStart, timeEnd, flushTime } from './utils/flushTime.js';
import { basename } from './utils/path.js'; import { basename } from './utils/path.js';
import { writeFile } from './utils/fs.js'; import { writeFile } from './utils/fs.js';
import { assign, keys } from './utils/object.js'; import { assign, keys } from './utils/object.js';
@ -21,6 +22,7 @@ const ALLOWED_KEYS = [
'format', 'format',
'globals', 'globals',
'indent', 'indent',
'interop',
'intro', 'intro',
'moduleId', 'moduleId',
'moduleName', 'moduleName',
@ -54,10 +56,18 @@ export function rollup ( options ) {
const bundle = new Bundle( options ); const bundle = new Bundle( options );
timeStart( '--BUILD--' );
return bundle.build().then( () => { return bundle.build().then( () => {
timeEnd( '--BUILD--' );
function generate ( options ) { function generate ( options ) {
timeStart( '--GENERATE--' );
const rendered = bundle.render( options ); const rendered = bundle.render( options );
timeEnd( '--GENERATE--' );
bundle.plugins.forEach( plugin => { bundle.plugins.forEach( plugin => {
if ( plugin.ongenerate ) { if ( plugin.ongenerate ) {
plugin.ongenerate( assign({ plugin.ongenerate( assign({
@ -66,6 +76,8 @@ export function rollup ( options ) {
} }
}); });
flushTime();
return rendered; return rendered;
} }
@ -96,7 +108,7 @@ export function rollup ( options ) {
promises.push( writeFile( dest + '.map', map.toString() ) ); promises.push( writeFile( dest + '.map', map.toString() ) );
} }
code += `\n//# ${SOURCEMAPPING_URL}=${url}\n`; code += `//# ${SOURCEMAPPING_URL}=${url}\n`;
} }
promises.push( writeFile( dest, code ) ); promises.push( writeFile( dest, code ) );

17
src/utils/defaults.js

@ -1,5 +1,5 @@
import { isFile, readFileSync } from './fs.js'; import { isFile, readdirSync, readFileSync } from './fs.js';
import { dirname, isAbsolute, resolve } from './path.js'; import { basename, dirname, isAbsolute, resolve } from './path.js';
import { blank } from './object.js'; import { blank } from './object.js';
export function load ( id ) { export function load ( id ) {
@ -7,10 +7,15 @@ export function load ( id ) {
} }
function addJsExtensionIfNecessary ( file ) { function addJsExtensionIfNecessary ( file ) {
if ( isFile( file ) ) return file; try {
const name = basename( file );
file += '.js'; const files = readdirSync( dirname( file ) );
if ( isFile( file ) ) return file;
if ( ~files.indexOf( name ) && isFile( file ) ) return file;
if ( ~files.indexOf( `${name}.js` ) && isFile( `${file}.js` ) ) return `${file}.js`;
} catch ( err ) {
// noop
}
return null; return null;
} }

55
src/utils/flushTime.js

@ -0,0 +1,55 @@
const DEBUG = false;
const map = new Map;
let timeStartHelper;
let timeEndHelper;
if ( typeof process === 'undefined' ) {
timeStartHelper = function timeStartHelper () {
return window.performance.now();
};
timeEndHelper = function timeEndHelper ( previous ) {
return window.performance.now() - previous;
};
} else {
timeStartHelper = function timeStartHelper () {
return process.hrtime();
};
timeEndHelper = function timeEndHelper ( previous ) {
const hrtime = process.hrtime( previous );
return hrtime[0] * 1e3 + Math.floor( hrtime[1] / 1e6 );
};
}
export function timeStart ( label ) {
if ( !map.has( label ) ) {
map.set( label, {
time: 0
});
}
map.get( label ).start = timeStartHelper();
}
export function timeEnd ( label ) {
if ( map.has( label ) ) {
const item = map.get( label );
item.time += timeEndHelper( item.start );
}
}
export function flushTime ( log = defaultLog ) {
for ( const item of map.entries() ) {
log( item[0], item[1].time );
}
map.clear();
}
function defaultLog ( label, time ) {
if ( DEBUG ) {
/* eslint-disable no-console */
console.info( '%dms: %s', time, label );
/* eslint-enable no-console */
}
}

21
src/utils/object.js

@ -17,3 +17,24 @@ export function assign ( target, ...sources ) {
return target; 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, '/' ); 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;
}

4
src/utils/transformBundle.js

@ -1,13 +1,13 @@
import { decode } from 'sourcemap-codec'; import { decode } from 'sourcemap-codec';
export default function transformBundle ( code, plugins, sourceMapChain ) { export default function transformBundle ( code, plugins, sourceMapChain, options ) {
return plugins.reduce( ( code, plugin ) => { return plugins.reduce( ( code, plugin ) => {
if ( !plugin.transformBundle ) return code; if ( !plugin.transformBundle ) return code;
let result; let result;
try { try {
result = plugin.transformBundle( code ); result = plugin.transformBundle( code, { format : options.format } );
} catch ( err ) { } catch ( err ) {
err.plugin = plugin.name; err.plugin = plugin.name;
err.message = `Error transforming bundle${plugin.name ? ` with '${plugin.name}' plugin` : ''}: ${err.message}`; err.message = `Error transforming bundle${plugin.name ? ` with '${plugin.name}' plugin` : ''}: ${err.message}`;

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

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

3
test/form/body-less-for-loops/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'supports body-less for loops'
};

16
test/form/body-less-for-loops/_expected/amd.js

@ -0,0 +1,16 @@
define(function () { 'use strict';
for ( let i = 0; i < 10; i += 1 ) console.log( i );
for ( const letter of array ) console.log( letter );
for ( const index in array ) console.log( index );
let i;
for ( i = 0; i < 10; i += 1 ) console.log( i );
let letter;
for ( letter of array ) console.log( letter );
let index;
for ( index in array ) console.log( index );
});

14
test/form/body-less-for-loops/_expected/cjs.js

@ -0,0 +1,14 @@
'use strict';
for ( let i = 0; i < 10; i += 1 ) console.log( i );
for ( const letter of array ) console.log( letter );
for ( const index in array ) console.log( index );
let i;
for ( i = 0; i < 10; i += 1 ) console.log( i );
let letter;
for ( letter of array ) console.log( letter );
let index;
for ( index in array ) console.log( index );

12
test/form/body-less-for-loops/_expected/es.js

@ -0,0 +1,12 @@
for ( let i = 0; i < 10; i += 1 ) console.log( i );
for ( const letter of array ) console.log( letter );
for ( const index in array ) console.log( index );
let i;
for ( i = 0; i < 10; i += 1 ) console.log( i );
let letter;
for ( letter of array ) console.log( letter );
let index;
for ( index in array ) console.log( index );

17
test/form/body-less-for-loops/_expected/iife.js

@ -0,0 +1,17 @@
(function () {
'use strict';
for ( let i = 0; i < 10; i += 1 ) console.log( i );
for ( const letter of array ) console.log( letter );
for ( const index in array ) console.log( index );
let i;
for ( i = 0; i < 10; i += 1 ) console.log( i );
let letter;
for ( letter of array ) console.log( letter );
let index;
for ( index in array ) console.log( index );
}());

20
test/form/body-less-for-loops/_expected/umd.js

@ -0,0 +1,20 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, (function () { 'use strict';
for ( let i = 0; i < 10; i += 1 ) console.log( i );
for ( const letter of array ) console.log( letter );
for ( const index in array ) console.log( index );
let i;
for ( i = 0; i < 10; i += 1 ) console.log( i );
let letter;
for ( letter of array ) console.log( letter );
let index;
for ( index in array ) console.log( index );
})));

12
test/form/body-less-for-loops/main.js

@ -0,0 +1,12 @@
for ( let i = 0; i < 10; i += 1 ) console.log( i );
for ( const letter of array ) console.log( letter );
for ( const index in array ) console.log( index );
let i;
for ( i = 0; i < 10; i += 1 ) console.log( i );
let letter;
for ( letter of array ) console.log( letter );
let index;
for ( index in array ) console.log( index );

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 );
}());

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

Loading…
Cancel
Save