[Pkg-javascript-commits] [node-buble] 01/02: New upstream version 0.15.2

Julien Puydt julien.puydt at laposte.net
Sat Sep 9 18:55:12 UTC 2017


This is an automated email from the git hooks/post-receive script.

jpuydt-guest pushed a commit to branch master
in repository node-buble.

commit 3cbc22d6cfc0747519614a8235f15b05510d0f5b
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Sat Sep 9 19:22:10 2017 +0200

    New upstream version 0.15.2
---
 .editorconfig                                      |    6 +
 .eslintrc                                          |   23 +
 .gitignore                                         |    5 +
 .gitlab-ci.yml                                     |   12 +
 CHANGELOG.md                                       |  449 +++++++
 LICENSE.md                                         |   21 +
 README.md                                          |   28 +
 bin/buble                                          |   28 +
 bin/handleError.js                                 |   46 +
 bin/help.md                                        |   44 +
 bin/runBuble.js                                    |  149 +++
 bin/showHelp.js                                    |   13 +
 "bubl\303\251.gif"                                 |  Bin 0 -> 299534 bytes
 package.json                                       |   74 ++
 register.js                                        |   90 ++
 rollup.config.js                                   |   36 +
 src/index.js                                       |   81 ++
 src/program/BlockStatement.js                      |  304 +++++
 src/program/Node.js                                |  124 ++
 src/program/Program.js                             |   53 +
 src/program/Scope.js                               |  100 ++
 src/program/extractNames.js                        |   31 +
 src/program/keys.js                                |    4 +
 src/program/types/ArrayExpression.js               |   55 +
 src/program/types/ArrowFunctionExpression.js       |   36 +
 src/program/types/AssignmentExpression.js          |  268 +++++
 src/program/types/BinaryExpression.js              |   12 +
 src/program/types/BreakStatement.js                |   22 +
 src/program/types/CallExpression.js                |   98 ++
 src/program/types/ClassBody.js                     |  186 +++
 src/program/types/ClassDeclaration.js              |   62 +
 src/program/types/ClassExpression.js               |   45 +
 src/program/types/ContinueStatement.js             |   13 +
 src/program/types/ExportDefaultDeclaration.js      |    9 +
 src/program/types/ExportNamedDeclaration.js        |    9 +
 src/program/types/ForInStatement.js                |   22 +
 src/program/types/ForOfStatement.js                |   75 ++
 src/program/types/ForStatement.js                  |   38 +
 src/program/types/FunctionDeclaration.js           |   15 +
 src/program/types/FunctionExpression.js            |   64 +
 src/program/types/Identifier.js                    |   43 +
 src/program/types/IfStatement.js                   |   24 +
 src/program/types/ImportDeclaration.js             |    9 +
 src/program/types/ImportDefaultSpecifier.js        |    8 +
 src/program/types/ImportSpecifier.js               |    8 +
 src/program/types/JSXAttribute.js                  |   20 +
 src/program/types/JSXClosingElement.js             |   22 +
 src/program/types/JSXElement.js                    |   50 +
 src/program/types/JSXExpressionContainer.js        |   10 +
 src/program/types/JSXOpeningElement.js             |   85 ++
 src/program/types/JSXSpreadAttribute.js            |   10 +
 src/program/types/Literal.js                       |   29 +
 src/program/types/MemberExpression.js              |   13 +
 src/program/types/NewExpression.js                 |   37 +
 src/program/types/ObjectExpression.js              |  142 +++
 src/program/types/Property.js                      |   40 +
 src/program/types/ReturnStatement.js               |   28 +
 src/program/types/SpreadProperty.js                |   10 +
 src/program/types/Super.js                         |   69 ++
 src/program/types/TaggedTemplateExpression.js      |   37 +
 src/program/types/TemplateElement.js               |    7 +
 src/program/types/TemplateLiteral.js               |   70 ++
 src/program/types/ThisExpression.js                |   24 +
 src/program/types/UpdateExpression.js              |   21 +
 src/program/types/VariableDeclaration.js           |   86 ++
 src/program/types/VariableDeclarator.js            |   35 +
 src/program/types/index.js                         |   92 ++
 src/program/types/shared/LoopStatement.js          |   91 ++
 src/program/types/shared/ModuleDeclaration.js      |    9 +
 src/program/wrap.js                                |   54 +
 src/support.js                                     |   77 ++
 src/utils/CompileError.js                          |   23 +
 src/utils/array.js                                 |   11 +
 src/utils/deindent.js                              |   28 +
 src/utils/destructure.js                           |  187 +++
 src/utils/getSnippet.js                            |   30 +
 src/utils/isReference.js                           |   37 +
 src/utils/locate.js                                |   20 +
 src/utils/patterns.js                              |    1 +
 src/utils/reserved.js                              |    5 +
 src/utils/spread.js                                |   60 +
 test/cli/basic/command.sh                          |    1 +
 test/cli/basic/expected/output.js                  |    1 +
 test/cli/basic/input.js                            |    1 +
 test/cli/compiles-directory/command.sh             |    1 +
 test/cli/compiles-directory/expected/bar.js        |    2 +
 test/cli/compiles-directory/expected/bar.js.map    |    1 +
 test/cli/compiles-directory/expected/baz.js        |    2 +
 test/cli/compiles-directory/expected/baz.js.map    |    1 +
 test/cli/compiles-directory/expected/foo.js        |    2 +
 test/cli/compiles-directory/expected/foo.js.map    |    1 +
 test/cli/compiles-directory/src/bar.jsm            |    1 +
 test/cli/compiles-directory/src/baz.es6            |    1 +
 test/cli/compiles-directory/src/foo.js             |    1 +
 test/cli/compiles-directory/src/nope.txt           |    1 +
 test/cli/creates-inline-sourcemap/command.sh       |    1 +
 .../creates-inline-sourcemap/expected/output.js    |    2 +
 test/cli/creates-inline-sourcemap/input.js         |    1 +
 test/cli/creates-sourcemap/command.sh              |    1 +
 test/cli/creates-sourcemap/expected/output.js      |    2 +
 test/cli/creates-sourcemap/expected/output.js.map  |    1 +
 test/cli/creates-sourcemap/input.js                |    1 +
 test/cli/supports-jsx-pragma/command.sh            |    1 +
 test/cli/supports-jsx-pragma/expected/output.js    |    1 +
 test/cli/supports-jsx-pragma/input.js              |    1 +
 test/cli/supports-jsx/command.sh                   |    1 +
 test/cli/supports-jsx/expected/output.js           |    1 +
 test/cli/supports-jsx/input.jsx                    |    1 +
 test/cli/uses-overrides/command.sh                 |    1 +
 test/cli/uses-overrides/expected/output.js         |    1 +
 test/cli/uses-overrides/input.js                   |    1 +
 test/cli/uses-targets/command.sh                   |    1 +
 test/cli/uses-targets/expected/output.js           |    1 +
 test/cli/uses-targets/input.js                     |    1 +
 test/cli/writes-to-stdout/command.sh               |    1 +
 test/cli/writes-to-stdout/expected/output.js       |    1 +
 test/cli/writes-to-stdout/input.js                 |    1 +
 test/samples/arrow-functions.js                    |  190 +++
 test/samples/binary-and-octal.js                   |   32 +
 test/samples/block-scoping.js                      |  444 +++++++
 .../classes-no-named-function-expressions.js       | 1225 ++++++++++++++++++++
 test/samples/classes.js                            | 1203 +++++++++++++++++++
 test/samples/computed-properties.js                |  231 ++++
 test/samples/default-parameters.js                 |  129 +++
 test/samples/destructuring.js                      |  760 ++++++++++++
 test/samples/exponentiation-operator.js            |  178 +++
 test/samples/for-of.js                             |  227 ++++
 test/samples/generators.js                         |   88 ++
 test/samples/jsx.js                                |  227 ++++
 test/samples/loops.js                              |  756 ++++++++++++
 test/samples/misc.js                               |  189 +++
 test/samples/modules.js                            |   34 +
 ...ect-properties-no-named-function-expressions.js |  118 ++
 test/samples/object-properties.js                  |  129 +++
 test/samples/object-rest-spread.js                 |  177 +++
 test/samples/regex.js                              |   27 +
 test/samples/reserved-properties.js                |   27 +
 test/samples/rest-parameters.js                    |   50 +
 test/samples/spread-operator.js                    |  564 +++++++++
 test/samples/template-strings.js                   |  106 ++
 test/test.js                                       |  313 +++++
 test/utils/getLocation.js                          |   24 +
 142 files changed, 11901 insertions(+)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4836c86
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+[**.js]
+indent_style = tab
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..ed8a063
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,23 @@
+{
+    "rules": {
+        "indent": [ 2, "tab", { "SwitchCase": 1 } ],
+        "linebreak-style": [ 2, "unix" ],
+        "semi": [ 2, "always" ],
+        "keyword-spacing": [ 2, { "before": true, "after": true } ],
+        "space-before-blocks": [ 2, "always" ],
+        "space-before-function-paren": [ 2, "always" ],
+        "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
+        "no-cond-assign": [ 0 ]
+    },
+    "env": {
+        "es6": true,
+        "browser": true,
+        "mocha": true,
+        "node": true
+    },
+    "extends": "eslint:recommended",
+    "parserOptions": {
+        "ecmaVersion": 6,
+        "sourceType": "module"
+    }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3f3c66f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+node_modules
+dist
+test/**/actual
+sandbox
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..b421d68
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,12 @@
+before_script:
+  - npm install
+
+test:0.12:
+  image: node:0.12
+  script:
+  - npm test
+
+test:latest:
+  image: node:latest
+  script:
+  - npm test
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1a986af
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,449 @@
+# buble changelog
+
+## 0.15.2
+
+* Don't create function names for object methods with `namedFunctionExpressions: false`
+* Simplify destructuring assignment statements
+* Give unique names to methods that shadow free variables ([#166](https://gitlab.com/Rich-Harris/buble/issues/166))
+
+## 0.15.1
+
+* Fix `Object.assign` regression ([#163](https://gitlab.com/Rich-Harris/buble/issues/163))
+
+## 0.15.0
+
+* More informative CLI errors when input comes from stdin ([#155](https://gitlab.com/Rich-Harris/buble/issues/155))
+* Prevent PhantomJS shadowing errors ([#154](https://gitlab.com/Rich-Harris/buble/issues/154))
+* Use local `register.js` in tests ([#153](https://gitlab.com/Rich-Harris/buble/issues/153))
+* Correct CLI output filename with .jsx input ([#151](https://gitlab.com/Rich-Harris/buble/issues/151))
+* Fix whitespace removal bug ([#159](https://gitlab.com/Rich-Harris/buble/issues/159))
+* Allow computed properties in object destructuring ([#146](https://gitlab.com/Rich-Harris/buble/issues/146))
+* Support rest elements in array destructuring ([#147](https://gitlab.com/Rich-Harris/buble/issues/147))
+* Fix array swap assignment expression ([#148](https://gitlab.com/Rich-Harris/buble/issues/148))
+* Allow template string as destructuring default ([#145](https://gitlab.com/Rich-Harris/buble/issues/145))
+* Support multiple returning loops with block scoping ([cbc17ad5e](https://gitlab.com/Rich-Harris/buble/commit/cbc17ad5e1dc6e8af820fee372e6fb68e475afa4))
+* Fix `super` with spread arguments ([#129](https://gitlab.com/Rich-Harris/buble/issues/129))
+* Arrow function returning computed property ([#126](https://gitlab.com/Rich-Harris/buble/issues/126))
+* Allow computed property and object spread to coexist ([#144](https://gitlab.com/Rich-Harris/buble/issues/144))
+* Add `namedFunctionExpressions` option to prevent scope leak in old browsers ([#130](https://gitlab.com/Rich-Harris/buble/issues/130))
+* Fix exponentiation assignment edge case ([#122](https://gitlab.com/Rich-Harris/buble/issues/122))
+* Allow CLI `--output` flag to work with stdin input
+
+## 0.14.3
+
+* Prevent crashing on Node versions more recent than the latest 'supported' version ([#102](https://gitlab.com/Rich-Harris/buble/merge_requests/102))
+
+## 0.14.2
+
+* Allow `.jsx` file extension ([#127](https://gitlab.com/Rich-Harris/buble/issues/127))
+* Handle trailing comma in spread operator ([#133](https://gitlab.com/Rich-Harris/buble/issues/133))
+* Allow empty code blocks in JSX ([#131](https://gitlab.com/Rich-Harris/buble/issues/131))
+* Allow computed shorthand function name with spread in body ([#135](https://gitlab.com/Rich-Harris/buble/issues/135))
+* Add `--objectAssign` CLI option ([#113](https://gitlab.com/Rich-Harris/buble/issues/113))
+* Allow numeric literals as shorthand method keys ([#139](https://gitlab.com/Rich-Harris/buble/issues/139))
+
+## 0.14.1
+
+* fix initialization of block-scoped variables in for-of and for-in loops ([#124](https://gitlab.com/Rich-Harris/buble/issues/124))
+
+## 0.14.0
+
+* Always wrap block-less bodies in curlies ([#110](https://gitlab.com/Rich-Harris/buble/issues/110), [#117](https://gitlab.com/Rich-Harris/buble/issues/117), [!80](https://gitlab.com/Rich-Harris/buble/merge_requests/80))
+* Make sure block-scoped variables in loops have an initializer ([#124](https://gitlab.com/Rich-Harris/buble/issues/124))
+* Destructuring assignments ([!82](https://gitlab.com/Rich-Harris/buble/merge_requests/82))
+* Support string literals in object destructuring ([!81](https://gitlab.com/Rich-Harris/buble/merge_requests/81))
+* Standalone arrow function expression statements ([!79](https://gitlab.com/Rich-Harris/buble/merge_requests/79))
+
+## 0.13.2
+
+* Fix spread operator when used with `new` and `this` ([#104](https://gitlab.com/Rich-Harris/buble/issues/104), [#115](https://gitlab.com/Rich-Harris/buble/issues/115))
+
+## 0.13.1
+
+* Handle destructuring in for/for-of loop heads ([#95](https://gitlab.com/Rich-Harris/buble/issues/95))
+* Allow early return (without value) from loops ([#103](https://gitlab.com/Rich-Harris/buble/issues/103), [#105](https://gitlab.com/Rich-Harris/buble/issues/105))
+
+## 0.13.0
+
+* Require an `objectAssign` option to be specified if using object spread operator ([#93](https://gitlab.com/Rich-Harris/buble/issues/93))
+* Fix spread operator with expression method calls and template strings ([!74](https://gitlab.com/Rich-Harris/buble/merge_requests/74))
+
+## 0.12.5
+
+* Prevent reserved words being used as identifiers ([#86](https://gitlab.com/Rich-Harris/buble/issues/86))
+* Use correct `this` when transpiling `super` inside arrow function ([#89](https://gitlab.com/Rich-Harris/buble/issues/89))
+* Handle body-less `for-of` loops ([#80](https://gitlab.com/Rich-Harris/buble/issues/80))
+
+## 0.12.4
+
+* Allow references to precede declaration (inside function) in block scoping ([#87](https://gitlab.com/Rich-Harris/buble/issues/87))
+
+## 0.12.3
+
+* Argh, npm
+
+## 0.12.2
+
+* Files missing in 0.12.1 (???)
+
+## 0.12.1
+
+* Don't require space before parens of shorthand computed method ([#82](https://gitlab.com/Rich-Harris/buble/issues/82))
+* Allow string keys for shorthand methods ([#82](https://gitlab.com/Rich-Harris/buble/issues/82))
+
+## 0.12.0
+
+* Support `u` flag in regular expression literals ([!62](https://gitlab.com/Rich-Harris/buble/merge_requests/62))
+* Save `buble/register` transformations to `$HOME/.buble-cache` ([!63](https://gitlab.com/Rich-Harris/buble/merge_requests/63))
+
+## 0.11.6
+
+* Allow shorthand methods with computed names ([#78](https://gitlab.com/Rich-Harris/buble/issues/78))
+* Include code snippet in `error.toString()` ([#79](https://gitlab.com/Rich-Harris/buble/issues/79))
+
+## 0.11.5
+
+* Preserve whitespace between JSX tags on single line ([#65](https://gitlab.com/Rich-Harris/buble/issues/65))
+
+## 0.11.4
+
+* Allow computed class methods, except accessors ([!56](https://gitlab.com/Rich-Harris/buble/merge_requests/56))
+* Compound destructuring ([!58](https://gitlab.com/Rich-Harris/buble/merge_requests/58))
+
+## 0.11.3
+
+* Ensure inserted statements follow use strict pragma ([#72](https://gitlab.com/Rich-Harris/buble/issues/72))
+
+## 0.11.2
+
+* Ensure closing parenthesis is in correct place when transpiling inline computed property object expressions ([#73](https://gitlab.com/Rich-Harris/buble/issues/73))
+
+## 0.11.1
+
+* Fix computed property followed by non-computed property in inline expression
+
+## 0.11.0
+
+* Computed properties ([#67](https://gitlab.com/Rich-Harris/buble/issues/67))
+* Allow `super(...)` to use rest arguments ([#69](https://gitlab.com/Rich-Harris/buble/issues/69))
+
+## 0.10.7
+
+* Allow customisation of `Object.assign` (used in object spread) ([!51](https://gitlab.com/Rich-Harris/buble/merge_requests/51))
+
+## 0.10.6
+
+* Handle sparse arrays ([#62](https://gitlab.com/Rich-Harris/buble/issues/62))
+* Handle spread expressions in JSX ([#64](https://gitlab.com/Rich-Harris/buble/issues/64))
+
+## 0.10.5
+
+* Create intermediate directories when transforming via CLI ([#63](https://gitlab.com/Rich-Harris/buble/issues/63))
+* Update README ([#57](https://gitlab.com/Rich-Harris/buble/issues/57))
+
+## 0.10.4
+
+* Support spread operator in object literals ([!45](https://gitlab.com/Rich-Harris/buble/merge_requests/45)) and JSX elements ([!46](https://gitlab.com/Rich-Harris/buble/merge_requests/46))
+
+## 0.10.3
+
+* Disable intelligent destructuring, temporarily ([#53](https://gitlab.com/Rich-Harris/buble/issues/53))
+* Fix whitespace in JSX literals ([!39](https://gitlab.com/Rich-Harris/buble/merge_requests/39))
+* Add `: true` to value-less JSX attributes ([!40](https://gitlab.com/Rich-Harris/buble/merge_requests/40))
+* Quote invalid attribute names ([!41](https://gitlab.com/Rich-Harris/buble/merge_requests/41))
+
+## 0.10.2
+
+* Don't add closing quote to JSX component without attributes ([#58](https://gitlab.com/Rich-Harris/buble/issues/58))
+
+## 0.10.1
+
+* Fix handling of literals inside JSX
+
+## 0.10.0
+
+* Basic JSX support
+
+## 0.9.3
+
+* Better spread operator support, including with `arguments` ([#40](https://gitlab.com/Rich-Harris/buble/issues/40))
+* Fix indentation of inserted statements in class constructors ([#39](https://gitlab.com/Rich-Harris/buble/issues/39))
+
+## 0.9.2
+
+* Allow class to have accessors and no constructor ([#48](https://gitlab.com/Rich-Harris/buble/issues/48))
+* Fix help message in CLI
+
+## 0.9.1
+
+* Prevent confusion over `Literal` node keys
+
+## 0.9.0
+
+* More complete and robust destructuring support ([#37](https://gitlab.com/Rich-Harris/buble/issues/37), [#43](https://gitlab.com/Rich-Harris/buble/issues/43))
+* Correct `this`/`arguments` references inside for-of loop
+
+## 0.8.5
+
+* Allow destructured parameter to have default ([#43](https://gitlab.com/Rich-Harris/buble/issues/43))
+* Allow `continue`/`break` statements inside a for-of loop
+
+## 0.8.4
+
+* Allow class body to follow ID/superclass without whitespace ([#46](https://gitlab.com/Rich-Harris/buble/issues/46))
+
+## 0.8.3
+
+* Performance enhancements ([!23](https://gitlab.com/Rich-Harris/buble/merge_requests/23))
+
+## 0.8.2
+
+* More robust version of ([!22](https://gitlab.com/Rich-Harris/buble/merge_requests/22))
+
+## 0.8.1
+
+* Fix `export default class A extends B` (broken in 0.8.0) ([!22](https://gitlab.com/Rich-Harris/buble/merge_requests/22))
+
+## 0.8.0
+
+* Subclasses inherit static methods ([#33](https://gitlab.com/Rich-Harris/buble/issues/33))
+* Performance enhancements ([!21](https://gitlab.com/Rich-Harris/buble/merge_requests/21))
+
+## 0.7.1
+
+* Prevent omission of closing paren in template string ([#42](https://gitlab.com/Rich-Harris/buble/issues/42))
+* Separate variable declarations for each name in destructured declaration ([#18](https://gitlab.com/Rich-Harris/buble/merge_requests/18))
+
+## 0.7.0
+
+* Allow arrow functions to be used as default parameter values ([#36](https://gitlab.com/Rich-Harris/buble/issues/36))
+
+## 0.6.7
+
+* Support `static get` and `set` in classes ([#34](https://gitlab.com/Rich-Harris/buble/issues/34))
+* Support spread operator in expression method call ([!14](https://gitlab.com/Rich-Harris/buble/merge_requests/14))
+* Fix `for-of` loops with no space after opening paren ([#35](https://gitlab.com/Rich-Harris/buble/issues/35))
+
+## 0.6.6
+
+* Fix another subclass `super()` bug ([#32](https://gitlab.com/Rich-Harris/buble/issues/32))
+
+## 0.6.5
+
+* Fix `super()` call in subclass expression ([#32](https://gitlab.com/Rich-Harris/buble/issues/32))
+* Less defensive template string parenthesising ([!9](https://gitlab.com/Rich-Harris/buble/merge_requests/9))
+
+## 0.6.4
+
+* Add Node 6 to support matrix
+
+## 0.6.3
+
+* Handle empty template strings ([#28](https://gitlab.com/Rich-Harris/buble/issues/28))
+
+## 0.6.2
+
+* Handle body-less do-while blocks ([#27](https://gitlab.com/Rich-Harris/buble/issues/27))
+
+## 0.6.1
+
+* Always remember to close parens in template strings
+
+## 0.6.0
+
+* Strip unnecessary empty strings from template literals
+* Intelligent destructuring for object patterns in parameters ([#17](https://gitlab.com/Rich-Harris/buble/issues/17))
+
+## 0.5.8
+
+* Fix exponentiation assignment operator edge case
+
+## 0.5.7
+
+* Exponentiation operator support ([#24](https://gitlab.com/Rich-Harris/buble/issues/24))
+* More informative error messages for for-of and tagged template strings
+
+## 0.5.6
+
+* Add `dangerousTaggedTemplateString` ([!2](https://gitlab.com/Rich-Harris/buble/merge_requests/2)) and `dangerousForOf` ([!3](https://gitlab.com/Rich-Harris/buble/merge_requests/3)) transforms
+* Prevent deindentation causing errors with removed whitespace in class methods
+* Use correct identifier with default destructured function parameters ([#23](https://gitlab.com/Rich-Harris/buble/issues/23))
+
+
+## 0.5.5
+
+* Ensure `return` is in correct place when creating bodies for arrow functions ([#21](https://gitlab.com/Rich-Harris/buble/issues/21))
+* Prevent deindentation of class methods causing breakage with destructuring statements ([#22](https://gitlab.com/Rich-Harris/buble/issues/22))
+
+## 0.5.4
+
+* Install missing `chalk` dependency
+* Informative error messages when `buble/register` fails
+
+## 0.5.3
+
+* Add `register.js` to package. Yes I'm an idiot
+
+## 0.5.2
+
+* Add `buble/register` for use with e.g. Mocha
+
+## 0.5.1
+
+* Remove unused dependency
+
+## 0.5.0
+
+* Support `--target`, `--yes` and `--no` in CLI
+* Compile entire directory of files via CLI
+* Sourcemap support in CLI
+* All transforms can be disabled (or errors suppressed) with the `transforms` option (or `--yes` and `--no`, in the CLI)
+* `import` and `export` will throw an error unless `--no modules` transform is specified
+* Fix bug with destructuring
+* Fix bug with block scoping and loops
+
+
+## 0.4.24
+
+* Throw if `let`/`const` is redeclared, or `var` is redeclared with a `let`/`const` (0.4.22 regression)
+
+## 0.4.23
+
+* Add `buble.VERSION`
+* Tidy up build process (don't bundle Acorn incorrectly)
+
+## 0.4.22
+
+* Allow double `var` declarations (only throw if `let` or `const` is redeclared)
+
+## 0.4.21
+
+* Add `find` and `findIndex` helpers for 0.12 support
+
+## 0.4.20
+
+* Bump to resolve unknown npm issue
+
+## 0.4.19
+
+* Fix block scoping bug with for loops that don't need to be rewritten as functions
+
+## 0.4.18
+
+* Fix break-inside-switch bug
+
+## 0.4.17
+
+* Support `for...in` loops and block scoping
+
+## 0.4.16
+
+* Add `ie` and `edge` to support matrix
+
+## 0.4.15
+
+* Rewrite reserved properties if specified ([#9](https://gitlab.com/Rich-Harris/buble/issues/9))
+
+## 0.4.14
+
+* Allow classes to extend expressions ([#15](https://gitlab.com/Rich-Harris/buble/issues/15))
+* Experimental (partially implemented) API for disabling transforms based on target environment or custom requirements
+
+## 0.4.13
+
+* Fix return statement bug
+
+## 0.4.12
+
+* More complete and robust transpilation of loops that need to be rewritten as functions to simulate block scoping ([#11](https://gitlab.com/Rich-Harris/buble/issues/11), [#12](https://gitlab.com/Rich-Harris/buble/issues/12), [#13](https://gitlab.com/Rich-Harris/buble/issues/13))
+
+## 0.4.11
+
+* Remove source-map-support from CLI (only useful during development)
+
+## 0.4.10
+
+* Basic support for spread operator
+
+## 0.4.9
+
+* Support getters and setters on subclasses
+* Disallow unsupported features e.g. generators
+
+## 0.4.8
+
+* Support getters and setters on classes
+* Allow identifiers to be renamed in block-scoped destructuring ([#8](https://gitlab.com/Rich-Harris/buble/issues/8))
+* Transpile body-less arrow functions correctly ([#9](https://gitlab.com/Rich-Harris/buble/issues/4))
+
+## 0.4.7
+
+* Add browser version
+
+## 0.4.6
+
+* Report locations of parse/compile errors ([#4](https://gitlab.com/Rich-Harris/buble/issues/4))
+
+## 0.4.5
+
+* Sourcemap support
+
+## 0.4.4
+
+* Support for class expressions
+* More robust deconflicting
+* Various bugfixes
+
+## 0.4.3
+
+* Handle arbitrary whitespace inside template literals
+
+## 0.4.2
+
+* Fix bug-inducing typo
+
+## 0.4.1
+
+* Rest parameters
+
+## 0.4.0
+
+* Self-hosting!
+
+## 0.3.4
+
+* Class inheritance
+
+## 0.3.3
+
+* Handle quote marks in template literals
+
+## 0.3.2
+
+* Handle empty `class` declarations
+
+## 0.3.1
+
+* Add `bin` to package
+
+## 0.3.0
+
+* (Very) basic CLI
+* Handle `export default class ...`
+
+## 0.2.2
+
+* Initialise children of Property nodes
+* Prevent false positives with reference detection
+
+## 0.2.1
+
+* Add missing files
+
+## 0.2.0
+
+* Support for a bunch more ES2015 features
+
+## 0.1.0
+
+* First (experimental) release
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..5454a14
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Rich Harris and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e440acc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# Bublé
+
+## The blazing fast, batteries-included ES2015 compiler
+
+* Try it out at [buble.surge.sh](https://buble.surge.sh)
+* Read the docs at [buble.surge.sh/guide](https://buble.surge.sh/guide)
+
+
+## Quickstart
+
+Via the command line...
+
+```bash
+npm install -g buble
+buble input.js > output.js
+```
+
+...or via the JavaScript API:
+
+```js
+var buble = require( 'buble' );
+var result = buble.transform( source ); // { code: ..., map: ... }
+```
+
+
+## License
+
+MIT
diff --git a/bin/buble b/bin/buble
new file mode 100755
index 0000000..35643d9
--- /dev/null
+++ b/bin/buble
@@ -0,0 +1,28 @@
+#!/usr/bin/env node
+var minimist = require( 'minimist' );
+
+var command = minimist( process.argv.slice( 2 ), {
+	alias: {
+		// Short options
+		h: 'help',
+		i: 'input',
+		m: 'sourcemap',
+		o: 'output',
+		v: 'version',
+		t: 'target',
+		y: 'yes',
+		n: 'no'
+	}
+});
+
+if ( command.help || ( process.argv.length <= 2 && process.stdin.isTTY ) ) {
+	require( './showHelp' )();
+}
+
+else if ( command.version ) {
+	console.log( 'Bublé version ' + require( '../package.json' ).version );
+}
+
+else {
+	require( './runBuble' )( command );
+}
diff --git a/bin/handleError.js b/bin/handleError.js
new file mode 100644
index 0000000..ab9b69f
--- /dev/null
+++ b/bin/handleError.js
@@ -0,0 +1,46 @@
+var chalk = require( 'chalk' );
+
+var handlers = {
+	MISSING_INPUT_OPTION: function () {
+		console.error( chalk.red( 'You must specify an --input (-i) option' ) );
+	},
+
+	MISSING_OUTPUT_DIR: function () {
+		console.error( chalk.red( 'You must specify an --output (-o) option when compiling a directory of files' ) );
+	},
+
+	MISSING_OUTPUT_FILE: function () {
+		console.error( chalk.red( 'You must specify an --output (-o) option when creating a file with a sourcemap' ) );
+	},
+
+	ONE_AT_A_TIME: function ( err ) {
+		console.error( chalk.red( 'Bublé can only compile one file/directory at a time' ) );
+	},
+
+	DUPLICATE_IMPORT_OPTIONS: function ( err ) {
+		console.error( chalk.red( 'use --input, or pass input path as argument – not both' ) );
+	},
+
+	BAD_TARGET: function ( err ) {
+		console.error( chalk.red( 'illegal --target option' ) );
+	}
+};
+
+module.exports = function handleError ( err ) {
+	var handler;
+
+	if ( handler = handlers[ err && err.code ] ) {
+		handler( err );
+	} else {
+		if ( err.snippet ) console.error( chalk.red( '---\n' + err.snippet ) );
+		console.error( chalk.red( err.message || err ) );
+
+		if ( err.stack ) {
+			console.error( chalk.grey( err.stack ) );
+		}
+	}
+
+	console.error( 'Type ' + chalk.cyan( 'buble --help' ) + ' for help, or visit https://buble.surge.sh/guide/' );
+
+	process.exit( 1 );
+};
diff --git a/bin/help.md b/bin/help.md
new file mode 100644
index 0000000..a3f5bb3
--- /dev/null
+++ b/bin/help.md
@@ -0,0 +1,44 @@
+Bublé version <%= version %>
+=====================================
+
+Usage: buble [options] <entry file>
+
+Basic options:
+
+-v, --version            Show version number
+-h, --help               Show this help message
+-i, --input              Input (alternative to <entry file>)
+-o, --output <output>    Output (if absent, prints to stdout)
+-m, --sourcemap          Generate sourcemap (`-m inline` for inline map)
+-t, --target             Select compilation targets
+-y, --yes                Transforms to always apply (overrides --target)
+-n, --no                 Transforms to always skip (overrides --target)
+--jsx                    Custom JSX pragma
+--objectAssign           Specify Object.assign or equivalent polyfill
+--no-named-function-expr Don't output named function expressions
+
+Examples:
+
+# Compile input.js to output.js
+buble input.js > output.js
+
+# Compile input.js to output.js, write sourcemap to output.js.map
+buble input.js -o output.js -m
+
+# Compile input.js to output.js with inline sourcemap
+buble input.js -o output.js -m inline
+
+# Only use transforms necessary for output.js to run in FF43 and Node 5
+buble input.js -o output.js -t firefox:43,node:5
+
+# As above, but use arrow function and destructuring transforms
+buble input.js -o output.js -t firefox:43,node:5 -y arrow,destructuring
+
+# Compile all the files in src/ to dest/
+buble src -o dest
+
+Notes:
+
+* When piping to stdout, only inline sourcemaps are permitted
+
+For more information visit http://buble.surge.sh/guide
diff --git a/bin/runBuble.js b/bin/runBuble.js
new file mode 100644
index 0000000..0e57474
--- /dev/null
+++ b/bin/runBuble.js
@@ -0,0 +1,149 @@
+var fs = require( 'fs' );
+var path = require( 'path' );
+var buble = require( '../dist/buble.deps.js' );
+var handleError = require( './handleError.js' );
+var EOL = require('os').EOL;
+
+function compile ( from, to, command, options ) {
+	try {
+		var stats = fs.statSync( from );
+		if ( stats.isDirectory() ) {
+			compileDir( from, to, command, options );
+		} else {
+			compileFile( from, to, command, options );
+		}
+	} catch ( err ) {
+		handleError( err );
+	}
+}
+
+function compileDir ( from, to, command, options ) {
+	if ( !command.output ) handleError({ code: 'MISSING_OUTPUT_DIR' });
+
+	try {
+		fs.mkdirSync( to )
+	} catch ( e ) {
+		if ( e.code !== 'EEXIST' ) throw e
+	}
+
+	fs.readdirSync( from ).forEach( function ( file ) {
+		compile( path.resolve( from, file ), path.resolve( to, file ), command, options );
+	});
+}
+
+function compileFile ( from, to, command, options ) {
+	var ext = path.extname( from );
+
+	if ( ext !== '.js' && ext !== '.jsm' && ext !== '.es6' && ext !== '.jsx') return;
+
+	if ( to ) {
+		var extTo = path.extname( to );
+		to = to.slice( 0, -extTo.length ) + '.js';
+	}
+
+	var source = fs.readFileSync( from, 'utf-8' );
+	var result = buble.transform( source, {
+		target: options.target,
+		transforms: options.transforms,
+		source: from,
+		file: to,
+		jsx: options.jsx,
+		objectAssign: options.objectAssign,
+		namedFunctionExpressions: options.namedFunctionExpressions
+	});
+
+	write( result, to, command );
+}
+
+function write ( result, to, command ) {
+	if ( command.sourcemap === 'inline' ) {
+		result.code += EOL + '//# sourceMappingURL=' + result.map.toUrl();
+	} else if ( command.sourcemap ) {
+		if ( !to ) {
+			handleError({ code: 'MISSING_OUTPUT_FILE' });
+		}
+
+		result.code += EOL + '//# sourceMappingURL=' + path.basename( to ) + '.map';
+		fs.writeFileSync( to + '.map', result.map.toString() );
+	}
+
+	if ( to ) {
+		fs.writeFileSync( to, result.code );
+	} else {
+		console.log( result.code ); // eslint-disable-line no-console
+	}
+}
+
+module.exports = function ( command ) {
+	if ( command._.length > 1 ) {
+		handleError({ code: 'ONE_AT_A_TIME' });
+	}
+
+	if ( command._.length === 1 ) {
+		if ( command.input ) {
+			handleError({ code: 'DUPLICATE_IMPORT_OPTIONS' });
+		}
+
+		command.input = command._[0];
+	}
+
+	var options = {
+		target: {},
+		transforms: {},
+		jsx: command.jsx,
+		objectAssign: command.objectAssign === true ? "Object.assign" : command.objectAssign,
+		namedFunctionExpressions: command["named-function-expr"] !== false
+	};
+
+	if ( command.target ) {
+		if ( !/^(?:(\w+):([\d\.]+),)*(\w+):([\d\.]+)$/.test( command.target ) ) {
+			handleError({ code: 'BAD_TARGET' });
+		}
+
+		command.target.split( ',' )
+			.map( function ( target ) {
+				return target.split( ':' );
+			})
+			.forEach( function ( pair ) {
+				options.target[ pair[0] ] = pair[1];
+			});
+	}
+
+	if ( command.yes ) {
+		command.yes.split( ',' ).forEach( function ( transform ) {
+			options.transforms[ transform ] = true;
+		});
+	}
+
+	if ( command.no ) {
+		command.no.split( ',' ).forEach( function ( transform ) {
+			options.transforms[ transform ] = false;
+		});
+	}
+
+	if ( command.input ) {
+		compile( command.input, command.output, command, options );
+	}
+
+	else {
+		process.stdin.resume();
+		process.stdin.setEncoding( 'utf8' );
+
+		var source = '';
+
+		process.stdin.on( 'data', function ( chunk ) {
+			source += chunk;
+		});
+
+		process.stdin.on( 'end', function () {
+			options.source = command.input = "stdin";
+			options.file = command.output;
+			try {
+				var result = buble.transform( source, options );
+				write( result, command.output, command );
+			} catch ( err ) {
+				handleError( err );
+			}
+		});
+	}
+};
diff --git a/bin/showHelp.js b/bin/showHelp.js
new file mode 100644
index 0000000..1cc19b6
--- /dev/null
+++ b/bin/showHelp.js
@@ -0,0 +1,13 @@
+var fs = require( 'fs' );
+var path = require( 'path' );
+
+module.exports = function () {
+	fs.readFile( path.join( __dirname, 'help.md' ), function ( err, result ) {
+		var help;
+
+		if ( err ) throw err;
+
+		help = result.toString().replace( '<%= version %>', require( '../package.json' ).version );
+		console.log( '\n' + help + '\n' );
+	});
+};
diff --git "a/bubl\303\251.gif" "b/bubl\303\251.gif"
new file mode 100644
index 0000000..f007879
Binary files /dev/null and "b/bubl\303\251.gif" differ
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..39b9649
--- /dev/null
+++ b/package.json
@@ -0,0 +1,74 @@
+{
+  "name": "buble",
+  "version": "0.15.2",
+  "description": "The blazing fast, batteries-included ES2015 compiler",
+  "main": "dist/buble.umd.js",
+  "jsnext:main": "dist/buble.es.js",
+  "files": [
+    "bin",
+    "src",
+    "dist",
+    "register.js",
+    "README.md"
+  ],
+  "scripts": {
+    "build": "npm run build:umd && npm run build:es && npm run build:browser",
+    "build:umd": "rollup -c -f umd -o dist/buble.umd.js",
+    "build:browser": "rollup -c --environment DEPS -f umd -o dist/buble.deps.js",
+    "build:es": "rollup -c -f es6 -o dist/buble.es.js",
+    "test": "mocha test/test.js --compilers js:./register",
+    "pretest": "npm run build:umd && npm run build:browser",
+    "test:node": "mocha test/test.js --compilers js:./register",
+    "pretest:node": "npm run build:umd",
+    "prepublish": "npm test && npm run build:es",
+    "lint": "eslint src"
+  },
+  "bin": {
+    "buble": "./bin/buble"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://gitlab.com/Rich-Harris/buble.git"
+  },
+  "keywords": [
+    "javascript",
+    "transpilation",
+    "compilation",
+    "esnext",
+    "es2015",
+    "es2017",
+    "es6",
+    "es7"
+  ],
+  "author": "Rich Harris",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://gitlab.com/Rich-Harris/buble/issues"
+  },
+  "homepage": "https://gitlab.com/Rich-Harris/buble#README",
+  "devDependencies": {
+    "buble": "0.8.2",
+    "console-group": "^0.2.1",
+    "eslint": "^2.10.1",
+    "glob": "^7.0.3",
+    "mocha": "^2.4.5",
+    "regexpu-core": "^2.0.0",
+    "rimraf": "^2.5.2",
+    "rollup": "^0.26.3",
+    "rollup-plugin-buble": "^0.8.0",
+    "rollup-plugin-commonjs": "^2.2.1",
+    "rollup-plugin-json": "^2.0.0",
+    "rollup-plugin-node-resolve": "^1.5.0",
+    "source-map": "^0.5.6",
+    "source-map-support": "^0.4.0"
+  },
+  "dependencies": {
+    "acorn": "^3.3.0",
+    "acorn-jsx": "^3.0.1",
+    "acorn-object-spread": "^1.0.0",
+    "chalk": "^1.1.3",
+    "magic-string": "^0.14.0",
+    "minimist": "^1.2.0",
+    "os-homedir": "^1.0.1"
+  }
+}
diff --git a/register.js b/register.js
new file mode 100644
index 0000000..5e1243a
--- /dev/null
+++ b/register.js
@@ -0,0 +1,90 @@
+var fs = require( 'fs' );
+var path = require( 'path' );
+var crypto = require( 'crypto' );
+var homedir = require( 'os-homedir' );
+var buble = require( './' );
+
+var original = require.extensions[ '.js' ];
+var nodeModulesPattern = path.sep === '/' ? /\/node_modules\// : /\\node_modules\\/;
+
+var nodeVersion = /(?:0\.)?\d+/.exec( process.version )[0];
+var versions = [ '0.10', '0.12', '4', '5', '6' ];
+
+if ( !~versions.indexOf( nodeVersion ) ) {
+	if ( +nodeVersion > 6 ) {
+		nodeVersion = '6';
+	} else {
+		throw new Error( 'Unsupported version (' + nodeVersion + '). Please raise an issue at https://gitlab.com/Rich-Harris/buble/issues' );
+	}
+}
+
+var options = {
+	target: {
+		node: nodeVersion
+	}
+};
+
+function mkdirp ( dir ) {
+	var parent = path.dirname( dir );
+	if ( dir === parent ) return;
+	mkdirp( parent );
+
+	try {
+		fs.mkdirSync( dir );
+	} catch ( err ) {
+		if ( err.code !== 'EEXIST' ) throw err;
+	}
+}
+
+var home = homedir();
+if ( home ) {
+	var cachedir = path.join( home, '.buble-cache', nodeVersion );
+	mkdirp( cachedir );
+	fs.writeFileSync( path.join( home, '.buble-cache/README.txt' ), 'These files enable a faster startup when using buble/register. You can safely delete this folder at any time. See https://buble.surge.sh/guide/ for more information.' );
+}
+
+var optionsStringified = JSON.stringify( options );
+
+require.extensions[ '.js' ] = function ( m, filename ) {
+	if ( nodeModulesPattern.test( filename ) ) return original( m, filename );
+
+	var source = fs.readFileSync( filename, 'utf-8' );
+	var hash = crypto.createHash( 'sha256' );
+	hash.update( buble.VERSION );
+	hash.update( optionsStringified );
+	hash.update( source );
+	var key = hash.digest( 'hex' ) + '.json';
+	var cachepath = path.join( cachedir, key );
+
+	var compiled;
+
+	if ( cachedir ) {
+		try {
+			compiled = JSON.parse( fs.readFileSync( cachepath, 'utf-8' ) );
+		} catch ( err ) {
+			// noop
+		}
+	}
+
+	if ( !compiled ) {
+		try {
+			compiled = buble.transform( source, options );
+
+			if ( cachedir ) {
+				fs.writeFileSync( cachepath, JSON.stringify( compiled ) );
+			}
+		} catch ( err ) {
+			if ( err.snippet ) {
+				console.log( 'Error compiling ' + filename + ':\n---' );
+				console.log( err.snippet );
+				console.log( err.message );
+				console.log( '' )
+				process.exit( 1 );
+			}
+
+			throw err;
+		}
+	}
+
+	m._compile( '"use strict";\n' + compiled.code, filename );
+};
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..ce79caa
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,36 @@
+import buble from 'rollup-plugin-buble';
+import json from 'rollup-plugin-json';
+import nodeResolve from 'rollup-plugin-node-resolve';
+import commonjs from 'rollup-plugin-commonjs';
+
+var external = process.env.DEPS ? [] : [ 'acorn/dist/acorn.js', 'magic-string' ];
+
+export default {
+	entry: 'src/index.js',
+	moduleName: 'buble',
+	plugins: [
+		json(),
+		commonjs(),
+		buble({
+			include: [
+				'src/**',
+				'node_modules/acorn-object-spread/**',
+				'node_modules/unicode-loose-match/**',
+				'node_modules/regexpu-core/**'
+			],
+			transforms: {
+				dangerousForOf: true
+			}
+		}),
+		nodeResolve({
+			jsnext: true,
+			skip: external
+		})
+	],
+	external: external,
+	globals: {
+		'acorn/dist/acorn.js': 'acorn',
+		'magic-string': 'MagicString'
+	},
+	sourceMap: true
+};
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..56edecb
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,81 @@
+import acorn from 'acorn/dist/acorn.js';
+import acornJsx from 'acorn-jsx/inject';
+import acornObjectSpread from 'acorn-object-spread/inject';
+import Program from './program/Program.js';
+import { features, matrix } from './support.js';
+import getSnippet from './utils/getSnippet.js';
+
+const { parse } = [
+	acornObjectSpread,
+	acornJsx
+].reduce( ( final, plugin ) => plugin( final ), acorn );
+
+const dangerousTransforms = [
+	'dangerousTaggedTemplateString',
+	'dangerousForOf'
+];
+
+export function target ( target ) {
+	const targets = Object.keys( target );
+	let bitmask = targets.length ?
+		0b1111111111111111111111111111111 :
+		0b1000000000000000000000000000000;
+
+	Object.keys( target ).forEach( environment => {
+		const versions = matrix[ environment ];
+		if ( !versions ) throw new Error( `Unknown environment '${environment}'. Please raise an issue at https://gitlab.com/Rich-Harris/buble/issues` );
+
+		const targetVersion = target[ environment ];
+		if ( !( targetVersion in versions ) ) throw new Error( `Support data exists for the following versions of ${environment}: ${Object.keys( versions ).join( ', ')}. Please raise an issue at https://gitlab.com/Rich-Harris/buble/issues` );
+		const support = versions[ targetVersion ];
+
+		bitmask &= support;
+	});
+
+	let transforms = Object.create( null );
+	features.forEach( ( name, i ) => {
+		transforms[ name ] = !( bitmask & 1 << i );
+	});
+
+	dangerousTransforms.forEach( name => {
+		transforms[ name ] = false;
+	});
+
+	return transforms;
+}
+
+export function transform ( source, options = {} ) {
+	let ast;
+
+	try {
+		ast = parse( source, {
+			ecmaVersion: 7,
+			preserveParens: true,
+			sourceType: 'module',
+			plugins: {
+				jsx: true,
+				objectSpread: true
+			}
+		});
+	} catch ( err ) {
+		err.snippet = getSnippet( source, err.loc );
+		err.toString = () => `${err.name}: ${err.message}\n${err.snippet}`;
+		throw err;
+	}
+
+	let transforms = target( options.target || {} );
+	Object.keys( options.transforms || {} ).forEach( name => {
+		if ( name === 'modules' ) {
+			if ( !( 'moduleImport' in options.transforms ) ) transforms.moduleImport = options.transforms.modules;
+			if ( !( 'moduleExport' in options.transforms ) ) transforms.moduleExport = options.transforms.modules;
+			return;
+		}
+
+		if ( !( name in transforms ) ) throw new Error( `Unknown transform '${name}'` );
+		transforms[ name ] = options.transforms[ name ];
+	});
+
+	return new Program( source, ast, transforms, options ).export( options );
+}
+
+export { version as VERSION } from '../package.json';
diff --git a/src/program/BlockStatement.js b/src/program/BlockStatement.js
new file mode 100644
index 0000000..719ca59
--- /dev/null
+++ b/src/program/BlockStatement.js
@@ -0,0 +1,304 @@
+import './wrap.js'; // TODO necessary for ordering. sort it out
+import Node from './Node.js';
+import Scope from './Scope.js';
+import destructure from '../utils/destructure.js';
+
+function isUseStrict ( node ) {
+	if ( !node ) return false;
+	if ( node.type !== 'ExpressionStatement' ) return false;
+	if ( node.expression.type !== 'Literal' ) return false;
+	return node.expression.value === 'use strict';
+}
+
+export default class BlockStatement extends Node {
+	createScope () {
+		this.parentIsFunction = /Function/.test( this.parent.type );
+		this.isFunctionBlock = this.parentIsFunction || this.parent.type === 'Root';
+		this.scope = new Scope({
+			block: !this.isFunctionBlock,
+			parent: this.parent.findScope( false )
+		});
+
+		if ( this.parentIsFunction ) {
+			this.parent.params.forEach( node => {
+				this.scope.addDeclaration( node, 'param' );
+			});
+		}
+	}
+
+	initialise ( transforms ) {
+		this.thisAlias = null;
+		this.argumentsAlias = null;
+		this.defaultParameters = [];
+
+		// normally the scope gets created here, during initialisation,
+		// but in some cases (e.g. `for` statements), we need to create
+		// the scope early, as it pertains to both the init block and
+		// the body of the statement
+		if ( !this.scope ) this.createScope();
+
+		this.body.forEach( node => node.initialise( transforms ) );
+
+		this.scope.consolidate();
+	}
+
+	findLexicalBoundary () {
+		if ( this.type === 'Program' ) return this;
+		if ( /^Function/.test( this.parent.type ) ) return this;
+
+		return this.parent.findLexicalBoundary();
+	}
+
+	findScope ( functionScope ) {
+		if ( functionScope && !this.isFunctionBlock ) return this.parent.findScope( functionScope );
+		return this.scope;
+	}
+
+	getArgumentsAlias () {
+		if ( !this.argumentsAlias ) {
+			this.argumentsAlias = this.scope.createIdentifier( 'arguments' );
+		}
+
+		return this.argumentsAlias;
+	}
+
+	getArgumentsArrayAlias () {
+		if ( !this.argumentsArrayAlias ) {
+			this.argumentsArrayAlias = this.scope.createIdentifier( 'argsArray' );
+		}
+
+		return this.argumentsArrayAlias;
+	}
+
+	getThisAlias () {
+		if ( !this.thisAlias ) {
+			this.thisAlias = this.scope.createIdentifier( 'this' );
+		}
+
+		return this.thisAlias;
+	}
+
+	getIndentation () {
+		if ( this.indentation === undefined ) {
+			const source = this.program.magicString.original;
+
+			const useOuter = this.synthetic || !this.body.length;
+			let c = useOuter ? this.start : this.body[0].start;
+
+			while ( c && source[c] !== '\n' ) c -= 1;
+
+			this.indentation = '';
+
+			while ( true ) { // eslint-disable-line no-constant-condition
+				c += 1;
+				const char = source[c];
+
+				if ( char !== ' ' && char !== '\t' ) break;
+
+				this.indentation += char;
+			}
+
+			const indentString = this.program.magicString.getIndentString();
+
+			// account for dedented class constructors
+			let parent = this.parent;
+			while ( parent ) {
+				if ( parent.kind === 'constructor' && !parent.parent.parent.superClass ) {
+					this.indentation = this.indentation.replace( indentString, '' );
+				}
+
+				parent = parent.parent;
+			}
+
+			if ( useOuter ) this.indentation += indentString;
+		}
+
+		return this.indentation;
+	}
+
+	transpile ( code, transforms ) {
+		const indentation = this.getIndentation();
+
+		let introStatementGenerators = [];
+
+		if ( this.argumentsAlias ) {
+			introStatementGenerators.push( ( start, prefix, suffix ) => {
+				const assignment = `${prefix}var ${this.argumentsAlias} = arguments${suffix}`;
+				code.insertLeft( start, assignment );
+			});
+		}
+
+		if ( this.thisAlias ) {
+			introStatementGenerators.push( ( start, prefix, suffix ) => {
+				const assignment = `${prefix}var ${this.thisAlias} = this${suffix}`;
+				code.insertLeft( start, assignment );
+			});
+		}
+
+		if ( this.argumentsArrayAlias ) {
+			introStatementGenerators.push( ( start, prefix, suffix ) => {
+				const i = this.scope.createIdentifier( 'i' );
+				const assignment = `${prefix}var ${i} = arguments.length, ${this.argumentsArrayAlias} = Array(${i});\n${indentation}while ( ${i}-- ) ${this.argumentsArrayAlias}[${i}] = arguments[${i}]${suffix}`;
+				code.insertLeft( start, assignment );
+			});
+		}
+
+		if ( /Function/.test( this.parent.type ) ) {
+			this.transpileParameters( code, transforms, indentation, introStatementGenerators );
+		}
+
+		if ( transforms.letConst && this.isFunctionBlock ) {
+			this.transpileBlockScopedIdentifiers( code );
+		}
+
+		super.transpile( code, transforms );
+
+		if ( this.synthetic ) {
+			if ( this.parent.type === 'ArrowFunctionExpression' ) {
+				const expr = this.body[0];
+
+				if ( introStatementGenerators.length ) {
+					code.insertLeft( this.start, `{` ).insertRight( this.end, `${this.parent.getIndentation()}}` );
+
+					code.insertRight( expr.start, `\n${indentation}return ` );
+					code.insertLeft( expr.end, `;\n` );
+				} else if ( transforms.arrow ) {
+					code.insertLeft( expr.start, `{ return ` );
+					code.insertLeft( expr.end, `; }` );
+				}
+			}
+
+			else if ( introStatementGenerators.length ) {
+				code.insertLeft( this.start, `{` ).insertRight( this.end, `}` );
+			}
+		}
+
+		let start;
+		if ( isUseStrict( this.body[0] ) ) {
+			start = this.body[0].end;
+		} else if ( this.synthetic || this.parent.type === 'Root' ) {
+			start = this.start;
+		} else {
+			start = this.start + 1;
+		}
+
+		let prefix = `\n${indentation}`;
+		let suffix = ';';
+		introStatementGenerators.forEach( ( fn, i ) => {
+			if ( i === introStatementGenerators.length - 1 ) suffix = `;\n`;
+			fn( start, prefix, suffix );
+		});
+	}
+
+	transpileParameters ( code, transforms, indentation, introStatementGenerators ) {
+		const params = this.parent.params;
+
+		params.forEach( param => {
+			if ( param.type === 'AssignmentPattern' && param.left.type === 'Identifier' ) {
+				if ( transforms.defaultParameter ) {
+					introStatementGenerators.push( ( start, prefix, suffix ) => {
+						const lhs = `${prefix}if ( ${param.left.name} === void 0 ) ${param.left.name}`;
+
+						code
+							.insertRight( param.left.end, lhs )
+							.move( param.left.end, param.right.end, start )
+							.insertLeft( param.right.end, suffix );
+					});
+				}
+			}
+
+			else if ( param.type === 'RestElement' ) {
+				if ( transforms.spreadRest ) {
+					introStatementGenerators.push( ( start, prefix, suffix ) => {
+						const penultimateParam = params[ params.length - 2 ];
+
+						if ( penultimateParam ) {
+							code.remove( penultimateParam ? penultimateParam.end : param.start, param.end );
+						} else {
+							let start = param.start, end = param.end; // TODO https://gitlab.com/Rich-Harris/buble/issues/8
+
+							while ( /\s/.test( code.original[ start - 1 ] ) ) start -= 1;
+							while ( /\s/.test( code.original[ end ] ) ) end += 1;
+
+							code.remove( start, end );
+						}
+
+						const name = param.argument.name;
+						const len = this.scope.createIdentifier( 'len' );
+						const count = params.length - 1;
+
+						if ( count ) {
+							code.insertLeft( start, `${prefix}var ${name} = [], ${len} = arguments.length - ${count};\n${indentation}while ( ${len}-- > 0 ) ${name}[ ${len} ] = arguments[ ${len} + ${count} ]${suffix}` );
+						} else {
+							code.insertLeft( start, `${prefix}var ${name} = [], ${len} = arguments.length;\n${indentation}while ( ${len}-- ) ${name}[ ${len} ] = arguments[ ${len} ]${suffix}` );
+						}
+					});
+				}
+			}
+
+			else if ( param.type !== 'Identifier' ) {
+				if ( transforms.parameterDestructuring ) {
+					const ref = this.scope.createIdentifier( 'ref' );
+					destructure( code, this.scope, param, ref, false, introStatementGenerators );
+					code.insertLeft( param.start, ref );
+				}
+			}
+		});
+	}
+
+	transpileBlockScopedIdentifiers ( code ) {
+		Object.keys( this.scope.blockScopedDeclarations ).forEach( name => {
+			const declarations = this.scope.blockScopedDeclarations[ name ];
+
+			for ( let declaration of declarations ) {
+				let cont = false; // TODO implement proper continue...
+
+				if ( declaration.kind === 'for.let' ) {
+					// special case
+					const forStatement = declaration.node.findNearest( 'ForStatement' );
+
+					if ( forStatement.shouldRewriteAsFunction ) {
+						const outerAlias = this.scope.createIdentifier( name );
+						const innerAlias = forStatement.reassigned[ name ] ?
+							this.scope.createIdentifier( name ) :
+							name;
+
+						declaration.name = outerAlias;
+						code.overwrite( declaration.node.start, declaration.node.end, outerAlias, true );
+
+						forStatement.aliases[ name ] = {
+							outer: outerAlias,
+							inner: innerAlias
+						};
+
+						for ( const identifier of declaration.instances ) {
+							const alias = forStatement.body.contains( identifier ) ?
+								innerAlias :
+								outerAlias;
+
+							if ( name !== alias ) {
+								code.overwrite( identifier.start, identifier.end, alias, true );
+							}
+						}
+
+						cont = true;
+					}
+				}
+
+				if ( !cont ) {
+					const alias = this.scope.createIdentifier( name );
+
+					if ( name !== alias ) {
+						declaration.name = alias;
+						code.overwrite( declaration.node.start, declaration.node.end, alias, true );
+
+						for ( const identifier of declaration.instances ) {
+							identifier.rewritten = true;
+							code.overwrite( identifier.start, identifier.end, alias, true );
+						}
+					}
+				}
+			}
+		});
+	}
+}
diff --git a/src/program/Node.js b/src/program/Node.js
new file mode 100644
index 0000000..1b29677
--- /dev/null
+++ b/src/program/Node.js
@@ -0,0 +1,124 @@
+import wrap from './wrap.js';
+import keys from './keys.js';
+
+// used for debugging, without the noise created by
+// circular references
+function toJSON ( node ) {
+	var obj = {};
+
+	Object.keys( node ).forEach( key => {
+		if ( key === 'parent' || key === 'program' || key === 'keys' || key === '__wrapped' ) return;
+
+		if ( Array.isArray( node[ key ] ) ) {
+			obj[ key ] = node[ key ].map( toJSON );
+		} else if ( node[ key ] && node[ key ].toJSON ) {
+			obj[ key ] = node[ key ].toJSON();
+		} else {
+			obj[ key ] = node[ key ];
+		}
+	});
+
+	return obj;
+}
+
+export default class Node {
+	constructor ( raw, parent ) {
+		raw.parent = parent;
+		raw.program = parent.program || parent;
+		raw.depth = parent.depth + 1;
+		raw.keys = keys[ raw.type ];
+		raw.indentation = undefined;
+
+		for ( const key of keys[ raw.type ] ) {
+			wrap( raw[ key ], raw );
+		}
+
+		raw.program.magicString.addSourcemapLocation( raw.start );
+		raw.program.magicString.addSourcemapLocation( raw.end );
+	}
+
+	ancestor ( level ) {
+		let node = this;
+		while ( level-- ) {
+			node = node.parent;
+			if ( !node ) return null;
+		}
+
+		return node;
+	}
+
+	contains ( node ) {
+		while ( node ) {
+			if ( node === this ) return true;
+			node = node.parent;
+		}
+
+		return false;
+	}
+
+	findLexicalBoundary () {
+		return this.parent.findLexicalBoundary();
+	}
+
+	findNearest ( type ) {
+		if ( typeof type === 'string' ) type = new RegExp( `^${type}$` );
+		if ( type.test( this.type ) ) return this;
+		return this.parent.findNearest( type );
+	}
+
+	unparenthesizedParent () {
+		let node = this.parent;
+		while ( node && node.type === 'ParenthesizedExpression' ) {
+			node = node.parent;
+		}
+		return node;
+	}
+
+	unparenthesize () {
+		let node = this;
+		while ( node.type === 'ParenthesizedExpression' ) {
+			node = node.expression;
+		}
+		return node;
+	}
+
+	findScope ( functionScope ) {
+		return this.parent.findScope( functionScope );
+	}
+
+	getIndentation () {
+		return this.parent.getIndentation();
+	}
+
+	initialise ( transforms ) {
+		for ( var key of this.keys ) {
+			const value = this[ key ];
+
+			if ( Array.isArray( value ) ) {
+				value.forEach( node => node && node.initialise( transforms ) );
+			} else if ( value && typeof value === 'object' ) {
+				value.initialise( transforms );
+			}
+		}
+	}
+
+	toJSON () {
+		return toJSON( this );
+	}
+
+	toString () {
+		return this.program.magicString.original.slice( this.start, this.end );
+	}
+
+	transpile ( code, transforms ) {
+		for ( const key of this.keys ) {
+			const value = this[ key ];
+
+			if ( Array.isArray( value ) ) {
+				value.forEach( node => node && node.transpile( code, transforms ) );
+			} else if ( value && typeof value === 'object' ) {
+				value.transpile( code, transforms );
+			}
+		}
+	}
+}
diff --git a/src/program/Program.js b/src/program/Program.js
new file mode 100644
index 0000000..73c2436
--- /dev/null
+++ b/src/program/Program.js
@@ -0,0 +1,53 @@
+import MagicString from 'magic-string';
+import BlockStatement from './BlockStatement.js';
+import wrap from './wrap.js';
+
+export default function Program ( source, ast, transforms, options ) {
+	this.type = 'Root';
+
+	// options
+	this.jsx = options.jsx || 'React.createElement';
+	this.options = options;
+
+	this.source = source;
+	this.magicString = new MagicString( source );
+
+	this.ast = ast;
+	this.depth = 0;
+
+	wrap( this.body = ast, this );
+	this.body.__proto__ = BlockStatement.prototype;
+
+	this.indentExclusionElements = [];
+	this.body.initialise( transforms );
+
+	this.indentExclusions = Object.create( null );
+	for ( const node of this.indentExclusionElements ) {
+		for ( let i = node.start; i < node.end; i += 1 ) {
+			this.indentExclusions[ i ] = true;
+		}
+	}
+
+	this.body.transpile( this.magicString, transforms );
+}
+
+Program.prototype = {
+	export ( options = {} ) {
+		return {
+			code: this.magicString.toString(),
+			map: this.magicString.generateMap({
+				file: options.file,
+				source: options.source,
+				includeContent: options.includeContent !== false
+			})
+		};
+	},
+
+	findNearest () {
+		return null;
+	},
+
+	findScope () {
+		return null;
+	}
+};
diff --git a/src/program/Scope.js b/src/program/Scope.js
new file mode 100644
index 0000000..b844b6b
--- /dev/null
+++ b/src/program/Scope.js
@@ -0,0 +1,100 @@
+import extractNames from './extractNames.js';
+import reserved from '../utils/reserved.js';
+import CompileError from '../utils/CompileError.js';
+
+const letConst = /^(?:let|const)$/;
+
+export default function Scope ( options ) {
+	options = options || {};
+
+	this.parent = options.parent;
+	this.isBlockScope = !!options.block;
+
+	let scope = this;
+	while ( scope.isBlockScope ) scope = scope.parent;
+	this.functionScope = scope;
+
+	this.identifiers = [];
+	this.declarations = Object.create( null );
+	this.references = Object.create( null );
+	this.blockScopedDeclarations = this.isBlockScope ? null : Object.create( null );
+	this.aliases = this.isBlockScope ? null : Object.create( null );
+}
+
+Scope.prototype = {
+	addDeclaration ( node, kind ) {
+		for ( const identifier of extractNames( node ) ) {
+			const name = identifier.name;
+			const existingDeclaration = this.declarations[ name ];
+			if ( existingDeclaration && ( letConst.test( kind ) || letConst.test( existingDeclaration.kind ) ) ) {
+				// TODO warn about double var declarations?
+				throw new CompileError( identifier, `${name} is already declared` );
+			}
+
+			const declaration = { name, node: identifier, kind, instances: [] };
+			this.declarations[ name ] = declaration;
+
+			if ( this.isBlockScope ) {
+				if ( !this.functionScope.blockScopedDeclarations[ name ] ) this.functionScope.blockScopedDeclarations[ name ] = [];
+				this.functionScope.blockScopedDeclarations[ name ].push( declaration );
+			}
+		}
+	},
+
+	addReference ( identifier ) {
+		if ( this.consolidated ) {
+			this.consolidateReference( identifier );
+		} else {
+			this.identifiers.push( identifier );
+		}
+	},
+
+	consolidate () {
+		for ( let i = 0; i < this.identifiers.length; i += 1 ) { // we might push to the array during consolidation, so don't cache length
+			const identifier = this.identifiers[i];
+			this.consolidateReference( identifier );
+		}
+
+		this.consolidated = true; // TODO understand why this is necessary... seems bad
+	},
+
+	consolidateReference ( identifier ) {
+		const declaration = this.declarations[ identifier.name ];
+		if ( declaration ) {
+			declaration.instances.push( identifier );
+		} else {
+			this.references[ identifier.name ] = true;
+			if ( this.parent ) this.parent.addReference( identifier );
+		}
+	},
+
+	contains ( name ) {
+		return this.declarations[ name ] ||
+		       ( this.parent ? this.parent.contains( name ) : false );
+	},
+
+	createIdentifier ( base ) {
+		if ( typeof base === 'number' ) base = base.toString();
+
+		base = base
+			.replace( /\s/g, '' )
+			.replace( /\[([^\]]+)\]/g, '_$1' )
+			.replace( /[^a-zA-Z0-9_$]/g, '_' )
+			.replace( /_{2,}/, '_' );
+
+		let name = base;
+		let counter = 1;
+
+		while ( this.declarations[ name ] || this.references[ name ] || this.aliases[ name ] || name in reserved ) {
+			name = `${base}$${counter++}`;
+		}
+
+		this.aliases[ name ] = true;
+		return name;
+	},
+
+	findDeclaration ( name ) {
+		return this.declarations[ name ] ||
+		       ( this.parent && this.parent.findDeclaration( name ) );
+	}
+};
diff --git a/src/program/extractNames.js b/src/program/extractNames.js
new file mode 100644
index 0000000..84825e7
--- /dev/null
+++ b/src/program/extractNames.js
@@ -0,0 +1,31 @@
+export default function extractNames ( node ) {
+	const names = [];
+	extractors[ node.type ]( names, node );
+	return names;
+}
+
+const extractors = {
+	Identifier ( names, node ) {
+		names.push( node );
+	},
+
+	ObjectPattern ( names, node ) {
+		for ( const prop of node.properties ) {
+			extractors[ prop.value.type ]( names, prop.value );
+		}
+	},
+
+	ArrayPattern ( names, node ) {
+		for ( const element of node.elements )  {
+			if ( element ) extractors[ element.type ]( names, element );
+		}
+	},
+
+	RestElement ( names, node ) {
+		extractors[ node.argument.type ]( names, node.argument );
+	},
+
+	AssignmentPattern ( names, node ) {
+		extractors[ node.left.type ]( names, node.left );
+	}
+};
diff --git a/src/program/keys.js b/src/program/keys.js
new file mode 100644
index 0000000..194ccf7
--- /dev/null
+++ b/src/program/keys.js
@@ -0,0 +1,4 @@
+export default {
+	Program: [ 'body' ],
+	Literal: []
+};
diff --git a/src/program/types/ArrayExpression.js b/src/program/types/ArrayExpression.js
new file mode 100644
index 0000000..15c04dc
--- /dev/null
+++ b/src/program/types/ArrayExpression.js
@@ -0,0 +1,55 @@
+import Node from '../Node.js';
+import spread, { isArguments } from '../../utils/spread.js';
+
+export default class ArrayExpression extends Node {
+	initialise ( transforms ) {
+		if ( transforms.spreadRest && this.elements.length ) {
+			const lexicalBoundary = this.findLexicalBoundary();
+
+			let i = this.elements.length;
+			while ( i-- ) {
+				const element = this.elements[i];
+				if ( element && element.type === 'SpreadElement' && isArguments( element.argument ) ) {
+					this.argumentsArrayAlias = lexicalBoundary.getArgumentsArrayAlias();
+				}
+			}
+		}
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.spreadRest ) {
+			// erase trailing comma after last array element if not an array hole
+			if ( this.elements.length ) {
+				let lastElement = this.elements[ this.elements.length - 1 ];
+				if ( lastElement && /\s*,/.test( code.original.slice( lastElement.end, this.end ) ) ) {
+					code.overwrite( lastElement.end, this.end - 1, ' ' );
+				}
+			}
+
+			if ( this.elements.length === 1 ) {
+				const element = this.elements[0];
+
+				if ( element && element.type === 'SpreadElement' ) {
+					// special case – [ ...arguments ]
+					if ( isArguments( element.argument ) ) {
+						code.overwrite( this.start, this.end, `[].concat( ${this.argumentsArrayAlias} )` ); // TODO if this is the only use of argsArray, don't bother concating
+					} else {
+						code.overwrite( this.start, element.argument.start, '[].concat( ' );
+						code.overwrite( element.end, this.end, ' )' );
+					}
+				}
+			}
+			else {
+				const hasSpreadElements = spread( code, this.elements, this.start, this.argumentsArrayAlias );
+
+				if ( hasSpreadElements ) {
+					code.overwrite( this.end - 1, this.end, ')' );
+				}
+			}
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ArrowFunctionExpression.js b/src/program/types/ArrowFunctionExpression.js
new file mode 100644
index 0000000..9951d59
--- /dev/null
+++ b/src/program/types/ArrowFunctionExpression.js
@@ -0,0 +1,36 @@
+import Node from '../Node.js';
+
+export default class ArrowFunctionExpression extends Node {
+	initialise ( transforms ) {
+		this.body.createScope();
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.arrow ) {
+			// remove arrow
+			let charIndex = this.body.start;
+			while ( code.original[ charIndex ] !== '=' ) {
+				charIndex -= 1;
+			}
+			code.remove( charIndex, this.body.start );
+
+			// wrap naked parameter
+			if ( this.params.length === 1 && this.start === this.params[0].start ) {
+				code.insertRight( this.params[0].start, '(' );
+				code.insertLeft( this.params[0].end, ')' );
+			}
+
+			// add function
+			if ( this.parent && this.parent.type === 'ExpressionStatement' ) {
+				// standalone expression statement
+				code.insertRight( this.start, '(function' );
+				code.insertRight( this.end, ')' );
+			} else {
+				code.insertRight( this.start, 'function ' );
+			}
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/AssignmentExpression.js b/src/program/types/AssignmentExpression.js
new file mode 100644
index 0000000..d4969c4
--- /dev/null
+++ b/src/program/types/AssignmentExpression.js
@@ -0,0 +1,268 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class AssignmentExpression extends Node {
+	initialise ( transforms ) {
+		if ( this.left.type === 'Identifier' ) {
+			const declaration = this.findScope( false ).findDeclaration( this.left.name );
+			if ( declaration && declaration.kind === 'const' ) {
+				throw new CompileError( this.left, `${this.left.name} is read-only` );
+			}
+
+			// special case – https://gitlab.com/Rich-Harris/buble/issues/11
+			const statement = declaration && declaration.node.ancestor( 3 );
+			if ( statement && statement.type === 'ForStatement' && statement.body.contains( this ) ) {
+				statement.reassigned[ this.left.name ] = true;
+			}
+		}
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( this.operator === '**=' && transforms.exponentiation ) {
+			this.transpileExponentiation( code, transforms );
+		}
+
+		else if ( /Pattern/.test( this.left.type ) && transforms.destructuring ) {
+			this.transpileDestructuring( code, transforms );
+		}
+
+		super.transpile( code, transforms );
+	}
+
+	transpileDestructuring ( code ) {
+		const scope = this.findScope( true );
+		const assign = scope.createIdentifier( 'assign' );
+		const temporaries = [ assign ];
+
+		const start = this.start;
+
+		// We need to pick out some elements from the original code,
+		// interleaved with generated code. These helpers are used to
+		// easily do that while keeping the order of the output
+		// predictable.
+		let text = '';
+		function use ( node ) {
+			code.insertRight( node.start, text );
+			code.move( node.start, node.end, start );
+			text = '';
+		}
+		function write ( string ) {
+			text += string;
+		}
+
+		write( `(${assign} = ` );
+		use( this.right );
+
+		// Walk `pattern`, generating code that assigns the value in
+		// `ref` to it. When `mayDuplicate` is false, the function
+		// must take care to only output `ref` once.
+		function destructure ( pattern, ref, mayDuplicate ) {
+			if ( pattern.type === 'Identifier' || pattern.type === 'MemberExpression' ) {
+				write( ', ' );
+				use( pattern );
+				write( ` = ${ref}` );
+			}
+
+			else if ( pattern.type === 'AssignmentPattern' ) {
+				if ( pattern.left.type === 'Identifier' ) {
+					const target = pattern.left.name;
+					let source = ref;
+					if ( !mayDuplicate ) {
+						write( `, ${target} = ${ref}` );
+						source = target;
+					}
+					write( `, ${target} = ${source} === void 0 ? ` );
+					use( pattern.right );
+					write( ` : ${source}` );
+				}
+				else {
+					const target = scope.createIdentifier( 'temp' );
+					let source = ref;
+					temporaries.push( target );
+					if ( !mayDuplicate ) {
+						write( `, ${target} = ${ref}` );
+						source = target;
+					}
+					write( `, ${target} = ${source} === void 0 ? ` );
+					use( pattern.right );
+					write( ` : ${source}` );
+					destructure( pattern.left, target, true );
+				}
+			}
+
+			else if ( pattern.type === 'ArrayPattern' ) {
+				const elements = pattern.elements;
+				if ( elements.length === 1 ) {
+					destructure( elements[0], `${ref}[0]`, false );
+				}
+				else {
+					if ( !mayDuplicate ) {
+						const temp = scope.createIdentifier( 'array' );
+						temporaries.push( temp );
+						write( `, ${temp} = ${ref}` );
+						ref = temp;
+					}
+					elements.forEach( ( element, i ) => {
+						if ( element ) {
+							if ( element.type === 'RestElement' ) {
+								destructure( element.argument, `${ref}.slice(${i})`, false );
+							} else {
+								destructure( element, `${ref}[${i}]`, false );
+							}
+						}
+					} );
+				}
+			}
+
+			else if ( pattern.type === 'ObjectPattern' ) {
+				const props = pattern.properties;
+				if ( props.length == 1 ) {
+					const prop = props[0];
+					const value = prop.computed || prop.key.type !== 'Identifier' ? `${ref}[${code.slice(prop.key.start, prop.key.end)}]` : `${ref}.${prop.key.name}`;
+					destructure( prop.value, value, false );
+				}
+				else {
+					if ( !mayDuplicate ) {
+						const temp = scope.createIdentifier( 'obj' );
+						temporaries.push( temp );
+						write( `, ${temp} = ${ref}` );
+						ref = temp;
+					}
+					props.forEach( prop => {
+						const value = prop.computed || prop.key.type !== 'Identifier' ? `${ref}[${code.slice(prop.key.start, prop.key.end)}]` : `${ref}.${prop.key.name}`;
+						destructure( prop.value, value, false );
+					} );
+				}
+			}
+
+			else {
+				throw new Error( `Unexpected node type in destructuring assignment (${pattern.type})` );
+			}
+		}
+		destructure( this.left, assign, true );
+
+		if ( this.unparenthesizedParent().type === 'ExpressionStatement' ) {
+			// no rvalue needed for expression statement
+			code.insertRight( start, `${text})` );
+		} else {
+			// destructuring is part of an expression - need an rvalue
+			code.insertRight( start, `${text}, ${assign})` );
+		}
+
+		code.remove( start, this.right.start );
+
+		const statement = this.findNearest( /(?:Statement|Declaration)$/ );
+		code.insertLeft( statement.start, `var ${temporaries.join( ', ' )};\n${statement.getIndentation()}` );
+	}
+
+	transpileExponentiation ( code ) {
+		const scope = this.findScope( false );
+		const getAlias = name => {
+			const declaration = scope.findDeclaration( name );
+			return declaration ? declaration.name : name;
+		};
+
+		// first, the easy part – `**=` -> `=`
+		let charIndex = this.left.end;
+		while ( code.original[ charIndex ] !== '*' ) charIndex += 1;
+		code.remove( charIndex, charIndex + 2 );
+
+		// how we do the next part depends on a number of factors – whether
+		// this is a top-level statement, and whether we're updating a
+		// simple or complex reference
+		let base;
+
+		const left = this.left.unparenthesize();
+
+		if ( left.type === 'Identifier' ) {
+			base = getAlias( left.name );
+		} else if ( left.type === 'MemberExpression' ) {
+			let object;
+			let needsObjectVar = false;
+			let property;
+			let needsPropertyVar = false;
+
+			const statement = this.findNearest( /(?:Statement|Declaration)$/ );
+			const i0 = statement.getIndentation();
+
+			if ( left.property.type === 'Identifier' ) {
+				property = left.computed ? getAlias( left.property.name ) : left.property.name;
+			} else {
+				property = scope.createIdentifier( 'property' );
+				needsPropertyVar = true;
+			}
+
+			if ( left.object.type === 'Identifier' ) {
+				object = getAlias( left.object.name );
+			} else {
+				object = scope.createIdentifier( 'object' );
+				needsObjectVar = true;
+			}
+
+			if ( left.start === statement.start ) {
+				if ( needsObjectVar && needsPropertyVar ) {
+					code.insertRight( statement.start, `var ${object} = ` );
+					code.overwrite( left.object.end, left.property.start, `;\n${i0}var ${property} = ` );
+					code.overwrite( left.property.end, left.end, `;\n${i0}${object}[${property}]` );
+				}
+
+				else if ( needsObjectVar ) {
+					code.insertRight( statement.start, `var ${object} = ` );
+					code.insertLeft( left.object.end, `;\n${i0}` );
+					code.insertLeft( left.object.end, object );
+				}
+
+				else if ( needsPropertyVar ) {
+					code.insertRight( left.property.start, `var ${property} = ` );
+					code.insertLeft( left.property.end, `;\n${i0}` );
+					code.move( left.property.start, left.property.end, this.start );
+
+					code.insertLeft( left.object.end, `[${property}]` );
+					code.remove( left.object.end, left.property.start );
+					code.remove( left.property.end, left.end );
+				}
+			}
+
+			else {
+				let declarators = [];
+				if ( needsObjectVar ) declarators.push( object );
+				if ( needsPropertyVar ) declarators.push( property );
+
+				if ( declarators.length ) {
+					code.insertRight( statement.start, `var ${declarators.join( ', ' )};\n${i0}` );
+				}
+
+				if ( needsObjectVar && needsPropertyVar ) {
+					code.insertRight( left.start, `( ${object} = ` );
+					code.overwrite( left.object.end, left.property.start, `, ${property} = ` );
+					code.overwrite( left.property.end, left.end, `, ${object}[${property}]` );
+				}
+
+				else if ( needsObjectVar ) {
+					code.insertRight( left.start, `( ${object} = ` );
+					code.insertLeft( left.object.end, `, ${object}` );
+				}
+
+				else if ( needsPropertyVar ) {
+					code.insertRight( left.property.start, `( ${property} = ` );
+					code.insertLeft( left.property.end, `, ` );
+					code.move( left.property.start, left.property.end, left.start );
+
+					code.overwrite( left.object.end, left.property.start, `[${property}]` );
+					code.remove( left.property.end, left.end );
+				}
+
+				if ( needsPropertyVar ) {
+					code.insertLeft( this.end, ` )` );
+				}
+			}
+
+			base = object + ( left.computed || needsPropertyVar ? `[${property}]` : `.${property}` );
+		}
+
+		code.insertRight( this.right.start, `Math.pow( ${base}, ` );
+		code.insertLeft( this.right.end, ` )` );
+	}
+}
diff --git a/src/program/types/BinaryExpression.js b/src/program/types/BinaryExpression.js
new file mode 100644
index 0000000..8d0bf61
--- /dev/null
+++ b/src/program/types/BinaryExpression.js
@@ -0,0 +1,12 @@
+import Node from '../Node.js';
+
+export default class BinaryExpression extends Node {
+	transpile ( code, transforms ) {
+		if ( this.operator === '**' && transforms.exponentiation ) {
+			code.insertRight( this.start, `Math.pow( ` );
+			code.overwrite( this.left.end, this.right.start, `, ` );
+			code.insertLeft( this.end, ` )` );
+		}
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/BreakStatement.js b/src/program/types/BreakStatement.js
new file mode 100644
index 0000000..ea24dd9
--- /dev/null
+++ b/src/program/types/BreakStatement.js
@@ -0,0 +1,22 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class BreakStatement extends Node {
+	initialise () {
+		const loop = this.findNearest( loopStatement );
+		const switchCase = this.findNearest( 'SwitchCase' );
+
+		if ( loop && ( !switchCase || loop.depth > switchCase.depth ) ) {
+			loop.canBreak = true;
+			this.loop = loop;
+		}
+	}
+
+	transpile ( code ) {
+		if ( this.loop && this.loop.shouldRewriteAsFunction ) {
+			if ( this.label ) throw new CompileError( this, 'Labels are not currently supported in a loop with locally-scoped variables' );
+			code.overwrite( this.start, this.start + 5, `return 'break'` );
+		}
+	}
+}
diff --git a/src/program/types/CallExpression.js b/src/program/types/CallExpression.js
new file mode 100644
index 0000000..1e07ede
--- /dev/null
+++ b/src/program/types/CallExpression.js
@@ -0,0 +1,98 @@
+import Node from '../Node.js';
+import spread, { isArguments } from '../../utils/spread.js';
+
+export default class CallExpression extends Node {
+	initialise ( transforms ) {
+		if ( transforms.spreadRest && this.arguments.length > 1 ) {
+			const lexicalBoundary = this.findLexicalBoundary();
+
+			let i = this.arguments.length;
+			while ( i-- ) {
+				const arg = this.arguments[i];
+				if ( arg.type === 'SpreadElement' && isArguments( arg.argument ) ) {
+					this.argumentsArrayAlias = lexicalBoundary.getArgumentsArrayAlias();
+				}
+			}
+		}
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.spreadRest && this.arguments.length ) {
+			let hasSpreadElements = false;
+			let context;
+
+			const firstArgument = this.arguments[0];
+
+			if ( this.arguments.length === 1 ) {
+				if ( firstArgument.type === 'SpreadElement' ) {
+					code.remove( firstArgument.start, firstArgument.argument.start );
+					hasSpreadElements = true;
+				}
+			} else {
+				hasSpreadElements = spread( code, this.arguments, firstArgument.start, this.argumentsArrayAlias );
+			}
+
+			if ( hasSpreadElements ) {
+
+				// we need to handle super() and super.method() differently
+				// due to its instance
+				let _super = null;
+				if ( this.callee.type === 'Super' ) {
+					_super = this.callee;
+				}
+				else if ( this.callee.type === 'MemberExpression' && this.callee.object.type === 'Super' ) {
+					_super = this.callee.object;
+				}
+
+				if ( !_super && this.callee.type === 'MemberExpression' ) {
+					if ( this.callee.object.type === 'Identifier' ) {
+						context = this.callee.object.name;
+					} else {
+						context = this.findScope( true ).createIdentifier( 'ref' );
+						const callExpression = this.callee.object;
+						const enclosure = callExpression.findNearest( /Function/ );
+						const block = enclosure ? enclosure.body.body
+							: callExpression.findNearest( /^Program$/ ).body;
+						const lastStatementInBlock = block[ block.length - 1 ];
+						const i0 = lastStatementInBlock.getIndentation();
+						code.insertRight( callExpression.start, `(${context} = ` );
+						code.insertLeft( callExpression.end, `)` );
+						code.insertLeft( lastStatementInBlock.end, `\n${i0}var ${context};` );
+					}
+				} else {
+					context = 'void 0';
+				}
+
+				code.insertLeft( this.callee.end, '.apply' );
+
+				if ( _super ) {
+					_super.noCall = true; // bit hacky...
+
+					if ( this.arguments.length > 1 ) {
+						if ( firstArgument.type !== 'SpreadElement' ) {
+							code.insertRight( firstArgument.start, `[ ` );
+						}
+
+						code.insertLeft( this.arguments[ this.arguments.length - 1 ].end, ' )' );
+					}
+				}
+
+				else if ( this.arguments.length === 1 ) {
+					code.insertRight( firstArgument.start, `${context}, ` );
+				} else {
+					if ( firstArgument.type === 'SpreadElement' ) {
+						code.insertLeft( firstArgument.start, `${context}, ` );
+					} else {
+						code.insertLeft( firstArgument.start, `${context}, [ ` );
+					}
+
+					code.insertLeft( this.arguments[ this.arguments.length - 1 ].end, ' )' );
+				}
+			}
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ClassBody.js b/src/program/types/ClassBody.js
new file mode 100644
index 0000000..8df3398
--- /dev/null
+++ b/src/program/types/ClassBody.js
@@ -0,0 +1,186 @@
+import Node from '../Node.js';
+import { findIndex } from '../../utils/array.js';
+import reserved from '../../utils/reserved.js';
+
+// TODO this code is pretty wild, tidy it up
+export default class ClassBody extends Node {
+	transpile ( code, transforms, inFunctionExpression, superName ) {
+		if ( transforms.classes ) {
+			const name = this.parent.name;
+
+			const indentStr = code.getIndentString();
+			const i0 = this.getIndentation() + ( inFunctionExpression ? indentStr : '' );
+			const i1 = i0 + indentStr;
+
+			const constructorIndex = findIndex( this.body, node => node.kind === 'constructor' );
+			const constructor = this.body[ constructorIndex ];
+
+			let introBlock = '';
+			let outroBlock = '';
+
+			if ( this.body.length ) {
+				code.remove( this.start, this.body[0].start );
+				code.remove( this.body[ this.body.length - 1 ].end, this.end );
+			} else {
+				code.remove( this.start, this.end );
+			}
+
+			if ( constructor ) {
+				constructor.value.body.isConstructorBody = true;
+
+				const previousMethod = this.body[ constructorIndex - 1 ];
+				const nextMethod = this.body[ constructorIndex + 1 ];
+
+				// ensure constructor is first
+				if ( constructorIndex > 0 ) {
+					code.remove( previousMethod.end, constructor.start );
+					code.move( constructor.start, nextMethod ? nextMethod.start : this.end - 1, this.body[0].start );
+				}
+
+				if ( !inFunctionExpression ) code.insertLeft( constructor.end, ';' );
+			}
+
+			let namedFunctions = this.program.options.namedFunctionExpressions !== false;
+			let namedConstructor = namedFunctions || this.parent.superClass || this.parent.type !== 'ClassDeclaration';
+			if ( this.parent.superClass ) {
+				let inheritanceBlock = `if ( ${superName} ) ${name}.__proto__ = ${superName};\n${i0}${name}.prototype = Object.create( ${superName} && ${superName}.prototype );\n${i0}${name}.prototype.constructor = ${name};`;
+
+				if ( constructor ) {
+					introBlock += `\n\n${i0}` + inheritanceBlock;
+				} else {
+					const fn = `function ${name} () {` + ( superName ?
+						`\n${i1}${superName}.apply(this, arguments);\n${i0}}` :
+						`}` ) + ( inFunctionExpression ? '' : ';' ) + ( this.body.length ? `\n\n${i0}` : '' );
+
+					inheritanceBlock = fn + inheritanceBlock;
+					introBlock += inheritanceBlock + `\n\n${i0}`;
+				}
+			} else if ( !constructor ) {
+				let fn = 'function ' + (namedConstructor ? name + ' ' : '') + '() {}';
+				if ( this.parent.type === 'ClassDeclaration' ) fn += ';';
+				if ( this.body.length ) fn += `\n\n${i0}`;
+
+				introBlock += fn;
+			}
+
+			const scope = this.findScope( false );
+
+			let prototypeGettersAndSetters = [];
+			let staticGettersAndSetters = [];
+			let prototypeAccessors;
+			let staticAccessors;
+
+			this.body.forEach( ( method, i ) => {
+				if ( method.kind === 'constructor' ) {
+					let constructorName = namedConstructor ? ' ' + name : '';
+					code.overwrite( method.key.start, method.key.end, `function${constructorName}` );
+					return;
+				}
+
+				if ( method.static ) {
+					const len = code.original[ method.start + 6 ] == ' ' ? 7 : 6;
+					code.remove( method.start, method.start + len );
+				}
+
+				const isAccessor = method.kind !== 'method';
+				let lhs;
+
+				let methodName = method.key.name;
+				if ( reserved[ methodName ] || method.value.body.scope.references[methodName] ) {
+					methodName = scope.createIdentifier( methodName );
+				}
+
+				// when method name is a string or a number let's pretend it's a computed method
+
+				let fake_computed = false;
+				if ( ! method.computed && method.key.type === 'Literal' ) {
+					fake_computed = true;
+					method.computed = true;
+				}
+
+				if ( isAccessor ) {
+					if ( method.computed ) {
+						throw new Error( 'Computed accessor properties are not currently supported' );
+					}
+
+					code.remove( method.start, method.key.start );
+
+					if ( method.static ) {
+						if ( !~staticGettersAndSetters.indexOf( method.key.name ) ) staticGettersAndSetters.push( method.key.name );
+						if ( !staticAccessors ) staticAccessors = scope.createIdentifier( 'staticAccessors' );
+
+						lhs = `${staticAccessors}`;
+					} else {
+						if ( !~prototypeGettersAndSetters.indexOf( method.key.name ) ) prototypeGettersAndSetters.push( method.key.name );
+						if ( !prototypeAccessors ) prototypeAccessors = scope.createIdentifier( 'prototypeAccessors' );
+
+						lhs = `${prototypeAccessors}`;
+					}
+				} else {
+					lhs = method.static ?
+						`${name}` :
+						`${name}.prototype`;
+				}
+
+				if ( !method.computed ) lhs += '.';
+
+				const insertNewlines = ( constructorIndex > 0 && i === constructorIndex + 1 ) ||
+				                       ( i === 0 && constructorIndex === this.body.length - 1 );
+
+				if ( insertNewlines ) lhs = `\n\n${i0}${lhs}`;
+
+				let c = method.key.end;
+				if ( method.computed ) {
+					if ( fake_computed ) {
+						code.insertRight( method.key.start, '[' );
+						code.insertLeft( method.key.end, ']' );
+					} else {
+						while ( code.original[c] !== ']' ) c += 1;
+						c += 1;
+					}
+				}
+
+				code.insertRight( method.start, lhs );
+
+				const funcName = method.computed || isAccessor || !namedFunctions ? '' : `${methodName} `;
+				const rhs = ( isAccessor ? `.${method.kind}` : '' ) + ` = function` + ( method.value.generator ? '* ' : ' ' ) + funcName;
+				code.remove( c, method.value.start );
+				code.insertRight( method.value.start, rhs );
+				code.insertLeft( method.end, ';' );
+
+				if ( method.value.generator ) code.remove( method.start, method.key.start );
+			});
+
+			if ( prototypeGettersAndSetters.length || staticGettersAndSetters.length ) {
+				let intro = [];
+				let outro = [];
+
+				if ( prototypeGettersAndSetters.length ) {
+					intro.push( `var ${prototypeAccessors} = { ${prototypeGettersAndSetters.map( name => `${name}: {}` ).join( ',' )} };` );
+					outro.push( `Object.defineProperties( ${name}.prototype, ${prototypeAccessors} );` );
+				}
+
+				if ( staticGettersAndSetters.length ) {
+					intro.push( `var ${staticAccessors} = { ${staticGettersAndSetters.map( name => `${name}: {}` ).join( ',' )} };` );
+					outro.push( `Object.defineProperties( ${name}, ${staticAccessors} );` );
+				}
+
+				if ( constructor ) introBlock += `\n\n${i0}`;
+				introBlock += intro.join( `\n${i0}` );
+				if ( !constructor ) introBlock += `\n\n${i0}`;
+
+				outroBlock += `\n\n${i0}` + outro.join( `\n${i0}` );
+			}
+
+			if ( constructor ) {
+				code.insertLeft( constructor.end, introBlock );
+			} else {
+				code.insertRight( this.start, introBlock );
+			}
+
+			code.insertLeft( this.end, outroBlock );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ClassDeclaration.js b/src/program/types/ClassDeclaration.js
new file mode 100644
index 0000000..6447737
--- /dev/null
+++ b/src/program/types/ClassDeclaration.js
@@ -0,0 +1,62 @@
+import Node from '../Node.js';
+import deindent from '../../utils/deindent.js';
+
+export default class ClassDeclaration extends Node {
+	initialise ( transforms ) {
+		this.name = this.id.name;
+		this.findScope( true ).addDeclaration( this.id, 'class' );
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.classes ) {
+			if ( !this.superClass ) deindent( this.body, code );
+
+			const superName = this.superClass && ( this.superClass.name || 'superclass' );
+
+			const i0 = this.getIndentation();
+			const i1 = i0 + code.getIndentString();
+
+			// if this is an export default statement, we have to move the export to
+			// after the declaration, because `export default var Foo = ...` is illegal
+			const syntheticDefaultExport = this.parent.type === 'ExportDefaultDeclaration' ?
+				`\n\n${i0}export default ${this.id.name};` :
+				'';
+
+			if ( syntheticDefaultExport ) code.remove( this.parent.start, this.start );
+
+			code.overwrite( this.start, this.id.start, 'var ' );
+
+			if ( this.superClass ) {
+				if ( this.superClass.end === this.body.start ) {
+					code.remove( this.id.end, this.superClass.start );
+					code.insertLeft( this.id.end, ` = (function (${superName}) {\n${i1}` );
+				} else {
+					code.overwrite( this.id.end, this.superClass.start, ' = ' );
+					code.overwrite( this.superClass.end, this.body.start, `(function (${superName}) {\n${i1}` );
+				}
+			} else {
+				if ( this.id.end === this.body.start ) {
+					code.insertLeft( this.id.end, ' = ' );
+				} else {
+					code.overwrite( this.id.end, this.body.start, ' = ' );
+				}
+			}
+
+			this.body.transpile( code, transforms, !!this.superClass, superName );
+
+			if ( this.superClass ) {
+				code.insertLeft( this.end, `\n\n${i1}return ${this.name};\n${i0}}(` );
+				code.move( this.superClass.start, this.superClass.end, this.end );
+				code.insertRight( this.end, `));${syntheticDefaultExport}` );
+			} else if ( syntheticDefaultExport ) {
+				code.insertRight( this.end, syntheticDefaultExport );
+			}
+		}
+
+		else {
+			this.body.transpile( code, transforms, false, null );
+		}
+	}
+}
diff --git a/src/program/types/ClassExpression.js b/src/program/types/ClassExpression.js
new file mode 100644
index 0000000..e9209ff
--- /dev/null
+++ b/src/program/types/ClassExpression.js
@@ -0,0 +1,45 @@
+import Node from '../Node.js';
+
+export default class ClassExpression extends Node {
+	initialise ( transforms ) {
+		this.name = this.id ? this.id.name :
+		            this.parent.type === 'VariableDeclarator' ? this.parent.id.name :
+		            this.parent.type === 'AssignmentExpression' ? this.parent.left.name :
+		            this.findScope( true ).createIdentifier( 'anonymous' );
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.classes ) {
+			const superName = this.superClass && ( this.superClass.name || 'superclass' );
+
+			const i0 = this.getIndentation();
+			const i1 = i0 + code.getIndentString();
+
+			if ( this.superClass ) {
+				code.remove( this.start, this.superClass.start );
+				code.remove( this.superClass.end, this.body.start );
+				code.insertLeft( this.start, `(function (${superName}) {\n${i1}` );
+			} else {
+				code.overwrite( this.start, this.body.start, `(function () {\n${i1}` );
+			}
+
+			this.body.transpile( code, transforms, true, superName );
+
+			const outro = `\n\n${i1}return ${this.name};\n${i0}}(`;
+
+			if ( this.superClass ) {
+				code.insertLeft( this.end, outro );
+				code.move( this.superClass.start, this.superClass.end, this.end );
+				code.insertRight( this.end, '))' );
+			} else {
+				code.insertLeft( this.end, `\n\n${i1}return ${this.name};\n${i0}}())` );
+			}
+		}
+
+		else {
+			this.body.transpile( code, transforms, false );
+		}
+	}
+}
diff --git a/src/program/types/ContinueStatement.js b/src/program/types/ContinueStatement.js
new file mode 100644
index 0000000..a9e50de
--- /dev/null
+++ b/src/program/types/ContinueStatement.js
@@ -0,0 +1,13 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class ContinueStatement extends Node {
+	transpile ( code ) {
+		const loop = this.findNearest( loopStatement );
+		if ( loop.shouldRewriteAsFunction ) {
+			if ( this.label ) throw new CompileError( this, 'Labels are not currently supported in a loop with locally-scoped variables' );
+			code.overwrite( this.start, this.start + 8, 'return' );
+		}
+	}
+}
diff --git a/src/program/types/ExportDefaultDeclaration.js b/src/program/types/ExportDefaultDeclaration.js
new file mode 100644
index 0000000..d361a75
--- /dev/null
+++ b/src/program/types/ExportDefaultDeclaration.js
@@ -0,0 +1,9 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class ExportDefaultDeclaration extends Node {
+	initialise ( transforms ) {
+		if ( transforms.moduleExport ) throw new CompileError( this, 'export is not supported' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/ExportNamedDeclaration.js b/src/program/types/ExportNamedDeclaration.js
new file mode 100644
index 0000000..2db6a76
--- /dev/null
+++ b/src/program/types/ExportNamedDeclaration.js
@@ -0,0 +1,9 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class ExportNamedDeclaration extends Node {
+	initialise ( transforms ) {
+		if ( transforms.moduleExport ) throw new CompileError( this, 'export is not supported' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/ForInStatement.js b/src/program/types/ForInStatement.js
new file mode 100644
index 0000000..07e63d7
--- /dev/null
+++ b/src/program/types/ForInStatement.js
@@ -0,0 +1,22 @@
+import LoopStatement from './shared/LoopStatement.js';
+import extractNames from '../extractNames.js';
+
+export default class ForInStatement extends LoopStatement {
+	findScope ( functionScope ) {
+		return functionScope || !this.createdScope ? this.parent.findScope( functionScope ) : this.body.scope;
+	}
+
+	transpile ( code, transforms ) {
+		if ( this.shouldRewriteAsFunction ) {
+			// which variables are declared in the init statement?
+			const names = this.left.type === 'VariableDeclaration' ?
+				[].concat.apply( [], this.left.declarations.map( declarator => extractNames( declarator.id ) ) ) :
+				[];
+
+			this.args = names.map( name => name in this.aliases ? this.aliases[ name ].outer : name );
+			this.params = names.map( name => name in this.aliases ? this.aliases[ name ].inner : name );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ForOfStatement.js b/src/program/types/ForOfStatement.js
new file mode 100644
index 0000000..afe479c
--- /dev/null
+++ b/src/program/types/ForOfStatement.js
@@ -0,0 +1,75 @@
+import LoopStatement from './shared/LoopStatement.js';
+import CompileError from '../../utils/CompileError.js';
+import destructure from '../../utils/destructure.js';
+
+export default class ForOfStatement extends LoopStatement {
+	initialise ( transforms ) {
+		if ( transforms.forOf && !transforms.dangerousForOf ) throw new CompileError( this, 'for...of statements are not supported. Use `transforms: { forOf: false }` to skip transformation and disable this error, or `transforms: { dangerousForOf: true }` if you know what you\'re doing' );
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( !transforms.dangerousForOf ) {
+			super.transpile( code, transforms );
+			return;
+		}
+
+		// edge case (#80)
+		if ( !this.body.body[0] ) {
+			if ( this.left.type === 'VariableDeclaration' && this.left.kind === 'var' ) {
+				code.remove( this.start, this.left.start );
+				code.insertLeft( this.left.end, ';' );
+				code.remove( this.left.end, this.end );
+			} else {
+				code.remove( this.start, this.end );
+			}
+
+			return;
+		}
+
+		const scope = this.findScope( true );
+		const i0 = this.getIndentation();
+		const i1 = i0 + code.getIndentString();
+
+		const key = scope.createIdentifier( 'i' );
+		const list = scope.createIdentifier( 'list' );
+
+		if ( this.body.synthetic ) {
+			code.insertRight( this.left.start, `{\n${i1}` );
+			code.insertLeft( this.body.body[0].end, `\n${i0}}` );
+		}
+
+		const bodyStart = this.body.body[0].start;
+
+		code.remove( this.left.end, this.right.start );
+		code.move( this.left.start, this.left.end, bodyStart );
+
+
+		code.insertRight( this.right.start, `var ${key} = 0, ${list} = ` );
+		code.insertLeft( this.right.end, `; ${key} < ${list}.length; ${key} += 1` );
+
+		// destructuring. TODO non declaration destructuring
+		const declarator = this.left.type === 'VariableDeclaration' && this.left.declarations[0];
+		if ( declarator && declarator.id.type !== 'Identifier' ) {
+			let statementGenerators = [];
+			const ref = scope.createIdentifier( 'ref' );
+			destructure( code, scope, declarator.id, ref, false, statementGenerators );
+
+			let suffix = `;\n${i1}`;
+			statementGenerators.forEach( ( fn, i ) => {
+				if ( i === statementGenerators.length - 1 ) {
+					suffix = `;\n\n${i1}`;
+				}
+
+				fn( bodyStart, '', suffix );
+			});
+
+			code.insertLeft( this.left.start + this.left.kind.length + 1, ref );
+			code.insertLeft( this.left.end, ` = ${list}[${key}];\n${i1}` );
+		} else {
+			code.insertLeft( this.left.end, ` = ${list}[${key}];\n\n${i1}` );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ForStatement.js b/src/program/types/ForStatement.js
new file mode 100644
index 0000000..8f25b2f
--- /dev/null
+++ b/src/program/types/ForStatement.js
@@ -0,0 +1,38 @@
+import LoopStatement from './shared/LoopStatement.js';
+import extractNames from '../extractNames.js';
+
+export default class ForStatement extends LoopStatement {
+	findScope ( functionScope ) {
+		return functionScope || !this.createdScope ? this.parent.findScope( functionScope ) : this.body.scope;
+	}
+
+	transpile ( code, transforms ) {
+		const i1 = this.getIndentation() + code.getIndentString();
+
+		if ( this.shouldRewriteAsFunction ) {
+			// which variables are declared in the init statement?
+			const names = this.init.type === 'VariableDeclaration' ?
+				[].concat.apply( [], this.init.declarations.map( declarator => extractNames( declarator.id ) ) ) :
+				[];
+
+			const aliases = this.aliases;
+
+			this.args = names.map( name => name in this.aliases ? this.aliases[ name ].outer : name );
+			this.params = names.map( name => name in this.aliases ? this.aliases[ name ].inner : name );
+
+			const updates = Object.keys( this.reassigned )
+				.map( name => `${aliases[ name ].outer} = ${aliases[ name ].inner};` );
+
+			if ( updates.length ) {
+				if ( this.body.synthetic ) {
+					code.insertLeft( this.body.body[0].end, `; ${updates.join(` `)}` );
+				} else {
+					const lastStatement = this.body.body[ this.body.body.length - 1 ];
+					code.insertLeft( lastStatement.end, `\n\n${i1}${updates.join(`\n${i1}`)}` );
+				}
+			}
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/FunctionDeclaration.js b/src/program/types/FunctionDeclaration.js
new file mode 100644
index 0000000..d0fd207
--- /dev/null
+++ b/src/program/types/FunctionDeclaration.js
@@ -0,0 +1,15 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class FunctionDeclaration extends Node {
+	initialise ( transforms ) {
+		if ( this.generator && transforms.generator ) {
+			throw new CompileError( this, 'Generators are not supported' );
+		}
+
+		this.body.createScope();
+
+		this.findScope( true ).addDeclaration( this.id, 'function' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/FunctionExpression.js b/src/program/types/FunctionExpression.js
new file mode 100644
index 0000000..86c0e1c
--- /dev/null
+++ b/src/program/types/FunctionExpression.js
@@ -0,0 +1,64 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class FunctionExpression extends Node {
+	initialise ( transforms ) {
+		if ( this.generator && transforms.generator ) {
+			throw new CompileError( this, 'Generators are not supported' );
+		}
+
+		this.body.createScope();
+
+		if ( this.id ) {
+			// function expression IDs belong to the child scope...
+			this.body.scope.addDeclaration( this.id, 'function' );
+		}
+
+		super.initialise( transforms );
+
+		const parent = this.parent;
+		let methodName;
+
+		if ( transforms.conciseMethodProperty
+				&& parent.type === 'Property'
+				&& parent.kind === 'init'
+				&& parent.method
+				&& parent.key.type === 'Identifier' ) {
+			// object literal concise method
+			methodName = parent.key.name;
+		}
+		else if ( transforms.classes
+				&& parent.type === 'MethodDefinition'
+				&& parent.kind === 'method'
+				&& parent.key.type === 'Identifier' ) {
+			// method definition in a class
+			methodName = parent.key.name;
+		}
+		else if ( this.id && this.id.type === 'Identifier' ) {
+			// naked function expression
+			methodName = this.id.alias || this.id.name;
+		}
+
+		if ( methodName ) {
+			for ( const param of this.params ) {
+				if ( param.type === 'Identifier' && methodName === param.name ) {
+					// workaround for Safari 9/WebKit bug:
+					// https://gitlab.com/Rich-Harris/buble/issues/154
+					// change parameter name when same as method name
+
+					const scope = this.body.scope;
+					const declaration = scope.declarations[ methodName ];
+
+					const alias = scope.createIdentifier( methodName );
+					param.alias = alias;
+
+					for ( const identifier of declaration.instances ) {
+						identifier.alias = alias;
+					}
+
+					break;
+				}
+			}
+		}
+	}
+}
diff --git a/src/program/types/Identifier.js b/src/program/types/Identifier.js
new file mode 100644
index 0000000..8cb6d1c
--- /dev/null
+++ b/src/program/types/Identifier.js
@@ -0,0 +1,43 @@
+import Node from '../Node.js';
+import isReference from '../../utils/isReference.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class Identifier extends Node {
+	findScope ( functionScope ) {
+		if ( this.parent.params && ~this.parent.params.indexOf( this ) ) {
+			return this.parent.body.scope;
+		}
+
+		if ( this.parent.type === 'FunctionExpression' && this === this.parent.id ) {
+			return this.parent.body.scope;
+		}
+
+		return this.parent.findScope( functionScope	);
+	}
+
+	initialise ( transforms ) {
+		if ( transforms.arrow && isReference( this, this.parent ) ) {
+			if ( this.name === 'arguments' && !this.findScope( false ).contains( this.name ) ) {
+				const lexicalBoundary = this.findLexicalBoundary();
+				const arrowFunction = this.findNearest( 'ArrowFunctionExpression' );
+				const loop = this.findNearest( loopStatement );
+
+				if ( arrowFunction && arrowFunction.depth > lexicalBoundary.depth ) {
+					this.alias = lexicalBoundary.getArgumentsAlias();
+				}
+
+				if ( loop && loop.body.contains( this ) && loop.depth > lexicalBoundary.depth ) {
+					this.alias = lexicalBoundary.getArgumentsAlias();
+				}
+			}
+
+			this.findScope( false ).addReference( this );
+		}
+	}
+
+	transpile ( code ) {
+		if ( this.alias ) {
+			code.overwrite( this.start, this.end, this.alias, true );
+		}
+	}
+}
diff --git a/src/program/types/IfStatement.js b/src/program/types/IfStatement.js
new file mode 100644
index 0000000..87ad989
--- /dev/null
+++ b/src/program/types/IfStatement.js
@@ -0,0 +1,24 @@
+import Node from '../Node.js';
+
+export default class IfStatement extends Node {
+	initialise ( transforms ) {
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( this.consequent.type !== 'BlockStatement'
+				|| this.consequent.type === 'BlockStatement' && this.consequent.synthetic ) {
+			code.insertLeft( this.consequent.start, '{ ' );
+			code.insertRight( this.consequent.end, ' }' );
+		}
+
+		if ( this.alternate && this.alternate.type !== 'IfStatement' && (
+				this.alternate.type !== 'BlockStatement'
+				|| this.alternate.type === 'BlockStatement' && this.alternate.synthetic ) ) {
+			code.insertLeft( this.alternate.start, '{ ' );
+			code.insertRight( this.alternate.end, ' }' );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ImportDeclaration.js b/src/program/types/ImportDeclaration.js
new file mode 100644
index 0000000..3c07fc5
--- /dev/null
+++ b/src/program/types/ImportDeclaration.js
@@ -0,0 +1,9 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class ImportDeclaration extends Node {
+	initialise ( transforms ) {
+		if ( transforms.moduleImport ) throw new CompileError( this, 'import is not supported' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/ImportDefaultSpecifier.js b/src/program/types/ImportDefaultSpecifier.js
new file mode 100644
index 0000000..f1505d0
--- /dev/null
+++ b/src/program/types/ImportDefaultSpecifier.js
@@ -0,0 +1,8 @@
+import Node from '../Node.js';
+
+export default class ImportDefaultSpecifier extends Node {
+	initialise ( transforms ) {
+		this.findScope( true ).addDeclaration( this.local, 'import' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/ImportSpecifier.js b/src/program/types/ImportSpecifier.js
new file mode 100644
index 0000000..1e99f3f
--- /dev/null
+++ b/src/program/types/ImportSpecifier.js
@@ -0,0 +1,8 @@
+import Node from '../Node.js';
+
+export default class ImportSpecifier extends Node {
+	initialise ( transforms ) {
+		this.findScope( true ).addDeclaration( this.local, 'import' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/JSXAttribute.js b/src/program/types/JSXAttribute.js
new file mode 100644
index 0000000..e5645db
--- /dev/null
+++ b/src/program/types/JSXAttribute.js
@@ -0,0 +1,20 @@
+import Node from '../Node.js';
+
+const IS_DATA_ATTRIBUTE = /-/;
+
+export default class JSXAttribute extends Node {
+	transpile ( code, transforms ) {
+		if ( this.value ) {
+			code.overwrite( this.name.end, this.value.start, ': ' );
+		} else {
+			// tag without value
+			code.overwrite( this.name.start, this.name.end, `${this.name.name}: true` );
+		}
+
+		if ( IS_DATA_ATTRIBUTE.test( this.name.name ) ) {
+			code.overwrite( this.name.start, this.name.end, `'${this.name.name}'` );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/JSXClosingElement.js b/src/program/types/JSXClosingElement.js
new file mode 100644
index 0000000..e3e03fe
--- /dev/null
+++ b/src/program/types/JSXClosingElement.js
@@ -0,0 +1,22 @@
+import Node from '../Node.js';
+
+function containsNewLine ( node ) {
+	return node.type === 'Literal' && !/\S/.test( node.value ) && /\n/.test( node.value );
+}
+
+export default class JSXClosingElement extends Node {
+	transpile ( code ) {
+		let spaceBeforeParen = true;
+
+		const lastChild = this.parent.children[ this.parent.children.length - 1 ];
+
+		// omit space before closing paren if
+		//   a) this is on a separate line, or
+		//   b) there are no children but there are attributes
+		if ( ( lastChild && containsNewLine( lastChild ) ) || ( this.parent.openingElement.attributes.length ) ) {
+			spaceBeforeParen = false;
+		}
+
+		code.overwrite( this.start, this.end, spaceBeforeParen ? ' )' : ')' );
+	}
+}
diff --git a/src/program/types/JSXElement.js b/src/program/types/JSXElement.js
new file mode 100644
index 0000000..b0f5892
--- /dev/null
+++ b/src/program/types/JSXElement.js
@@ -0,0 +1,50 @@
+import Node from '../Node.js';
+
+function normalise ( str, removeTrailingWhitespace ) {
+	if ( removeTrailingWhitespace && /\n/.test( str ) ) {
+		str = str.replace( /\s+$/, '' );
+	}
+
+	str = str
+		.replace( /^\n\r?\s+/, '' )       // remove leading newline + space
+		.replace( /\s*\n\r?\s*/gm, ' ' ); // replace newlines with spaces
+
+	// TODO prefer single quotes?
+	return JSON.stringify( str );
+}
+
+export default class JSXElement extends Node {
+	transpile ( code, transforms ) {
+		super.transpile( code, transforms );
+
+		const children = this.children.filter( child => {
+			if ( child.type !== 'Literal' ) return true;
+
+			// remove whitespace-only literals, unless on a single line
+			return /\S/.test( child.value ) || !/\n/.test( child.value );
+		});
+
+		if ( children.length ) {
+			let c = this.openingElement.end;
+
+			let i;
+			for ( i = 0; i < children.length; i += 1 ) {
+				const child = children[i];
+
+				if ( child.type === 'JSXExpressionContainer' && child.expression.type === 'JSXEmptyExpression' ) {
+					// empty block is a no op
+				} else {
+					const tail = code.original[ c ] === '\n' && child.type !== 'Literal' ? '' : ' ';
+					code.insertLeft( c, `,${tail}` );
+				}
+
+				if ( child.type === 'Literal' ) {
+					const str = normalise( child.value, i === children.length - 1 );
+					code.overwrite( child.start, child.end, str );
+				}
+
+				c = child.end;
+			}
+		}
+	}
+}
diff --git a/src/program/types/JSXExpressionContainer.js b/src/program/types/JSXExpressionContainer.js
new file mode 100644
index 0000000..e45e66d
--- /dev/null
+++ b/src/program/types/JSXExpressionContainer.js
@@ -0,0 +1,10 @@
+import Node from '../Node.js';
+
+export default class JSXExpressionContainer extends Node {
+	transpile ( code, transforms ) {
+		code.remove( this.start, this.expression.start );
+		code.remove( this.expression.end, this.end );
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/JSXOpeningElement.js b/src/program/types/JSXOpeningElement.js
new file mode 100644
index 0000000..1147b7b
--- /dev/null
+++ b/src/program/types/JSXOpeningElement.js
@@ -0,0 +1,85 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class JSXOpeningElement extends Node {
+	transpile ( code, transforms ) {
+		code.overwrite( this.start, this.name.start, `${this.program.jsx}( ` );
+
+		const html = this.name.type === 'JSXIdentifier' && this.name.name[0] === this.name.name[0].toLowerCase();
+		if ( html ) code.insertRight( this.name.start, `'` );
+
+		const len = this.attributes.length;
+		let c = this.name.end;
+
+		if ( len ) {
+			let i;
+
+			let hasSpread = false;
+			for ( i = 0; i < len; i += 1 ) {
+				if ( this.attributes[i].type === 'JSXSpreadAttribute' ) {
+					hasSpread = true;
+					break;
+				}
+			}
+
+			c = this.attributes[0].end;
+
+			for ( i = 0; i < len; i += 1 ) {
+				const attr = this.attributes[i];
+
+				if ( i > 0 ) {
+					code.overwrite( c, attr.start, ', ' );
+				}
+
+				if ( hasSpread && attr.type !== 'JSXSpreadAttribute' ) {
+					const lastAttr = this.attributes[ i - 1 ];
+					const nextAttr = this.attributes[ i + 1 ];
+
+					if ( !lastAttr || lastAttr.type === 'JSXSpreadAttribute' ) {
+						code.insertRight( attr.start, '{ ' );
+					}
+
+					if ( !nextAttr || nextAttr.type === 'JSXSpreadAttribute' ) {
+						code.insertLeft( attr.end, ' }' );
+					}
+				}
+
+				c = attr.end;
+			}
+
+			let after;
+			let before;
+			if ( hasSpread ) {
+				if ( len === 1 ) {
+					before = html ? `',` : ',';
+				} else {
+					if (!this.program.options.objectAssign) {
+						throw new CompileError( this, 'Mixed JSX attributes ending in spread requires specified objectAssign option with \'Object.assign\' or polyfill helper.' );
+					}
+					before = html ? `', ${this.program.options.objectAssign}({},` : `, ${this.program.options.objectAssign}({},`;
+					after = ')';
+				}
+			} else {
+				before = html ? `', {` : ', {';
+				after = ' }';
+			}
+
+			code.insertRight( this.name.end, before );
+
+			if ( after ) {
+				code.insertLeft( this.attributes[ len - 1 ].end, after );
+			}
+		} else {
+			code.insertLeft( this.name.end, html ? `', null` : `, null` );
+			c = this.name.end;
+		}
+
+		super.transpile( code, transforms );
+
+		if ( this.selfClosing ) {
+			code.overwrite( c, this.end, this.attributes.length ? `)` : ` )` );
+		} else {
+			code.remove( c, this.end );
+		}
+	}
+}
diff --git a/src/program/types/JSXSpreadAttribute.js b/src/program/types/JSXSpreadAttribute.js
new file mode 100644
index 0000000..0a68076
--- /dev/null
+++ b/src/program/types/JSXSpreadAttribute.js
@@ -0,0 +1,10 @@
+import Node from '../Node.js';
+
+export default class JSXSpreadAttribute extends Node {
+	transpile ( code, transforms ) {
+		code.remove( this.start, this.argument.start );
+		code.remove( this.argument.end, this.end );
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/Literal.js b/src/program/types/Literal.js
new file mode 100644
index 0000000..3718c56
--- /dev/null
+++ b/src/program/types/Literal.js
@@ -0,0 +1,29 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+import rewritePattern from 'regexpu-core';
+
+export default class Literal extends Node {
+	initialise () {
+		if ( typeof this.value === 'string' ) {
+			this.program.indentExclusionElements.push( this );
+		}
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.numericLiteral ) {
+			const leading = this.raw.slice( 0, 2 );
+			if ( leading === '0b' || leading === '0o' ) {
+				code.overwrite( this.start, this.end, String( this.value ), true );
+			}
+		}
+
+		if ( this.regex ) {
+			const { pattern, flags } = this.regex;
+
+			if ( transforms.stickyRegExp && /y/.test( flags ) ) throw new CompileError( this, 'Regular expression sticky flag is not supported' );
+			if ( transforms.unicodeRegExp && /u/.test( flags ) ) {
+				code.overwrite( this.start, this.end, `/${rewritePattern( pattern, flags )}/${flags.replace( 'u', '' )}` );
+			}
+		}
+	}
+}
diff --git a/src/program/types/MemberExpression.js b/src/program/types/MemberExpression.js
new file mode 100644
index 0000000..977a2ff
--- /dev/null
+++ b/src/program/types/MemberExpression.js
@@ -0,0 +1,13 @@
+import Node from '../Node.js';
+import reserved from '../../utils/reserved.js';
+
+export default class MemberExpression extends Node {
+	transpile ( code, transforms ) {
+		if ( transforms.reservedProperties && reserved[ this.property.name ] ) {
+			code.overwrite( this.object.end, this.property.start, `['` );
+			code.insertLeft( this.property.end, `']` );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/NewExpression.js b/src/program/types/NewExpression.js
new file mode 100644
index 0000000..dce5310
--- /dev/null
+++ b/src/program/types/NewExpression.js
@@ -0,0 +1,37 @@
+import Node from '../Node.js';
+import spread, { isArguments } from '../../utils/spread.js';
+
+export default class NewExpression extends Node {
+	initialise ( transforms ) {
+		if ( transforms.spreadRest && this.arguments.length ) {
+			const lexicalBoundary = this.findLexicalBoundary();
+
+			let i = this.arguments.length;
+			while ( i-- ) {
+				const arg = this.arguments[i];
+				if ( arg.type === 'SpreadElement' && isArguments( arg.argument ) ) {
+					this.argumentsArrayAlias = lexicalBoundary.getArgumentsArrayAlias();
+					break;
+				}
+			}
+		}
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.spreadRest && this.arguments.length ) {
+			const firstArgument = this.arguments[0];
+			const isNew = true;
+			let hasSpreadElements = spread( code, this.arguments, firstArgument.start, this.argumentsArrayAlias, isNew );
+
+			if ( hasSpreadElements ) {
+				code.insertRight( this.start + 'new'.length, ' (Function.prototype.bind.apply(' );
+				code.overwrite( this.callee.end, firstArgument.start, ', [ null ].concat( ' );
+				code.insertLeft( this.end, ' ))' );
+			}
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ObjectExpression.js b/src/program/types/ObjectExpression.js
new file mode 100644
index 0000000..9c3c393
--- /dev/null
+++ b/src/program/types/ObjectExpression.js
@@ -0,0 +1,142 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class ObjectExpression extends Node {
+	transpile ( code, transforms ) {
+		super.transpile( code, transforms );
+
+		let firstPropertyStart = this.start + 1;
+		let regularPropertyCount = 0;
+		let spreadPropertyCount = 0;
+		let computedPropertyCount = 0;
+
+		for ( let prop of this.properties ) {
+			if ( prop.type === 'SpreadProperty' ) {
+				spreadPropertyCount += 1;
+			} else if ( prop.computed ) {
+				computedPropertyCount += 1;
+			} else if ( prop.type === 'Property' ) {
+				regularPropertyCount += 1;
+			}
+		}
+
+		if ( spreadPropertyCount ) {
+			if ( !this.program.options.objectAssign ) {
+				throw new CompileError( this, 'Object spread operator requires specified objectAssign option with \'Object.assign\' or polyfill helper.' );
+			}
+			// enclose run of non-spread properties in curlies
+			let i = this.properties.length;
+			if ( regularPropertyCount ) {
+				while ( i-- ) {
+					const prop = this.properties[i];
+
+					if ( prop.type === 'Property' && !prop.computed ) {
+						const lastProp = this.properties[ i - 1 ];
+						const nextProp = this.properties[ i + 1 ];
+
+						if ( !lastProp || lastProp.type !== 'Property' || lastProp.computed ) {
+							code.insertRight( prop.start, '{' );
+						}
+
+						if ( !nextProp || nextProp.type !== 'Property' || nextProp.computed ) {
+							code.insertLeft( prop.end, '}' );
+						}
+					}
+				}
+			}
+
+			// wrap the whole thing in Object.assign
+			firstPropertyStart = this.properties[0].start;
+			code.overwrite( this.start, firstPropertyStart, `${this.program.options.objectAssign}({}, `);
+			code.overwrite( this.properties[ this.properties.length - 1 ].end, this.end, ')' );
+		}
+
+		if ( computedPropertyCount && transforms.computedProperty ) {
+			const i0 = this.getIndentation();
+
+			let isSimpleAssignment;
+			let name;
+
+			if ( this.parent.type === 'VariableDeclarator' && this.parent.parent.declarations.length === 1 ) {
+				isSimpleAssignment = true;
+				name = this.parent.id.alias || this.parent.id.name; // TODO is this right?
+			} else if ( this.parent.type === 'AssignmentExpression' && this.parent.parent.type === 'ExpressionStatement' && this.parent.left.type === 'Identifier' ) {
+				isSimpleAssignment = true;
+				name = this.parent.left.alias || this.parent.left.name; // TODO is this right?
+			} else if ( this.parent.type === 'AssignmentPattern' && this.parent.left.type === 'Identifier' ) {
+				isSimpleAssignment = true;
+				name = this.parent.left.alias || this.parent.left.name; // TODO is this right?
+			}
+
+			// handle block scoping
+			const declaration = this.findScope( false ).findDeclaration( name );
+			if ( declaration ) name = declaration.name;
+
+			const start = firstPropertyStart;
+			const end = this.end;
+
+			if ( isSimpleAssignment ) {
+				// ???
+			} else {
+				name = this.findScope( true ).createIdentifier( 'obj' );
+
+				const statement = this.findNearest( /(?:Statement|Declaration)$/ );
+				code.insertLeft( statement.end, `\n${i0}var ${name};` );
+
+				code.insertRight( this.start, `( ${name} = ` );
+			}
+
+			const len = this.properties.length;
+			let lastComputedProp;
+			let sawNonComputedProperty = false;
+
+			for ( let i = 0; i < len; i += 1 ) {
+				const prop = this.properties[i];
+
+				if ( prop.computed ) {
+					lastComputedProp = prop;
+					let moveStart = i > 0 ? this.properties[ i - 1 ].end : start;
+
+					const propId = isSimpleAssignment ? `;\n${i0}${name}` : `, ${name}`;
+
+					if (moveStart < prop.start) {
+						code.overwrite( moveStart, prop.start, propId );
+					} else {
+						code.insertRight( prop.start, propId );
+					}
+
+					let c = prop.key.end;
+					while ( code.original[c] !== ']' ) c += 1;
+					c += 1;
+
+					if ( prop.value.start > c ) code.remove( c, prop.value.start );
+					code.insertLeft( c, ' = ' );
+					code.move( moveStart, prop.end, end );
+
+					if ( i < len - 1 && ! sawNonComputedProperty ) {
+						// remove trailing comma
+						c = prop.end;
+						while ( code.original[c] !== ',' ) c += 1;
+
+						code.remove( prop.end, c + 1 );
+					}
+
+					if ( prop.method && transforms.conciseMethodProperty ) {
+						code.insertRight( prop.value.start, 'function ' );
+					}
+				} else {
+					sawNonComputedProperty = true;
+				}
+			}
+
+			// special case
+			if ( computedPropertyCount === len ) {
+				code.remove( this.properties[ len - 1 ].end, this.end - 1 );
+			}
+
+			if ( !isSimpleAssignment ) {
+				code.insertLeft( lastComputedProp.end, `, ${name} )` );
+			}
+		}
+	}
+}
diff --git a/src/program/types/Property.js b/src/program/types/Property.js
new file mode 100644
index 0000000..520fc3c
--- /dev/null
+++ b/src/program/types/Property.js
@@ -0,0 +1,40 @@
+import Node from '../Node.js';
+import reserved from '../../utils/reserved.js';
+
+export default class Property extends Node {
+	transpile ( code, transforms ) {
+		if ( transforms.conciseMethodProperty && !this.computed && this.parent.type !== 'ObjectPattern' ) {
+			if ( this.shorthand ) {
+				code.insertRight( this.start, `${this.key.name}: ` );
+			} else if ( this.method ) {
+				let name = '';
+				if ( this.program.options.namedFunctionExpressions !== false ) {
+					if ( this.key.type === 'Literal' && typeof this.key.value === 'number' ) {
+						name = "";
+					} else if ( this.key.type === 'Identifier' ) {
+						if ( reserved[ this.key.name ] ||
+							 ! /^[a-z_$][a-z0-9_$]*$/i.test( this.key.name ) ||
+						     this.value.body.scope.references[this.key.name] ) {
+							name = this.findScope( true ).createIdentifier( this.key.name );
+						} else {
+							name = this.key.name;
+						}
+					} else {
+						name = this.findScope( true ).createIdentifier( this.key.value );
+					}
+					name = ' ' + name;
+				}
+
+				if ( this.value.generator ) code.remove( this.start, this.key.start );
+				code.insertLeft( this.key.end, `: function${this.value.generator ? '*' : ''}${name}` );
+			}
+		}
+
+		if ( transforms.reservedProperties && reserved[ this.key.name ] ) {
+			code.insertRight( this.key.start, `'` );
+			code.insertLeft( this.key.end, `'` );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ReturnStatement.js b/src/program/types/ReturnStatement.js
new file mode 100644
index 0000000..edf5618
--- /dev/null
+++ b/src/program/types/ReturnStatement.js
@@ -0,0 +1,28 @@
+import Node from '../Node.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class ReturnStatement extends Node {
+	initialise ( transforms ) {
+		this.loop = this.findNearest( loopStatement );
+		this.nearestFunction = this.findNearest( /Function/ );
+
+		if ( this.loop && ( !this.nearestFunction || this.loop.depth > this.nearestFunction.depth ) ) {
+			this.loop.canReturn = true;
+			this.shouldWrap = true;
+		}
+
+		if ( this.argument ) this.argument.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		const shouldWrap = this.shouldWrap && this.loop && this.loop.shouldRewriteAsFunction;
+
+		if ( this.argument ) {
+			if ( shouldWrap ) code.insertRight( this.argument.start, `{ v: ` );
+			this.argument.transpile( code, transforms );
+			if ( shouldWrap ) code.insertLeft( this.argument.end, ` }` );
+		} else if ( shouldWrap ) {
+			code.insertLeft( this.start + 6, ' {}' );
+		}
+	}
+}
diff --git a/src/program/types/SpreadProperty.js b/src/program/types/SpreadProperty.js
new file mode 100644
index 0000000..ed90e25
--- /dev/null
+++ b/src/program/types/SpreadProperty.js
@@ -0,0 +1,10 @@
+import Node from '../Node.js';
+
+export default class SpreadProperty extends Node {
+	transpile ( code, transforms ) {
+		code.remove( this.start, this.argument.start );
+		code.remove( this.argument.end, this.end );
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/Super.js b/src/program/types/Super.js
new file mode 100644
index 0000000..c112909
--- /dev/null
+++ b/src/program/types/Super.js
@@ -0,0 +1,69 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class Super extends Node {
+	initialise ( transforms ) {
+		if ( transforms.classes ) {
+			this.method = this.findNearest( 'MethodDefinition' );
+			if ( !this.method ) throw new CompileError( this, 'use of super outside class method' );
+
+			const parentClass = this.findNearest( 'ClassBody' ).parent;
+			this.superClassName = parentClass.superClass && (parentClass.superClass.name || 'superclass');
+
+			if ( !this.superClassName ) throw new CompileError( this, 'super used in base class' );
+
+			this.isCalled = this.parent.type === 'CallExpression' && this === this.parent.callee;
+
+			if ( this.method.kind !== 'constructor' && this.isCalled ) {
+				throw new CompileError( this, 'super() not allowed outside class constructor' );
+			}
+
+			this.isMember = this.parent.type === 'MemberExpression';
+
+			if ( !this.isCalled && !this.isMember ) {
+				throw new CompileError( this, 'Unexpected use of `super` (expected `super(...)` or `super.*`)' );
+			}
+		}
+
+		if ( transforms.arrow ) {
+			const lexicalBoundary = this.findLexicalBoundary();
+			const arrowFunction = this.findNearest( 'ArrowFunctionExpression' );
+			const loop = this.findNearest( loopStatement );
+
+			if ( arrowFunction && arrowFunction.depth > lexicalBoundary.depth ) {
+				this.thisAlias = lexicalBoundary.getThisAlias();
+			}
+
+			if ( loop && loop.body.contains( this ) && loop.depth > lexicalBoundary.depth ) {
+				this.thisAlias = lexicalBoundary.getThisAlias();
+			}
+		}
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.classes ) {
+			const expression = ( this.isCalled || this.method.static ) ?
+				this.superClassName :
+				`${this.superClassName}.prototype`;
+
+			code.overwrite( this.start, this.end, expression, true );
+
+			const callExpression = this.isCalled ? this.parent : this.parent.parent;
+
+			if ( callExpression && callExpression.type === 'CallExpression' ) {
+				if ( !this.noCall ) { // special case – `super( ...args )`
+					code.insertLeft( callExpression.callee.end, '.call' );
+				}
+
+				const thisAlias = this.thisAlias || 'this';
+
+				if ( callExpression.arguments.length ) {
+					code.insertLeft( callExpression.arguments[0].start, `${thisAlias}, ` );
+				} else {
+					code.insertLeft( callExpression.end - 1, `${thisAlias}` );
+				}
+			}
+		}
+	}
+}
diff --git a/src/program/types/TaggedTemplateExpression.js b/src/program/types/TaggedTemplateExpression.js
new file mode 100644
index 0000000..c70aa01
--- /dev/null
+++ b/src/program/types/TaggedTemplateExpression.js
@@ -0,0 +1,37 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class TaggedTemplateExpression extends Node {
+	initialise ( transforms ) {
+		if ( transforms.templateString && !transforms.dangerousTaggedTemplateString ) {
+			throw new CompileError( this, 'Tagged template strings are not supported. Use `transforms: { templateString: false }` to skip transformation and disable this error, or `transforms: { dangerousTaggedTemplateString: true }` if you know what you\'re doing' );
+		}
+
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( transforms.templateString && transforms.dangerousTaggedTemplateString ) {
+			const ordered = this.quasi.expressions.concat( this.quasi.quasis ).sort( ( a, b ) => a.start - b.start );
+
+			// insert strings at start
+			const templateStrings = this.quasi.quasis.map( quasi => JSON.stringify( quasi.value.cooked ) );
+			code.overwrite( this.tag.end, ordered[0].start, `([${templateStrings.join(', ')}]` );
+
+			let lastIndex = ordered[0].start;
+			ordered.forEach( node => {
+				if ( node.type === 'TemplateElement' ) {
+					code.remove( lastIndex, node.end );
+				} else {
+					code.overwrite( lastIndex, node.start, ', ' );
+				}
+
+				lastIndex = node.end;
+			});
+
+			code.overwrite( lastIndex, this.end, ')' );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/TemplateElement.js b/src/program/types/TemplateElement.js
new file mode 100644
index 0000000..b9658b5
--- /dev/null
+++ b/src/program/types/TemplateElement.js
@@ -0,0 +1,7 @@
+import Node from '../Node.js';
+
+export default class TemplateElement extends Node {
+	initialise () {
+		this.program.indentExclusionElements.push( this );
+	}
+}
diff --git a/src/program/types/TemplateLiteral.js b/src/program/types/TemplateLiteral.js
new file mode 100644
index 0000000..af3b503
--- /dev/null
+++ b/src/program/types/TemplateLiteral.js
@@ -0,0 +1,70 @@
+import Node from '../Node.js';
+
+export default class TemplateLiteral extends Node {
+	transpile ( code, transforms ) {
+		if ( transforms.templateString && this.parent.type !== 'TaggedTemplateExpression' ) {
+			let ordered = this.expressions.concat( this.quasis )
+				.sort( ( a, b ) => a.start - b.start || a.end - b.end )
+				.filter( ( node, i ) => {
+					// include all expressions
+					if ( node.type !== 'TemplateElement' ) return true;
+
+					// include all non-empty strings
+					if ( node.value.raw ) return true;
+
+					// exclude all empty strings not at the head
+					return !i;
+				});
+
+			// special case – we may be able to skip the first element,
+			// if it's the empty string, but only if the second and
+			// third elements aren't both expressions (since they maybe
+			// be numeric, and `1 + 2 + '3' === '33'`)
+			if ( ordered.length >= 3 ) {
+				const [ first, , third ] = ordered;
+				if ( first.type === 'TemplateElement' && first.value.raw === '' && third.type === 'TemplateElement' ) {
+					ordered.shift();
+				}
+			}
+
+			const parenthesise = ( this.quasis.length !== 1 || this.expressions.length !== 0 ) &&
+			                     this.parent.type !== 'AssignmentExpression' &&
+			                     this.parent.type !== 'AssignmentPattern' &&
+			                     this.parent.type !== 'VariableDeclarator' &&
+			                     ( this.parent.type !== 'BinaryExpression' || this.parent.operator !== '+' );
+
+			if ( parenthesise ) code.insertRight( this.start, '(' );
+
+			let lastIndex = this.start;
+
+			ordered.forEach( ( node, i ) => {
+				if ( node.type === 'TemplateElement' ) {
+					let replacement = '';
+					if ( i ) replacement += ' + ';
+					replacement += JSON.stringify( node.value.cooked );
+
+					code.overwrite( lastIndex, node.end, replacement );
+				} else {
+					const parenthesise = node.type !== 'Identifier'; // TODO other cases where it's safe
+
+					let replacement = '';
+					if ( i ) replacement += ' + ';
+					if ( parenthesise ) replacement += '(';
+
+					code.overwrite( lastIndex, node.start, replacement );
+
+					if ( parenthesise ) code.insertLeft( node.end, ')' );
+				}
+
+				lastIndex = node.end;
+			});
+
+			let close = '';
+			if ( parenthesise ) close += ')';
+
+			code.overwrite( lastIndex, this.end, close );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/ThisExpression.js b/src/program/types/ThisExpression.js
new file mode 100644
index 0000000..049ebfb
--- /dev/null
+++ b/src/program/types/ThisExpression.js
@@ -0,0 +1,24 @@
+import Node from '../Node.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class ThisExpression extends Node {
+	initialise ( transforms ) {
+		if ( transforms.arrow ) {
+			const lexicalBoundary = this.findLexicalBoundary();
+			const arrowFunction = this.findNearest( 'ArrowFunctionExpression' );
+			const loop = this.findNearest( loopStatement );
+
+			if ( ( arrowFunction && arrowFunction.depth > lexicalBoundary.depth )
+			|| ( loop && loop.body.contains( this ) && loop.depth > lexicalBoundary.depth )
+			|| ( loop && loop.right && loop.right.contains( this ) ) ) {
+				this.alias = lexicalBoundary.getThisAlias();
+			}
+		}
+	}
+
+	transpile ( code ) {
+		if ( this.alias ) {
+			code.overwrite( this.start, this.end, this.alias, true );
+		}
+	}
+}
diff --git a/src/program/types/UpdateExpression.js b/src/program/types/UpdateExpression.js
new file mode 100644
index 0000000..9cd6580
--- /dev/null
+++ b/src/program/types/UpdateExpression.js
@@ -0,0 +1,21 @@
+import Node from '../Node.js';
+import CompileError from '../../utils/CompileError.js';
+
+export default class UpdateExpression extends Node {
+	initialise ( transforms ) {
+		if ( this.argument.type === 'Identifier' ) {
+			const declaration = this.findScope( false ).findDeclaration( this.argument.name );
+			if ( declaration && declaration.kind === 'const' ) {
+				throw new CompileError( this, `${this.argument.name} is read-only` );
+			}
+
+			// special case – https://gitlab.com/Rich-Harris/buble/issues/150
+			const statement = declaration && declaration.node.ancestor( 3 );
+			if ( statement && statement.type === 'ForStatement' && statement.body.contains( this ) ) {
+				statement.reassigned[ this.argument.name ] = true;
+			}
+		}
+
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/types/VariableDeclaration.js b/src/program/types/VariableDeclaration.js
new file mode 100644
index 0000000..3cbd4c9
--- /dev/null
+++ b/src/program/types/VariableDeclaration.js
@@ -0,0 +1,86 @@
+import Node from '../Node.js';
+import destructure from '../../utils/destructure.js';
+import { loopStatement } from '../../utils/patterns.js';
+
+export default class VariableDeclaration extends Node {
+	initialise ( transforms ) {
+		this.scope = this.findScope( this.kind === 'var' );
+		this.declarations.forEach( declarator => declarator.initialise( transforms ) );
+	}
+
+	transpile ( code, transforms ) {
+		const i0 = this.getIndentation();
+		let kind = this.kind;
+
+		if ( transforms.letConst && kind !== 'var' ) {
+			kind = 'var';
+			code.overwrite( this.start, this.start + this.kind.length, kind, true );
+		}
+
+		if ( transforms.destructuring && this.parent.type !== 'ForOfStatement' ) {
+			let c = this.start;
+			let lastDeclaratorIsPattern;
+
+			this.declarations.forEach( ( declarator, i ) => {
+				if ( declarator.id.type === 'Identifier' ) {
+					if ( i > 0 && this.declarations[ i - 1 ].id.type !== 'Identifier' ) {
+						code.overwrite( c, declarator.id.start, `var ` );
+					}
+				} else {
+					const inline = loopStatement.test( this.parent.type );
+
+					if ( i === 0 ) {
+						code.remove( c, declarator.id.start );
+					} else {
+						code.overwrite( c, declarator.id.start, `;\n${i0}` );
+					}
+
+					const simple = declarator.init.type === 'Identifier' && !declarator.init.rewritten;
+
+					const name = simple ? declarator.init.name : declarator.findScope( true ).createIdentifier( 'ref' );
+
+					let c = declarator.start;
+
+					let statementGenerators = [];
+
+					if ( simple ) {
+						code.remove( declarator.id.end, declarator.end );
+					} else {
+						statementGenerators.push( ( start, prefix, suffix ) => {
+							code.insertRight( declarator.id.end, `var ${name}` );
+							code.insertLeft( declarator.init.end, `${suffix}` );
+							code.move( declarator.id.end, declarator.end, start );
+						});
+					}
+
+					destructure( code, declarator.findScope( false ), declarator.id, name, inline, statementGenerators );
+
+					let prefix = inline ? 'var ' : '';
+					let suffix = inline ? `, ` : `;\n${i0}`;
+					statementGenerators.forEach( ( fn, j ) => {
+						if ( i === this.declarations.length - 1 && j === statementGenerators.length - 1 ) {
+							suffix = inline ? '' : ';';
+						}
+
+						fn( declarator.start, j === 0 ? prefix : '', suffix );
+					});
+				}
+
+				declarator.transpile( code, transforms );
+
+				c = declarator.end;
+				lastDeclaratorIsPattern = declarator.id.type !== 'Identifier';
+			});
+
+			if ( lastDeclaratorIsPattern ) {
+				code.remove( c, this.end );
+			}
+		}
+
+		else {
+			this.declarations.forEach( declarator => {
+				declarator.transpile( code, transforms );
+			});
+		}
+	}
+}
diff --git a/src/program/types/VariableDeclarator.js b/src/program/types/VariableDeclarator.js
new file mode 100644
index 0000000..d3944c0
--- /dev/null
+++ b/src/program/types/VariableDeclarator.js
@@ -0,0 +1,35 @@
+import Node from '../Node.js';
+
+export default class VariableDeclarator extends Node {
+	initialise ( transforms ) {
+		let kind = this.parent.kind;
+		if ( kind === 'let' && this.parent.parent.type === 'ForStatement' ) {
+			kind = 'for.let'; // special case...
+		}
+
+		this.parent.scope.addDeclaration( this.id, kind );
+		super.initialise( transforms );
+	}
+
+	transpile ( code, transforms ) {
+		if ( !this.init && transforms.letConst && this.parent.kind !== 'var' ) {
+			let inLoop = this.findNearest( /Function|^For(In|Of)?Statement|^(?:Do)?WhileStatement/ );
+			if ( inLoop && ! /Function/.test( inLoop.type ) && ! this.isLeftDeclaratorOfLoop() ) {
+				code.insertLeft( this.id.end, ' = (void 0)' );
+			}
+		}
+
+		if ( this.id ) this.id.transpile( code, transforms );
+		if ( this.init ) this.init.transpile( code, transforms );
+	}
+
+	isLeftDeclaratorOfLoop () {
+		return this.parent
+			&& this.parent.type === 'VariableDeclaration'
+			&& this.parent.parent
+			&& (this.parent.parent.type === 'ForInStatement'
+				|| this.parent.parent.type === 'ForOfStatement')
+			&& this.parent.parent.left
+			&& this.parent.parent.left.declarations[0] === this;
+	}
+}
diff --git a/src/program/types/index.js b/src/program/types/index.js
new file mode 100644
index 0000000..7591d21
--- /dev/null
+++ b/src/program/types/index.js
@@ -0,0 +1,92 @@
+import ArrayExpression from './ArrayExpression.js';
+import ArrowFunctionExpression from './ArrowFunctionExpression.js';
+import AssignmentExpression from './AssignmentExpression.js';
+import BinaryExpression from './BinaryExpression.js';
+import BreakStatement from './BreakStatement.js';
+import CallExpression from './CallExpression.js';
+import ClassBody from './ClassBody.js';
+import ClassDeclaration from './ClassDeclaration.js';
+import ClassExpression from './ClassExpression.js';
+import ContinueStatement from './ContinueStatement.js';
+import ExportDefaultDeclaration from './ExportDefaultDeclaration.js';
+import ExportNamedDeclaration from './ExportNamedDeclaration.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 ImportDefaultSpecifier from './ImportDefaultSpecifier.js';
+import ImportSpecifier from './ImportSpecifier.js';
+import JSXAttribute from './JSXAttribute.js';
+import JSXClosingElement from './JSXClosingElement.js';
+import JSXElement from './JSXElement.js';
+import JSXExpressionContainer from './JSXExpressionContainer.js';
+import JSXOpeningElement from './JSXOpeningElement.js';
+import JSXSpreadAttribute from './JSXSpreadAttribute.js';
+import Literal from './Literal.js';
+import LoopStatement from './shared/LoopStatement.js';
+import MemberExpression from './MemberExpression.js';
+import NewExpression from './NewExpression.js';
+import ObjectExpression from './ObjectExpression.js';
+import Property from './Property.js';
+import ReturnStatement from './ReturnStatement.js';
+import SpreadProperty from './SpreadProperty.js';
+import Super from './Super.js';
+import TaggedTemplateExpression from './TaggedTemplateExpression.js';
+import TemplateElement from './TemplateElement.js';
+import TemplateLiteral from './TemplateLiteral.js';
+import ThisExpression from './ThisExpression.js';
+import UpdateExpression from './UpdateExpression.js';
+import VariableDeclaration from './VariableDeclaration.js';
+import VariableDeclarator from './VariableDeclarator.js';
+
+export default {
+	ArrayExpression,
+	ArrowFunctionExpression,
+	AssignmentExpression,
+	BinaryExpression,
+	BreakStatement,
+	CallExpression,
+	ClassBody,
+	ClassDeclaration,
+	ClassExpression,
+	ContinueStatement,
+	DoWhileStatement: LoopStatement,
+	ExportNamedDeclaration,
+	ExportDefaultDeclaration,
+	ForStatement,
+	ForInStatement,
+	ForOfStatement,
+	FunctionDeclaration,
+	FunctionExpression,
+	Identifier,
+	IfStatement,
+	ImportDeclaration,
+	ImportDefaultSpecifier,
+	ImportSpecifier,
+	JSXAttribute,
+	JSXClosingElement,
+	JSXElement,
+	JSXExpressionContainer,
+	JSXOpeningElement,
+	JSXSpreadAttribute,
+	Literal,
+	MemberExpression,
+	NewExpression,
+	ObjectExpression,
+	Property,
+	ReturnStatement,
+	SpreadProperty,
+	Super,
+	TaggedTemplateExpression,
+	TemplateElement,
+	TemplateLiteral,
+	ThisExpression,
+	UpdateExpression,
+	VariableDeclaration,
+	VariableDeclarator,
+	WhileStatement: LoopStatement
+};
diff --git a/src/program/types/shared/LoopStatement.js b/src/program/types/shared/LoopStatement.js
new file mode 100644
index 0000000..e53ed29
--- /dev/null
+++ b/src/program/types/shared/LoopStatement.js
@@ -0,0 +1,91 @@
+import Node from '../../Node.js';
+
+export default class LoopStatement extends Node {
+	findScope ( functionScope ) {
+		return functionScope || !this.createdScope ? this.parent.findScope( functionScope ) : this.body.scope;
+	}
+
+	initialise ( transforms ) {
+		this.body.createScope();
+		this.createdScope = true;
+
+		// this is populated as and when reassignments occur
+		this.reassigned = Object.create( null );
+		this.aliases = Object.create( null );
+
+		super.initialise( transforms );
+
+		if ( transforms.letConst ) {
+			// see if any block-scoped declarations are referenced
+			// inside function expressions
+			const names = Object.keys( this.body.scope.declarations );
+
+			let i = names.length;
+			while ( i-- ) {
+				const name = names[i];
+				const declaration = this.body.scope.declarations[ name ];
+
+				let j = declaration.instances.length;
+				while ( j-- ) {
+					const instance = declaration.instances[j];
+					const nearestFunctionExpression = instance.findNearest( /Function/ );
+
+					if ( nearestFunctionExpression && nearestFunctionExpression.depth > this.depth ) {
+						this.shouldRewriteAsFunction = true;
+						break;
+					}
+				}
+
+				if ( this.shouldRewriteAsFunction ) break;
+			}
+		}
+	}
+
+	transpile ( code, transforms ) {
+		const needsBlock = this.type != 'ForOfStatement' && (
+			this.body.type !== 'BlockStatement'
+			|| this.body.type === 'BlockStatement' && this.body.synthetic );
+
+		if ( this.shouldRewriteAsFunction ) {
+			const i0 = this.getIndentation();
+			const i1 = i0 + code.getIndentString();
+
+			const argString = this.args ? ` ${this.args.join( ', ' )} ` : '';
+			const paramString = this.params ? ` ${this.params.join( ', ' )} ` : '';
+
+			const functionScope = this.findScope( true );
+			const loop = functionScope.createIdentifier( 'loop' );
+
+			const before = `var ${loop} = function (${paramString}) ` + ( this.body.synthetic ? `{\n${i0}${code.getIndentString()}` : '' );
+			const after = ( this.body.synthetic ? `\n${i0}}` : '' ) + `;\n\n${i0}`;
+
+			code.insertRight( this.body.start, before );
+			code.insertLeft( this.body.end, after );
+			code.move( this.start, this.body.start, this.body.end );
+
+			if ( this.canBreak || this.canReturn ) {
+				const returned = functionScope.createIdentifier( 'returned' );
+
+				let insert = `{\n${i1}var ${returned} = ${loop}(${argString});\n`;
+				if ( this.canBreak ) insert += `\n${i1}if ( ${returned} === 'break' ) break;`;
+				if ( this.canReturn ) insert += `\n${i1}if ( ${returned} ) return ${returned}.v;`;
+				insert += `\n${i0}}`;
+
+				code.insertRight( this.body.end, insert );
+			} else {
+				const callExpression = `${loop}(${argString});`;
+
+				if ( this.type === 'DoWhileStatement' ) {
+					code.overwrite( this.start, this.body.start, `do {\n${i1}${callExpression}\n${i0}}` );
+				} else {
+					code.insertRight( this.body.end, callExpression );
+				}
+			}
+		} else if ( needsBlock ) {
+			code.insertLeft( this.body.start, '{ ' );
+			code.insertRight( this.body.end, ' }' );
+		}
+
+		super.transpile( code, transforms );
+	}
+}
diff --git a/src/program/types/shared/ModuleDeclaration.js b/src/program/types/shared/ModuleDeclaration.js
new file mode 100644
index 0000000..db07ded
--- /dev/null
+++ b/src/program/types/shared/ModuleDeclaration.js
@@ -0,0 +1,9 @@
+import Node from '../../Node.js';
+import CompileError from '../../../utils/CompileError.js';
+
+export default class ModuleDeclaration extends Node {
+	initialise ( transforms ) {
+		if ( transforms.moduleImport ) throw new CompileError( this, 'Modules are not supported' );
+		super.initialise( transforms );
+	}
+}
diff --git a/src/program/wrap.js b/src/program/wrap.js
new file mode 100644
index 0000000..1e0a130
--- /dev/null
+++ b/src/program/wrap.js
@@ -0,0 +1,54 @@
+import types from './types/index.js';
+import BlockStatement from './BlockStatement.js';
+import Node from './Node.js';
+import keys from './keys.js';
+
+const statementsWithBlocks = {
+	IfStatement: 'consequent',
+	ForStatement: 'body',
+	ForInStatement: 'body',
+	ForOfStatement: 'body',
+	WhileStatement: 'body',
+	DoWhileStatement: 'body',
+	ArrowFunctionExpression: 'body'
+};
+
+export default function wrap ( raw, parent ) {
+	if ( !raw ) return;
+
+	if ( 'length' in raw ) {
+		let i = raw.length;
+		while ( i-- ) wrap( raw[i], parent );
+		return;
+	}
+
+	// with e.g. shorthand properties, key and value are
+	// the same node. We don't want to wrap an object twice
+	if ( raw.__wrapped ) return;
+	raw.__wrapped = true;
+
+	if ( !keys[ raw.type ] ) {
+		keys[ raw.type ] = Object.keys( raw ).filter( key => typeof raw[ key ] === 'object' );
+	}
+
+	// special case – body-less if/for/while statements. TODO others?
+	const bodyType = statementsWithBlocks[ raw.type ];
+	if ( bodyType && raw[ bodyType ].type !== 'BlockStatement' ) {
+		const expression = raw[ bodyType ];
+
+		// create a synthetic block statement, otherwise all hell
+		// breaks loose when it comes to block scoping
+		raw[ bodyType ] = {
+			start: expression.start,
+			end: expression.end,
+			type: 'BlockStatement',
+			body: [ expression ],
+			synthetic: true
+		};
+	}
+
+	new Node( raw, parent );
+
+	const type = ( raw.type === 'BlockStatement' ? BlockStatement : types[ raw.type ] ) || Node;
+	raw.__proto__ = type.prototype;
+}
diff --git a/src/support.js b/src/support.js
new file mode 100644
index 0000000..d40d4ec
--- /dev/null
+++ b/src/support.js
@@ -0,0 +1,77 @@
+export const matrix = {
+	chrome: {
+		    48: 0b1001111011111100111110101111101,
+		    49: 0b1001111111111100111111111111111,
+		    50: 0b1011111111111100111111111111111,
+		    51: 0b1011111111111100111111111111111,
+		    52: 0b1111111111111100111111111111111
+	},
+	firefox: {
+		    43: 0b1000111111101100000110111011101,
+		    44: 0b1000111111101100000110111011101,
+		    45: 0b1000111111101100000110111011101,
+		    46: 0b1010111111111100000110111011101,
+		    47: 0b1010111111111100111111111011111,
+		    48: 0b1010111111111100111111111011111
+	},
+	safari: {
+		     8: 0b1000000000000000000000000000000,
+		     9: 0b1001111001101100000011101011110
+	},
+	ie: {
+		     8: 0b0000000000000000000000000000000,
+		     9: 0b1000000000000000000000000000000,
+		    10: 0b1000000000000000000000000000000,
+		    11: 0b1000000000000000111000001100000
+	},
+	edge: {
+		    12: 0b1011110110111100011010001011101,
+		    13: 0b1011111110111100011111001011111
+	},
+	node: {
+		'0.10': 0b1000000000101000000000001000000,
+		'0.12': 0b1000001000101000000010001000100,
+		     4: 0b1001111000111100111111001111111,
+		     5: 0b1001111000111100111111001111111,
+		     6: 0b1011111111111100111111111111111
+	}
+};
+
+export const features = [
+	'arrow',
+	'classes',
+	'collections',
+	'computedProperty',
+	'conciseMethodProperty',
+	'constLoop',
+	'constRedef',
+	'defaultParameter',
+	'destructuring',
+	'extendNatives',
+	'forOf',
+	'generator',
+	'letConst',
+	'letLoop',
+	'letLoopScope',
+	'moduleExport',
+	'moduleImport',
+	'numericLiteral',
+	'objectProto',
+	'objectSuper',
+	'oldOctalLiteral',
+	'parameterDestructuring',
+	'spreadRest',
+	'stickyRegExp',
+	'symbol',
+	'templateString',
+	'unicodeEscape',
+	'unicodeIdentifier',
+	'unicodeRegExp',
+
+	// ES2016
+	'exponentiation',
+
+	// additional transforms, not from
+	// https://featuretests.io
+	'reservedProperties'
+];
diff --git a/src/utils/CompileError.js b/src/utils/CompileError.js
new file mode 100644
index 0000000..a9027e1
--- /dev/null
+++ b/src/utils/CompileError.js
@@ -0,0 +1,23 @@
+import locate from './locate.js';
+import getSnippet from './getSnippet.js';
+
+export default class CompileError extends Error {
+	constructor ( node, message ) {
+		super();
+
+		const source = node.program.magicString.original;
+		const loc = locate( source, node.start );
+
+		this.name = 'CompileError';
+		this.message = message + ` (${loc.line}:${loc.column})`;
+
+		this.stack = new Error().stack.replace( new RegExp( `.+new ${this.name}.+\\n`, 'm' ), '' );
+
+		this.loc = loc;
+		this.snippet = getSnippet( source, loc, node.end - node.start );
+	}
+
+	toString () {
+		return `${this.name}: ${this.message}\n${this.snippet}`;
+	}
+}
diff --git a/src/utils/array.js b/src/utils/array.js
new file mode 100644
index 0000000..70dbe6c
--- /dev/null
+++ b/src/utils/array.js
@@ -0,0 +1,11 @@
+export function findIndex ( array, fn ) {
+	for ( let i = 0; i < array.length; i += 1 ) {
+		if ( fn( array[i], i ) ) return i;
+	}
+
+	return -1;
+}
+
+export function find ( array, fn ) {
+	return array[ findIndex( array, fn ) ];
+}
diff --git a/src/utils/deindent.js b/src/utils/deindent.js
new file mode 100644
index 0000000..6b49f82
--- /dev/null
+++ b/src/utils/deindent.js
@@ -0,0 +1,28 @@
+// TODO this function is slightly flawed – it works on the original string,
+// not its current edited state.
+// That's not a problem for the way that it's currently used, but it could
+// be in future...
+export default function deindent ( node, code ) {
+	const start = node.start;
+	const end = node.end;
+
+	const indentStr = code.getIndentString();
+	const indentStrLen = indentStr.length;
+	const indentStart = start - indentStrLen;
+
+	if ( !node.program.indentExclusions[ indentStart ]
+	&& code.original.slice( indentStart, start ) === indentStr ) {
+		code.remove( indentStart, start );
+	}
+
+	const pattern = new RegExp( indentStr + '\\S', 'g' );
+	const slice = code.original.slice( start, end );
+	let match;
+
+	while ( match = pattern.exec( slice ) ) {
+		const removeStart = start + match.index;
+		if ( !node.program.indentExclusions[ removeStart ] ) {
+			code.remove( removeStart, removeStart + indentStrLen );
+		}
+	}
+}
diff --git a/src/utils/destructure.js b/src/utils/destructure.js
new file mode 100644
index 0000000..28cb023
--- /dev/null
+++ b/src/utils/destructure.js
@@ -0,0 +1,187 @@
+import { findIndex } from './array.js';
+
+const handlers = {
+	Identifier: destructureIdentifier,
+	AssignmentPattern: destructureAssignmentPattern,
+	ArrayPattern: destructureArrayPattern,
+	ObjectPattern: destructureObjectPattern
+};
+
+export default function destructure ( code, scope, node, ref, inline, statementGenerators ) {
+	handlers[ node.type ]( code, scope, node, ref, inline, statementGenerators );
+}
+
+function destructureIdentifier ( code, scope, node, ref, inline, statementGenerators ) {
+	statementGenerators.push( ( start, prefix, suffix ) => {
+		code.insertRight( node.start, inline ? prefix : `${prefix}var ` );
+		code.insertLeft( node.end, ` = ${ref}${suffix}` );
+		code.move( node.start, node.end, start );
+	});
+}
+
+function destructureAssignmentPattern ( code, scope, node, ref, inline, statementGenerators ) {
+	const isIdentifier = node.left.type === 'Identifier';
+	const name = isIdentifier ? node.left.name : ref;
+
+	if ( !inline ) {
+		statementGenerators.push( ( start, prefix, suffix ) => {
+			code.insertRight( node.left.end, `${prefix}if ( ${name} === void 0 ) ${name}` );
+			code.move( node.left.end, node.right.end, start );
+			code.insertLeft( node.right.end, suffix );
+		});
+	}
+
+	if ( !isIdentifier ) {
+		destructure( code, scope, node.left, ref, inline, statementGenerators );
+	}
+}
+
+function destructureArrayPattern ( code, scope, node, ref, inline, statementGenerators ) {
+	let c = node.start;
+
+	node.elements.forEach( ( element, i ) => {
+		if ( !element ) return;
+
+		if ( element.type === 'RestElement' ) {
+			handleProperty( code, scope, c, element.argument, `${ref}.slice(${i})`, inline, statementGenerators );
+		} else {
+			handleProperty( code, scope, c, element, `${ref}[${i}]`, inline, statementGenerators );
+		}
+		c = element.end;
+	});
+
+	code.remove( c, node.end );
+}
+
+function destructureObjectPattern ( code, scope, node, ref, inline, statementGenerators ) {
+	let c = node.start;
+
+	node.properties.forEach( prop => {
+		let value = prop.computed || prop.key.type !== 'Identifier' ? `${ref}[${code.slice(prop.key.start, prop.key.end)}]` : `${ref}.${prop.key.name}`;
+		handleProperty( code, scope, c, prop.value, value, inline, statementGenerators );
+		c = prop.end;
+	});
+
+	code.remove( c, node.end );
+}
+
+function handleProperty ( code, scope, c, node, value, inline, statementGenerators ) {
+	switch ( node.type ) {
+		case 'Identifier': {
+			code.remove( c, node.start );
+			destructureIdentifier( code, scope, node, value, inline, statementGenerators );
+			break;
+		}
+
+		case 'AssignmentPattern': {
+			let name;
+
+			const isIdentifier = node.left.type === 'Identifier';
+
+			if ( isIdentifier ) {
+				name = node.left.name;
+				const declaration = scope.findDeclaration( name );
+				if ( declaration ) name = declaration.name;
+			} else {
+				name = scope.createIdentifier( value );
+			}
+
+			statementGenerators.push( ( start, prefix, suffix ) => {
+				if ( inline ) {
+					code.insertRight( node.right.start, `${name} = ${value} === undefined ? ` );
+					code.insertLeft( node.right.end, ` : ${value}` );
+				} else {
+					code.insertRight( node.right.start, `${prefix}var ${name} = ${value}; if ( ${name} === void 0 ) ${name} = ` );
+					code.insertLeft( node.right.end, suffix );
+				}
+
+				code.move( node.right.start, node.right.end, start );
+			});
+
+			if ( isIdentifier ) {
+				code.remove( c, node.right.start );
+			} else {
+				code.remove( c, node.left.start );
+				code.remove( node.left.end, node.right.start );
+				handleProperty( code, scope, c, node.left, name, inline, statementGenerators );
+			}
+
+			break;
+		}
+
+		case 'ObjectPattern': {
+			code.remove( c, c = node.start );
+
+			if ( node.properties.length > 1 ) {
+				const ref = scope.createIdentifier( value );
+
+				statementGenerators.push( ( start, prefix, suffix ) => {
+					// this feels a tiny bit hacky, but we can't do a
+					// straightforward insertLeft and keep correct order...
+					code.insertRight( node.start, `${prefix}var ${ref} = ` );
+					code.overwrite( node.start, c = node.start + 1, value );
+					code.insertLeft( c, suffix );
+
+					code.move( node.start, c, start );
+				});
+
+				node.properties.forEach( prop => {
+					const value = prop.computed || prop.key.type !== 'Identifier' ? `${ref}[${code.slice(prop.key.start, prop.key.end)}]` : `${ref}.${prop.key.name}`;
+					handleProperty( code, scope, c, prop.value, value, inline, statementGenerators );
+					c = prop.end;
+				});
+			} else {
+				const prop = node.properties[0];
+				const value_suffix = prop.computed || prop.key.type !== 'Identifier' ? `[${code.slice(prop.key.start, prop.key.end)}]` : `.${prop.key.name}`;
+				handleProperty( code, scope, c, prop.value, `${value}${value_suffix}`, inline, statementGenerators );
+				c = prop.end;
+			}
+
+			code.remove( c, node.end );
+			break;
+		}
+
+		case 'ArrayPattern': {
+			code.remove( c, c = node.start );
+
+			if ( node.elements.filter( Boolean ).length > 1 ) {
+				const ref = scope.createIdentifier( value );
+
+				statementGenerators.push( ( start, prefix, suffix ) => {
+					code.insertRight( node.start, `${prefix}var ${ref} = ` );
+					code.overwrite( node.start, c = node.start + 1, value );
+					code.insertLeft( c, suffix );
+
+					code.move( node.start, c, start );
+				});
+
+				node.elements.forEach( ( element, i ) => {
+					if ( !element ) return;
+
+					if ( element.type === 'RestElement' ) {
+						handleProperty( code, scope, c, element.argument, `${ref}.slice(${i})`, inline, statementGenerators );
+					} else {
+						handleProperty( code, scope, c, element, `${ref}[${i}]`, inline, statementGenerators );
+					}
+					c = element.end;
+				});
+			} else {
+				const index = findIndex( node.elements, Boolean );
+				const element = node.elements[ index ];
+				if ( element.type === 'RestElement' ) {
+					handleProperty( code, scope, c, element.argument, `${value}.slice(${index})`, inline, statementGenerators );
+				} else {
+					handleProperty( code, scope, c, element, `${value}[${index}]`, inline, statementGenerators );
+				}
+				c = element.end;
+			}
+
+			code.remove( c, node.end );
+			break;
+		}
+
+		default: {
+			throw new Error( `Unexpected node type in destructuring (${node.type})` );
+		}
+	}
+}
diff --git a/src/utils/getSnippet.js b/src/utils/getSnippet.js
new file mode 100644
index 0000000..950a515
--- /dev/null
+++ b/src/utils/getSnippet.js
@@ -0,0 +1,30 @@
+function pad ( num, len ) {
+	let result = String( num );
+	return result + repeat( ' ', len - result.length );
+}
+
+function repeat ( str, times ) {
+	let result = '';
+	while ( times-- ) result += str;
+	return result;
+}
+
+export default function getSnippet ( source, loc, length = 1 ) {
+	const first = Math.max( loc.line - 5, 0 );
+	const last = loc.line;
+
+	const numDigits = String( last ).length;
+
+	const lines = source.split( '\n' ).slice( first, last );
+
+	const lastLine = lines[ lines.length - 1 ];
+	const offset = lastLine.slice( 0, loc.column ).replace( /\t/g, '  ' ).length;
+
+	let snippet = lines
+		.map( ( line, i ) => `${pad( i + first + 1, numDigits )} : ${line.replace( /\t/g, '  ')}` )
+		.join( '\n' );
+
+	snippet += '\n' + repeat( ' ', numDigits + 3 + offset ) + repeat( '^', length );
+
+	return snippet;
+}
diff --git a/src/utils/isReference.js b/src/utils/isReference.js
new file mode 100644
index 0000000..2084681
--- /dev/null
+++ b/src/utils/isReference.js
@@ -0,0 +1,37 @@
+export default function isReference ( node, parent ) {
+	if ( node.type === 'MemberExpression' ) {
+		return !node.computed && isReference( node.object, node );
+	}
+
+	if ( node.type === 'Identifier' ) {
+		// the only time we could have an identifier node without a parent is
+		// if it's the entire body of a function without a block statement –
+		// i.e. an arrow function expression like `a => a`
+		if ( !parent ) return true;
+
+		if ( /(Function|Class)Expression/.test( parent.type ) ) return false;
+
+		if ( parent.type === 'VariableDeclarator' ) return node === parent.init;
+
+		// TODO is this right?
+		if ( parent.type === 'MemberExpression' || parent.type === 'MethodDefinition' ) {
+			return parent.computed || node === parent.object;
+		}
+
+		if ( parent.type === 'ArrayPattern' ) return false;
+
+		// disregard the `bar` in `{ bar: foo }`, but keep it in `{ [bar]: foo }`
+		if ( parent.type === 'Property' ) {
+			if ( parent.parent.type === 'ObjectPattern' ) return false;
+			return parent.computed || node === parent.value;
+		}
+
+		// disregard the `bar` in `class Foo { bar () {...} }`
+		if ( parent.type === 'MethodDefinition' ) return false;
+
+		// disregard the `bar` in `export { foo as bar }`
+		if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return false;
+
+		return true;
+	}
+}
diff --git a/src/utils/locate.js b/src/utils/locate.js
new file mode 100644
index 0000000..03ebc42
--- /dev/null
+++ b/src/utils/locate.js
@@ -0,0 +1,20 @@
+export default function locate ( source, index ) {
+	var lines = source.split( '\n' );
+	var len = lines.length;
+
+	var lineStart = 0;
+	var i;
+
+	for ( i = 0; i < len; i += 1 ) {
+		var line = lines[i];
+		var lineEnd =  lineStart + line.length + 1; // +1 for newline
+
+		if ( lineEnd > index ) {
+			return { line: i + 1, column: index - lineStart, char: i };
+		}
+
+		lineStart = lineEnd;
+	}
+
+	throw new Error( 'Could not determine location of character' );
+}
diff --git a/src/utils/patterns.js b/src/utils/patterns.js
new file mode 100644
index 0000000..25fd54f
--- /dev/null
+++ b/src/utils/patterns.js
@@ -0,0 +1 @@
+export const loopStatement = /(?:For(?:In|Of)?|While)Statement/;
diff --git a/src/utils/reserved.js b/src/utils/reserved.js
new file mode 100644
index 0000000..fa3e838
--- /dev/null
+++ b/src/utils/reserved.js
@@ -0,0 +1,5 @@
+let reserved = Object.create( null );
+'do if in for let new try var case else enum eval null this true void with await break catch class const false super throw while yield delete export import public return static switch typeof default extends finally package private continue debugger function arguments interface protected implements instanceof'.split( ' ' )
+	.forEach( word => reserved[ word ] = true );
+
+export default reserved;
diff --git a/src/utils/spread.js b/src/utils/spread.js
new file mode 100644
index 0000000..73e4235
--- /dev/null
+++ b/src/utils/spread.js
@@ -0,0 +1,60 @@
+export function isArguments ( node ) {
+	return node.type === 'Identifier' && node.name === 'arguments';
+}
+
+export default function spread ( code, elements, start, argumentsArrayAlias, isNew ) {
+	let i = elements.length;
+	let firstSpreadIndex = -1;
+
+	while ( i-- ) {
+		const element = elements[i];
+		if ( element && element.type === 'SpreadElement' ) {
+			if ( isArguments( element.argument ) ) {
+				code.overwrite( element.argument.start, element.argument.end, argumentsArrayAlias );
+			}
+
+			firstSpreadIndex = i;
+		}
+	}
+
+	if ( firstSpreadIndex === -1 ) return false; // false indicates no spread elements
+
+	if (isNew) {
+		for ( i = 0; i < elements.length; i += 1 ) {
+			let element = elements[i];
+			if ( element.type === 'SpreadElement' ) {
+				code.remove( element.start, element.argument.start );
+			} else {
+				code.insertRight( element.start, '[' );
+				code.insertRight( element.end, ']' );
+			}
+		}
+
+		return true; // true indicates some spread elements
+	}
+
+	let element = elements[ firstSpreadIndex ];
+	const previousElement = elements[ firstSpreadIndex - 1 ];
+
+	if ( !previousElement ) {
+		code.remove( start, element.start );
+		code.overwrite( element.end, elements[1].start, '.concat( ' );
+	} else {
+		code.overwrite( previousElement.end, element.start, ' ].concat( ' );
+	}
+
+	for ( i = firstSpreadIndex; i < elements.length; i += 1 ) {
+		element = elements[i];
+
+		if ( element ) {
+			if ( element.type === 'SpreadElement' ) {
+				code.remove( element.start, element.argument.start );
+			} else {
+				code.insertLeft( element.start, '[' );
+				code.insertLeft( element.end, ']' );
+			}
+		}
+	}
+
+	return true; // true indicates some spread elements
+}
diff --git a/test/cli/basic/command.sh b/test/cli/basic/command.sh
new file mode 100755
index 0000000..55062ae
--- /dev/null
+++ b/test/cli/basic/command.sh
@@ -0,0 +1 @@
+buble input.js -o actual/output.js
diff --git a/test/cli/basic/expected/output.js b/test/cli/basic/expected/output.js
new file mode 100644
index 0000000..ed31167
--- /dev/null
+++ b/test/cli/basic/expected/output.js
@@ -0,0 +1 @@
+var answer = function () { return 42; };
diff --git a/test/cli/basic/input.js b/test/cli/basic/input.js
new file mode 100644
index 0000000..e2ce27d
--- /dev/null
+++ b/test/cli/basic/input.js
@@ -0,0 +1 @@
+const answer = () => 42;
diff --git a/test/cli/compiles-directory/command.sh b/test/cli/compiles-directory/command.sh
new file mode 100755
index 0000000..de53b9d
--- /dev/null
+++ b/test/cli/compiles-directory/command.sh
@@ -0,0 +1 @@
+buble src -o actual -m
diff --git a/test/cli/compiles-directory/expected/bar.js b/test/cli/compiles-directory/expected/bar.js
new file mode 100644
index 0000000..a6d1507
--- /dev/null
+++ b/test/cli/compiles-directory/expected/bar.js
@@ -0,0 +1,2 @@
+console.log( 'bar' );
+//# sourceMappingURL=bar.js.map
diff --git a/test/cli/compiles-directory/expected/bar.js.map b/test/cli/compiles-directory/expected/bar.js.map
new file mode 100644
index 0000000..f6d8f68
--- /dev/null
+++ b/test/cli/compiles-directory/expected/bar.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"bar.js","sources":["../src/bar.jsm"],"sourcesContent":["console.log( 'bar' );"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE"}
diff --git a/test/cli/compiles-directory/expected/baz.js b/test/cli/compiles-directory/expected/baz.js
new file mode 100644
index 0000000..e8985a6
--- /dev/null
+++ b/test/cli/compiles-directory/expected/baz.js
@@ -0,0 +1,2 @@
+console.log( 'baz' );
+//# sourceMappingURL=baz.js.map
\ No newline at end of file
diff --git a/test/cli/compiles-directory/expected/baz.js.map b/test/cli/compiles-directory/expected/baz.js.map
new file mode 100644
index 0000000..0ca8168
--- /dev/null
+++ b/test/cli/compiles-directory/expected/baz.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"baz.js","sources":["../src/baz.es6"],"sourcesContent":["console.log( 'baz' );"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE"}
\ No newline at end of file
diff --git a/test/cli/compiles-directory/expected/foo.js b/test/cli/compiles-directory/expected/foo.js
new file mode 100644
index 0000000..380852d
--- /dev/null
+++ b/test/cli/compiles-directory/expected/foo.js
@@ -0,0 +1,2 @@
+console.log( 'foo' );
+//# sourceMappingURL=foo.js.map
diff --git a/test/cli/compiles-directory/expected/foo.js.map b/test/cli/compiles-directory/expected/foo.js.map
new file mode 100644
index 0000000..71906e7
--- /dev/null
+++ b/test/cli/compiles-directory/expected/foo.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"foo.js","sources":["../src/foo.js"],"sourcesContent":["console.log( 'foo' );"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE"}
diff --git a/test/cli/compiles-directory/src/bar.jsm b/test/cli/compiles-directory/src/bar.jsm
new file mode 100644
index 0000000..ad24083
--- /dev/null
+++ b/test/cli/compiles-directory/src/bar.jsm
@@ -0,0 +1 @@
+console.log( 'bar' );
\ No newline at end of file
diff --git a/test/cli/compiles-directory/src/baz.es6 b/test/cli/compiles-directory/src/baz.es6
new file mode 100644
index 0000000..e5aeea4
--- /dev/null
+++ b/test/cli/compiles-directory/src/baz.es6
@@ -0,0 +1 @@
+console.log( 'baz' );
\ No newline at end of file
diff --git a/test/cli/compiles-directory/src/foo.js b/test/cli/compiles-directory/src/foo.js
new file mode 100644
index 0000000..db1b707
--- /dev/null
+++ b/test/cli/compiles-directory/src/foo.js
@@ -0,0 +1 @@
+console.log( 'foo' );
\ No newline at end of file
diff --git a/test/cli/compiles-directory/src/nope.txt b/test/cli/compiles-directory/src/nope.txt
new file mode 100644
index 0000000..1634764
--- /dev/null
+++ b/test/cli/compiles-directory/src/nope.txt
@@ -0,0 +1 @@
+nope
diff --git a/test/cli/creates-inline-sourcemap/command.sh b/test/cli/creates-inline-sourcemap/command.sh
new file mode 100755
index 0000000..39591a4
--- /dev/null
+++ b/test/cli/creates-inline-sourcemap/command.sh
@@ -0,0 +1 @@
+buble input.js -o actual/output.js -m inline
diff --git a/test/cli/creates-inline-sourcemap/expected/output.js b/test/cli/creates-inline-sourcemap/expected/output.js
new file mode 100644
index 0000000..31e80fc
--- /dev/null
+++ b/test/cli/creates-inline-sourcemap/expected/output.js
@@ -0,0 +1,2 @@
+var answer = function () { return 42; };
+//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0cHV0LmpzIiwic291cmNlcyI6WyIuLi9pbnB1dC5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBhbnN3ZXIgPSAoKSA9PiA0MjsiXSwibmFtZXMiOlsiY29uc3QiXSwibWFwcGluZ3MiOiJBQUFBQSxHQUFLLENBQUMsTUFBTSxHQUFHLFNBQUEsR0FBRyxBQUFHLFNBQUEsRUFBRSxHQUFBIn0=
\ No newline at end of file
diff --git a/test/cli/creates-inline-sourcemap/input.js b/test/cli/creates-inline-sourcemap/input.js
new file mode 100644
index 0000000..1a3ed38
--- /dev/null
+++ b/test/cli/creates-inline-sourcemap/input.js
@@ -0,0 +1 @@
+const answer = () => 42;
\ No newline at end of file
diff --git a/test/cli/creates-sourcemap/command.sh b/test/cli/creates-sourcemap/command.sh
new file mode 100755
index 0000000..fba89be
--- /dev/null
+++ b/test/cli/creates-sourcemap/command.sh
@@ -0,0 +1 @@
+buble input.js -o actual/output.js -m
diff --git a/test/cli/creates-sourcemap/expected/output.js b/test/cli/creates-sourcemap/expected/output.js
new file mode 100644
index 0000000..7e74c47
--- /dev/null
+++ b/test/cli/creates-sourcemap/expected/output.js
@@ -0,0 +1,2 @@
+var answer = function () { return 42; };
+//# sourceMappingURL=output.js.map
diff --git a/test/cli/creates-sourcemap/expected/output.js.map b/test/cli/creates-sourcemap/expected/output.js.map
new file mode 100644
index 0000000..8b4f877
--- /dev/null
+++ b/test/cli/creates-sourcemap/expected/output.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"output.js","sources":["../input.js"],"sourcesContent":["const answer = () => 42;"],"names":["const"],"mappings":"AAAAA,GAAK,CAAC,MAAM,GAAG,SAAA,GAAG,AAAG,SAAA,EAAE,GAAA"}
diff --git a/test/cli/creates-sourcemap/input.js b/test/cli/creates-sourcemap/input.js
new file mode 100644
index 0000000..1a3ed38
--- /dev/null
+++ b/test/cli/creates-sourcemap/input.js
@@ -0,0 +1 @@
+const answer = () => 42;
\ No newline at end of file
diff --git a/test/cli/supports-jsx-pragma/command.sh b/test/cli/supports-jsx-pragma/command.sh
new file mode 100644
index 0000000..9385984
--- /dev/null
+++ b/test/cli/supports-jsx-pragma/command.sh
@@ -0,0 +1 @@
+buble input.js -o actual/output.js --jsx NotReact.createElement
diff --git a/test/cli/supports-jsx-pragma/expected/output.js b/test/cli/supports-jsx-pragma/expected/output.js
new file mode 100644
index 0000000..1175b68
--- /dev/null
+++ b/test/cli/supports-jsx-pragma/expected/output.js
@@ -0,0 +1 @@
+var img = NotReact.createElement( 'img', { src: 'foo.gif' });
diff --git a/test/cli/supports-jsx-pragma/input.js b/test/cli/supports-jsx-pragma/input.js
new file mode 100644
index 0000000..fe2d823
--- /dev/null
+++ b/test/cli/supports-jsx-pragma/input.js
@@ -0,0 +1 @@
+var img = <img src='foo.gif'/>;
diff --git a/test/cli/supports-jsx/command.sh b/test/cli/supports-jsx/command.sh
new file mode 100644
index 0000000..3e992cf
--- /dev/null
+++ b/test/cli/supports-jsx/command.sh
@@ -0,0 +1 @@
+buble input.jsx -o actual/output.js
diff --git a/test/cli/supports-jsx/expected/output.js b/test/cli/supports-jsx/expected/output.js
new file mode 100644
index 0000000..5342cdd
--- /dev/null
+++ b/test/cli/supports-jsx/expected/output.js
@@ -0,0 +1 @@
+var img = React.createElement( 'img', { src: 'foo.gif' });
diff --git a/test/cli/supports-jsx/input.jsx b/test/cli/supports-jsx/input.jsx
new file mode 100644
index 0000000..fe2d823
--- /dev/null
+++ b/test/cli/supports-jsx/input.jsx
@@ -0,0 +1 @@
+var img = <img src='foo.gif'/>;
diff --git a/test/cli/uses-overrides/command.sh b/test/cli/uses-overrides/command.sh
new file mode 100755
index 0000000..0d906ec
--- /dev/null
+++ b/test/cli/uses-overrides/command.sh
@@ -0,0 +1 @@
+buble input.js -o actual/output.js -n letConst
diff --git a/test/cli/uses-overrides/expected/output.js b/test/cli/uses-overrides/expected/output.js
new file mode 100644
index 0000000..223c0b0
--- /dev/null
+++ b/test/cli/uses-overrides/expected/output.js
@@ -0,0 +1 @@
+const answer = function () { return 42; };
diff --git a/test/cli/uses-overrides/input.js b/test/cli/uses-overrides/input.js
new file mode 100644
index 0000000..e2ce27d
--- /dev/null
+++ b/test/cli/uses-overrides/input.js
@@ -0,0 +1 @@
+const answer = () => 42;
diff --git a/test/cli/uses-targets/command.sh b/test/cli/uses-targets/command.sh
new file mode 100755
index 0000000..3b0792d
--- /dev/null
+++ b/test/cli/uses-targets/command.sh
@@ -0,0 +1 @@
+buble input.js -o actual/output.js -t firefox:43
diff --git a/test/cli/uses-targets/expected/output.js b/test/cli/uses-targets/expected/output.js
new file mode 100644
index 0000000..819889a
--- /dev/null
+++ b/test/cli/uses-targets/expected/output.js
@@ -0,0 +1 @@
+var answer = () => 42;
diff --git a/test/cli/uses-targets/input.js b/test/cli/uses-targets/input.js
new file mode 100644
index 0000000..e2ce27d
--- /dev/null
+++ b/test/cli/uses-targets/input.js
@@ -0,0 +1 @@
+const answer = () => 42;
diff --git a/test/cli/writes-to-stdout/command.sh b/test/cli/writes-to-stdout/command.sh
new file mode 100755
index 0000000..a200a3d
--- /dev/null
+++ b/test/cli/writes-to-stdout/command.sh
@@ -0,0 +1 @@
+buble input.js > actual/output.js
diff --git a/test/cli/writes-to-stdout/expected/output.js b/test/cli/writes-to-stdout/expected/output.js
new file mode 100644
index 0000000..ed31167
--- /dev/null
+++ b/test/cli/writes-to-stdout/expected/output.js
@@ -0,0 +1 @@
+var answer = function () { return 42; };
diff --git a/test/cli/writes-to-stdout/input.js b/test/cli/writes-to-stdout/input.js
new file mode 100644
index 0000000..e2ce27d
--- /dev/null
+++ b/test/cli/writes-to-stdout/input.js
@@ -0,0 +1 @@
+const answer = () => 42;
diff --git a/test/samples/arrow-functions.js b/test/samples/arrow-functions.js
new file mode 100644
index 0000000..d76da1a
--- /dev/null
+++ b/test/samples/arrow-functions.js
@@ -0,0 +1,190 @@
+module.exports = [
+	{
+		description: 'transpiles an arrow function',
+		input: `var answer = () => 42`,
+		output: `var answer = function () { return 42; }`
+	},
+
+	{
+		description: 'transpiles an arrow function with a naked parameter',
+		input: `var double = x => x * 2`,
+		output: `var double = function (x) { return x * 2; }`
+	},
+
+	{
+		description: 'transpiles an arrow function with single wrapped parameter',
+		input: `var double = (x) => x * 2`,
+		output: `var double = function (x) { return x * 2; }`
+	},
+
+	{
+		description: 'transpiles an arrow function with parenthesised parameters',
+		input: `var add = ( a, b ) => a + b`,
+		output: `var add = function ( a, b ) { return a + b; }`
+	},
+
+	{
+		description: 'transpiles an arrow function with a body',
+
+		input: `
+			var add = ( a, b ) => {
+				return a + b;
+			};`,
+
+		output: `
+			var add = function ( a, b ) {
+				return a + b;
+			};`
+	},
+
+	{
+		description: 'replaces `this` inside an arrow function',
+
+		input: `
+			this.foo = 'bar';
+			var lexicallyScoped = () => this.foo;`,
+
+		output: `
+			var this$1 = this;
+
+			this.foo = 'bar';
+			var lexicallyScoped = function () { return this$1.foo; };`
+	},
+
+	{
+		description: 'replaces `arguments` inside an arrow function',
+
+		input: `
+			function firstArgument () {
+				return () => arguments[0];
+			}
+			equal( firstArgument( 1, 2, 3 )(), 1 )`,
+
+		output: `
+			function firstArgument () {
+				var arguments$1 = arguments;
+
+				return function () { return arguments$1[0]; };
+			}
+			equal( firstArgument( 1, 2, 3 )(), 1 )`
+	},
+
+	{
+		description: 'only adds one `this` or `arguments` per context',
+
+		input: `
+			function multiply () {
+				return () => {
+					return () => this * arguments[0];
+				};
+			}
+			equal( multiply.call( 2, 3 )()(), 6 )`,
+
+		output: `
+			function multiply () {
+				var arguments$1 = arguments;
+				var this$1 = this;
+
+				return function () {
+					return function () { return this$1 * arguments$1[0]; };
+				};
+			}
+			equal( multiply.call( 2, 3 )()(), 6 )`
+	},
+
+	{
+		description: 'transpiles a body-less arrow function with rest params',
+
+		input: `
+			const sum = ( ...nums ) => nums.reduce( ( t, n ) => t + n, 0 );`,
+
+		output: `
+			var sum = function () {
+				var nums = [], len = arguments.length;
+				while ( len-- ) nums[ len ] = arguments[ len ];
+
+				return nums.reduce( function ( t, n ) { return t + n; }, 0 );
+			};`
+	},
+
+	{
+		description: 'handles combination of destructuring and template strings',
+
+		input: `
+			var shoutHello = ({ name }) => \`\${name}! Hello \${name}!\`.toUpperCase();`,
+
+		output: `
+			var shoutHello = function (ref) {
+				var name = ref.name;
+
+				return (name + "! Hello " + name + "!").toUpperCase();
+			};`
+	},
+
+	{
+		description: 'can be disabled with `transforms.arrow: false`',
+		options: { transforms: { arrow: false } },
+
+		input: `
+			var add = ( a, b ) => {
+				console.log( 'this, arguments', this, arguments )
+				a = b;
+			}`,
+
+		output: `
+			var add = ( a, b ) => {
+				console.log( 'this, arguments', this, arguments )
+				a = b;
+			}`
+	},
+
+	{
+		description: 'inserts statements after use strict pragma (#72)',
+
+		input: `
+			'use strict';
+			setTimeout( () => console.log( this ) );
+
+			function foo () {
+				'use strict';
+				setTimeout( () => console.log( this ) );
+			}`,
+
+		output: `
+			'use strict';
+			var this$1 = this;
+
+			setTimeout( function () { return console.log( this$1 ); } );
+
+			function foo () {
+				'use strict';
+				var this$1 = this;
+
+				setTimeout( function () { return console.log( this$1 ); } );
+			}`
+	},
+
+	{
+		description: 'handles standalone arrow function expression statement',
+
+		input: `
+			() => console.log( 'not printed' );`,
+
+		output: `
+			(function() { return console.log( 'not printed' ); });`
+	},
+
+	{
+		description: 'handles standalone arrow function expression statement within a function',
+
+		input: `
+			function no_op () {
+				() => console.log( 'not printed' );
+			}`,
+
+		output: `
+			function no_op () {
+				(function() { return console.log( 'not printed' ); });
+			}`
+	}
+];
diff --git a/test/samples/binary-and-octal.js b/test/samples/binary-and-octal.js
new file mode 100644
index 0000000..8bcb48b
--- /dev/null
+++ b/test/samples/binary-and-octal.js
@@ -0,0 +1,32 @@
+module.exports = [
+	{
+		description: 'transpiles binary numbers',
+
+		input: `
+			var num = 0b111110111;
+			var str = '0b111110111';`,
+
+		output: `
+			var num = 503;
+			var str = '0b111110111';`
+	},
+
+	{
+		description: 'transpiles octal numbers',
+
+		input: `
+			var num = 0o767;
+			var str = '0o767';`,
+
+		output: `
+			var num = 503;
+			var str = '0o767';`
+	},
+
+	{
+		description: 'can be disabled with `transforms.numericLiteral: false`',
+		options: { transforms: { numericLiteral: false } },
+		input: '0b111110111',
+		output: '0b111110111'
+	}
+];
diff --git a/test/samples/block-scoping.js b/test/samples/block-scoping.js
new file mode 100644
index 0000000..c486493
--- /dev/null
+++ b/test/samples/block-scoping.js
@@ -0,0 +1,444 @@
+module.exports = [
+	{
+		description: 'transpiles let',
+		input: `let x = 'y';`,
+		output: `var x = 'y';`
+	},
+
+	{
+		description: 'deconflicts blocks in top-level scope',
+
+		input: `
+			if ( a ) {
+				let x = 1;
+				console.log( x );
+			} else if ( b ) {
+				let x = 2;
+				console.log( x );
+			} else {
+				let x = 3;
+				console.log( x );
+			}`,
+
+		output: `
+			if ( a ) {
+				var x = 1;
+				console.log( x );
+			} else if ( b ) {
+				var x$1 = 2;
+				console.log( x$1 );
+			} else {
+				var x$2 = 3;
+				console.log( x$2 );
+			}`
+	},
+
+	{
+		description: 'deconflicts blocks in same function scope',
+
+		input: `
+			var x = 'y';
+			function foo () {
+				if ( a ) {
+					let x = 1;
+					console.log( x );
+				} else if ( b ) {
+					let x = 2;
+					console.log( x );
+				} else {
+					let x = 3;
+					console.log( x );
+				}
+			}`,
+
+		output: `
+			var x = 'y';
+			function foo () {
+				if ( a ) {
+					var x = 1;
+					console.log( x );
+				} else if ( b ) {
+					var x$1 = 2;
+					console.log( x$1 );
+				} else {
+					var x$2 = 3;
+					console.log( x$2 );
+				}
+			}`
+	},
+
+	{
+		description: 'disallows duplicate declarations',
+		input: `
+			let x = 1;
+			let x = 2;
+		`,
+		error: /x is already declared/
+	},
+
+	{
+		description: 'disallows reassignment to constants',
+		input: `
+			const x = 1;
+			x = 2;
+		`,
+		error: /x is read-only/
+	},
+
+	{
+		description: 'disallows updates to constants',
+		input: `
+			const x = 1;
+			x++;
+		`,
+		error: /x is read-only/
+	},
+
+	{
+		description: 'does not rewrite properties',
+
+		input: `
+			var foo = 'x';
+			if ( true ) {
+				let foo = 'y';
+				this.foo = 'z';
+				this[ foo ] = 'q';
+			}`,
+
+		output: `
+			var foo = 'x';
+			if ( true ) {
+				var foo$1 = 'y';
+				this.foo = 'z';
+				this[ foo$1 ] = 'q';
+			}`
+	},
+
+	{
+		description: 'deconflicts with default imports',
+		options: { transforms: { moduleImport: false } },
+
+		input: `
+			import foo from './foo.js';
+
+			if ( x ) {
+				let foo = 'y';
+				console.log( foo );
+			}`,
+
+		output: `
+			import foo from './foo.js';
+
+			if ( x ) {
+				var foo$1 = 'y';
+				console.log( foo$1 );
+			}`
+	},
+
+	{
+		description: 'deconflicts with named imports',
+		options: { transforms: { moduleImport: false } },
+
+		input: `
+			import { foo } from './foo.js';
+
+			if ( x ) {
+				let foo = 'y';
+				console.log( foo );
+			}`,
+
+		output: `
+			import { foo } from './foo.js';
+
+			if ( x ) {
+				var foo$1 = 'y';
+				console.log( foo$1 );
+			}`
+	},
+
+	{
+		description: 'deconflicts with function declarations',
+
+		input: `
+			function foo () {}
+
+			if ( x ) {
+				let foo = 'y';
+				console.log( foo );
+			}`,
+
+		output: `
+			function foo () {}
+
+			if ( x ) {
+				var foo$1 = 'y';
+				console.log( foo$1 );
+			}`
+	},
+
+	{
+		description: 'does not deconflict with function expressions',
+
+		input: `
+			var bar = function foo () {};
+
+			if ( x ) {
+				let foo = 'y';
+				console.log( foo );
+			}`,
+
+		output: `
+			var bar = function foo () {};
+
+			if ( x ) {
+				var foo = 'y';
+				console.log( foo );
+			}`
+	},
+
+	{
+		description: 'deconflicts with function expression inside function body',
+
+		input: `
+			var bar = function foo () {
+				if ( x ) {
+					let foo = 'y';
+					console.log( foo );
+				}
+			};`,
+
+		output: `
+			var bar = function foo () {
+				if ( x ) {
+					var foo$1 = 'y';
+					console.log( foo$1 );
+				}
+			};`
+	},
+
+	{
+		description: 'deconflicts with parameters',
+
+		input: `
+			function bar ( foo ) {
+				if ( x ) {
+					let foo = 'y';
+					console.log( foo );
+				}
+			}`,
+
+		output: `
+			function bar ( foo ) {
+				if ( x ) {
+					var foo$1 = 'y';
+					console.log( foo$1 );
+				}
+			}`
+	},
+
+	{
+		description: 'deconflicts with class declarations',
+
+		input: `
+			class foo {}
+
+			if ( x ) {
+				let foo = 'y';
+				console.log( foo );
+			}`,
+
+		output: `
+			var foo = function foo () {};
+
+			if ( x ) {
+				var foo$1 = 'y';
+				console.log( foo$1 );
+			}`
+	},
+
+	{
+		description: 'does not deconflict with class expressions',
+
+		input: `
+			var bar = class foo {};
+
+			if ( x ) {
+				let foo = 'y';
+				console.log( foo );
+			}`,
+
+		output: `
+			var bar = (function () {
+				function foo () {}
+
+				return foo;
+			}());
+
+			if ( x ) {
+				var foo = 'y';
+				console.log( foo );
+			}`
+	},
+
+	{
+		description: 'deconflicts across multiple function boundaries',
+
+		input: `
+			function foo ( x ) {
+				return function () {
+					if ( true ) {
+						const x = 'y';
+						console.log( x );
+					}
+
+					console.log( x );
+				};
+			}`,
+
+		output: `
+			function foo ( x ) {
+				return function () {
+					if ( true ) {
+						var x$1 = 'y';
+						console.log( x$1 );
+					}
+
+					console.log( x );
+				};
+			}`
+	},
+
+	{
+		description: 'does not deconflict unnecessarily',
+
+		input: `
+			function foo ( x ) {
+				return function () {
+					if ( true ) {
+						const x = 'y';
+						console.log( x );
+					}
+				};
+			}`,
+
+		output: `
+			function foo ( x ) {
+				return function () {
+					if ( true ) {
+						var x = 'y';
+						console.log( x );
+					}
+				};
+			}`
+	},
+
+	{
+		description: 'deconflicts object pattern declarations',
+
+		input: `
+			let x;
+
+			if ( true ) {
+				let { x, y } = point;
+				console.log( x );
+			}`,
+
+		output: `
+			var x;
+
+			if ( true ) {
+				var x$1 = point.x;
+				var y = point.y;
+				console.log( x$1 );
+			}`
+	},
+
+	{
+		description: 'deconflicts array pattern declarations',
+
+		input: `
+			let x;
+
+			if ( true ) {
+				let [ x, y ] = point;
+				console.log( x );
+			}`,
+
+		output: `
+			var x;
+
+			if ( true ) {
+				var x$1 = point[0];
+				var y = point[1];
+				console.log( x$1 );
+			}`
+	},
+
+	{
+		skip: true,
+		description: 'deconflicts rest element declarations',
+
+		input: `
+			let x;
+
+			if ( true ) {
+				let [ first, second, ...x ] = y;
+				console.log( x );
+			}`,
+
+		output: `
+			var x;
+
+			if ( true ) {
+				var first = y[0], second = y[1], x$1 = y.slice( 2 );
+				console.log( x$1 );
+			}`
+	},
+
+	{
+		description: 'can be disabled with `transforms.letConst: false`',
+		options: { transforms: { letConst: false } },
+
+		input: `
+			let a = 1;
+
+			if ( x ) {
+				let a = 2;
+				console.log( a );
+			}
+
+			console.log( a );`,
+
+		output: `
+			let a = 1;
+
+			if ( x ) {
+				let a = 2;
+				console.log( a );
+			}
+
+			console.log( a );`
+	},
+
+	{
+		description: 'reference preceding declaration (#87)',
+
+		input: `
+			if ( x ) {
+				let a = function () { b(); };
+				let b = function () { alert( 'hello' ); };
+
+				a();
+				b();
+			}`,
+
+		output: `
+			if ( x ) {
+				var a = function () { b(); };
+				var b = function () { alert( 'hello' ); };
+
+				a();
+				b();
+			}`
+	}
+];
diff --git a/test/samples/classes-no-named-function-expressions.js b/test/samples/classes-no-named-function-expressions.js
new file mode 100644
index 0000000..8d94e29
--- /dev/null
+++ b/test/samples/classes-no-named-function-expressions.js
@@ -0,0 +1,1225 @@
+module.exports = [
+	{
+		description: 'transpiles a class declaration',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				constructor ( answer ) {
+					this.answer = answer;
+				}
+			}`,
+
+		output: `
+			var Foo = function ( answer ) {
+				this.answer = answer;
+			};`
+	},
+
+	{
+		description: 'transpiles a class declaration with a non-constructor method',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				constructor ( answer ) {
+					this.answer = answer;
+				}
+
+				bar ( str ) {
+					return str + 'bar';
+				}
+			}`,
+
+		output: `
+			var Foo = function ( answer ) {
+				this.answer = answer;
+			};
+
+			Foo.prototype.bar = function ( str ) {
+				return str + 'bar';
+			};`
+	},
+
+	{
+		description: 'transpiles a class declaration without a constructor function',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				bar ( str ) {
+					return str + 'bar';
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo.prototype.bar = function ( str ) {
+				return str + 'bar';
+			};`
+	},
+
+	{
+		description: 'no unnecessary deshadowing of method names',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			var bar = 'x';
+
+			class Foo {
+				bar ( str ) {
+					return str + 'bar';
+				}
+			}`,
+
+		output: `
+			var bar = 'x';
+
+			var Foo = function () {};
+
+			Foo.prototype.bar = function ( str ) {
+				return str + 'bar';
+			};`
+	},
+
+	{
+		description: 'transpiles a class declaration with a static method',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				bar ( str ) {
+					return str + 'bar';
+				}
+
+				static baz ( str ) {
+					return str + 'baz';
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo.prototype.bar = function ( str ) {
+				return str + 'bar';
+			};
+
+			Foo.baz = function ( str ) {
+				return str + 'baz';
+			};`
+	},
+
+	{
+		description: 'transpiles a subclass',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo extends Bar {
+				baz ( str ) {
+					return str + 'baz';
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.baz = function ( str ) {
+					return str + 'baz';
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'transpiles a subclass with super calls',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo extends Bar {
+				constructor ( x ) {
+					super( x );
+					this.y = 'z';
+				}
+
+				baz ( a, b, c ) {
+					super.baz( a, b, c );
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo ( x ) {
+					Bar.call( this, x );
+					this.y = 'z';
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.baz = function ( a, b, c ) {
+					Bar.prototype.baz.call( this, a, b, c );
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'transpiles a subclass with super calls with spread arguments',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo extends Bar {
+				baz ( ...args ) {
+					super.baz(...args);
+				}
+				boz ( x, y, ...z ) {
+					super.boz(x, y, ...z);
+				}
+				fab ( x, ...y ) {
+					super.qux(...x, ...y);
+				}
+				fob ( x, y, ...z ) {
+					((x, y, z) => super.qux(x, ...y, ...z))(x, y, z);
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.baz = function () {
+					var args = [], len = arguments.length;
+					while ( len-- ) args[ len ] = arguments[ len ];
+
+					Bar.prototype.baz.apply(this, args);
+				};
+				Foo.prototype.boz = function ( x, y ) {
+					var z = [], len = arguments.length - 2;
+					while ( len-- > 0 ) z[ len ] = arguments[ len + 2 ];
+
+					Bar.prototype.boz.apply(this, [ x, y ].concat( z ));
+				};
+				Foo.prototype.fab = function ( x ) {
+					var y = [], len = arguments.length - 1;
+					while ( len-- > 0 ) y[ len ] = arguments[ len + 1 ];
+
+					Bar.prototype.qux.apply(this, x.concat( y ));
+				};
+				Foo.prototype.fob = function ( x, y ) {
+					var this$1 = this;
+					var z = [], len = arguments.length - 2;
+					while ( len-- > 0 ) z[ len ] = arguments[ len + 2 ];
+
+					(function (x, y, z) { return Bar.prototype.qux.apply(this$1, [ x ].concat( y, z )); })(x, y, z);
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'transpiles export default class',
+		options: { transforms: { moduleExport: false }, namedFunctionExpressions: false },
+
+		input: `
+			export default class Foo {
+				bar () {}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo.prototype.bar = function () {};
+
+			export default Foo;`
+	},
+
+	{
+		description: 'transpiles export default subclass',
+		options: { transforms: { moduleExport: false }, namedFunctionExpressions: false },
+
+		input: `
+			export default class Foo extends Bar {
+				bar () {}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.bar = function () {};
+
+				return Foo;
+			}(Bar));
+
+			export default Foo;`
+	},
+
+	{
+		description: 'transpiles export default subclass with subsequent statement',
+		options: { transforms: { moduleExport: false }, namedFunctionExpressions: false },
+
+		input: `
+			export default class Foo extends Bar {
+				bar () {}
+			}
+
+			new Foo().bar();`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.bar = function () {};
+
+				return Foo;
+			}(Bar));
+
+			export default Foo;
+
+			new Foo().bar();`
+	},
+
+	{
+		description: 'transpiles empty class',
+		options: { namedFunctionExpressions: false },
+
+		input: `class Foo {}`,
+		output: `var Foo = function () {};`
+	},
+
+	{
+		description: 'transpiles an anonymous empty class expression',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			var Foo = class {};`,
+
+		output: `
+			var Foo = (function () {
+				function Foo () {}
+
+				return Foo;
+			}());`
+	},
+
+	{
+		description: 'transpiles an anonymous class expression with a constructor',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			var Foo = class {
+				constructor ( x ) {
+					this.x = x;
+				}
+			};`,
+
+		output: `
+			var Foo = (function () {
+				function Foo ( x ) {
+					this.x = x;
+				}
+
+				return Foo;
+			}());`
+	},
+
+	{
+		description: 'transpiles an anonymous class expression with a non-constructor method',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			var Foo = class {
+				bar ( x ) {
+					console.log( x );
+				}
+			};`,
+
+		output: `
+			var Foo = (function () {
+				function Foo () {}
+
+				Foo.prototype.bar = function ( x ) {
+					console.log( x );
+				};
+
+				return Foo;
+			}());`
+	},
+
+	{
+		description: 'allows constructor to be in middle of body',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				before () {
+					// code goes here
+				}
+
+				constructor () {
+					// constructor goes here
+				}
+
+				after () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {
+				// constructor goes here
+			};
+
+			Foo.prototype.before = function () {
+				// code goes here
+			};
+
+			Foo.prototype.after = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'allows constructor to be at end of body',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				before () {
+					// code goes here
+				}
+
+				constructor () {
+					// constructor goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {
+				// constructor goes here
+			};
+
+			Foo.prototype.before = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'transpiles getters and setters',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Circle {
+				constructor ( radius ) {
+					this.radius = radius;
+				}
+
+				get area () {
+					return Math.PI * Math.pow( this.radius, 2 );
+				}
+
+				set area ( area ) {
+					this.radius = Math.sqrt( area / Math.PI );
+				}
+
+				static get description () {
+					return 'round';
+				}
+			}`,
+
+		output: `
+			var Circle = function ( radius ) {
+				this.radius = radius;
+			};
+
+			var prototypeAccessors = { area: {} };
+			var staticAccessors = { description: {} };
+
+			prototypeAccessors.area.get = function () {
+				return Math.PI * Math.pow( this.radius, 2 );
+			};
+
+			prototypeAccessors.area.set = function ( area ) {
+				this.radius = Math.sqrt( area / Math.PI );
+			};
+
+			staticAccessors.description.get = function () {
+				return 'round';
+			};
+
+			Object.defineProperties( Circle.prototype, prototypeAccessors );
+			Object.defineProperties( Circle, staticAccessors );`
+	},
+
+	{
+		description: 'transpiles getters and setters in subclass',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Circle extends Shape {
+				constructor ( radius ) {
+					super();
+					this.radius = radius;
+				}
+
+				get area () {
+					return Math.PI * Math.pow( this.radius, 2 );
+				}
+
+				set area ( area ) {
+					this.radius = Math.sqrt( area / Math.PI );
+				}
+
+				static get description () {
+					return 'round';
+				}
+			}`,
+
+		output: `
+			var Circle = (function (Shape) {
+				function Circle ( radius ) {
+					Shape.call(this);
+					this.radius = radius;
+				}
+
+				if ( Shape ) Circle.__proto__ = Shape;
+				Circle.prototype = Object.create( Shape && Shape.prototype );
+				Circle.prototype.constructor = Circle;
+
+				var prototypeAccessors = { area: {} };
+				var staticAccessors = { description: {} };
+
+				prototypeAccessors.area.get = function () {
+					return Math.PI * Math.pow( this.radius, 2 );
+				};
+
+				prototypeAccessors.area.set = function ( area ) {
+					this.radius = Math.sqrt( area / Math.PI );
+				};
+
+				staticAccessors.description.get = function () {
+					return 'round';
+				};
+
+				Object.defineProperties( Circle.prototype, prototypeAccessors );
+				Object.defineProperties( Circle, staticAccessors );
+
+				return Circle;
+			}(Shape));`
+	},
+
+	{
+		description: 'can be disabled with `transforms.classes: false`',
+		options: { namedFunctionExpressions: false, transforms: { classes: false } },
+
+		input: `
+			class Foo extends Bar {
+				constructor ( answer ) {
+					super();
+					this.answer = answer;
+				}
+			}`,
+
+		output: `
+			class Foo extends Bar {
+				constructor ( answer ) {
+					super();
+					this.answer = answer;
+				}
+			}`
+	},
+
+	{
+		description: 'declaration extends from an expression (#15)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			const q = {a: class {}};
+
+			class b extends q.a {
+				c () {}
+			}`,
+
+		output: `
+			var q = {a: (function () {
+				function anonymous () {}
+
+				return anonymous;
+			}())};
+
+			var b = (function (superclass) {
+				function b () {
+					superclass.apply(this, arguments);
+				}
+
+				if ( superclass ) b.__proto__ = superclass;
+				b.prototype = Object.create( superclass && superclass.prototype );
+				b.prototype.constructor = b;
+
+				b.prototype.c = function () {};
+
+				return b;
+			}(q.a));`
+	},
+
+	{
+		description: 'expression extends from an expression (#15)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			const q = {a: class {}};
+
+			const b = class b extends q.a {
+				c () {}
+			};`,
+
+		output: `
+			var q = {a: (function () {
+				function anonymous () {}
+
+				return anonymous;
+			}())};
+
+			var b = (function (superclass) {
+				function b () {
+					superclass.apply(this, arguments);
+				}
+
+				if ( superclass ) b.__proto__ = superclass;
+				b.prototype = Object.create( superclass && superclass.prototype );
+				b.prototype.constructor = b;
+
+				b.prototype.c = function () {};
+
+				return b;
+			}(q.a));`
+	},
+
+	{
+		description: 'expression extends from an expression with super calls (#31)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class b extends x.y.z {
+				constructor() {
+					super();
+				}
+			}`,
+
+		output: `
+			var b = (function (superclass) {
+				function b() {
+					superclass.call(this);
+				}
+
+				if ( superclass ) b.__proto__ = superclass;
+				b.prototype = Object.create( superclass && superclass.prototype );
+				b.prototype.constructor = b;
+
+				return b;
+			}(x.y.z));`
+	},
+
+	{
+		description: 'anonymous expression extends named class (#31)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			SubClass = class extends SuperClass {
+				constructor() {
+					super();
+				}
+			};`,
+
+		output: `
+			SubClass = (function (SuperClass) {
+				function SubClass() {
+					SuperClass.call(this);
+				}
+
+				if ( SuperClass ) SubClass.__proto__ = SuperClass;
+				SubClass.prototype = Object.create( SuperClass && SuperClass.prototype );
+				SubClass.prototype.constructor = SubClass;
+
+				return SubClass;
+			}(SuperClass));`
+	},
+
+	{
+		description: 'verify deindent() does not corrupt string literals in class methods (#159)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				bar() {
+					var s = "0\t1\t\t2\t\t\t3\t\t\t\t4\t\t\t\t\t5";
+					return s + '\t';
+				}
+				baz() {
+					return \`\t\`;
+				}
+			}
+		`,
+		output: `
+			var Foo = function () {};
+
+			Foo.prototype.bar = function () {
+				var s = "0\t1\t\t2\t\t\t3\t\t\t\t4\t\t\t\t\t5";
+				return s + '\t';
+			};
+			Foo.prototype.baz = function () {
+				return "\\t";
+			};
+		`
+	},
+
+	{
+		description: 'deindents a function body with destructuring (#22)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				constructor ( options ) {
+					const {
+						a,
+						b
+					} = options;
+				}
+			}`,
+
+		output: `
+			var Foo = function ( options ) {
+				var a = options.a;
+				var b = options.b;
+			};`
+	},
+
+	{
+		description: 'allows super in static methods',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo extends Bar {
+				static baz () {
+					super.baz();
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.baz = function () {
+					Bar.baz.call(this);
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'allows zero space between class id and body (#46)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class A{
+				x(){}
+			}
+
+			var B = class B{
+				x(){}
+			};
+
+			class C extends D{
+				x(){}
+			}
+
+			var E = class E extends F{
+				x(){}
+			}`,
+
+		output: `
+			var A = function () {};
+
+			A.prototype.x = function (){};
+
+			var B = (function () {
+				function B () {}
+
+				B.prototype.x = function (){};
+
+				return B;
+			}());
+
+			var C = (function (D) {
+				function C () {
+					D.apply(this, arguments);
+				}
+
+				if ( D ) C.__proto__ = D;
+				C.prototype = Object.create( D && D.prototype );
+				C.prototype.constructor = C;
+
+				C.prototype.x = function (){};
+
+				return C;
+			}(D));
+
+			var E = (function (F) {
+				function E () {
+					F.apply(this, arguments);
+				}
+
+				if ( F ) E.__proto__ = F;
+				E.prototype = Object.create( F && F.prototype );
+				E.prototype.constructor = E;
+
+				E.prototype.x = function (){};
+
+				return E;
+			}(F))`
+	},
+
+	{
+		description: 'transpiles a class with an accessor and no constructor (#48)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				static get bar() { return 'baz' }
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			var staticAccessors = { bar: {} };
+
+			staticAccessors.bar.get = function () { return 'baz' };
+
+			Object.defineProperties( Foo, staticAccessors );`
+	},
+
+	{
+		description: 'uses correct indentation for inserted statements in constructor (#39)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				constructor ( options, { a2, b2 } ) {
+					const { a, b } = options;
+
+					const render = () => {
+						requestAnimationFrame( render );
+						this.render();
+					};
+
+					render();
+				}
+
+				render () {
+					// code goes here...
+				}
+			}`,
+
+		output: `
+			var Foo = function ( options, ref ) {
+				var this$1 = this;
+				var a2 = ref.a2;
+				var b2 = ref.b2;
+
+				var a = options.a;
+				var b = options.b;
+
+				var render = function () {
+					requestAnimationFrame( render );
+					this$1.render();
+				};
+
+				render();
+			};
+
+			Foo.prototype.render = function () {
+				// code goes here...
+			};`
+	},
+
+	{
+		description: 'uses correct indentation for inserted statements in subclass constructor (#39)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo extends Bar {
+				constructor ( options, { a2, b2 } ) {
+					super();
+
+					const { a, b } = options;
+
+					const render = () => {
+						requestAnimationFrame( render );
+						this.render();
+					};
+
+					render();
+				}
+
+				render () {
+					// code goes here...
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo ( options, ref ) {
+					var this$1 = this;
+					var a2 = ref.a2;
+					var b2 = ref.b2;
+
+					Bar.call(this);
+
+					var a = options.a;
+					var b = options.b;
+
+					var render = function () {
+						requestAnimationFrame( render );
+						this$1.render();
+					};
+
+					render();
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.render = function () {
+					// code goes here...
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'allows subclass to use rest parameters',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class SubClass extends SuperClass {
+				constructor( ...args ) {
+					super( ...args );
+				}
+			}`,
+
+		output: `
+			var SubClass = (function (SuperClass) {
+				function SubClass() {
+					var args = [], len = arguments.length;
+					while ( len-- ) args[ len ] = arguments[ len ];
+
+					SuperClass.apply( this, args );
+				}
+
+				if ( SuperClass ) SubClass.__proto__ = SuperClass;
+				SubClass.prototype = Object.create( SuperClass && SuperClass.prototype );
+				SubClass.prototype.constructor = SubClass;
+
+				return SubClass;
+			}(SuperClass));`
+	},
+
+	{
+		description: 'allows subclass to use rest parameters with other arguments',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class SubClass extends SuperClass {
+				constructor( ...args ) {
+					super( 1, ...args, 2 );
+				}
+			}`,
+
+		output: `
+			var SubClass = (function (SuperClass) {
+				function SubClass() {
+					var args = [], len = arguments.length;
+					while ( len-- ) args[ len ] = arguments[ len ];
+
+					SuperClass.apply( this, [ 1 ].concat( args, [2] ) );
+				}
+
+				if ( SuperClass ) SubClass.__proto__ = SuperClass;
+				SubClass.prototype = Object.create( SuperClass && SuperClass.prototype );
+				SubClass.prototype.constructor = SubClass;
+
+				return SubClass;
+			}(SuperClass));`
+	},
+
+	{
+		description: 'transpiles computed class properties',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				[a.b.c] () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo.prototype[a.b.c] = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'transpiles static computed class properties',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				static [a.b.c] () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo[a.b.c] = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		skip: true,
+		description: 'transpiles computed class accessors',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				get [a.b.c] () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			var prototypeAccessors = {};
+			var ref = a.b.c;
+			prototypeAccessors[ref] = {};
+
+			prototypeAccessors[ref].get = function () {
+				// code goes here
+			};
+
+			Object.defineProperties( Foo.prototype, prototypeAccessors );`
+	},
+
+	{
+		description: 'transpiles reserved class properties (!68)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				catch () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo.prototype.catch = function () {
+				// code goes here
+			};`,
+	},
+
+	{
+		description: 'transpiles static reserved class properties (!68)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class Foo {
+				static catch () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function () {};
+
+			Foo.catch = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'uses correct `this` when transpiling `super` (#89)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class A extends B {
+				constructor () {
+					super();
+					this.doSomething(() => {
+						super.doSomething();
+					});
+				}
+			}`,
+
+		output: `
+			var A = (function (B) {
+				function A () {
+					var this$1 = this;
+
+					B.call(this);
+					this.doSomething(function () {
+						B.prototype.doSomething.call(this$1);
+					});
+				}
+
+				if ( B ) A.__proto__ = B;
+				A.prototype = Object.create( B && B.prototype );
+				A.prototype.constructor = A;
+
+				return A;
+			}(B));`
+	},
+
+	{
+		description: 'methods with computed names',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class A {
+				[x](){}
+				[0](){}
+				[1 + 2](){}
+				[normal + " Method"](){}
+			}
+		`,
+		output: `
+			var A = function () {};
+
+			A.prototype[x] = function (){};
+			A.prototype[0] = function (){};
+			A.prototype[1 + 2] = function (){};
+			A.prototype[normal + " Method"] = function (){};
+		`
+	},
+
+	{
+		description: 'static methods with computed names with varied spacing (#139)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class B {
+				static[.000004](){}
+				static [x](){}
+				static  [x-y](){}
+				static[\`Static computed \${name}\`](){}
+			}
+		`,
+		output: `
+			var B = function () {};
+
+			B[.000004] = function (){};
+			B[x] = function (){};
+			B [x-y] = function (){};
+			B[("Static computed " + name)] = function (){};
+		`
+	},
+
+	{
+		description: 'methods with numeric or string names (#139)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class C {
+				0(){}
+				0b101(){}
+				80(){}
+				.12e3(){}
+				0o753(){}
+				12e34(){}
+				0xFFFF(){}
+				"var"(){}
+			}
+		`,
+		output: `
+			var C = function () {};
+
+			C.prototype[0] = function (){};
+			C.prototype[5] = function (){};
+			C.prototype[80] = function (){};
+			C.prototype[.12e3] = function (){};
+			C.prototype[491] = function (){};
+			C.prototype[12e34] = function (){};
+			C.prototype[0xFFFF] = function (){};
+			C.prototype["var"] = function (){};
+		`
+	},
+
+	{
+		description: 'static methods with numeric or string names with varied spacing (#139)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			class D {
+				static .75(){}
+				static"Static Method"(){}
+				static "foo"(){}
+			}
+		`,
+		output: `
+			var D = function () {};
+
+			D[.75] = function (){};
+			D["Static Method"] = function (){};
+			D["foo"] = function (){};
+		`
+	},
+
+	// TODO more tests. e.g. getters and setters.
+	// 'super.*' is not allowed before super()
+];
diff --git a/test/samples/classes.js b/test/samples/classes.js
new file mode 100644
index 0000000..f33d3dd
--- /dev/null
+++ b/test/samples/classes.js
@@ -0,0 +1,1203 @@
+module.exports = [
+	{
+		description: 'transpiles a class declaration',
+
+		input: `
+			class Foo {
+				constructor ( answer ) {
+					this.answer = answer;
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo ( answer ) {
+				this.answer = answer;
+			};`
+	},
+
+	{
+		description: 'transpiles a class declaration with a non-constructor method',
+
+		input: `
+			class Foo {
+				constructor ( answer ) {
+					this.answer = answer;
+				}
+
+				bar ( str ) {
+					return str + 'bar';
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo ( answer ) {
+				this.answer = answer;
+			};
+
+			Foo.prototype.bar = function bar ( str ) {
+				return str + 'bar';
+			};`
+	},
+
+	{
+		description: 'transpiles a class declaration without a constructor function',
+
+		input: `
+			class Foo {
+				bar ( str ) {
+					return str + 'bar';
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype.bar = function bar ( str ) {
+				return str + 'bar';
+			};`
+	},
+
+	{
+		description: 'no unnecessary deshadowing of method names',
+
+		input: `
+			var bar = 'x';
+
+			class Foo {
+				bar ( str ) {
+					return str + 'bar';
+				}
+			}`,
+
+		output: `
+			var bar = 'x';
+
+			var Foo = function Foo () {};
+
+			Foo.prototype.bar = function bar ( str ) {
+				return str + 'bar';
+			};`
+	},
+
+	{
+		description: 'transpiles a class declaration with a static method',
+
+		input: `
+			class Foo {
+				bar ( str ) {
+					return str + 'bar';
+				}
+
+				static baz ( str ) {
+					return str + 'baz';
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype.bar = function bar ( str ) {
+				return str + 'bar';
+			};
+
+			Foo.baz = function baz ( str ) {
+				return str + 'baz';
+			};`
+	},
+
+	{
+		description: 'transpiles a subclass',
+
+		input: `
+			class Foo extends Bar {
+				baz ( str ) {
+					return str + 'baz';
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.baz = function baz ( str ) {
+					return str + 'baz';
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'transpiles a subclass with super calls',
+
+		input: `
+			class Foo extends Bar {
+				constructor ( x ) {
+					super( x );
+					this.y = 'z';
+				}
+
+				baz ( a, b, c ) {
+					super.baz( a, b, c );
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo ( x ) {
+					Bar.call( this, x );
+					this.y = 'z';
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.baz = function baz ( a, b, c ) {
+					Bar.prototype.baz.call( this, a, b, c );
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'transpiles a subclass with super calls with spread arguments',
+
+		input: `
+			class Foo extends Bar {
+				baz ( ...args ) {
+					super.baz(...args);
+				}
+				boz ( x, y, ...z ) {
+					super.boz(x, y, ...z);
+				}
+				fab ( x, ...y ) {
+					super.qux(...x, ...y);
+				}
+				fob ( x, y, ...z ) {
+					((x, y, z) => super.qux(x, ...y, ...z))(x, y, z);
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.baz = function baz () {
+					var args = [], len = arguments.length;
+					while ( len-- ) args[ len ] = arguments[ len ];
+
+					Bar.prototype.baz.apply(this, args);
+				};
+				Foo.prototype.boz = function boz ( x, y ) {
+					var z = [], len = arguments.length - 2;
+					while ( len-- > 0 ) z[ len ] = arguments[ len + 2 ];
+
+					Bar.prototype.boz.apply(this, [ x, y ].concat( z ));
+				};
+				Foo.prototype.fab = function fab ( x ) {
+					var y = [], len = arguments.length - 1;
+					while ( len-- > 0 ) y[ len ] = arguments[ len + 1 ];
+
+					Bar.prototype.qux.apply(this, x.concat( y ));
+				};
+				Foo.prototype.fob = function fob ( x, y ) {
+					var this$1 = this;
+					var z = [], len = arguments.length - 2;
+					while ( len-- > 0 ) z[ len ] = arguments[ len + 2 ];
+
+					(function (x, y, z) { return Bar.prototype.qux.apply(this$1, [ x ].concat( y, z )); })(x, y, z);
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'transpiles export default class',
+		options: { transforms: { moduleExport: false } },
+
+		input: `
+			export default class Foo {
+				bar () {}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype.bar = function bar () {};
+
+			export default Foo;`
+	},
+
+	{
+		description: 'transpiles export default subclass',
+		options: { transforms: { moduleExport: false } },
+
+		input: `
+			export default class Foo extends Bar {
+				bar () {}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.bar = function bar () {};
+
+				return Foo;
+			}(Bar));
+
+			export default Foo;`
+	},
+
+	{
+		description: 'transpiles export default subclass with subsequent statement',
+		options: { transforms: { moduleExport: false } },
+
+		input: `
+			export default class Foo extends Bar {
+				bar () {}
+			}
+
+			new Foo().bar();`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.bar = function bar () {};
+
+				return Foo;
+			}(Bar));
+
+			export default Foo;
+
+			new Foo().bar();`
+	},
+
+	{
+		description: 'transpiles empty class',
+
+		input: `class Foo {}`,
+		output: `var Foo = function Foo () {};`
+	},
+
+	{
+		description: 'transpiles an anonymous empty class expression',
+
+		input: `
+			var Foo = class {};`,
+
+		output: `
+			var Foo = (function () {
+				function Foo () {}
+
+				return Foo;
+			}());`
+	},
+
+	{
+		description: 'transpiles an anonymous class expression with a constructor',
+
+		input: `
+			var Foo = class {
+				constructor ( x ) {
+					this.x = x;
+				}
+			};`,
+
+		output: `
+			var Foo = (function () {
+				function Foo ( x ) {
+					this.x = x;
+				}
+
+				return Foo;
+			}());`
+	},
+
+	{
+		description: 'transpiles an anonymous class expression with a non-constructor method',
+
+		input: `
+			var Foo = class {
+				bar ( x ) {
+					console.log( x );
+				}
+			};`,
+
+		output: `
+			var Foo = (function () {
+				function Foo () {}
+
+				Foo.prototype.bar = function bar ( x ) {
+					console.log( x );
+				};
+
+				return Foo;
+			}());`
+	},
+
+	{
+		description: 'allows constructor to be in middle of body',
+
+		input: `
+			class Foo {
+				before () {
+					// code goes here
+				}
+
+				constructor () {
+					// constructor goes here
+				}
+
+				after () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {
+				// constructor goes here
+			};
+
+			Foo.prototype.before = function before () {
+				// code goes here
+			};
+
+			Foo.prototype.after = function after () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'allows constructor to be at end of body',
+
+		input: `
+			class Foo {
+				before () {
+					// code goes here
+				}
+
+				constructor () {
+					// constructor goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {
+				// constructor goes here
+			};
+
+			Foo.prototype.before = function before () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'transpiles getters and setters',
+
+		input: `
+			class Circle {
+				constructor ( radius ) {
+					this.radius = radius;
+				}
+
+				get area () {
+					return Math.PI * Math.pow( this.radius, 2 );
+				}
+
+				set area ( area ) {
+					this.radius = Math.sqrt( area / Math.PI );
+				}
+
+				static get description () {
+					return 'round';
+				}
+			}`,
+
+		output: `
+			var Circle = function Circle ( radius ) {
+				this.radius = radius;
+			};
+
+			var prototypeAccessors = { area: {} };
+			var staticAccessors = { description: {} };
+
+			prototypeAccessors.area.get = function () {
+				return Math.PI * Math.pow( this.radius, 2 );
+			};
+
+			prototypeAccessors.area.set = function ( area ) {
+				this.radius = Math.sqrt( area / Math.PI );
+			};
+
+			staticAccessors.description.get = function () {
+				return 'round';
+			};
+
+			Object.defineProperties( Circle.prototype, prototypeAccessors );
+			Object.defineProperties( Circle, staticAccessors );`
+	},
+
+	{
+		description: 'transpiles getters and setters in subclass',
+
+		input: `
+			class Circle extends Shape {
+				constructor ( radius ) {
+					super();
+					this.radius = radius;
+				}
+
+				get area () {
+					return Math.PI * Math.pow( this.radius, 2 );
+				}
+
+				set area ( area ) {
+					this.radius = Math.sqrt( area / Math.PI );
+				}
+
+				static get description () {
+					return 'round';
+				}
+			}`,
+
+		output: `
+			var Circle = (function (Shape) {
+				function Circle ( radius ) {
+					Shape.call(this);
+					this.radius = radius;
+				}
+
+				if ( Shape ) Circle.__proto__ = Shape;
+				Circle.prototype = Object.create( Shape && Shape.prototype );
+				Circle.prototype.constructor = Circle;
+
+				var prototypeAccessors = { area: {} };
+				var staticAccessors = { description: {} };
+
+				prototypeAccessors.area.get = function () {
+					return Math.PI * Math.pow( this.radius, 2 );
+				};
+
+				prototypeAccessors.area.set = function ( area ) {
+					this.radius = Math.sqrt( area / Math.PI );
+				};
+
+				staticAccessors.description.get = function () {
+					return 'round';
+				};
+
+				Object.defineProperties( Circle.prototype, prototypeAccessors );
+				Object.defineProperties( Circle, staticAccessors );
+
+				return Circle;
+			}(Shape));`
+	},
+
+	{
+		description: 'can be disabled with `transforms.classes: false`',
+		options: { transforms: { classes: false } },
+
+		input: `
+			class Foo extends Bar {
+				constructor ( answer ) {
+					super();
+					this.answer = answer;
+				}
+			}`,
+
+		output: `
+			class Foo extends Bar {
+				constructor ( answer ) {
+					super();
+					this.answer = answer;
+				}
+			}`
+	},
+
+	{
+		description: 'declaration extends from an expression (#15)',
+
+		input: `
+			const q = {a: class {}};
+
+			class b extends q.a {
+				c () {}
+			}`,
+
+		output: `
+			var q = {a: (function () {
+				function anonymous () {}
+
+				return anonymous;
+			}())};
+
+			var b = (function (superclass) {
+				function b () {
+					superclass.apply(this, arguments);
+				}
+
+				if ( superclass ) b.__proto__ = superclass;
+				b.prototype = Object.create( superclass && superclass.prototype );
+				b.prototype.constructor = b;
+
+				b.prototype.c = function c () {};
+
+				return b;
+			}(q.a));`
+	},
+
+	{
+		description: 'expression extends from an expression (#15)',
+
+		input: `
+			const q = {a: class {}};
+
+			const b = class b extends q.a {
+				c () {}
+			};`,
+
+		output: `
+			var q = {a: (function () {
+				function anonymous () {}
+
+				return anonymous;
+			}())};
+
+			var b = (function (superclass) {
+				function b () {
+					superclass.apply(this, arguments);
+				}
+
+				if ( superclass ) b.__proto__ = superclass;
+				b.prototype = Object.create( superclass && superclass.prototype );
+				b.prototype.constructor = b;
+
+				b.prototype.c = function c () {};
+
+				return b;
+			}(q.a));`
+	},
+
+	{
+		description: 'expression extends from an expression with super calls (#31)',
+
+		input: `
+			class b extends x.y.z {
+				constructor() {
+					super();
+				}
+			}`,
+
+		output: `
+			var b = (function (superclass) {
+				function b() {
+					superclass.call(this);
+				}
+
+				if ( superclass ) b.__proto__ = superclass;
+				b.prototype = Object.create( superclass && superclass.prototype );
+				b.prototype.constructor = b;
+
+				return b;
+			}(x.y.z));`
+	},
+
+	{
+		description: 'anonymous expression extends named class (#31)',
+
+		input: `
+			SubClass = class extends SuperClass {
+				constructor() {
+					super();
+				}
+			};`,
+
+		output: `
+			SubClass = (function (SuperClass) {
+				function SubClass() {
+					SuperClass.call(this);
+				}
+
+				if ( SuperClass ) SubClass.__proto__ = SuperClass;
+				SubClass.prototype = Object.create( SuperClass && SuperClass.prototype );
+				SubClass.prototype.constructor = SubClass;
+
+				return SubClass;
+			}(SuperClass));`
+	},
+
+	{
+		description: 'verify deindent() does not corrupt string literals in class methods (#159)',
+
+		input: `
+			class Foo {
+				bar() {
+					var s = "0\t1\t\t2\t\t\t3\t\t\t\t4\t\t\t\t\t5";
+					return s + '\t';
+				}
+				baz() {
+					return \`\t\`;
+				}
+			}
+		`,
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype.bar = function bar () {
+				var s = "0\t1\t\t2\t\t\t3\t\t\t\t4\t\t\t\t\t5";
+				return s + '\t';
+			};
+			Foo.prototype.baz = function baz () {
+				return "\\t";
+			};
+		`
+	},
+
+	{
+		description: 'deindents a function body with destructuring (#22)',
+
+		input: `
+			class Foo {
+				constructor ( options ) {
+					const {
+						a,
+						b
+					} = options;
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo ( options ) {
+				var a = options.a;
+				var b = options.b;
+			};`
+	},
+
+	{
+		description: 'allows super in static methods',
+
+		input: `
+			class Foo extends Bar {
+				static baz () {
+					super.baz();
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo () {
+					Bar.apply(this, arguments);
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.baz = function baz () {
+					Bar.baz.call(this);
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'allows zero space between class id and body (#46)',
+
+		input: `
+			class A{
+				x(){}
+			}
+
+			var B = class B{
+				x(){}
+			};
+
+			class C extends D{
+				x(){}
+			}
+
+			var E = class E extends F{
+				x(){}
+			}`,
+
+		output: `
+			var A = function A () {};
+
+			A.prototype.x = function x (){};
+
+			var B = (function () {
+				function B () {}
+
+				B.prototype.x = function x (){};
+
+				return B;
+			}());
+
+			var C = (function (D) {
+				function C () {
+					D.apply(this, arguments);
+				}
+
+				if ( D ) C.__proto__ = D;
+				C.prototype = Object.create( D && D.prototype );
+				C.prototype.constructor = C;
+
+				C.prototype.x = function x (){};
+
+				return C;
+			}(D));
+
+			var E = (function (F) {
+				function E () {
+					F.apply(this, arguments);
+				}
+
+				if ( F ) E.__proto__ = F;
+				E.prototype = Object.create( F && F.prototype );
+				E.prototype.constructor = E;
+
+				E.prototype.x = function x (){};
+
+				return E;
+			}(F))`
+	},
+
+	{
+		description: 'transpiles a class with an accessor and no constructor (#48)',
+
+		input: `
+			class Foo {
+				static get bar() { return 'baz' }
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			var staticAccessors = { bar: {} };
+
+			staticAccessors.bar.get = function () { return 'baz' };
+
+			Object.defineProperties( Foo, staticAccessors );`
+	},
+
+	{
+		description: 'uses correct indentation for inserted statements in constructor (#39)',
+
+		input: `
+			class Foo {
+				constructor ( options, { a2, b2 } ) {
+					const { a, b } = options;
+
+					const render = () => {
+						requestAnimationFrame( render );
+						this.render();
+					};
+
+					render();
+				}
+
+				render () {
+					// code goes here...
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo ( options, ref ) {
+				var this$1 = this;
+				var a2 = ref.a2;
+				var b2 = ref.b2;
+
+				var a = options.a;
+				var b = options.b;
+
+				var render = function () {
+					requestAnimationFrame( render );
+					this$1.render();
+				};
+
+				render();
+			};
+
+			Foo.prototype.render = function render () {
+				// code goes here...
+			};`
+	},
+
+	{
+		description: 'uses correct indentation for inserted statements in subclass constructor (#39)',
+
+		input: `
+			class Foo extends Bar {
+				constructor ( options, { a2, b2 } ) {
+					super();
+
+					const { a, b } = options;
+
+					const render = () => {
+						requestAnimationFrame( render );
+						this.render();
+					};
+
+					render();
+				}
+
+				render () {
+					// code goes here...
+				}
+			}`,
+
+		output: `
+			var Foo = (function (Bar) {
+				function Foo ( options, ref ) {
+					var this$1 = this;
+					var a2 = ref.a2;
+					var b2 = ref.b2;
+
+					Bar.call(this);
+
+					var a = options.a;
+					var b = options.b;
+
+					var render = function () {
+						requestAnimationFrame( render );
+						this$1.render();
+					};
+
+					render();
+				}
+
+				if ( Bar ) Foo.__proto__ = Bar;
+				Foo.prototype = Object.create( Bar && Bar.prototype );
+				Foo.prototype.constructor = Foo;
+
+				Foo.prototype.render = function render () {
+					// code goes here...
+				};
+
+				return Foo;
+			}(Bar));`
+	},
+
+	{
+		description: 'allows subclass to use rest parameters',
+
+		input: `
+			class SubClass extends SuperClass {
+				constructor( ...args ) {
+					super( ...args );
+				}
+			}`,
+
+		output: `
+			var SubClass = (function (SuperClass) {
+				function SubClass() {
+					var args = [], len = arguments.length;
+					while ( len-- ) args[ len ] = arguments[ len ];
+
+					SuperClass.apply( this, args );
+				}
+
+				if ( SuperClass ) SubClass.__proto__ = SuperClass;
+				SubClass.prototype = Object.create( SuperClass && SuperClass.prototype );
+				SubClass.prototype.constructor = SubClass;
+
+				return SubClass;
+			}(SuperClass));`
+	},
+
+	{
+		description: 'allows subclass to use rest parameters with other arguments',
+
+		input: `
+			class SubClass extends SuperClass {
+				constructor( ...args ) {
+					super( 1, ...args, 2 );
+				}
+			}`,
+
+		output: `
+			var SubClass = (function (SuperClass) {
+				function SubClass() {
+					var args = [], len = arguments.length;
+					while ( len-- ) args[ len ] = arguments[ len ];
+
+					SuperClass.apply( this, [ 1 ].concat( args, [2] ) );
+				}
+
+				if ( SuperClass ) SubClass.__proto__ = SuperClass;
+				SubClass.prototype = Object.create( SuperClass && SuperClass.prototype );
+				SubClass.prototype.constructor = SubClass;
+
+				return SubClass;
+			}(SuperClass));`
+	},
+
+	{
+		description: 'transpiles computed class properties',
+
+		input: `
+			class Foo {
+				[a.b.c] () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype[a.b.c] = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'transpiles static computed class properties',
+
+		input: `
+			class Foo {
+				static [a.b.c] () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo[a.b.c] = function () {
+				// code goes here
+			};`
+	},
+
+	{
+		skip: true,
+		description: 'transpiles computed class accessors',
+
+		input: `
+			class Foo {
+				get [a.b.c] () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			var prototypeAccessors = {};
+			var ref = a.b.c;
+			prototypeAccessors[ref] = {};
+
+			prototypeAccessors[ref].get = function () {
+				// code goes here
+			};
+
+			Object.defineProperties( Foo.prototype, prototypeAccessors );`
+	},
+
+	{
+		description: 'transpiles reserved class properties (!68)',
+
+		input: `
+			class Foo {
+				catch () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype.catch = function catch$1 () {
+				// code goes here
+			};`,
+	},
+
+	{
+		description: 'transpiles static reserved class properties (!68)',
+
+		input: `
+			class Foo {
+				static catch () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.catch = function catch$1 () {
+				// code goes here
+			};`
+	},
+
+	{
+		description: 'uses correct `this` when transpiling `super` (#89)',
+
+		input: `
+			class A extends B {
+				constructor () {
+					super();
+					this.doSomething(() => {
+						super.doSomething();
+					});
+				}
+			}`,
+
+		output: `
+			var A = (function (B) {
+				function A () {
+					var this$1 = this;
+
+					B.call(this);
+					this.doSomething(function () {
+						B.prototype.doSomething.call(this$1);
+					});
+				}
+
+				if ( B ) A.__proto__ = B;
+				A.prototype = Object.create( B && B.prototype );
+				A.prototype.constructor = A;
+
+				return A;
+			}(B));`
+	},
+
+	{
+		description: 'methods with computed names',
+
+		input: `
+			class A {
+				[x](){}
+				[0](){}
+				[1 + 2](){}
+				[normal + " Method"](){}
+			}
+		`,
+		output: `
+			var A = function A () {};
+
+			A.prototype[x] = function (){};
+			A.prototype[0] = function (){};
+			A.prototype[1 + 2] = function (){};
+			A.prototype[normal + " Method"] = function (){};
+		`
+	},
+
+	{
+		description: 'static methods with computed names with varied spacing (#139)',
+
+		input: `
+			class B {
+				static[.000004](){}
+				static [x](){}
+				static  [x-y](){}
+				static[\`Static computed \${name}\`](){}
+			}
+		`,
+		output: `
+			var B = function B () {};
+
+			B[.000004] = function (){};
+			B[x] = function (){};
+			B [x-y] = function (){};
+			B[("Static computed " + name)] = function (){};
+		`
+	},
+
+	{
+		description: 'methods with numeric or string names (#139)',
+
+		input: `
+			class C {
+				0(){}
+				0b101(){}
+				80(){}
+				.12e3(){}
+				0o753(){}
+				12e34(){}
+				0xFFFF(){}
+				"var"(){}
+			}
+		`,
+		output: `
+			var C = function C () {};
+
+			C.prototype[0] = function (){};
+			C.prototype[5] = function (){};
+			C.prototype[80] = function (){};
+			C.prototype[.12e3] = function (){};
+			C.prototype[491] = function (){};
+			C.prototype[12e34] = function (){};
+			C.prototype[0xFFFF] = function (){};
+			C.prototype["var"] = function (){};
+		`
+	},
+
+	{
+		description: 'static methods with numeric or string names with varied spacing (#139)',
+
+		input: `
+			class D {
+				static .75(){}
+				static"Static Method"(){}
+				static "foo"(){}
+			}
+		`,
+		output: `
+			var D = function D () {};
+
+			D[.75] = function (){};
+			D["Static Method"] = function (){};
+			D["foo"] = function (){};
+		`
+	},
+
+	{
+		description: "don't shadow variables with function names (#166)",
+
+		input: `
+			class X {
+				foo() { return foo }
+				bar() {}
+			}
+		`,
+		output: `
+			var X = function X () {};
+
+			X.prototype.foo = function foo$1 () { return foo };
+			X.prototype.bar = function bar () {};
+		`
+	},
+
+	// TODO more tests. e.g. getters and setters.
+	// 'super.*' is not allowed before super()
+];
diff --git a/test/samples/computed-properties.js b/test/samples/computed-properties.js
new file mode 100644
index 0000000..48f648f
--- /dev/null
+++ b/test/samples/computed-properties.js
@@ -0,0 +1,231 @@
+module.exports = [
+	{
+		description: 'creates a computed property',
+
+		input: `
+			var obj = {
+				[a]: 1
+			};`,
+
+		output: `
+			var obj = {};
+			obj[a] = 1;`
+	},
+
+	{
+		description: 'creates a computed property with a non-identifier expression',
+
+		input: `
+			var obj = {
+				[a()]: 1
+			};`,
+
+		output: `
+			var obj = {};
+			obj[a()] = 1;`
+	},
+
+	{
+		description: 'creates a computed property at start of literal',
+
+		input: `
+			var obj = {
+				[a]: 1,
+				b: 2
+			};`,
+
+		output: `
+			var obj = {
+				b: 2
+			};
+			obj[a] = 1;`
+	},
+
+	{
+		description: 'creates a computed property at end of literal',
+
+		input: `
+			var obj = {
+				a: 1,
+				[b]: 2
+			};`,
+
+		output: `
+			var obj = {
+				a: 1
+			};
+			obj[b] = 2;`
+	},
+
+	{
+		description: 'creates a computed property in middle of literal',
+
+		input: `
+			var obj = {
+				a: 1,
+				[b]: 2,
+				c: 3
+			};`,
+
+		output: `
+			var obj = {
+				a: 1,
+				c: 3
+			};
+			obj[b] = 2;`
+	},
+
+	{
+		description: 'creates multiple computed properties',
+
+		input: `
+			var obj = {
+				[a]: 1,
+				b: 2,
+				[c]: 3,
+				[d]: 4,
+				e: 5,
+				[f]: 6
+			};`,
+
+		output: `
+			var obj = {
+				b: 2,
+				e: 5
+			};
+			obj[a] = 1;
+			obj[c] = 3;
+			obj[d] = 4;
+			obj[f] = 6;`
+	},
+
+	{
+		description: 'creates computed property in complex expression',
+
+		input: `
+			var a = 'foo', obj = { [a]: 'bar', x: 42 }, bar = obj.foo;`,
+
+		output: `
+			var a = 'foo', obj = ( obj$1 = { x: 42 }, obj$1[a] = 'bar', obj$1 ), bar = obj.foo;
+			var obj$1;`
+	},
+
+	{
+		description: 'creates computed property in block with conflicts',
+
+		input: `
+			var x;
+
+			if ( true ) {
+				let x = {
+					[a]: 1
+				};
+			}`,
+
+		output: `
+			var x;
+
+			if ( true ) {
+				var x$1 = {};
+				x$1[a] = 1;
+			}`
+	},
+
+	{
+		description: 'closing parenthesis put in correct place (#73)',
+
+		input: `
+			call({ [a]: 5 });`,
+
+		output: `
+			call(( obj = {}, obj[a] = 5, obj ));
+			var obj;`
+	},
+
+	{
+		description: 'creates a computed method (#78)',
+
+		input: `
+			var obj = {
+				[a] () {
+					// code goes here
+				}
+			};`,
+
+		output: `
+			var obj = {};
+			obj[a] = function () {
+					// code goes here
+				};`
+	},
+
+	{
+		description: 'creates a computed method with a non-identifier expression (#78)',
+
+		input: `
+			var obj = {
+				[a()] () {
+						// code goes here
+					}
+			};`,
+
+		output: `
+			var obj = {};
+			obj[a()] = function () {
+						// code goes here
+					};`
+	},
+
+	{
+		description: 'does not require space before parens of computed method (#82)',
+
+		input: `
+			var obj = {
+				[a]() {
+					// code goes here
+				}
+			};`,
+
+		output: `
+			var obj = {};
+			obj[a] = function () {
+					// code goes here
+				};`
+	},
+
+	{
+		description: 'supports computed shorthand function with object spread in body (#135)',
+
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `
+			let a = {
+				[foo] (x, y) {
+					return {
+						...{abc: '123'}
+					};
+				},
+			};
+		`,
+		output: `
+			var a = {};
+			a[foo] = function (x, y) {
+					return Object.assign({}, {abc: '123'});
+				};
+		`
+	},
+
+	{
+		description: 'object literal with computed property within arrow expression (#126)',
+
+		input: `
+			foo => bar({[x - y]: obj});
+		`,
+		output: `
+			(function(foo) { return bar(( obj$1 = {}, obj$1[x - y] = obj, obj$1 ))
+				var obj$1;; });
+		`
+	},
+
+];
diff --git a/test/samples/default-parameters.js b/test/samples/default-parameters.js
new file mode 100644
index 0000000..ba5ccc9
--- /dev/null
+++ b/test/samples/default-parameters.js
@@ -0,0 +1,129 @@
+module.exports = [
+	{
+		description: 'transpiles default parameters',
+
+		input: `
+			function foo ( a = 1, b = 2 ) {
+				console.log( a, b );
+			}
+
+			var bar = function ( a = 1, b = 2 ) {
+				console.log( a, b );
+			};`,
+
+		output: `
+			function foo ( a, b ) {
+				if ( a === void 0 ) a = 1;
+				if ( b === void 0 ) b = 2;
+
+				console.log( a, b );
+			}
+
+			var bar = function ( a, b ) {
+				if ( a === void 0 ) a = 1;
+				if ( b === void 0 ) b = 2;
+
+				console.log( a, b );
+			};`
+	},
+
+	{
+		description: 'transpiles default parameters in object pattern (#23)',
+
+		input: `
+			function foo ({ a = 1 }) {
+				console.log( a );
+			}`,
+
+		output: `
+			function foo (ref) {
+				var a = ref.a; if ( a === void 0 ) a = 1;
+
+				console.log( a );
+			}`
+	},
+
+	{
+		description: 'transpiles multiple default parameters in object pattern',
+
+		input: `
+			function foo ({ a = 1 }, { b = 2 }) {
+				console.log( a, b );
+			}
+
+			var bar = function ({ a = 1 }, { b = 2 }) {
+				console.log( a, b );
+			};`,
+
+		output: `
+			function foo (ref, ref$1) {
+				var a = ref.a; if ( a === void 0 ) a = 1;
+				var b = ref$1.b; if ( b === void 0 ) b = 2;
+
+				console.log( a, b );
+			}
+
+			var bar = function (ref, ref$1) {
+				var a = ref.a; if ( a === void 0 ) a = 1;
+				var b = ref$1.b; if ( b === void 0 ) b = 2;
+
+				console.log( a, b );
+			};`
+	},
+
+	{
+		description: 'can be disabled with `transforms.defaultParameter: false`',
+		options: { transforms: { defaultParameter: false } },
+
+		input: `
+			function foo ( a = 1, b = 2 ) {
+				console.log( a, b );
+			}
+
+			var bar = function ( a = 1, b = 2 ) {
+				console.log( a, b );
+			};`,
+
+		output: `
+			function foo ( a = 1, b = 2 ) {
+				console.log( a, b );
+			}
+
+			var bar = function ( a = 1, b = 2 ) {
+				console.log( a, b );
+			};`
+	},
+
+	{
+		description: 'transpiles default arrow function parameters',
+
+		input: `
+			function a(x, f = () => x) {
+				console.log( f() );
+			}`,
+
+		output: `
+			function a(x, f) {
+				if ( f === void 0 ) f = function () { return x; };
+
+				console.log( f() );
+			}`
+	},
+
+	{
+		description: 'transpiles destructured default parameters (#43)',
+
+		input: `
+			function a({ x = 1 } = {}) {
+				console.log( x );
+			}`,
+
+		output: `
+			function a(ref) {
+				if ( ref === void 0 ) ref = {};
+				var x = ref.x; if ( x === void 0 ) x = 1;
+
+				console.log( x );
+			}`
+	}
+];
diff --git a/test/samples/destructuring.js b/test/samples/destructuring.js
new file mode 100644
index 0000000..97bf918
--- /dev/null
+++ b/test/samples/destructuring.js
@@ -0,0 +1,760 @@
+module.exports = [
+	{
+		description: 'destructures an identifier with an object pattern',
+		input: `
+			var { x, y } = point;`,
+		output: `
+			var x = point.x;
+			var y = point.y;`
+	},
+
+	{
+		description: 'destructures a non-identifier with an object pattern',
+		input: `
+			var { x, y } = getPoint();`,
+		output: `
+			var ref = getPoint();
+			var x = ref.x;
+			var y = ref.y;`
+	},
+
+	{
+		description: 'destructures a parameter with an object pattern',
+
+		input: `
+			function pythag ( { x, y: z = 1 } ) {
+				return Math.sqrt( x * x + z * z );
+			}`,
+
+		output: `
+			function pythag ( ref ) {
+				var x = ref.x;
+				var z = ref.y; if ( z === void 0 ) z = 1;
+
+				return Math.sqrt( x * x + z * z );
+			}`
+	},
+
+	{
+		description: 'uses different name than the property in a declaration',
+		input: `var { foo: bar } = obj;`,
+		output: `var bar = obj.foo;`
+	},
+
+	{
+		description: 'destructures an identifier with an array pattern',
+		input: `
+			var [ x, y ] = point;`,
+		output: `
+			var x = point[0];
+			var y = point[1];`
+	},
+
+	{
+		description: 'destructures an identifier with a sparse array pattern',
+		input: `
+			var [ x, , z ] = point;`,
+		output: `
+			var x = point[0];
+			var z = point[2];`
+	},
+
+	{
+		description: 'destructures a non-identifier with an array pattern',
+		input: `
+			var [ x, y ] = getPoint();`,
+		output: `
+			var ref = getPoint();
+			var x = ref[0];
+			var y = ref[1];`
+	},
+
+	{
+		description: 'destructures a parameter with an array pattern',
+
+		input: `
+			function pythag ( [ x, z = 1 ] ) {
+				return Math.sqrt( x * x + z * z );
+			}`,
+
+		output: `
+			function pythag ( ref ) {
+				var x = ref[0];
+				var z = ref[1]; if ( z === void 0 ) z = 1;
+
+				return Math.sqrt( x * x + z * z );
+			}`
+	},
+
+	{
+		description: 'can be disabled in declarations with `transforms.destructuring === false`',
+		options: { transforms: { destructuring: false } },
+		input: `var { x, y } = point;`,
+		output: `var { x, y } = point;`
+	},
+
+	{
+		description: 'can be disabled in function parameters with `transforms.parameterDestructuring === false`',
+		options: { transforms: { parameterDestructuring: false } },
+		input: `function foo ({ x, y }) {}`,
+		output: `function foo ({ x, y }) {}`
+	},
+
+	{
+		description: 'does not destructure parameters intelligently (#53)',
+
+		input: `
+			function drawRect ( { ctx, x1, y1, x2, y2 } ) {
+				ctx.fillRect( x1, y1, x2 - x1, y2 - y1 );
+			}
+
+			function scale ([ d0, d1 ], [ r0, r1 ]) {
+				const m = ( r1 - r0 ) / ( d1 - d0 );
+				return function ( num ) {
+					return r0 + ( num - d0 ) * m;
+				}
+			}`,
+
+		output: `
+			function drawRect ( ref ) {
+				var ctx = ref.ctx;
+				var x1 = ref.x1;
+				var y1 = ref.y1;
+				var x2 = ref.x2;
+				var y2 = ref.y2;
+
+				ctx.fillRect( x1, y1, x2 - x1, y2 - y1 );
+			}
+
+			function scale (ref, ref$1) {
+				var d0 = ref[0];
+				var d1 = ref[1];
+				var r0 = ref$1[0];
+				var r1 = ref$1[1];
+
+				var m = ( r1 - r0 ) / ( d1 - d0 );
+				return function ( num ) {
+					return r0 + ( num - d0 ) * m;
+				}
+			}`
+	},
+
+	{
+		description: 'does not destructure variable declarations intelligently (#53)',
+
+		input: `
+			var { foo: bar, baz } = obj;
+			console.log( bar );
+			console.log( baz );
+			console.log( baz );`,
+
+		output: `
+			var bar = obj.foo;
+			var baz = obj.baz;
+			console.log( bar );
+			console.log( baz );
+			console.log( baz );`
+	},
+
+	{
+		description: 'destructures variables in the middle of a declaration',
+
+		input: `
+			var a, { x, y } = getPoint(), b = x;
+			console.log( x, y );`,
+
+		output: `
+			var a;
+			var ref = getPoint();
+			var x = ref.x;
+			var y = ref.y;
+			var b = x;
+			console.log( x, y );`
+	},
+
+	{
+		description: 'destructuring a destructured parameter',
+
+		input: `
+			function test ( { foo, bar } ) {
+				const { x, y } = foo;
+			}`,
+
+		output: `
+			function test ( ref ) {
+				var foo = ref.foo;
+				var bar = ref.bar;
+
+				var x = foo.x;
+				var y = foo.y;
+			}`
+	},
+
+	{
+		description: 'default value in destructured variable declaration (#37)',
+
+		input: `
+			var { name: value, description = null } = obj;
+			console.log( value, description );`,
+
+		output: `
+			var value = obj.name;
+			var description = obj.description; if ( description === void 0 ) description = null;
+			console.log( value, description );`
+	},
+
+	{
+		description: 'default values in destructured object parameter with a default value (#37)',
+
+		input: `
+			function foo ({ arg1 = 123, arg2 = 456 } = {}) {
+				console.log( arg1, arg2 );
+			}`,
+
+		output: `
+			function foo (ref) {
+				if ( ref === void 0 ) ref = {};
+				var arg1 = ref.arg1; if ( arg1 === void 0 ) arg1 = 123;
+				var arg2 = ref.arg2; if ( arg2 === void 0 ) arg2 = 456;
+
+				console.log( arg1, arg2 );
+			}`
+	},
+
+	{
+		description: 'destructures not replacing reference from parent scope',
+
+		input: `
+			function controller([element]) {
+				const mapState = function ({ filter }) {
+					console.log(element);
+				};
+			}`,
+
+		output: `
+			function controller(ref) {
+				var element = ref[0];
+
+				var mapState = function (ref) {
+					var filter = ref.filter;
+
+					console.log(element);
+				};
+			}`
+	},
+
+	{
+		description: 'deep matching with object patterns',
+
+		input: `
+			var { a: { b: c }, d: { e: f, g: h = 1 } } = x;`,
+
+		output: `
+			var c = x.a.b;
+			var x_d = x.d;
+			var f = x_d.e;
+			var h = x_d.g; if ( h === void 0 ) h = 1;`
+	},
+
+	{
+		description: 'deep matching with string literals in object patterns',
+
+		input: `
+			var { a, 'b-1': c } = x;`,
+
+		output: `
+			var a = x.a;
+			var c = x['b-1'];`
+	},
+
+	{
+		description: 'deep matching with object patterns and reference',
+
+		input: `
+			var { a: { b: c }, d: { e: f, g: h } } = x();`,
+
+		output: `
+			var ref = x();
+			var c = ref.a.b;
+			var ref_d = ref.d;
+			var f = ref_d.e;
+			var h = ref_d.g;`
+	},
+
+	{
+		description: 'deep matching with array patterns',
+
+		input: `
+			var [[[a]], [[b, c = 1]]] = x;`,
+
+		output: `
+			var a = x[0][0][0];
+			var x_1_0 = x[1][0];
+			var b = x_1_0[0];
+			var c = x_1_0[1]; if ( c === void 0 ) c = 1;`
+	},
+
+	{
+		description: 'deep matching with sparse array',
+
+		input: `
+			function foo ( [[[,x = 3] = []] = []] = [] ) {
+				console.log( x );
+			}`,
+
+		output: `
+			function foo ( ref ) {
+				if ( ref === void 0 ) ref = [];
+				var ref_0 = ref[0]; if ( ref_0 === void 0 ) ref_0 = [];
+				var ref_0_0 = ref_0[0]; if ( ref_0_0 === void 0 ) ref_0_0 = [];
+				var x = ref_0_0[1]; if ( x === void 0 ) x = 3;
+
+				console.log( x );
+			}`
+	},
+
+	{
+		description: 'deep matching in parameters',
+
+		input: `
+			function foo ({ a: { b: c }, d: { e: f, g: h } }) {
+				console.log( c, f, h );
+			}`,
+
+		output: `
+			function foo (ref) {
+				var c = ref.a.b;
+				var ref_d = ref.d;
+				var f = ref_d.e;
+				var h = ref_d.g;
+
+				console.log( c, f, h );
+			}`
+	},
+
+	{
+		description: 'destructured object assignment with computed properties',
+		input: `
+			let one, two, three, four;
+			({ [FirstProp]: one, [SecondProp]: two = 'Too', 3: three, Fore: four } = x);
+		`,
+		output: `
+			var one, two, three, four;
+			var assign;
+			((assign = x, one = assign[FirstProp], two = assign[SecondProp], two = two === void 0 ? 'Too' : two, three = assign[3], four = assign.Fore));
+		`
+	},
+
+	{
+		description: 'destructured object declaration with computed properties',
+		input: `
+			var { [FirstProp]: one, [SecondProp]: two = 'Too', 3: three, Fore: four } = x;
+		`,
+		output: `
+			var one = x[FirstProp];
+			var two = x[SecondProp]; if ( two === void 0 ) two = 'Too';
+			var three = x[3];
+			var four = x.Fore;
+		`
+	},
+
+	{
+		description: 'destructured object with computed properties in parameters',
+		input: `
+			function foo({ [FirstProp]: one, [SecondProp]: two = 'Too', 3: three, Fore: four } = x) {
+				console.log(one, two, three, four);
+			}
+		`,
+		output: `
+			function foo(ref) {
+				if ( ref === void 0 ) ref = x;
+				var one = ref[FirstProp];
+				var two = ref[SecondProp]; if ( two === void 0 ) two = 'Too';
+				var three = ref[3];
+				var four = ref.Fore;
+
+				console.log(one, two, three, four);
+			}
+		`
+	},
+
+	{
+		description: 'deep matching in parameters with computed properties',
+
+		input: `
+			function foo ({ [a]: { [b]: c }, d: { 'e': f, [g]: h }, [i + j]: { [k + l]: m, n: o } }) {
+				console.log( c, f, h, m, o );
+			}`,
+
+		output: `
+			function foo (ref) {
+				var c = ref[a][b];
+				var ref_d = ref.d;
+				var f = ref_d['e'];
+				var h = ref_d[g];
+				var ref_i_j = ref[i + j];
+				var m = ref_i_j[k + l];
+				var o = ref_i_j.n;
+
+				console.log( c, f, h, m, o );
+			}`
+	},
+
+	{
+		description: 'array destructuring declaration with rest element',
+
+		input: `
+			const [a, ...b] = [1, 2, 3, 4];
+			console.log(a, b);
+		`,
+		output: `
+			var ref = [1, 2, 3, 4];
+			var a = ref[0];
+			var b = ref.slice(1);
+			console.log(a, b);
+		`
+	},
+
+	{
+		description: 'array destructuring declaration with complex rest element',
+
+		input: `
+			const x = [1, 2, {r: 9}, 3], [a, ...[, {r: b, s: c = 4} ]] = x;
+			console.log(a, b, c);
+		`,
+		output: `
+			var x = [1, 2, {r: 9}, 3];
+			var a = x[0];
+			var x_slice_1_1 = x.slice(1)[1];
+			var b = x_slice_1_1.r;
+			var c = x_slice_1_1.s; if ( c === void 0 ) c = 4;
+			console.log(a, b, c);
+		`
+	},
+
+	{
+		description: 'destructuring function parameters with array rest element',
+
+		input: `
+			function foo([a, ...[, {r: b, s: c = 4} ]]) {
+				console.log(a, b, c);
+			}
+			foo( [1, 2, {r: 9}, 3] );
+		`,
+		output: `
+			function foo(ref) {
+				var a = ref[0];
+				var ref_slice_1_1 = ref.slice(1)[1];
+				var b = ref_slice_1_1.r;
+				var c = ref_slice_1_1.s; if ( c === void 0 ) c = 4;
+
+				console.log(a, b, c);
+			}
+			foo( [1, 2, {r: 9}, 3] );
+		`
+	},
+
+	{
+		description: 'destructuring array assignment with complex rest element',
+
+		input: `
+			let x = [1, 2, {r: 9}, {s: ["table"]} ];
+			let a, b, c, d;
+			([a, ...[ , {r: b}, {r: c = "nothing", s: [d] = "nope"} ]] = x);
+			console.log(a, b, c, d);
+		`,
+		output: `
+			var x = [1, 2, {r: 9}, {s: ["table"]} ];
+			var a, b, c, d;
+			var assign, array, obj, temp;
+			((assign = x, a = assign[0], array = assign.slice(1), b = array[1].r, obj = array[2], c = obj.r, c = c === void 0 ? "nothing" : c, temp = obj.s, temp = temp === void 0 ? "nope" : temp, d = temp[0]));
+			console.log(a, b, c, d);
+		`
+	},
+
+	{
+		description: 'destructuring array rest element within an object property',
+
+		input: `
+			let foo = ({p: [x, ...y] = [6, 7], q: [...z] = [8]} = {}) => {
+				console.log(x, y, z);
+			};
+			foo({p: [1, 2, 3], q: [4, 5]});
+			foo({q: []} );
+			foo();
+		`,
+		output: `
+			var foo = function (ref) {
+				if ( ref === void 0 ) ref = {};
+				var ref_p = ref.p; if ( ref_p === void 0 ) ref_p = [6, 7];
+				var ref_p$1 = ref_p;
+				var x = ref_p$1[0];
+				var y = ref_p$1.slice(1);
+				var ref_q = ref.q; if ( ref_q === void 0 ) ref_q = [8];
+				var z = ref_q.slice(0);
+
+				console.log(x, y, z);
+			};
+			foo({p: [1, 2, 3], q: [4, 5]});
+			foo({q: []} );
+			foo();
+		`
+	},
+
+	{
+		description: 'transpiles destructuring assignment of an array',
+		input: `
+			[x, y] = [1, 2];`,
+		output: `
+			var assign;
+			(assign = [1, 2], x = assign[0], y = assign[1]);`
+	},
+
+	{
+		description: 'transpiles destructuring assignment of an array with a default value',
+		input: `
+			[x = 4, y] = [1, 2];`,
+		output: `
+			var assign;
+			(assign = [1, 2], x = assign[0], x = x === void 0 ? 4 : x, y = assign[1]);`
+	},
+
+	{
+		description: 'transpiles nested destructuring assignment of an array',
+		input: `
+			[[x], y] = [1, 2];`,
+		output: `
+			var assign;
+			(assign = [1, 2], x = assign[0][0], y = assign[1]);`
+	},
+
+	{
+		description: 'transpiles nested destructuring assignment of an array without evaluating a memberexpr twice',
+		input: `
+			[[x, z], y] = [1, 2];`,
+		output: `
+			var assign, array;
+			(assign = [1, 2], array = assign[0], x = array[0], z = array[1], y = assign[1]);`
+	},
+
+	{
+		description: 'transpiles nested destructuring assignment of an array with a default',
+		input: `
+			[[x] = [], y] = [1, 2];`,
+		output: `
+			var assign, temp;
+			(assign = [1, 2], temp = assign[0], temp = temp === void 0 ? [] : temp, x = temp[0], y = assign[1]);`
+	},
+
+	{
+		description: 'leaves member expression patterns intact',
+		input: `
+			[x, y.z] = [1, 2];`,
+		output: `
+			var assign;
+			(assign = [1, 2], x = assign[0], y.z = assign[1]);`
+	},
+
+	{
+		description: 'only assigns to member expressions once',
+		input: `
+			[x, y.z = 3] = [1, 2];`,
+		output: `
+			var assign, temp;
+			(assign = [1, 2], x = assign[0], temp = assign[1], temp = temp === void 0 ? 3 : temp, y.z = temp);`
+	},
+
+	{
+		description: 'transpiles destructuring assignment of an object',
+		input: `
+			({x, y} = {x: 1});`,
+		output: `
+			var assign;
+			((assign = {x: 1}, x = assign.x, y = assign.y));`
+	},
+
+	{
+		description: 'transpiles destructuring assignment of an object where key and pattern names differ',
+		input: `
+			({x, y: z} = {x: 1});`,
+		output: `
+			var assign;
+			((assign = {x: 1}, x = assign.x, z = assign.y));`
+	},
+
+	{
+		description: 'transpiles nested destructuring assignment of an object',
+		input: `
+			({x, y: {z}} = {x: 1});`,
+		output: `
+			var assign;
+			((assign = {x: 1}, x = assign.x, z = assign.y.z));`
+	},
+
+	{
+		description: 'transpiles destructuring assignment of an object with a default value',
+		input: `
+			({x, y = 4} = {x: 1});`,
+		output: `
+			var assign;
+			((assign = {x: 1}, x = assign.x, y = assign.y, y = y === void 0 ? 4 : y));`
+	},
+
+	{
+		description: 'only evaluates a sub-object once',
+		input: `
+			({x, y: {z, q}} = {x: 1});`,
+		output: `
+			var assign, obj;
+			((assign = {x: 1}, x = assign.x, obj = assign.y, z = obj.z, q = obj.q));`
+	},
+
+	{
+		description: 'doesn\'t create an object temporary unless necessary',
+		input: `
+			({x, y: {z}} = {x: 1});`,
+		output: `
+			var assign;
+			((assign = {x: 1}, x = assign.x, z = assign.y.z));`
+	},
+
+	{
+		description: 'lifts its variable declarations out of the expression',
+		input: `
+			foo();
+			if ( bar([x, y] = [1, 2]) ) {
+				baz();
+			}`,
+		output: `
+			foo();
+			var assign;
+			if ( bar((assign = [1, 2], x = assign[0], y = assign[1], assign)) ) {
+				baz();
+			}`
+	},
+
+	{
+		description: 'puts its scratch variables in the parent scope',
+		input: `
+			function foo() {
+				[x, y] = [1, 2];
+			}`,
+		output: `
+			function foo() {
+				var assign;
+				(assign = [1, 2], x = assign[0], y = assign[1]);
+			}`
+	},
+
+	{
+		description: 'array destructuring default with template string (#145)',
+
+		input: 'const [ foo = `${baz() - 4}` ] = bar;',
+
+		output: `var foo = bar[0]; if ( foo === void 0 ) foo = "" + (baz() - 4);`
+	},
+
+	{
+		description: 'object destructuring default with template string (#145)',
+
+		input: 'const { foo = `${baz() - 4}` } = bar;',
+
+		output: `var foo = bar.foo; if ( foo === void 0 ) foo = "" + (baz() - 4);`
+	},
+
+	{
+		description: 'array destructuring with multiple defaults with hole',
+
+		// FIXME: unnecessary parens needed around complex defaults due to buble bugs
+		input: `
+			let [
+				a = \`A\${baz() - 4}\`,
+				, /* hole */
+				c = (x => -x),
+				d = ({ r: 5, [h()]: i }),
+			] = [ "ok" ];
+		`,
+		output: `
+			var ref = [ "ok" ];
+			var a = ref[0]; if ( a === void 0 ) a = "A" + (baz() - 4);
+			var c = ref[2]; if ( c === void 0 ) c = (function (x) { return -x; });
+			var d = ref[3]; if ( d === void 0 ) d = (( obj = { r: 5 }, obj[h()] = i, obj ));
+			var obj;
+		`
+	},
+
+	{
+		description: 'object destructuring with multiple defaults',
+
+		// FIXME: unnecessary parens needed around complex defaults due to buble bugs
+		input: `
+			let {
+				a = \`A\${baz() - 4}\`,
+				c = (x => -x),
+				d = ({ r: 5, [1 + 1]: 2, [h()]: i }),
+			} = { b: 3 };
+		`,
+		output: `
+			var ref = { b: 3 };
+			var a = ref.a; if ( a === void 0 ) a = "A" + (baz() - 4);
+			var c = ref.c; if ( c === void 0 ) c = (function (x) { return -x; });
+			var d = ref.d; if ( d === void 0 ) d = (( obj = { r: 5 }, obj[1 + 1] = 2, obj[h()] = i, obj ));
+			var obj;
+		`
+	},
+
+	{
+		description: 'destrucuring assignments requiring rvalues',
+
+		input: `
+			class Point {
+				set ( array ) {
+					return [ this.x, this.y ] = array;
+				}
+			}
+
+			let a, b, c = [ 1, 2, 3 ];
+			console.log( [ a, b ] = c );
+		`,
+		output: `
+			var Point = function Point () {};
+
+			Point.prototype.set = function set ( array ) {
+				var assign;
+					return (assign = array, this.x = assign[0], this.y = assign[1], assign);
+			};
+
+			var a, b, c = [ 1, 2, 3 ];
+			var assign;
+			console.log( (assign = c, a = assign[0], b = assign[1], assign) );
+		`
+	},
+
+	{
+		description: 'destrucuring assignments not requiring rvalues',
+
+		input: `
+			class Point {
+				set ( array ) {
+					[ this.x, this.y ] = array;
+				}
+			}
+
+			let a, b, c = [ 1, 2, 3 ];
+			[ a, b ] = c;
+		`,
+		output: `
+			var Point = function Point () {};
+
+			Point.prototype.set = function set ( array ) {
+				var assign;
+					(assign = array, this.x = assign[0], this.y = assign[1]);
+			};
+
+			var a, b, c = [ 1, 2, 3 ];
+			var assign;
+			(assign = c, a = assign[0], b = assign[1]);
+		`
+	},
+
+];
diff --git a/test/samples/exponentiation-operator.js b/test/samples/exponentiation-operator.js
new file mode 100644
index 0000000..6679cea
--- /dev/null
+++ b/test/samples/exponentiation-operator.js
@@ -0,0 +1,178 @@
+module.exports = [
+	{
+		description: 'transpiles an exponentiation operator',
+		input: `x ** y`,
+		output: `Math.pow( x, y )`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a simple reference',
+		input: `x **= y`,
+		output: `x = Math.pow( x, y )`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a simple parenthesized reference',
+		input: `( x ) **= y`,
+		output: `( x ) = Math.pow( x, y )`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a rewritten simple reference',
+
+		input: `
+			let x = 1;
+
+			if ( maybe ) {
+				let x = 2;
+				x **= y;
+			}`,
+
+		output: `
+			var x = 1;
+
+			if ( maybe ) {
+				var x$1 = 2;
+				x$1 = Math.pow( x$1, y );
+			}`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a simple member expression',
+
+		input: `
+			foo.bar **= y;`,
+
+		output: `
+			foo.bar = Math.pow( foo.bar, y );`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a simple deep member expression',
+
+		input: `
+			foo.bar.baz **= y;`,
+
+		output: `
+			var object = foo.bar;
+			object.baz = Math.pow( object.baz, y );`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a simple computed member expression',
+
+		input: `
+			foo[ bar ] **= y;`,
+
+		output: `
+			foo[ bar ] = Math.pow( foo[bar], y );`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a complex reference',
+
+		input: `
+			foo[ bar() ] **= y;`,
+
+		output: `
+			var property = bar();
+			foo[property] = Math.pow( foo[property], y );`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a contrivedly complex reference',
+
+		input: `
+			foo[ bar() ][ baz() ] **= y;`,
+
+		output: `
+			var object = foo[ bar() ];
+			var property = baz();
+			object[property] = Math.pow( object[property], y );`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a contrivedly complex reference (that is not a top-level statement)',
+
+		input: `
+			var baz = 1, lolwut = foo[ bar() ][ baz * 2 ] **= y;`,
+
+		output: `
+			var object, property;
+			var baz = 1, lolwut = ( object = foo[ bar() ], property = baz * 2, object[property] = Math.pow( object[property], y ) );`
+	},
+
+	{
+		description: 'transpiles an exponentiation assignment to a contrivedly complex reference with simple object (that is not a top-level statement)',
+
+		input: `
+			var baz = 1, lolwut = foo[ bar() ] **= y;`,
+
+		output: `
+			var property;
+			var baz = 1, lolwut = ( property = bar(), foo[property] = Math.pow( foo[property], y ) );`
+	},
+
+	{
+		description: 'handles pathological bastard case',
+
+		input: `
+			let i;
+
+			if ( maybe ) {
+				for ( let i = 1.1; i < 1e6; i **= i ) {
+					setTimeout( function () {
+						console.log( i );
+					}, i );
+				}
+			}`,
+
+		output: `
+			var i;
+
+			if ( maybe ) {
+				var loop = function ( i ) {
+					setTimeout( function () {
+						console.log( i );
+					}, i );
+				};
+
+				for ( var i$1 = 1.1; i$1 < 1e6; i$1 = Math.pow( i$1, i$1 ) ) loop( i$1 );
+			}`
+	},
+
+	{
+		description: 'handles assignment of exponentiation assignment to property',
+
+		input: `
+			x=a.b**=2;
+		`,
+		output: `
+			x=a.b=Math.pow( a.b, 2 );
+		`
+	},
+
+	{
+		description: 'handles assignment of exponentiation assignment to property with side effect',
+
+		input: `
+			x=a[bar()]**=2;
+		`,
+		output: `
+			var property;
+			x=( property = bar(), a[property]=Math.pow( a[property], 2 ) );
+		`
+	},
+
+	/* TODO: Test currently errors out with: TypeError: Cannot read property 'property' of null
+	{
+		description: 'handles assignment of exponentiation assignment to property with side effect within a block-less if',
+
+		input: `
+			if(y)x=a[foo()]**=2;
+		`,
+		output: `
+		`
+	},
+	*/
+];
diff --git a/test/samples/for-of.js b/test/samples/for-of.js
new file mode 100644
index 0000000..6bf4bf2
--- /dev/null
+++ b/test/samples/for-of.js
@@ -0,0 +1,227 @@
+module.exports = [
+	{
+		description: 'disallows for-of statements',
+		input: `for ( x of y ) {}`,
+		error: /for\.\.\.of statements are not supported/
+	},
+
+	{
+		description: 'ignores for-of with `transforms.forOf === false`',
+		options: { transforms: { forOf: false } },
+		input: `for ( x of y ) {}`,
+		output: `for ( x of y ) {}`
+	},
+
+	{
+		description: 'transpiles for-of with array assumption with `transforms.dangerousForOf`',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let member of array ) {
+				doSomething( member );
+			}`,
+
+		output: `
+			for ( var i = 0, list = array; i < list.length; i += 1 ) {
+				var member = list[i];
+
+				doSomething( member );
+			}`
+	},
+
+	{
+		description: 'transpiles for-of with expression',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let member of [ 'a', 'b', 'c' ] ) {
+				doSomething( member );
+			}`,
+
+		output: `
+			for ( var i = 0, list = [ 'a', 'b', 'c' ]; i < list.length; i += 1 ) {
+				var member = list[i];
+
+				doSomething( member );
+			}`
+	},
+
+	{
+		description: 'transpiles for-of that needs to be rewritten as function',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let member of [ 'a', 'b', 'c' ] ) {
+				setTimeout( function () {
+					doSomething( member );
+				});
+			}`,
+
+		output: `
+			var loop = function () {
+				var member = list[i];
+
+				setTimeout( function () {
+					doSomething( member );
+				});
+			};
+
+			for ( var i = 0, list = [ 'a', 'b', 'c' ]; i < list.length; i += 1 ) loop();`
+	},
+
+	{
+		description: 'transpiles body-less for-of',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let member of array ) console.log( member );`,
+
+		output: `
+			for ( var i = 0, list = array; i < list.length; i += 1 ) {
+				var member = list[i];
+
+				console.log( member );
+			}`
+	},
+
+	{
+		description: 'transpiles space-less for-of',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for (const key of this.keys) {
+				console.log(key);
+			}`,
+
+		output: `
+			var this$1 = this;
+
+			for (var i = 0, list = this$1.keys; i < list.length; i += 1) {
+				var key = list[i];
+
+				console.log(key);
+			}`
+	},
+
+	{
+		description: 'handles continue in for-of',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let item of items ) {
+				if ( item.foo ) continue;
+			}`,
+
+		output: `
+			for ( var i = 0, list = items; i < list.length; i += 1 ) {
+				var item = list[i];
+
+				if ( item.foo ) { continue; }
+			}`
+
+	},
+
+	{
+		description: 'handles this and arguments in for-of',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let item of items ) {
+				console.log( this, arguments, item );
+				setTimeout( () => {
+					console.log( item );
+				});
+			}`,
+
+		output: `
+			var arguments$1 = arguments;
+			var this$1 = this;
+
+			var loop = function () {
+				var item = list[i];
+
+				console.log( this$1, arguments$1, item );
+				setTimeout( function () {
+					console.log( item );
+				});
+			};
+
+			for ( var i = 0, list = items; i < list.length; i += 1 ) loop();`
+	},
+
+	{
+		description: 'for-of with empty block (#80)',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( let x of y ) {}`,
+
+		output: `
+			`
+	},
+
+	{
+		description: 'for-of with empty block and var (#80)',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for ( var x of y ) {}`,
+
+		output: `
+			var x;`
+	},
+
+	{
+		description: 'return from for-of loop rewritten as function',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			function foo () {
+				for ( let x of y ) {
+					setTimeout( function () {
+						console.log( x );
+					});
+
+					if ( x > 10 ) return;
+				}
+			}`,
+
+		output: `
+			function foo () {
+				var loop = function () {
+					var x = list[i];
+
+					setTimeout( function () {
+						console.log( x );
+					});
+
+					if ( x > 10 ) { return {}; }
+				};
+
+				for ( var i = 0, list = y; i < list.length; i += 1 ) {
+					var returned = loop();
+
+					if ( returned ) return returned.v;
+				}
+			}`
+	},
+
+	{
+		description: 'allows destructured variable declaration (#95)',
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for (var {x, y} of [{x: 1, y: 2}]) {
+				console.log(x, y);
+			}`,
+
+		output: `
+			for (var i = 0, list = [{x: 1, y: 2}]; i < list.length; i += 1) {
+				var ref = list[i];
+				var x = ref.x;
+				var y = ref.y;
+
+				console.log(x, y);
+			}`
+	}
+];
diff --git a/test/samples/generators.js b/test/samples/generators.js
new file mode 100644
index 0000000..b0a90c3
--- /dev/null
+++ b/test/samples/generators.js
@@ -0,0 +1,88 @@
+module.exports = [
+	{
+		description: 'disallows generator function declarations',
+
+		input: `
+			function* foo () {
+
+			}`,
+
+		error: /Generators are not supported/
+	},
+
+	{
+		description: 'disallows generator function expressions',
+
+		input: `
+			var fn = function* foo () {
+
+			}`,
+
+		error: /Generators are not supported/
+	},
+
+	{
+		description: 'disallows generator functions as object literal methods',
+
+		input: `
+			var obj = {
+				*foo () {
+
+				}
+			};`,
+
+		error: /Generators are not supported/
+	},
+
+	{
+		description: 'disallows generator functions as class methods',
+
+		input: `
+			class Foo {
+				*foo () {
+
+				}
+			}`,
+
+		error: /Generators are not supported/
+	},
+
+	{
+		description: 'ignores generator function declarations with `transforms.generator: false`',
+		options: { transforms: { generator: false } },
+		input: `function* foo () {}`,
+		output: `function* foo () {}`
+	},
+
+	{
+		description: 'ignores generator function expressions with `transforms.generator: false`',
+		options: { transforms: { generator: false } },
+		input: `var foo = function* foo () {}`,
+		output: `var foo = function* foo () {}`
+	},
+
+	{
+		description: 'ignores generator function methods with `transforms.generator: false`',
+		options: { transforms: { generator: false } },
+		input: `var obj = { *foo () {} }`,
+		output: `var obj = { foo: function* foo () {} }`
+	},
+
+	{
+		description: 'ignores generator function class methods with `transforms.generator: false`',
+		options: { transforms: { generator: false } },
+		input: `
+			class Foo {
+				*foo () {
+					// code goes here
+				}
+			}`,
+
+		output: `
+			var Foo = function Foo () {};
+
+			Foo.prototype.foo = function* foo () {
+				// code goes here
+			};`
+	}
+];
diff --git a/test/samples/jsx.js b/test/samples/jsx.js
new file mode 100644
index 0000000..9b18a5a
--- /dev/null
+++ b/test/samples/jsx.js
@@ -0,0 +1,227 @@
+module.exports = [
+	{
+		description: 'transpiles self-closing JSX tag',
+		input: `var img = <img src='foo.gif'/>;`,
+		output: `var img = React.createElement( 'img', { src: 'foo.gif' });`
+	},
+
+	{
+		description: 'transpiles non-self-closing JSX tag',
+		input: `var div = <div className='foo'></div>;`,
+		output: `var div = React.createElement( 'div', { className: 'foo' });`
+	},
+
+	{
+		description: 'transpiles non-self-closing JSX tag without attributes',
+		input: `var div = <div></div>;`,
+		output: `var div = React.createElement( 'div', null );`
+	},
+
+	{
+		description: 'transpiles nested JSX tags',
+
+		input: `
+			var div = (
+				<div className='foo'>
+					<img src='foo.gif'/>
+					<img src='bar.gif'/>
+				</div>
+			);`,
+
+		output: `
+			var div = (
+				React.createElement( 'div', { className: 'foo' },
+					React.createElement( 'img', { src: 'foo.gif' }),
+					React.createElement( 'img', { src: 'bar.gif' })
+				)
+			);`
+	},
+
+	{
+		description: 'transpiles JSX tag with expression attributes',
+		input: `var img = <img src={src}/>;`,
+		output: `var img = React.createElement( 'img', { src: src });`
+	},
+
+	{
+		description: 'transpiles JSX tag with expression children',
+
+		input: `
+			var div = (
+				<div>
+					{ images.map( src => <img src={src}/> ) }
+				</div>
+			);`,
+
+		output: `
+			var div = (
+				React.createElement( 'div', null,
+					images.map( function (src) { return React.createElement( 'img', { src: src }); } )
+				)
+			);`
+	},
+
+	{
+		description: 'transpiles JSX component',
+		input: `var element = <Hello name={name}/>;`,
+		output: `var element = React.createElement( Hello, { name: name });`
+	},
+
+	{
+		description: 'transpiles empty JSX expression block',
+		input: `var element = <Foo>{}</Foo>;`,
+		output: `var element = React.createElement( Foo, null );`
+	},
+
+	{
+		description: 'transpiles empty JSX expression block with comment',
+		input: `var element = <Foo>{/* comment */}</Foo>;`,
+		output: `var element = React.createElement( Foo, null/* comment */ );`
+	},
+
+	{
+		description: 'transpiles JSX component without attributes',
+		input: `var element = <Hello />;`,
+		output: `var element = React.createElement( Hello, null );`
+	},
+
+	{
+		description: 'transpiles JSX component without attributes with children',
+		input: `var element = <Hello>hello</Hello>;`,
+		output: `var element = React.createElement( Hello, null, "hello" );`
+	},
+
+	{
+		description: 'transpiles namespaced JSX component',
+		input: `var element = <Foo.Bar name={name}/>;`,
+		output: `var element = React.createElement( Foo.Bar, { name: name });`
+	},
+
+	{
+		description: 'supports pragmas',
+		options: { jsx: 'NotReact.createElement' },
+		input: `var img = <img src='foo.gif'/>;`,
+		output: `var img = NotReact.createElement( 'img', { src: 'foo.gif' });`
+	},
+
+	{
+		description: 'stringifies text children',
+		input: `<h1>Hello {name}!</h1>`,
+		output: `React.createElement( 'h1', null, "Hello ", name, "!" )`
+	},
+
+	{
+		description: 'handles whitespace and quotes appropriately',
+		input: `
+			<h1>
+				Hello {name}
+				!
+			</h1>`,
+		output: `
+			React.createElement( 'h1', null, "Hello ", name, "!" )`
+	},
+
+	{
+		description: 'handles single-line whitespace and quotes appropriately',
+		input: `
+			<h1>
+				Hello {name} – and goodbye!
+			</h1>`,
+		output: `
+			React.createElement( 'h1', null, "Hello ", name, " – and goodbye!" )`
+	},
+
+	{
+		description: 'handles single quotes in text children',
+		input: `
+			<h1>
+				Hello {name}
+				!${"      "}
+				It's  nice to meet you
+			</h1>`,
+		output: `
+			React.createElement( 'h1', null, "Hello ", name, "! It's  nice to meet you" )`
+	},
+
+	{
+		description: 'transpiles tag with data attribute',
+		input: `var element = <div data-name={name}/>;`,
+		output: `var element = React.createElement( 'div', { 'data-name': name });`
+	},
+
+	{
+		description: 'transpiles JSX tag without value',
+		input: `var div = <div contentEditable />;`,
+		output: `var div = React.createElement( 'div', { contentEditable: true });`
+	},
+
+	{
+		description: 'transpiles one JSX spread attributes',
+		input: `var element = <div {...props} />;`,
+		output: `var element = React.createElement( 'div', props);`
+	},
+
+	{
+		description: 'disallow mixed JSX spread attributes ending in spread',
+		input: `var element = <div a={1} {...props} {...stuff} />;`,
+		error: /Mixed JSX attributes ending in spread requires specified objectAssign option with 'Object\.assign' or polyfill helper\./
+	},
+
+	{
+		description: 'transpiles mixed JSX spread attributes ending in spread',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var element = <div a={1} {...props} {...stuff} />;`,
+		output: `var element = React.createElement( 'div', Object.assign({}, { a: 1 }, props, stuff));`
+	},
+
+	{
+		description: 'transpiles mixed JSX spread attributes ending in spread with custom Object.assign',
+		options: {
+			objectAssign: 'angular.extend'
+		},
+		input: `var element = <div a={1} {...props} {...stuff} />;`,
+		output: `var element = React.createElement( 'div', angular.extend({}, { a: 1 }, props, stuff));`
+	},
+
+	{
+		description: 'transpiles mixed JSX spread attributes ending in other values',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var element = <div a={1} {...props} b={2} c={3} {...stuff} more={things} />;`,
+		output: `var element = React.createElement( 'div', Object.assign({}, { a: 1 }, props, { b: 2, c: 3 }, stuff, { more: things }));`
+	},
+
+	{
+		description: 'transpiles spread expressions (#64)',
+		input: `<div {...this.props}/>`,
+		output: `React.createElement( 'div', this.props)`
+	},
+
+	{
+		description: 'handles whitespace between elements on same line (#65)',
+
+		input: `
+			<Foo> <h1>Hello {name}!</h1>   </Foo>`,
+
+		output: `
+			React.createElement( Foo, null, " ", React.createElement( 'h1', null, "Hello ", name, "!" ), "   " )`
+	},
+
+	{
+		description: 'fix Object.assign regression in JSXOpeningElement (#163)',
+
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `
+			<Thing two={"This no longer fails"} {...props}></Thing>
+		`,
+		output: `
+			React.createElement( Thing, Object.assign({}, { two: "This no longer fails" }, props))
+		`
+	},
+
+];
diff --git a/test/samples/loops.js b/test/samples/loops.js
new file mode 100644
index 0000000..dcc8aef
--- /dev/null
+++ b/test/samples/loops.js
@@ -0,0 +1,756 @@
+module.exports = [
+	{
+		description: 'transpiles block scoping inside loops with function bodies',
+
+		input: `
+			function log ( square ) {
+				console.log( square );
+			}
+
+			for ( let i = 0; i < 10; i += 1 ) {
+				const square = i * i;
+				setTimeout( function () {
+					log( square );
+				}, i * 100 );
+			}`,
+
+		output: `
+			function log ( square ) {
+				console.log( square );
+			}
+
+			var loop = function ( i ) {
+				var square = i * i;
+				setTimeout( function () {
+					log( square );
+				}, i * 100 );
+			};
+
+			for ( var i = 0; i < 10; i += 1 ) loop( i );`
+	},
+
+	{
+		description: 'transpiles block scoping inside while loops with function bodies',
+
+		input: `
+			function log ( square ) {
+				console.log( square );
+			}
+
+			while ( i-- ) {
+				const square = i * i;
+				setTimeout( function () {
+					log( square );
+				}, i * 100 );
+			}`,
+
+		output: `
+			function log ( square ) {
+				console.log( square );
+			}
+
+			var loop = function () {
+				var square = i * i;
+				setTimeout( function () {
+					log( square );
+				}, i * 100 );
+			};
+
+			while ( i-- ) loop();`
+	},
+
+	{
+		description: 'transpiles block scoping inside do-while loops with function bodies',
+
+		input: `
+			function log ( square ) {
+				console.log( square );
+			}
+
+			do {
+				const square = i * i;
+				setTimeout( function () {
+					log( square );
+				}, i * 100 );
+			} while ( i-- );`,
+
+		output: `
+			function log ( square ) {
+				console.log( square );
+			}
+
+			var loop = function () {
+				var square = i * i;
+				setTimeout( function () {
+					log( square );
+				}, i * 100 );
+			};
+
+			do {
+				loop();
+			} while ( i-- );`
+	},
+
+	{
+		description: 'transpiles block-less for loops with block-scoped declarations inside function body',
+
+		input: `
+			for ( let i = 0; i < 10; i += 1 ) setTimeout( () => console.log( i ), i * 100 );`,
+
+		output: `
+			var loop = function ( i ) {
+				setTimeout( function () { return console.log( i ); }, i * 100 );
+			};
+
+			for ( var i = 0; i < 10; i += 1 ) loop( i );`
+	},
+
+	{
+		description: 'transpiles block scoping inside loops without function bodies',
+
+		input: `
+			for ( let i = 0; i < 10; i += 1 ) {
+				const square = i * i;
+				console.log( square );
+			}`,
+
+		output: `
+			for ( var i = 0; i < 10; i += 1 ) {
+				var square = i * i;
+				console.log( square );
+			}`
+	},
+
+	{
+		description: 'transpiles block-less for loops without block-scoped declarations inside function body',
+
+		input: `
+			for ( let i = 0; i < 10; i += 1 ) console.log( i );`,
+
+		output: `
+			for ( var i = 0; i < 10; i += 1 ) { console.log( i ); }`
+	},
+
+	{
+		description: 'preserves correct `this` and `arguments` inside block scoped loop (#10)',
+
+		input: `
+			for ( let i = 0; i < 10; i += 1 ) {
+				console.log( this, arguments, i );
+				setTimeout( function () {
+					console.log( this, arguments, i );
+				}, i * 100 );
+			}`,
+
+		output: `
+			var arguments$1 = arguments;
+			var this$1 = this;
+
+			var loop = function ( i ) {
+				console.log( this$1, arguments$1, i );
+				setTimeout( function () {
+					console.log( this, arguments, i );
+				}, i * 100 );
+			};
+
+			for ( var i = 0; i < 10; i += 1 ) loop( i );`
+	},
+
+	{
+		description: 'maintains value of for loop variables between iterations (#11)',
+
+		input: `
+			var fns = [];
+
+			for ( let i = 0; i < 10; i += 1 ) {
+				fns.push(function () { return i; });
+				i += 1;
+			}`,
+
+		output: `
+			var fns = [];
+
+			var loop = function ( i$1 ) {
+				fns.push(function () { return i$1; });
+				i$1 += 1;
+
+				i = i$1;
+			};
+
+			for ( var i = 0; i < 10; i += 1 ) loop( i );`
+	},
+
+	{
+		description: 'maintains value of for loop variables between iterations, with conflict (#11)',
+
+		input: `
+			var i = 'conflicting';
+			var fns = [];
+
+			for ( let i = 0; i < 10; i += 1 ) {
+				fns.push(function () { return i; });
+				i += 1;
+			}`,
+
+		output: `
+			var i = 'conflicting';
+			var fns = [];
+
+			var loop = function ( i$2 ) {
+				fns.push(function () { return i$2; });
+				i$2 += 1;
+
+				i$1 = i$2;
+			};
+
+			for ( var i$1 = 0; i$1 < 10; i$1 += 1 ) loop( i$1 );`
+	},
+
+	{
+		description: 'loop variables with UpdateExpresssions between iterations (#150)',
+
+		input: `
+			var fns = [];
+
+			for ( let i = 0, j = 3; i < 10; i += 1 ) {
+				fns.push(function () { return i; });
+				++i;
+				j--;
+			}`,
+
+		output: `
+			var fns = [];
+
+			var loop = function ( i$1, j$1 ) {
+				fns.push(function () { return i$1; });
+				++i$1;
+				j$1--;
+
+				i = i$1;
+				j = j$1;
+			};
+
+			for ( var i = 0, j = 3; i < 10; i += 1 ) loop( i, j );`
+	},
+
+	{
+		description: 'loop variables with UpdateExpresssions between iterations, with conflict (#150)',
+
+		input: `
+			var i = 'conflicting';
+			var fns = [];
+
+			for ( let i = 0; i < 10; i += 1 ) {
+				fns.push(function () { return i; });
+				i++;
+			}`,
+
+		output: `
+			var i = 'conflicting';
+			var fns = [];
+
+			var loop = function ( i$2 ) {
+				fns.push(function () { return i$2; });
+				i$2++;
+
+				i$1 = i$2;
+			};
+
+			for ( var i$1 = 0; i$1 < 10; i$1 += 1 ) loop( i$1 );`
+	},
+
+	{
+		description: 'handles break and continue inside block-scoped loops (#12)',
+
+		input: `
+			function foo () {
+				for ( let i = 0; i < 10; i += 1 ) {
+					if ( i % 2 ) continue;
+					if ( i > 5 ) break;
+					if ( i === 'potato' ) return 'huh?';
+					setTimeout( () => console.log( i ) );
+				}
+			}`,
+
+		output: `
+			function foo () {
+				var loop = function ( i ) {
+					if ( i % 2 ) { return; }
+					if ( i > 5 ) { return 'break'; }
+					if ( i === 'potato' ) { return { v: 'huh?' }; }
+					setTimeout( function () { return console.log( i ); } );
+				};
+
+				for ( var i = 0; i < 10; i += 1 ) {
+					var returned = loop( i );
+
+					if ( returned === 'break' ) break;
+					if ( returned ) return returned.v;
+				}
+			}`
+	},
+
+	{
+		description: 'rewrites for-in loops as functions as necessary',
+
+		input: `
+			for ( let foo in bar ) {
+				setTimeout( function () { console.log( bar[ foo ] ) } );
+			}`,
+
+		output: `
+			var loop = function ( foo ) {
+				setTimeout( function () { console.log( bar[ foo ] ) } );
+			};
+
+			for ( var foo in bar ) loop( foo );`
+	},
+
+	{
+		description: 'allows breaking from for-in loops',
+
+		input: `
+			for ( let foo in bar ) {
+				if ( foo === 'baz' ) break;
+				setTimeout( function () { console.log( bar[ foo ] ) } );
+			}`,
+
+		output: `
+			var loop = function ( foo ) {
+				if ( foo === 'baz' ) { return 'break'; }
+				setTimeout( function () { console.log( bar[ foo ] ) } );
+			};
+
+			for ( var foo in bar ) {
+				var returned = loop( foo );
+
+				if ( returned === 'break' ) break;
+			}`
+	},
+
+	{
+		description: 'transpiles block-less for-in statements',
+		input: `for ( let foo in bar ) baz( foo );`,
+		output: `for ( var foo in bar ) { baz( foo ); }`
+	},
+
+	{
+		description: 'transpiles block-less for-in statements as functions',
+
+		input: `
+			for ( let foo in bar ) setTimeout( function () { log( foo ) } );`,
+
+		output: `
+			var loop = function ( foo ) {
+				setTimeout( function () { log( foo ) } );
+			};
+
+			for ( var foo in bar ) loop( foo );`
+	},
+
+	{
+		description: 'does not incorrectly rename variables declared in for loop head',
+
+		input: `
+			for ( let foo = 0; foo < 10; foo += 1 ) {
+				foo += 1;
+				console.log( foo );
+			}`,
+
+		output: `
+			for ( var foo = 0; foo < 10; foo += 1 ) {
+				foo += 1;
+				console.log( foo );
+			}`
+	},
+
+	{
+		description: 'does not rewrite as function if `transforms.letConst === false`',
+		options: { transforms: { letConst: false } },
+
+		input: `
+			for ( let i = 0; i < 10; i += 1 ) {
+				setTimeout( function () {
+					log( i );
+				}, i * 100 );
+			}`,
+
+		output: `
+			for ( let i = 0; i < 10; i += 1 ) {
+				setTimeout( function () {
+					log( i );
+				}, i * 100 );
+			}`
+	},
+
+	{
+		description: 'calls synthetic loop function with correct argument',
+
+		input: `
+			let i = 999;
+
+			for ( let i = 0; i < 10; i += 1 ) {
+				setTimeout( () => console.log( i ) );
+			}`,
+
+		output: `
+			var i = 999;
+
+			var loop = function ( i ) {
+				setTimeout( function () { return console.log( i ); } );
+			};
+
+			for ( var i$1 = 0; i$1 < 10; i$1 += 1 ) loop( i$1 );`
+	},
+
+	{
+		description: 'handles body-less do-while loops (#27)',
+		input: `do foo(); while (bar)`,
+		output: `do { foo(); } while (bar)`
+	},
+
+	{
+		description: 'returns without a value from loop',
+
+		input: `
+			function foo ( x ) {
+				for ( let i = 0; i < x; i += 1 ) {
+					setTimeout( () => {
+						console.log( i );
+					});
+
+					if ( x > 5 ) return;
+				}
+			}`,
+
+		output: `
+			function foo ( x ) {
+				var loop = function ( i ) {
+					setTimeout( function () {
+						console.log( i );
+					});
+
+					if ( x > 5 ) { return {}; }
+				};
+
+				for ( var i = 0; i < x; i += 1 ) {
+					var returned = loop( i );
+
+					if ( returned ) return returned.v;
+				}
+			}`
+	},
+
+	{
+		description: 'supports two compiled loops in one function',
+
+		input: `
+			function foo ( x ) {
+				for ( let i = 0; i < x; i += 1 ) {
+					setTimeout( () => {
+						console.log( i );
+					});
+
+					if ( x > 5 ) return;
+				}
+
+				for ( let i = 0; i < x; i += 1 ) {
+					setTimeout( () => {
+						console.log( i );
+					});
+
+					if ( x > 5 ) return;
+				}
+			}`,
+
+		output: `
+			function foo ( x ) {
+				var loop = function ( i ) {
+					setTimeout( function () {
+						console.log( i );
+					});
+
+					if ( x > 5 ) { return {}; }
+				};
+
+				for ( var i = 0; i < x; i += 1 ) {
+					var returned = loop( i );
+
+					if ( returned ) return returned.v;
+				}
+
+				var loop$1 = function ( i ) {
+					setTimeout( function () {
+						console.log( i );
+					});
+
+					if ( x > 5 ) { return {}; }
+				};
+
+				for ( var i$1 = 0; i$1 < x; i$1 += 1 ) {
+					var returned$1 = loop$1( i$1 );
+
+					if ( returned$1 ) return returned$1.v;
+				}
+			}`
+	},
+
+	{
+		description: 'destructures variable declaration in for loop head',
+
+		input: `
+			var range = { start: 10, end: 20 };
+
+			for ( var { start: i, end } = range; i < end; i += 1 ) {
+				console.log( i );
+			}`,
+
+		output: `
+			var range = { start: 10, end: 20 };
+
+			for ( var i = range.start, end = range.end; i < end; i += 1 ) {
+				console.log( i );
+			}`
+	},
+
+	{
+		description: 'complex destructuring in variable declaration in for loop head',
+
+		input: `
+			var range = function () {
+				return { start: 10, end: 20 };
+			}
+
+			for ( var { start: i, end = 100 } = range(); i < end; i += 1 ) {
+				console.log( i );
+			}`,
+
+		output: `
+			var range = function () {
+				return { start: 10, end: 20 };
+			}
+
+			for ( var ref = range(), i = ref.start, end = ref.end === undefined ? 100 : ref.end; i < end; i += 1 ) {
+				console.log( i );
+			}`
+	},
+
+	{
+		description: 'arrow functions in block-less for loops in a block-less if/else chain (#110)',
+
+		input: `
+			if (x)
+				for (let i = 0; i < a.length; ++i)
+					(() => { console.log(a[i]); })();
+			else if (y)
+				for (let i = 0; i < b.length; ++i)
+					(() => { console.log(b[i]); })();
+			else
+				for (let i = 0; i < c.length; ++i)
+					(() => { console.log(c[i]); })();
+		`,
+
+		// the indentation is not ideal, but the code is correct...
+		output: `
+			if (x)
+				{ var loop = function ( i ) {
+						(function () { console.log(a[i]); })();
+					};
+
+					for (var i = 0; i < a.length; ++i)
+					loop( i ); }
+			else if (y)
+				{ var loop$1 = function ( i ) {
+						(function () { console.log(b[i]); })();
+					};
+
+					for (var i$1 = 0; i$1 < b.length; ++i$1)
+					loop$1( i$1 ); }
+			else
+				{ var loop$2 = function ( i ) {
+				(function () { console.log(c[i]); })();
+			};
+
+			for (var i$2 = 0; i$2 < c.length; ++i$2)
+					loop$2( i$2 ); }
+		`
+	},
+
+	{
+		description: 'always initialises block-scoped variable in loop (#124)',
+
+		input: `
+			for (let i = 0; i < 10; i++) {
+				let something;
+				if (i % 2) something = true;
+				console.log(something);
+			}`,
+
+		output: `
+			for (var i = 0; i < 10; i++) {
+				var something = (void 0);
+				if (i % 2) { something = true; }
+				console.log(something);
+			}`
+	},
+
+	{
+		description: 'always initialises block-scoped variable in for-of loop (#125)',
+
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for (let a = 0; a < 10; a++) {
+				let j = 1, k;
+				for (let b of c) {
+					let x, y = 2
+					f(b, j, k, x, y)
+				}
+			}
+		`,
+		output: `
+			for (var a = 0; a < 10; a++) {
+				var j = 1, k = (void 0);
+				for (var i = 0, list = c; i < list.length; i += 1) {
+					var b = list[i];
+
+					var x = (void 0), y = 2
+					f(b, j, k, x, y)
+				}
+			}
+		`,
+	},
+
+	{
+		description: 'always initialises block-scoped variable in simple for-of loop (#125)',
+
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			for (let b of c) {
+				let x, y = 2, z;
+				f(b, x++, y++, z++)
+			}
+		`,
+		output: `
+			for (var i = 0, list = c; i < list.length; i += 1) {
+				var b = list[i];
+
+				var x = (void 0), y = 2, z = (void 0);
+				f(b, x++, y++, z++)
+			}
+		`,
+	},
+
+	{
+		description: 'always initialises block-scoped variable in for-in loop',
+
+		input: `
+			for (let k in obj) {
+				var r = 1, s, t;
+				let x, y = 2, z;
+				f(k, r++, s++, t++, x++, y++, z++)
+			}
+		`,
+		output: `
+			for (var k in obj) {
+				var r = 1, s, t;
+				var x = (void 0), y = 2, z = (void 0);
+				f(k, r++, s++, t++, x++, y++, z++)
+			}
+		`,
+	},
+
+	{
+		description: 'use alias for this in right side of nested for-in loop declaration (#142)',
+
+		input: `
+			let arr = [];
+			class Foo {
+				constructor () {
+					this.foo = { a: 1, b: 2 };
+				}
+				do() {
+					for ( let move = 0; move < 5; ++move ) {
+						for ( let id in this.foo )
+							arr.push( id );
+
+						( () => { arr.push( move ); } )( move );
+					}
+					console.log( arr.join( ' ' ) );
+				}
+			}
+			new Foo().do();
+		`,
+		output: `
+			var arr = [];
+			var Foo = function Foo () {
+				this.foo = { a: 1, b: 2 };
+			};
+			Foo.prototype.do = function do$1 () {
+					var this$1 = this;
+
+				var loop = function ( move ) {
+					for ( var id in this$1.foo )
+						{ arr.push( id ); }
+
+					( function () { arr.push( move ); } )( move );
+				};
+
+					for ( var move = 0; move < 5; ++move ) loop( move );
+				console.log( arr.join( ' ' ) );
+			};
+			new Foo().do();
+		`,
+	},
+
+	{
+		description: 'use alias for this in right side of nested for-of loop declaration (#142)',
+
+		options: { transforms: { dangerousForOf: true } },
+
+		input: `
+			let arr = [];
+			class Foo {
+				constructor () {
+					this.foo = [ 9, 7 ];
+				}
+				do() {
+					for ( let move = 0; move < 5; ++move ) {
+						for ( let id of this.foo )
+							arr.push( id );
+
+						( () => { arr.push( move ); } )( move );
+					}
+					console.log( arr.join( ' ' ) );
+				}
+			}
+			new Foo().do();
+		`,
+		output: `
+			var arr = [];
+			var Foo = function Foo () {
+				this.foo = [ 9, 7 ];
+			};
+			Foo.prototype.do = function do$1 () {
+					var this$1 = this;
+
+				var loop = function ( move ) {
+					for ( var i = 0, list = this$1.foo; i < list.length; i += 1 )
+						{
+							var id = list[i];
+
+							arr.push( id );
+						}
+
+					( function () { arr.push( move ); } )( move );
+				};
+
+					for ( var move = 0; move < 5; ++move ) loop( move );
+				console.log( arr.join( ' ' ) );
+			};
+			new Foo().do();
+		`,
+	},
+
+];
diff --git a/test/samples/misc.js b/test/samples/misc.js
new file mode 100644
index 0000000..0fc9ac2
--- /dev/null
+++ b/test/samples/misc.js
@@ -0,0 +1,189 @@
+module.exports = [
+	{
+		description: 'handles empty return',
+		input: `
+			function foo () {
+				return;
+			}`,
+		output: `
+			function foo () {
+				return;
+			}`
+	},
+
+	{
+		description: 'allows break statement inside switch',
+
+		input: `
+			switch ( foo ) {
+				case bar:
+				console.log( 'bar' );
+				break;
+
+				default:
+				console.log( 'default' );
+			}`,
+
+		output: `
+			switch ( foo ) {
+				case bar:
+				console.log( 'bar' );
+				break;
+
+				default:
+				console.log( 'default' );
+			}`
+	},
+
+	{
+		description: 'double var is okay',
+
+		input: `
+			function foo () {
+				var x = 1;
+				var x = 2;
+			}`,
+
+		output: `
+			function foo () {
+				var x = 1;
+				var x = 2;
+			}`
+	},
+
+	{
+		description: 'var followed by let is not okay',
+
+		input: `
+			function foo () {
+				var x = 1;
+				let x = 2;
+			}`,
+
+		error: /x is already declared/
+	},
+
+	{
+		description: 'let followed by var is not okay',
+
+		input: `
+			function foo () {
+				let x = 1;
+				var x = 2;
+			}`,
+
+		error: /x is already declared/
+	},
+
+	{
+		description: 'does not get confused about keys of Literal node',
+
+		input: `
+			console.log( null );
+			console.log( 'some string' );
+			console.log( null );`,
+
+		output: `
+			console.log( null );
+			console.log( 'some string' );
+			console.log( null );`
+	},
+
+	{
+		description: 'handles sparse arrays (#62)',
+		input: `var a = [ , 1 ], b = [ 1, ], c = [ 1, , 2 ], d = [ 1, , , ];`,
+		output: `var a = [ , 1 ], b = [ 1 ], c = [ 1, , 2 ], d = [ 1, , , ];`
+	},
+
+	{
+		description: 'Safari/WebKit bug workaround: parameter shadowing function expression name (#154)',
+
+		input: `
+			"use strict"; // necessary to trigger WebKit bug
+
+			class Foo {
+				bar (bar) {
+					return bar;
+				}
+				static baz (foo, bar, baz) {
+					return foo * baz - baz * bar;
+				}
+			}
+
+			var a = class Bar {
+				b (a, b, c) {
+					return a * b - c * b + b$1 - b$2;
+				}
+			};
+
+			var b = class {
+				b (a, b, c) {
+					return a * b - c * b;
+				}
+			};
+
+			var c = {
+				b (a, b, c) {
+					return a * b - c * b;
+				}
+			};
+
+			var d = function foo(foo) {
+				return foo;
+			};
+
+			// FunctionDeclaration is not subject to the WebKit bug
+			function bar(bar) {
+				return bar;
+			}
+		`,
+		output: `
+			"use strict"; // necessary to trigger WebKit bug
+
+			var Foo = function Foo () {};
+
+			Foo.prototype.bar = function bar (bar$1) {
+				return bar$1;
+			};
+			Foo.baz = function baz (foo, bar, baz$1) {
+				return foo * baz$1 - baz$1 * bar;
+			};
+
+			var a = (function () {
+				function Bar () {}
+
+				Bar.prototype.b = function b (a, b$3, c) {
+					return a * b$3 - c * b$3 + b$1 - b$2;
+				};
+
+				return Bar;
+			}());
+
+			var b = (function () {
+				function b () {}
+
+				b.prototype.b = function b (a, b$1, c) {
+					return a * b$1 - c * b$1;
+				};
+
+				return b;
+			}());
+
+			var c = {
+				b: function b (a, b$1, c) {
+					return a * b$1 - c * b$1;
+				}
+			};
+
+			var d = function foo(foo$1) {
+				return foo$1;
+			};
+
+			// FunctionDeclaration is not subject to the WebKit bug
+			function bar(bar) {
+				return bar;
+			}
+		`
+	},
+
+];
diff --git a/test/samples/modules.js b/test/samples/modules.js
new file mode 100644
index 0000000..6e443cb
--- /dev/null
+++ b/test/samples/modules.js
@@ -0,0 +1,34 @@
+module.exports = [
+	{
+		description: 'disallows import statement',
+		input: `import 'foo';`,
+		error: /import is not supported/
+	},
+
+	{
+		description: 'disallows export statement',
+		input: `export { foo };`,
+		error: /export is not supported/
+	},
+
+	{
+		description: 'imports are ignored with `transforms.moduleImport === false`',
+		options: { transforms: { moduleImport: false } },
+		input: `import 'foo';`,
+		output: `import 'foo';`
+	},
+
+	{
+		description: 'exports are ignored with `transforms.moduleExport === false`',
+		options: { transforms: { moduleExport: false } },
+		input: `export { foo };`,
+		output: `export { foo };`
+	},
+
+	{
+		description: 'imports and exports are ignored with `transforms.modules === false`',
+		options: { transforms: { modules: false } },
+		input: `import 'foo'; export { foo };`,
+		output: `import 'foo'; export { foo };`
+	}
+];
diff --git a/test/samples/object-properties-no-named-function-expressions.js b/test/samples/object-properties-no-named-function-expressions.js
new file mode 100644
index 0000000..d3a9910
--- /dev/null
+++ b/test/samples/object-properties-no-named-function-expressions.js
@@ -0,0 +1,118 @@
+module.exports = [
+	{
+		description: 'transpiles shorthand properties',
+		options: { namedFunctionExpressions: false },
+		input: `obj = { x, y }`,
+		output: `obj = { x: x, y: y }`
+	},
+
+	{
+		description: 'transpiles shorthand methods',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			obj = {
+				foo () { return 42; }
+			}`,
+
+		output: `
+			obj = {
+				foo: function () { return 42; }
+			}`
+	},
+
+	{
+		description: 'transpiles shorthand methods with quoted names (#82)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			obj = {
+				'foo-bar' () { return 42; }
+			}`,
+
+		output: `
+			obj = {
+				'foo-bar': function () { return 42; }
+			}`
+	},
+
+	{
+		description: 'transpiles shorthand methods with reserved names (!68)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			obj = {
+				catch () { return 42; }
+			}`,
+
+		output: `
+			obj = {
+				catch: function () { return 42; }
+			}`
+	},
+
+	{
+		description: 'transpiles shorthand methods with numeric or string names (#139)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			obj = {
+				0() {},
+				0b101() {},
+				80() {},
+				.12e3() {},
+				0o753() {},
+				12e34() {},
+				0xFFFF() {},
+				"a string"() {},
+				"var"() {},
+			}`,
+
+		output: `
+			obj = {
+				0: function() {},
+				5: function() {},
+				80: function() {},
+				.12e3: function() {},
+				491: function() {},
+				12e34: function() {},
+				0xFFFF: function() {},
+				"a string": function() {},
+				"var": function() {},
+			}`
+	},
+
+	{
+		description: 'shorthand properties can be disabled with `transforms.conciseMethodProperty === false`',
+		options: { namedFunctionExpressions: false, transforms: { conciseMethodProperty: false } },
+		input: `var obj = { x, y, z () {} }`,
+		output: `var obj = { x, y, z () {} }`
+	},
+
+	{
+		description: 'computed properties can be disabled with `transforms.computedProperty === false`',
+		options: { namedFunctionExpressions: false, transforms: { computedProperty: false } },
+		input: `var obj = { [x]: 'x' }`,
+		output: `var obj = { [x]: 'x' }`
+	},
+
+	{
+		description: 'transpiles computed properties without spacing (#117)',
+		options: { namedFunctionExpressions: false },
+
+		input: `
+			if (1)
+				console.log(JSON.stringify({['com'+'puted']:1,['foo']:2}));
+			else
+				console.log(JSON.stringify({['bar']:3}));
+		`,
+		output: `
+			if (1)
+				{ console.log(JSON.stringify(( obj = {}, obj['com'+'puted'] = 1, obj['foo'] = 2, obj )));
+					var obj; }
+			else
+				{ console.log(JSON.stringify(( obj$1 = {}, obj$1['bar'] = 3, obj$1 )));
+			var obj$1; }
+		`
+	},
+];
diff --git a/test/samples/object-properties.js b/test/samples/object-properties.js
new file mode 100644
index 0000000..4f28c9e
--- /dev/null
+++ b/test/samples/object-properties.js
@@ -0,0 +1,129 @@
+module.exports = [
+	{
+		description: 'transpiles shorthand properties',
+		input: `obj = { x, y }`,
+		output: `obj = { x: x, y: y }`
+	},
+
+	{
+		description: 'transpiles shorthand methods',
+
+		input: `
+			obj = {
+				foo () { return 42; }
+			}`,
+
+		output: `
+			obj = {
+				foo: function foo () { return 42; }
+			}`
+	},
+
+	{
+		description: 'transpiles shorthand methods with quoted names (#82)',
+
+		input: `
+			obj = {
+				'foo-bar' () { return 42; }
+			}`,
+
+		output: `
+			obj = {
+				'foo-bar': function foo_bar () { return 42; }
+			}`
+	},
+
+	{
+		description: 'transpiles shorthand methods with reserved names (!68)',
+
+		input: `
+			obj = {
+				catch () { return 42; }
+			}`,
+
+		output: `
+			obj = {
+				catch: function catch$1 () { return 42; }
+			}`
+	},
+
+	{
+		description: 'transpiles shorthand methods with numeric or string names (#139)',
+
+		input: `
+			obj = {
+				0() {},
+				0b101() {},
+				80() {},
+				.12e3() {},
+				0o753() {},
+				12e34() {},
+				0xFFFF() {},
+				"a string"() {},
+				"var"() {},
+			}`,
+
+		output: `
+			obj = {
+				0: function () {},
+				5: function () {},
+				80: function () {},
+				.12e3: function () {},
+				491: function () {},
+				12e34: function () {},
+				0xFFFF: function () {},
+				"a string": function astring() {},
+				"var": function var$1() {},
+			}`
+	},
+
+	{
+		description: 'shorthand properties can be disabled with `transforms.conciseMethodProperty === false`',
+		options: { transforms: { conciseMethodProperty: false } },
+		input: `var obj = { x, y, z () {} }`,
+		output: `var obj = { x, y, z () {} }`
+	},
+
+	{
+		description: 'computed properties can be disabled with `transforms.computedProperty === false`',
+		options: { transforms: { computedProperty: false } },
+		input: `var obj = { [x]: 'x' }`,
+		output: `var obj = { [x]: 'x' }`
+	},
+
+	{
+		description: 'transpiles computed properties without spacing (#117)',
+
+		input: `
+			if (1)
+				console.log(JSON.stringify({['com'+'puted']:1,['foo']:2}));
+			else
+				console.log(JSON.stringify({['bar']:3}));
+		`,
+		output: `
+			if (1)
+				{ console.log(JSON.stringify(( obj = {}, obj['com'+'puted'] = 1, obj['foo'] = 2, obj )));
+					var obj; }
+			else
+				{ console.log(JSON.stringify(( obj$1 = {}, obj$1['bar'] = 3, obj$1 )));
+			var obj$1; }
+		`
+	},
+
+	{
+		description: 'avoids shadowing free variables with method names (#166)',
+
+		input: `
+			let x = {
+				foo() { return foo },
+				bar() {}
+			}
+		`,
+		output: `
+			var x = {
+				foo: function foo$1() { return foo },
+				bar: function bar() {}
+			}
+		`
+	},
+];
diff --git a/test/samples/object-rest-spread.js b/test/samples/object-rest-spread.js
new file mode 100644
index 0000000..4c01ce0
--- /dev/null
+++ b/test/samples/object-rest-spread.js
@@ -0,0 +1,177 @@
+module.exports = [
+	{
+		description: 'disallows object spread operator',
+		input: 'var obj = {...a};',
+		error: /Object spread operator requires specified objectAssign option with 'Object\.assign' or polyfill helper\./
+	},
+
+	{
+		description: 'transpiles object spread with one object',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var obj = {...a};`,
+		output: `var obj = Object.assign({}, a);`
+	},
+
+	{
+		description: 'transpiles object spread with two objects',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var obj = {...a, ...b};`,
+		output: `var obj = Object.assign({}, a, b);`
+	},
+
+	{
+		description: 'transpiles object rest spread with regular keys in between',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var obj = { ...a, b: 1, c: 2 };`,
+		output: `var obj = Object.assign({}, a, {b: 1, c: 2});`
+	},
+
+	{
+		description: 'transpiles object rest spread mixed',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var obj = { ...a, b: 1, ...d, e};`,
+		output: `var obj = Object.assign({}, a, {b: 1}, d, {e: e});`
+	},
+
+	{
+		description: 'transpiles objects with rest spread with computed property (#144)',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `
+			var a0 = { [ x ] : true , ... y };
+			var a1 = { [ w ] : 0 , [ x ] : true , ... y };
+			var a2 = { v, [ w ] : 0, [ x ] : true , ... y };
+			var a3 = { [ w ] : 0, [ x ] : true };
+			var a4 = { [ w ] : 0 , [ x ] : true , y };
+			var a5 = { k : 9 , [ x ] : true, ... y };
+			var a6 = { ... y, [ x ] : true };
+			var a7 = { ... y, [ w ] : 0, [ x ] : true };
+			var a8 = { k : 9, ... y, [ x ] : true };
+			var a9 = { [ x ] : true , [ y ] : false , [ z ] : 9 };
+			var a10 = { [ x ] : true, ...y, p, ...q };
+			var a11 = { x, [c] : 9 , y };
+			var a12 = { ...b, [c]:3, d:4 };
+		`,
+		output: `
+			var a0 = Object.assign({},  y);
+			a0[ x ] = true;
+			var a1 = Object.assign({},  y);
+			a1[ w ] = 0;
+			a1[ x ] = true;
+			var a2 = Object.assign({}, {v: v} , y);
+			a2[ w ] = 0;
+			a2[ x ] = true;
+			var a3 = {};
+			a3[ w ] = 0;
+			a3[ x ] = true;
+			var a4 = { y: y };
+			a4[ w ] = 0;
+			a4[ x ] = true;
+			var a5 = Object.assign({}, {k : 9}, y);
+			a5[ x ] = true;
+			var a6 = Object.assign({}, y);
+			a6[ x ] = true;
+			var a7 = Object.assign({}, y);
+			a7[ w ] = 0;
+			a7[ x ] = true;
+			var a8 = Object.assign({}, {k : 9}, y);
+			a8[ x ] = true;
+			var a9 = {};
+			a9[ x ] = true;
+			a9[ y ] = false;
+			a9[ z ] = 9;
+			var a10 = Object.assign({},  y, {p: p}, q);
+			a10[ x ] = true;
+			var a11 = { x: x , y: y };
+			a11[c] = 9;
+			var a12 = Object.assign({}, b, {d:4});
+			a12[c] = 3;
+		`
+	},
+
+	{
+		description: 'transpiles inline objects with rest spread with computed property (#144)',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `
+			f0( { [ x ] : true , ... y } );
+			f1( { [ w ] : 0 , [ x ] : true , ... y } );
+			f2( { v, [ w ] : 0, [ x ] : true , ... y } );
+			f3( { [ w ] : 0, [ x ] : true } );
+			f4( { [ w ] : 0 , [ x ] : true , y } );
+			f5( { k : 9 , [ x ] : true, ... y } );
+			f6( { ... y, [ x ] : true } );
+			f7( { ... y, [ w ] : 0, [ x ] : true } );
+			f8( { k : 9, ... y, [ x ] : true } );
+			f9( { [ x ] : true , [ y ] : false , [ z ] : 9 } );
+			f10( { [ x ] : true, ...y, p, ...q } );
+			f11( { x, [c] : 9 , y } );
+			f12({ ...b, [c]:3, d:4 });
+		`,
+		output: `
+			f0( ( obj = Object.assign({},  y), obj[ x ] = true, obj ) );
+			var obj;
+			f1( ( obj$1 = Object.assign({},  y), obj$1[ w ] = 0, obj$1[ x ] = true, obj$1 ) );
+			var obj$1;
+			f2( ( obj$2 = Object.assign({}, {v: v} , y), obj$2[ w ] = 0, obj$2[ x ] = true, obj$2 ) );
+			var obj$2;
+			f3( ( obj$3 = {}, obj$3[ w ] = 0, obj$3[ x ] = true, obj$3 ) );
+			var obj$3;
+			f4( ( obj$4 = { y: y }, obj$4[ w ] = 0, obj$4[ x ] = true, obj$4 ) );
+			var obj$4;
+			f5( ( obj$5 = Object.assign({}, {k : 9}, y), obj$5[ x ] = true, obj$5 ) );
+			var obj$5;
+			f6( ( obj$6 = Object.assign({}, y), obj$6[ x ] = true, obj$6 ) );
+			var obj$6;
+			f7( ( obj$7 = Object.assign({}, y), obj$7[ w ] = 0, obj$7[ x ] = true, obj$7 ) );
+			var obj$7;
+			f8( ( obj$8 = Object.assign({}, {k : 9}, y), obj$8[ x ] = true, obj$8 ) );
+			var obj$8;
+			f9( ( obj$9 = {}, obj$9[ x ] = true, obj$9[ y ] = false, obj$9[ z ] = 9, obj$9 ) );
+			var obj$9;
+			f10( ( obj$10 = Object.assign({},  y, {p: p}, q), obj$10[ x ] = true, obj$10 ) );
+			var obj$10;
+			f11( ( obj$11 = { x: x , y: y }, obj$11[c] = 9, obj$11 ) );
+			var obj$11;
+			f12(( obj$12 = Object.assign({}, b, {d:4}), obj$12[c] = 3, obj$12 ));
+			var obj$12;
+		`
+	},
+
+	{
+		description: 'transpiles object rest spread nested',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `var obj = { ...a, b: 1, dd: {...d, f: 1}, e};`,
+		output: `var obj = Object.assign({}, a, {b: 1, dd: Object.assign({}, d, {f: 1}), e: e});`
+	},
+
+	{
+		description: 'transpiles object rest spread deeply nested',
+		options: {
+			objectAssign: 'Object.assign'
+		},
+		input: `const c = { ...a, b: 1, dd: {...d, f: 1, gg: {h, ...g, ii: {...i}}}, e};`,
+		output: `var c = Object.assign({}, a, {b: 1, dd: Object.assign({}, d, {f: 1, gg: Object.assign({}, {h: h}, g, {ii: Object.assign({}, i)})}), e: e});`
+	},
+
+	{
+		description: 'transpiles object reset spread with custom Object.assign',
+		options: {
+			objectAssign: 'angular.extend'
+		},
+		input: `var obj = { ...a, b: 1, dd: {...d, f: 1}, e};`,
+		output: `var obj = angular.extend({}, a, {b: 1, dd: angular.extend({}, d, {f: 1}), e: e});`
+	}
+];
diff --git a/test/samples/regex.js b/test/samples/regex.js
new file mode 100644
index 0000000..4df10e3
--- /dev/null
+++ b/test/samples/regex.js
@@ -0,0 +1,27 @@
+module.exports = [
+	{
+		description: 'transpiles regex with unicode flag',
+		input: `var regex = /foo.bar/u;`,
+		output: `var regex = /foo(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])bar/;`
+	},
+
+	{
+		description: 'disallows sticky flag in regex literals',
+		input: `var regex = /x/y;`,
+		error: /Regular expression sticky flag is not supported/
+	},
+
+	{
+		description: 'u flag is ignored with `transforms.unicodeRegExp === false`',
+		options: { transforms: { unicodeRegExp: false } },
+		input: `var regex = /x/u;`,
+		output: `var regex = /x/u;`
+	},
+
+	{
+		description: 'y flag is ignored with `transforms.stickyRegExp === false`',
+		options: { transforms: { stickyRegExp: false } },
+		input: `var regex = /x/y;`,
+		output: `var regex = /x/y;`
+	}
+];
diff --git a/test/samples/reserved-properties.js b/test/samples/reserved-properties.js
new file mode 100644
index 0000000..4f6861c
--- /dev/null
+++ b/test/samples/reserved-properties.js
@@ -0,0 +1,27 @@
+module.exports = [
+	{
+		description: 'rewrites member expressions that are reserved words',
+		options: { transforms: { reservedProperties: true } },
+		input: `foo.then + foo.catch`,
+		output: `foo.then + foo['catch']`
+	},
+
+	{
+		description: 'rewrites object literal properties that are reserved words',
+		options: { transforms: { reservedProperties: true } },
+		input: `obj = { then: 1, catch: 2 }`,
+		output: `obj = { then: 1, 'catch': 2 }`
+	},
+
+	{
+		description: 'does not rewrite member expressions by default',
+		input: `foo.then + foo.catch`,
+		output: `foo.then + foo.catch`
+	},
+
+	{
+		description: 'does not rewrite object literal properties by default',
+		input: `obj = { then: 1, catch: 2 }`,
+		output: `obj = { then: 1, catch: 2 }`
+	}
+];
diff --git a/test/samples/rest-parameters.js b/test/samples/rest-parameters.js
new file mode 100644
index 0000000..f8c1b78
--- /dev/null
+++ b/test/samples/rest-parameters.js
@@ -0,0 +1,50 @@
+module.exports = [
+	{
+		description: 'transpiles solo rest parameters',
+
+		input: `
+			function foo ( ...theRest ) {
+				console.log( theRest );
+			}`,
+
+		output: `
+			function foo () {
+				var theRest = [], len = arguments.length;
+				while ( len-- ) theRest[ len ] = arguments[ len ];
+
+				console.log( theRest );
+			}`
+	},
+
+	{
+		description: 'transpiles rest parameters following other parameters',
+
+		input: `
+			function foo ( a, b, c, ...theRest ) {
+				console.log( theRest );
+			}`,
+
+		output: `
+			function foo ( a, b, c ) {
+				var theRest = [], len = arguments.length - 3;
+				while ( len-- > 0 ) theRest[ len ] = arguments[ len + 3 ];
+
+				console.log( theRest );
+			}`
+	},
+
+	{
+		description: 'can be disabled with `transforms.spreadRest === false`',
+		options: { transforms: { spreadRest: false } },
+
+		input: `
+			function foo ( ...list ) {
+				// code goes here
+			}`,
+
+		output: `
+			function foo ( ...list ) {
+				// code goes here
+			}`
+	}
+];
diff --git a/test/samples/spread-operator.js b/test/samples/spread-operator.js
new file mode 100644
index 0000000..fe78410
--- /dev/null
+++ b/test/samples/spread-operator.js
@@ -0,0 +1,564 @@
+module.exports = [
+	{
+		description: 'transpiles a lone spread operator',
+		input: `var clone = [ ...arr ]`,
+		output: `var clone = [].concat( arr )`
+	},
+
+	{
+		description: 'transpiles a spread element in array with trailing comma',
+		input: `var clone = [ ...arr, ];`,
+		output: `var clone = [].concat( arr );`
+	},
+
+	{
+		description: 'transpiles a spread operator with other values',
+		input: `var list = [ a, b, ...remainder ]`,
+		output: `var list = [ a, b ].concat( remainder )` // TODO preserve whitespace conventions
+	},
+
+	{
+		description: 'transpiles a lone spread operator in a method call',
+		input: `var max = Math.max( ...values );`,
+		output: `var max = Math.max.apply( Math, values );`
+	},
+
+	{
+		description: 'transpiles a spread operator in a method call with other arguments',
+		input: `var max = Math.max( 0, ...values );`,
+		output: `var max = Math.max.apply( Math, [ 0 ].concat( values ) );`
+	},
+
+	{
+		description: 'transpiles a spread operator in a method call of an expression',
+
+		input: `
+			( foo || bar ).baz( ...values );`,
+
+		output: `
+			(ref = ( foo || bar )).baz.apply( ref, values );
+			var ref;`
+	},
+
+	{
+		description: 'transpiles a spread operator in a method call of this (#100)',
+
+		input: `
+			function a( args ) {
+				return this.go( ...args );
+			}`,
+		output: `
+			function a( args ) {
+				return (ref = this).go.apply( ref, args );
+				var ref;
+			}`
+	},
+
+	{
+		description: 'transpiles a spread operator in a call in an arrow function using this (#115)',
+
+		input: `
+			function foo(...args) {
+				return Domain.run(() => {
+					return this.go(...args);
+				});
+			}
+			function bar(args) {
+				return Domain.run(() => {
+					return this.go(...args);
+				});
+			}
+			function baz() {
+				return Domain.run(() => {
+					return this.go(...arguments);
+				});
+			}
+		`,
+		output: `
+			function foo() {
+				var this$1 = this;
+				var args = [], len = arguments.length;
+				while ( len-- ) args[ len ] = arguments[ len ];
+
+				return Domain.run(function () {
+					return (ref = this$1).go.apply(ref, args);
+					var ref;
+				});
+			}
+			function bar(args) {
+				var this$1 = this;
+
+				return Domain.run(function () {
+					return (ref = this$1).go.apply(ref, args);
+					var ref;
+				});
+			}
+			function baz() {
+				var arguments$1 = arguments;
+				var this$1 = this;
+
+				return Domain.run(function () {
+					return (ref = this$1).go.apply(ref, arguments$1);
+					var ref;
+				});
+			}
+		`
+	},
+
+	{
+		description: 'transpiles a spread operator in a new call in an arrow function using this',
+
+		input: `
+			function foo(...args) {
+				return Domain.run(() => {
+					return new this.Test(...args);
+				});
+			}
+			function bar(args) {
+				return Domain.run(() => {
+					return new this.Test(...args);
+				});
+			}
+			function baz() {
+				return Domain.run(() => {
+					return new this.Test(...arguments);
+				});
+			}
+		`,
+		output: `
+			function foo() {
+				var this$1 = this;
+				var args = [], len = arguments.length;
+				while ( len-- ) args[ len ] = arguments[ len ];
+
+				return Domain.run(function () {
+					return new (Function.prototype.bind.apply( this$1.Test, [ null ].concat( args) ));
+				});
+			}
+			function bar(args) {
+				var this$1 = this;
+
+				return Domain.run(function () {
+					return new (Function.prototype.bind.apply( this$1.Test, [ null ].concat( args) ));
+				});
+			}
+			function baz() {
+				var arguments$1 = arguments;
+				var this$1 = this;
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				return Domain.run(function () {
+					return new (Function.prototype.bind.apply( this$1.Test, [ null ].concat( arguments$1) ));
+				});
+			}
+		`
+	},
+
+	{
+		description: 'transpiles a spread operator in an expression method call within an if',
+
+		input: `
+			var result;
+			if ( ref )
+				result = expr().baz( ...values );
+			process( result );`,
+
+		output: `
+			var result;
+			if ( ref )
+				{ result = (ref$1 = expr()).baz.apply( ref$1, values ); }
+			process( result );
+			var ref$1;`
+	},
+
+	{
+		description: 'transpiles spread operators in expression method calls within a function',
+
+		input: `
+			function foo() {
+				stuff();
+				if ( ref )
+					return expr().baz( ...values );
+				return (up || down).bar( ...values );
+			}`,
+		output: `
+			function foo() {
+				stuff();
+				if ( ref )
+					{ return (ref$1 = expr()).baz.apply( ref$1, values ); }
+				return (ref$2 = (up || down)).bar.apply( ref$2, values );
+				var ref$1;
+				var ref$2;
+			}`
+	},
+
+	{
+		description: 'transpiles spread operators in a complex nested scenario',
+
+		input: `
+			function ref() {
+				stuff();
+				if ( ref$1 )
+					return expr().baz( a, ...values, (up || down).bar( c, ...values, d ) );
+				return other();
+			}`,
+		output: `
+			function ref() {
+				stuff();
+				if ( ref$1 )
+					{ return (ref = expr()).baz.apply( ref, [ a ].concat( values, [(ref$2 = (up || down)).bar.apply( ref$2, [ c ].concat( values, [d] ) )] ) ); }
+				return other();
+				var ref;
+				var ref$2;
+			}`
+	},
+
+	{
+		description: 'transpiles spread operators in issue #92',
+
+		input: `
+			var adder = {
+				add(...numbers) {
+					return numbers.reduce((a, b) => a + b, 0)
+				},
+				prepare() {
+					return this.add.bind(this, ...arguments)
+				}
+			}`,
+		output: `
+			var adder = {
+				add: function add() {
+					var numbers = [], len = arguments.length;
+					while ( len-- ) numbers[ len ] = arguments[ len ];
+
+					return numbers.reduce(function (a, b) { return a + b; }, 0)
+				},
+				prepare: function prepare() {
+					var i = arguments.length, argsArray = Array(i);
+					while ( i-- ) argsArray[i] = arguments[i];
+
+					return (ref = this.add).bind.apply(ref, [ this ].concat( argsArray ))
+					var ref;
+				}
+			}`
+	},
+
+	{
+		description: 'transpiles spread operators with template literals (issue #99)',
+		input: 'console.log( `%s ${label}:`, `${color}`, ...args );',
+		output: 'console.log.apply( console, [ ("%s " + label + ":"), ("" + color) ].concat( args ) );'
+	},
+
+	{
+		description: 'transpiles a lone spread operator in a function call',
+		input: `log( ...values );`,
+		output: `log.apply( void 0, values );`
+	},
+
+	{
+		description: 'transpiles a spread operator in a function call with other arguments',
+		input: `sprintf( str, ...values );`,
+		output: `sprintf.apply( void 0, [ str ].concat( values ) );`
+	},
+
+	{
+		description: 'transpiles a spread operator in an expression call',
+		input: `( foo || bar )( ...values );`,
+		output: `( foo || bar ).apply( void 0, values );`
+	},
+
+	{
+		description: 'can be disabled in array expressions `transforms.spreadRest: false`',
+		options: { transforms: { spreadRest: false } },
+		input: `var chars = [ ...string ]`,
+		output: `var chars = [ ...string ]`
+	},
+
+	{
+		description: 'can be disabled in call expressions with `transforms.spreadRest: false`',
+		options: { transforms: { spreadRest: false } },
+		input: `var max = Math.max( ...values );`,
+		output: `var max = Math.max( ...values );`
+	},
+
+	{
+		description: 'transpiles multiple spread operators in an array',
+		input: `var arr = [ ...a, ...b, ...c ];`,
+		output: `var arr = a.concat( b, c );`
+	},
+
+	{
+		description: 'transpiles multiple spread operators in an array with trailing comma',
+		input: `var arr = [ ...a, ...b, ...c, ];`,
+		output: `var arr = a.concat( b, c );`
+	},
+
+	{
+		description: 'transpiles mixture of spread and non-spread elements',
+		input: `var arr = [ ...a, b, ...c, d ];`,
+		output: `var arr = a.concat( [b], c, [d] );`
+	},
+
+	{
+		description: 'transpiles mixture of spread and non-spread elements in array with trailing comma',
+		input: `var arr = [ ...a, b, ...c, d, ];`,
+		output: `var arr = a.concat( [b], c, [d] );`
+	},
+
+	{
+		description: 'transpiles ...arguments',
+
+		input: `
+			function foo () {
+				var args = [ ...arguments ];
+				return args;
+			}`,
+
+		output: `
+			function foo () {
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				var args = [].concat( argsArray );
+				return args;
+			}` // TODO if this is the only use of argsArray, don't bother concating
+	},
+
+	{
+		description: 'transpiles ...arguments in array with trailing comma',
+
+		input: `
+			function foo () {
+				var args = [ ...arguments, ];
+				return args;
+			}`,
+
+		output: `
+			function foo () {
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				var args = [].concat( argsArray );
+				return args;
+			}` // TODO if this is the only use of argsArray, don't bother concating
+	},
+
+	{
+		description: 'transpiles ...arguments in middle of array',
+
+		input: `
+			function foo () {
+				var arr = [ a, ...arguments, b ];
+				return arr;
+			}`,
+
+		output: `
+			function foo () {
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				var arr = [ a ].concat( argsArray, [b] );
+				return arr;
+			}`
+	},
+
+	{
+		description: 'transpiles multiple spread operators in function call',
+		input: `var max = Math.max( ...theseValues, ...thoseValues );`,
+		output: `var max = Math.max.apply( Math, theseValues.concat( thoseValues ) );`
+	},
+
+	{
+		description: 'transpiles mixture of spread and non-spread operators in function call',
+		input: `var max = Math.max( ...a, b, ...c, d );`,
+		output: `var max = Math.max.apply( Math, a.concat( [b], c, [d] ) );`
+	},
+
+	{
+		description: 'transpiles ...arguments in function call',
+
+		input: `
+			function foo () {
+				return Math.max( ...arguments );
+			}`,
+
+		output: `
+			function foo () {
+				return Math.max.apply( Math, arguments );
+			}`
+	},
+
+	{
+		description: 'transpiles ...arguments in middle of function call',
+
+		input: `
+			function foo () {
+				return Math.max( a, ...arguments, b );
+			}`,
+
+		output: `
+			function foo () {
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				return Math.max.apply( Math, [ a ].concat( argsArray, [b] ) );
+			}`
+	},
+
+	{
+		description: 'transpiles new with spread args',
+
+		input: `
+			function Test() {
+				this.a = [...arguments];
+				console.log(JSON.stringify(this.a));
+			}
+			var obj = { Test };
+
+			new Test(...[1, 2]);
+			new obj.Test(...[1, 2]);
+			new (null || obj).Test(...[1, 2]);
+
+			new Test(0, ...[1, 2]);
+			new obj.Test(0, ...[1, 2]);
+			new (null || obj).Test(0, ...[1, 2]);
+
+			new Test(...[1, 2], ...[3, 4], 5);
+			new obj.Test(...[1, 2], ...[3, 4], 5);
+			new (null || obj).Test(...[1, 2], ...[3, 4], 5);
+
+			new Test(...[1, 2], new Test(...[7, 8]), ...[3, 4], 5);
+			new obj.Test(...[1, 2], new Test(...[7, 8]), ...[3, 4], 5);
+			new (null || obj).Test(...[1, 2], new Test(...[7, 8]), ...[3, 4], 5);
+
+			(function () {
+				new Test(...arguments);
+				new obj.Test(...arguments);
+				new (null || obj).Test(...arguments);
+
+				new Test(1, ...arguments);
+				new obj.Test(1, ...arguments);
+				new (null || obj).Test(1, ...arguments);
+			})(7, 8, 9);
+		`,
+		output: `
+			function Test() {
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				this.a = [].concat( argsArray );
+				console.log(JSON.stringify(this.a));
+			}
+			var obj = { Test: Test };
+
+			new (Function.prototype.bind.apply( Test, [ null ].concat( [1, 2]) ));
+			new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1, 2]) ));
+			new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1, 2]) ));
+
+			new (Function.prototype.bind.apply( Test, [ null ].concat( [0], [1, 2]) ));
+			new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [0], [1, 2]) ));
+			new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [0], [1, 2]) ));
+
+			new (Function.prototype.bind.apply( Test, [ null ].concat( [1, 2], [3, 4], [5]) ));
+			new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1, 2], [3, 4], [5]) ));
+			new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1, 2], [3, 4], [5]) ));
+
+			new (Function.prototype.bind.apply( Test, [ null ].concat( [1, 2], [new (Function.prototype.bind.apply( Test, [ null ].concat( [7, 8]) ))], [3, 4], [5]) ));
+			new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1, 2], [new (Function.prototype.bind.apply( Test, [ null ].concat( [7, 8]) ))], [3, 4], [5]) ));
+			new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1, 2], [new (Function.prototype.bind.apply( Test, [ null ].concat( [7, 8]) ))], [3, 4], [5]) ));
+
+			(function () {
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				new (Function.prototype.bind.apply( Test, [ null ].concat( argsArray) ));
+				new (Function.prototype.bind.apply( obj.Test, [ null ].concat( argsArray) ));
+				new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( argsArray) ));
+
+				new (Function.prototype.bind.apply( Test, [ null ].concat( [1], argsArray) ));
+				new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1], argsArray) ));
+				new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1], argsArray) ));
+			})(7, 8, 9);
+		`
+	},
+
+	{
+		description: 'transpiles `new` with spread parameter in an arrow function',
+
+		input: `
+			function foo (x) {
+				if ( x )
+					return ref => new (bar || baz).Test( ref, ...x );
+			}
+		`,
+		output: `
+			function foo (x) {
+				if ( x )
+					{ return function (ref) { return new (Function.prototype.bind.apply( (bar || baz).Test, [ null ].concat( [ref], x ) )); }; }
+			}
+		`
+	},
+
+	{
+		description: 'transpiles a call with spread parameter in an arrow function',
+
+		input: `
+			function foo (x) {
+				if ( x )
+					return ref => (bar || baz).Test( ref, ...x );
+			}
+		`,
+		output: `
+			function foo (x) {
+				if ( x )
+					{ return function (ref) { return (ref$1 = (bar || baz)).Test.apply( ref$1, [ ref ].concat( x ) )
+						var ref$1;; }; }
+			}
+		`
+	},
+
+	{
+		description: 'transpiles `new` with ...arguments in an arrow function',
+
+		input: `
+			function foo (x) {
+				if ( x )
+					return ref => new (bar || baz).Test( ref, ...arguments );
+			}
+		`,
+		output: `
+			function foo (x) {
+				var arguments$1 = arguments;
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				if ( x )
+					{ return function (ref) { return new (Function.prototype.bind.apply( (bar || baz).Test, [ null ].concat( [ref], arguments$1 ) )); }; }
+			}
+		`
+	},
+
+	{
+		description: 'transpiles a call with ...arguments in an arrow function',
+
+		input: `
+			function foo (x) {
+				if ( x )
+					return ref => (bar || baz).Test( ref, ...arguments );
+			}
+		`,
+		output: `
+			function foo (x) {
+				var arguments$1 = arguments;
+				var i = arguments.length, argsArray = Array(i);
+				while ( i-- ) argsArray[i] = arguments[i];
+
+				if ( x )
+					{ return function (ref) { return (ref$1 = (bar || baz)).Test.apply( ref$1, [ ref ].concat( arguments$1 ) )
+						var ref$1;; }; }
+			}
+		`
+	},
+
+];
diff --git a/test/samples/template-strings.js b/test/samples/template-strings.js
new file mode 100644
index 0000000..85ef6e8
--- /dev/null
+++ b/test/samples/template-strings.js
@@ -0,0 +1,106 @@
+module.exports = [
+	{
+		description: 'transpiles an untagged template literal',
+		input: 'var str = `foo${bar}baz`;',
+		output: `var str = "foo" + bar + "baz";`
+	},
+
+	{
+		description: 'handles arbitrary whitespace inside template elements',
+		input: 'var str = `foo${ bar }baz`;',
+		output: `var str = "foo" + bar + "baz";`
+	},
+
+	{
+		description: 'transpiles an untagged template literal containing complex expressions',
+		input: 'var str = `foo${bar + baz}qux`;',
+		output: `var str = "foo" + (bar + baz) + "qux";`
+	},
+
+	{
+		description: 'transpiles a template literal containing single quotes',
+		input: "var singleQuote = `'`;",
+		output: `var singleQuote = "'";`
+	},
+
+	{
+		description: 'transpiles a template literal containing double quotes',
+		input: 'var doubleQuote = `"`;',
+		output: `var doubleQuote = "\\"";`
+	},
+
+	{
+		description: 'does not transpile tagged template literals',
+		input: 'var str = x`y`',
+		error: /Tagged template strings are not supported/
+	},
+
+	{
+		description: 'transpiles tagged template literals with `transforms.dangerousTaggedTemplateString = true`',
+		options: { transforms: { dangerousTaggedTemplateString: true } },
+		input: 'var str = x`y${(() => 42)()}`;',
+		output: `var str = x(["y", ""], (function () { return 42; })());`
+	},
+
+	{
+		description: 'transpiles tagged template literals with `transforms.dangerousTaggedTemplateString = true`',
+		options: { transforms: { dangerousTaggedTemplateString: true } },
+		input: 'var str = x`${(() => 42)()}y`;',
+		output: `var str = x(["", "y"], (function () { return 42; })());`
+	},
+
+	{
+		description: 'parenthesises template strings as necessary',
+		input: 'var str = `x${y}`.toUpperCase();',
+		output: 'var str = ("x" + y).toUpperCase();'
+	},
+
+	{
+		description: 'does not parenthesise plain template strings',
+		input: 'var str = `x`.toUpperCase();',
+		output: 'var str = "x".toUpperCase();'
+	},
+
+	{
+		description: 'does not parenthesise template strings in arithmetic expressions',
+		input: 'var str = `x${y}` + z; var str2 = `x${y}` * z;',
+		output: 'var str = "x" + y + z; var str2 = ("x" + y) * z;'
+	},
+
+	{
+		description: 'can be disabled with `transforms.templateString === false`',
+		options: { transforms: { templateString: false } },
+		input: 'var a = `a`, b = c`b`;',
+		output: 'var a = `a`, b = c`b`;'
+	},
+
+	{
+		description: 'skips leading empty string if possible',
+		input: 'var str = `${a} ${b}`',
+		output: 'var str = a + " " + b'
+	},
+
+	{
+		description: 'includes leading empty string if necessary',
+		input: 'var str = `${a}${b}`',
+		output: 'var str = "" + a + b'
+	},
+
+	{
+		description: 'closes parens if final empty string is omitted',
+		input: 'var str = `1 + 1 = ${1 + 1}`;',
+		output: 'var str = "1 + 1 = " + (1 + 1);'
+	},
+
+	{
+		description: 'allows empty template string',
+		input: 'var str = ``;',
+		output: 'var str = "";'
+	},
+
+	{
+		description: 'concats expression with variable',
+		input: 'var str = `${a + b}${c}`;',
+		output: 'var str = "" + (a + b) + c;'
+	}
+];
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..c6758fc
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,313 @@
+var path = require( 'path' );
+var fs = require( 'fs' );
+var rimraf = require( 'rimraf' );
+var child_process = require( 'child_process' );
+var assert = require( 'assert' );
+var glob = require( 'glob' );
+var SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;
+var getLocation = require( './utils/getLocation.js' );
+var buble = require( '../dist/buble.umd.js' );
+
+require( 'source-map-support' ).install();
+require( 'console-group' ).install();
+
+function equal ( a, b ) {
+	assert.equal( showInvisibles( a ), showInvisibles( b ) );
+}
+
+function showInvisibles ( str ) {
+	return str
+		.replace( /^ +/gm, spaces => repeat( '•', spaces.length ) )
+		.replace( / +$/gm, spaces => repeat( '•', spaces.length ) )
+		.replace( /^\t+/gm, tabs => repeat( '›   ', tabs.length ) )
+		.replace( /\t+$/gm, tabs => repeat( '›   ', tabs.length ) );
+}
+
+function repeat ( str, times ) {
+	var result = '';
+	while ( times-- ) result += str;
+	return result;
+}
+
+const subsetIndex = process.argv.indexOf( '--subset' );
+const subset = ~subsetIndex ? process.argv[ subsetIndex + 1 ].split( ',' ).map( file => `${file}.js` ) : null;
+const subsetFilter = subset ? file => ~subset.indexOf( file ) : () => true;
+
+describe( 'buble', () => {
+	fs.readdirSync( 'test/samples' ).filter( subsetFilter ).forEach( file => {
+		if ( !/\.js$/.test( file ) ) return; // avoid vim .js.swp files
+		var samples = require( './samples/' + file );
+
+		describe( path.basename( file ), () => {
+			samples.forEach( sample => {
+				( sample.solo ? it.only : sample.skip ? it.skip : it )( sample.description, () => {
+					if ( sample.error ) {
+						assert.throws( () => {
+							buble.transform( sample.input, sample.options );
+						}, sample.error );
+					}
+
+					else {
+						equal( buble.transform( sample.input, sample.options  ).code, sample.output );
+					}
+				});
+			});
+		});
+	});
+
+	if ( subset ) return;
+
+	describe( 'cli', () => {
+		fs.readdirSync( 'test/cli' ).forEach( dir => {
+			if ( dir[0] === '.' ) return; // .DS_Store
+
+			it( dir, done => {
+				dir = path.resolve( 'test/cli', dir );
+				rimraf.sync( path.resolve( dir, 'actual' ) );
+				fs.mkdirSync( path.resolve( dir, 'actual' ) );
+
+				var binFile = path.resolve(__dirname, '../bin/buble');
+				var commandFile = path.resolve( dir, 'command.sh' );
+
+				var command = fs.readFileSync( commandFile, 'utf-8' )
+					.replace( 'buble', 'node "' + binFile + '"' );
+				child_process.exec( command, {
+					cwd: dir
+				}, ( err, stdout, stderr ) => {
+					if ( err ) return done( err );
+
+					if ( stdout ) console.log( stdout );
+					if ( stderr ) console.error( stderr );
+
+					function catalogue ( subdir ) {
+						subdir = path.resolve( dir, subdir );
+
+						return glob.sync( '**/*.js?(.map)', { cwd: subdir })
+							.sort()
+							.map( name => {
+								var contents = fs.readFileSync( path.resolve( subdir, name ), 'utf-8' ).trim();
+
+								if ( path.extname( name ) === '.map' ) {
+									contents = JSON.parse( contents );
+								}
+
+								return {
+									name: name,
+									contents: contents
+								};
+							});
+					}
+
+					var expected = catalogue( 'expected' );
+					var actual = catalogue( 'actual' );
+
+					try {
+						assert.deepEqual( actual, expected );
+						done();
+					} catch ( err ) {
+						done( err );
+					}
+				});
+			});
+		});
+	});
+
+	describe( 'errors', () => {
+		it( 'reports the location of a syntax error', () => {
+			var source = `var 42 = nope;`;
+
+			try {
+				buble.transform( source );
+			} catch ( err ) {
+				assert.equal( err.name, 'SyntaxError' );
+				assert.deepEqual( err.loc, { line: 1, column: 4 });
+				assert.equal( err.message, 'Unexpected token (1:4)' );
+				assert.equal( err.snippet, `1 : var 42 = nope;\n        ^` );
+				assert.equal( err.toString(), `SyntaxError: Unexpected token (1:4)\n1 : var 42 = nope;\n        ^` );
+			}
+		});
+
+		it( 'reports the location of a compile error', () => {
+			var source = `const x = 1; x++;`;
+
+			try {
+				buble.transform( source );
+			} catch ( err ) {
+				assert.equal( err.name, 'CompileError' );
+				assert.equal( err.loc.line, 1 );
+				assert.equal( err.loc.column, 13 );
+				assert.equal( err.message, 'x is read-only (1:13)' );
+				assert.equal( err.snippet, `1 : const x = 1; x++;\n                 ^^^` );
+				assert.equal( err.toString(), `CompileError: x is read-only (1:13)\n1 : const x = 1; x++;\n                 ^^^` );
+			}
+		});
+	});
+
+	describe( 'target', () => {
+		it( 'determines necessary transforms for a target environment', () => {
+			var transforms = buble.target({ chrome: 49 });
+
+			assert.ok( transforms.moduleImport );
+			assert.ok( !transforms.arrow );
+		});
+
+		it( 'returns lowest common denominator support info', () => {
+			var transforms = buble.target({ chrome: 49, node: 5 });
+
+			assert.ok( transforms.defaultParameter );
+			assert.ok( !transforms.arrow );
+		});
+
+		it( 'only applies necessary transforms', () => {
+			var source = `
+				const power = ( base, exponent = 2 ) => Math.pow( base, exponent );`;
+
+			var result = buble.transform( source, {
+				target: { chrome: 49, node: 5 }
+			}).code;
+
+			assert.equal( result, `
+				const power = ( base, exponent ) => {
+					if ( exponent === void 0 ) exponent = 2;
+
+					return Math.pow( base, exponent );
+				};` );
+		});
+	});
+
+	describe( 'sourcemaps', () => {
+		it( 'generates a valid sourcemap', () => {
+			var map = buble.transform( '' ).map;
+			assert.equal( map.version, 3 );
+		});
+
+		it( 'uses provided file and source', () => {
+			var map = buble.transform( '', {
+				file: 'output.js',
+				source: 'input.js'
+			}).map;
+
+			assert.equal( map.file, 'output.js' );
+			assert.deepEqual( map.sources, [ 'input.js' ] );
+		});
+
+		it( 'includes content by default', () => {
+			var source = `let { x, y } = foo();`;
+			var map = buble.transform( source ).map;
+
+			assert.deepEqual( map.sourcesContent, [ source ] );
+		});
+
+		it( 'excludes content if requested', () => {
+			var source = `let { x, y } = foo();`;
+			var map = buble.transform( source, {
+				includeContent: false
+			}).map;
+
+			assert.deepEqual( map.sourcesContent, [ null ] );
+		});
+
+		it( 'locates original content', () => {
+			var source = `const add = ( a, b ) => a + b;`;
+			var result = buble.transform( source, {
+				file: 'output.js',
+				source: 'input.js'
+			});
+
+			var smc = new SourceMapConsumer( result.map );
+
+			var location = getLocation( result.code, 'add' );
+			var expected = getLocation( source, 'add' );
+
+			var actual = smc.originalPositionFor( location );
+
+			assert.deepEqual( actual, {
+				line: expected.line,
+				column: expected.column,
+				source: 'input.js',
+				name: null
+			});
+
+			location = getLocation( result.code, 'a +' );
+			expected = getLocation( source, 'a +' );
+
+			actual = smc.originalPositionFor( location );
+
+			assert.deepEqual( actual, {
+				line: expected.line,
+				column: expected.column,
+				source: 'input.js',
+				name: null
+			});
+		});
+
+		it( 'recovers names', () => {
+			var source = `
+				const foo = 1;
+				if ( x ) {
+					const foo = 2;
+				}`;
+
+			var result = buble.transform( source, {
+				file: 'output.js',
+				source: 'input.js'
+			});
+			var smc = new SourceMapConsumer( result.map );
+
+			var location = getLocation( result.code, 'var' );
+			var actual = smc.originalPositionFor( location );
+
+			assert.equal( actual.name, 'const' );
+
+			location = getLocation( result.code, 'var', location.char + 1 );
+			actual = smc.originalPositionFor( location );
+
+			assert.equal( actual.name, 'const' );
+
+			location = getLocation( result.code, 'foo$1', location.char + 1 );
+			actual = smc.originalPositionFor( location );
+
+			assert.equal( actual.name, 'foo' );
+		});
+
+		it( 'handles moved content', () => {
+			var source = `
+				for ( let i = 0; i < 10; i += 1 ) {
+					const square = i * i;
+					setTimeout( function () {
+						log( square );
+					}, i * 100 );
+				}`;
+
+			var result = buble.transform( source, {
+				file: 'output.js',
+				source: 'input.js'
+			});
+			var smc = new SourceMapConsumer( result.map );
+
+			var location = getLocation( result.code, 'i < 10' );
+			var expected = getLocation( source, 'i < 10' );
+
+			var actual = smc.originalPositionFor( location );
+
+			assert.deepEqual( actual, {
+				line: expected.line,
+				column: expected.column,
+				source: 'input.js',
+				name: null
+			});
+
+			location = getLocation( result.code, 'setTimeout' );
+			expected = getLocation( source, 'setTimeout' );
+
+			actual = smc.originalPositionFor( location );
+
+			assert.deepEqual( actual, {
+				line: expected.line,
+				column: expected.column,
+				source: 'input.js',
+				name: null
+			});
+		});
+	});
+});
diff --git a/test/utils/getLocation.js b/test/utils/getLocation.js
new file mode 100644
index 0000000..78742cc
--- /dev/null
+++ b/test/utils/getLocation.js
@@ -0,0 +1,24 @@
+module.exports = function getLocation ( source, search, start ) {
+	if ( typeof search === 'string' ) {
+		search = source.indexOf( search, start );
+	}
+
+	var lines = source.split( '\n' );
+	var len = lines.length;
+
+	var lineStart = 0;
+	var i;
+
+	for ( i = 0; i < len; i += 1 ) {
+		var line = lines[i];
+		var lineEnd =  lineStart + line.length + 1; // +1 for newline
+
+		if ( lineEnd > search ) {
+			return { line: i + 1, column: search - lineStart, char: i };
+		}
+
+		lineStart = lineEnd;
+	}
+
+	throw new Error( 'Could not determine location of character' );
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-buble.git



More information about the Pkg-javascript-commits mailing list