[Pkg-javascript-commits] [node-es6-module-transpiler] 01/09: Imported Upstream version 0.10.0

Julien Puydt julien.puydt at laposte.net
Tue Oct 20 07:14:04 UTC 2015


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

jpuydt-guest pushed a commit to branch master
in repository node-es6-module-transpiler.

commit a279d996c70f1c441f40878f3a704461f7898fe5
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Mon Oct 19 17:31:39 2015 +0200

    Imported Upstream version 0.10.0
---
 .editorconfig                                      |   5 +
 .gitignore                                         |   2 +
 .jshintrc                                          |  27 ++
 .travis.yml                                        |   5 +
 AUTHORS                                            |   1 +
 CHANGELOG.md                                       | 152 ++++++
 LICENSE                                            |  13 +
 README.md                                          | 192 ++++++++
 TRANSITION.md                                      | 168 +++++++
 bin/compile-modules                                |  84 ++++
 lib/browser/capture_stack_trace_polyfill.js        |  75 +++
 lib/browser/fs.js                                  |   4 +
 lib/browser/index.js                               |  20 +
 lib/cli/convert.js                                 | 152 ++++++
 lib/container.js                                   | 255 ++++++++++
 lib/declaration_info.js                            |  82 ++++
 lib/exports.js                                     | 365 ++++++++++++++
 lib/file_resolver.js                               |  89 ++++
 lib/formatters.js                                  |   5 +
 lib/formatters/bundle_formatter.js                 | 497 +++++++++++++++++++
 lib/formatters/commonjs_formatter.js               | 537 +++++++++++++++++++++
 lib/formatters/formatter.js                        | 198 ++++++++
 lib/imports.js                                     | 221 +++++++++
 lib/index.js                                       |  11 +
 lib/module.js                                      | 202 ++++++++
 lib/module_binding_declaration.js                  | 133 +++++
 lib/module_binding_list.js                         | 244 ++++++++++
 lib/module_binding_specifier.js                    | 244 ++++++++++
 lib/replacement.js                                 |  95 ++++
 lib/rewriter.js                                    | 416 ++++++++++++++++
 lib/sorting.js                                     |  45 ++
 lib/utils.js                                       | 134 +++++
 lib/writer.js                                      |  78 +++
 package.json                                       |  60 +++
 test/examples/bare-import/exporter.js              |   3 +
 test/examples/bare-import/importer.js              |   5 +
 test/examples/bindings/exporter.js                 |   7 +
 test/examples/bindings/importer.js                 |   7 +
 test/examples/cycles-defaults/a.js                 |   5 +
 test/examples/cycles-defaults/b.js                 |   5 +
 test/examples/cycles-defaults/importer.js          |   9 +
 test/examples/cycles-immediate/evens.js            |  20 +
 test/examples/cycles-immediate/main.js             |  24 +
 test/examples/cycles-immediate/odds.js             |  18 +
 test/examples/cycles/a.js                          |   9 +
 test/examples/cycles/b.js                          |   9 +
 test/examples/cycles/c.js                          |   9 +
 test/examples/duplicate-import-fails/exporter.js   |   3 +
 test/examples/duplicate-import-fails/importer.js   |   7 +
 .../duplicate-import-specifier-fails/exporter.js   |   3 +
 .../duplicate-import-specifier-fails/importer.js   |   5 +
 .../export-and-import-reference-share-var/first.js |   4 +
 .../second.js                                      |  15 +
 test/examples/export-class-expression/exporter.js  |   3 +
 test/examples/export-class-expression/importer.js  |   5 +
 test/examples/export-class/exporter.js             |   3 +
 test/examples/export-class/importer.js             |   5 +
 test/examples/export-default-class/exporter.js     |   8 +
 test/examples/export-default-class/importer.js     |   6 +
 test/examples/export-default-function/exporter.js  |   5 +
 test/examples/export-default-function/importer.js  |   8 +
 .../export-default-named-function/exporter.js      |   7 +
 .../export-default-named-function/importer.js      |   4 +
 test/examples/export-default/exporter.js           |  16 +
 test/examples/export-default/importer.js           |  12 +
 test/examples/export-from-default/first.js         |   3 +
 test/examples/export-from-default/second.js        |   3 +
 test/examples/export-from-default/third.js         |   4 +
 test/examples/export-from/first.js                 |   3 +
 test/examples/export-from/second.js                |   7 +
 test/examples/export-from/third.js                 |   4 +
 test/examples/export-function/exporter.js          |   6 +
 test/examples/export-function/importer.js          |   4 +
 test/examples/export-list/exporter.js              |  11 +
 test/examples/export-list/importer.js              |   9 +
 test/examples/export-mixins/exporter.js            |   2 +
 test/examples/export-mixins/importer.js            |   4 +
 test/examples/export-named-class/exporter.js       |   4 +
 test/examples/export-named-class/importer.js       |   5 +
 .../export-not-at-top-level-fails/index.js         |   6 +
 test/examples/export-var/exporter.js               |   4 +
 test/examples/export-var/importer.js               |   4 +
 test/examples/import-as/exporter.js                |   5 +
 test/examples/import-as/importer.js                |   7 +
 test/examples/import-chain/first.js                |   3 +
 test/examples/import-chain/second.js               |   4 +
 test/examples/import-chain/third.js                |   4 +
 .../import-not-at-top-level-fails/index.js         |   6 +
 test/examples/module-level-declarations/mod.js     |   8 +
 .../examples/named-function-expression/exporter.js |   1 +
 .../examples/named-function-expression/importer.js |   9 +
 .../namespace-reassign-import-fails/exporter.js    |   3 +
 .../namespace-reassign-import-fails/importer.js    |   6 +
 .../namespace-update-import-fails/exporter.js      |   3 +
 .../namespace-update-import-fails/importer.js      |   6 +
 test/examples/namespaces/exporter.js               |   5 +
 test/examples/namespaces/importer.js               |  13 +
 test/examples/re-export-default-import/first.js    |   5 +
 test/examples/re-export-default-import/second.js   |   3 +
 test/examples/re-export-default-import/third.js    |   4 +
 test/examples/re-export-namespace-import/first.js  |   2 +
 test/examples/re-export-namespace-import/second.js |   2 +
 test/examples/re-export-namespace-import/third.js  |   4 +
 test/examples/reassign-import-fails/exporter.js    |   3 +
 test/examples/reassign-import-fails/importer.js    |  11 +
 .../exporter.js                                    |   3 +
 .../importer.js                                    |  11 +
 test/examples/this-is-global/mod.js                |   8 +
 .../update-expression-of-import-fails/exporter.js  |   3 +
 .../update-expression-of-import-fails/importer.js  |   6 +
 test/runner.js                                     | 250 ++++++++++
 test/support/expected_error.js                     | 116 +++++
 test/support/test_formatter.js                     |  72 +++
 test/support/test_resolver.js                      |  36 ++
 test/unit/container_test.js                        | 120 +++++
 test/unit/mkdirp_test.js                           |  56 +++
 test/unit/module_binding_specifier_test.js         | 110 +++++
 test/unit/rewriter_test.js                         |  30 ++
 test/unit/sorting_test.js                          |  77 +++
 119 files changed, 6158 insertions(+)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..2d0bffa
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+[*.js]
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..32efc03
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+test/results
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..fff515c
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,27 @@
+{
+  "node" : true,
+  "browser" : false,
+  "boss" : true,
+  "curly": false,
+  "debug": false,
+  "devel": false,
+  "eqeqeq": true,
+  "evil": true,
+  "forin": false,
+  "immed": false,
+  "laxbreak": false,
+  "newcap": true,
+  "noarg": true,
+  "noempty": false,
+  "nonew": false,
+  "nomen": false,
+  "onevar": false,
+  "plusplus": false,
+  "regexp": false,
+  "undef": false,
+  "sub": true,
+  "strict": false,
+  "white": false,
+  "eqnull": true,
+  "esnext": true
+}
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3e3b998
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+  - 0.10
+before_install:
+  - npm install -g grunt-cli
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..4ba0c7b
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Brian Donovan <donovan at squareup.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3387059
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,152 @@
+# Release Notes
+
+## v0.9.5 <span class="release-date">(2014-11-14)</span>
+
+* Fix source maps to include directory names as well as file names.
+
+## v0.9.4 <span class="release-date">(2014-11-14)</span>
+
+* Fix re-exporting module objects created by namespace imports (e.g. `import * as foo from 'foo'; export { foo }`).
+
+## v0.9.3 <span class="release-date">(2014-11-03)</span>
+
+* Fix bundle format support for named exports of class declarations.
+
+## v0.9.2 <span class="release-date">(2014-11-03)</span>
+
+* Add support for exporting ES6 class declarations and expressions.
+
+## v0.9.1 <span class="release-date">(2014-11-03)</span>
+
+* Clarify README to indicate that ES6 module syntax is now stable.
+* Add `Container#transform` to get transformed code without writing to the file system.
+
+## v0.9.0 <span class="release-date">(2014-10-22)</span>
+
+* Add support for namespace imports (e.g. `import * as foo from 'foo'`).
+
+## v0.8.3 <span class="release-date">(2014-10-17)</span>
+
+* Fixed internal helper `mkdirpSync` that could cause `Container#write` to fail on some machines.
+
+## v0.8.2 <span class="release-date">(2014-10-15)</span>
+
+* Fixed bundle formatter renaming of function or class declarations in export default. Previously they were not properly renamed to prevent collision with other modules.
+
+## v0.8.1 <span class="release-date">(2014-10-15)</span>
+
+* Fixed bundle formatter rewriting identifiers inside named function expressions if the identifier shared a name with a module-scope binding. For example, in this code sample the `a` in `return a` would be rewritten as `mod$$a` when it should have remained `a` (the [fix](https://github.com/benjamn/ast-types/pull/68) was in ast-types).
+
+```js
+// mod.js
+var a;
+var fn = function f() { var a; return a; };
+```
+
+## v0.8.0 <span class="release-date">(2014-09-30)</span>
+
+* Simplify the CommonJS formatter output to use ES3.
+
+## v0.7.0 <span class="release-date">(2014-09-30)</span>
+
+* Use a common base class for all built-in formatters.
+* Various internal cleanups.
+
+## v0.6.2 <span class="release-date">(2014-08-20)</span>
+
+* Prevent all instances of export reassignment, not just those at the top level.
+
+## v0.6.1 <span class="release-date">(2014-08-20)</span>
+
+* Reference a custom fork of esprima.
+* Allow resolvers to set the `Module#src` property to prevent direct file system access.
+
+## v0.6.0 <span class="release-date">(2014-07-28)</span>
+
+* Add support for source maps.
+* Allow formatters to handle export reassignment.
+* Update recast to v0.6.6.
+
+## v0.5.1 <span class="release-date">(2014-06-25)</span>
+
+* Fix the file list to be included in npm.
+
+## v0.5.0 <span class="release-date">(2014-06-24)</span>
+
+* Completely re-written using [recast](https://github.com/benjamn/recast).
+* Removed YUI support, to be re-added as a plugin.
+* Removed AMD support, to be re-added as a plugin.
+* Added "bundle" formatter for concatenating all modules together.
+* Assert on various invalid module syntax usage.
+
+## v0.4.0 <span class="release-date">(2014-04-03)</span>
+
+* Remove trailing whitespace after AMD define (#93)
+* (**breaking**) Default to anonymous modules for AMD output (use `--infer-name` flag for old behavior). Replaces `--anonymous` flag.
+
+## v0.3.6 <span class="release-date">(2013-12-16)</span>
+
+* Rebuilt & republished to fix regression on quoting `static` property (sorry!)
+
+## v0.3.5 <span class="release-date">(2013-12-01)</span>
+
+* Fixed incorrect module path strings in CJS output (#82)
+
+## v0.3.4 <span class="release-date">(2013-11-19)</span>
+
+* CJS: Build a module object when using `module foo from "foo"` for better forward-compatibility.
+* Added YUI transpiler, lovingly created & maintained by the YUI team
+* Fixed `'static'` keyword not being quoted in Esprima, causing issues in some JS runtimes
+
+## v0.3.3 <span class="release-date">(2013-10-25)</span>
+
+* Fix syntax error in CommonJS output with default imports and `compatFix` option
+
+## v0.3.2 <span class="release-date">(2013-10-18)</span>
+
+* Fixes path resolution on the command line (thanks rpflorence!)
+
+## v0.3.1 <span class="release-date">(2013-10-17)</span>
+
+* Use a working commit for Esprima
+
+## v0.3.0 <span class="release-date">(2013-10-16)</span>
+
+This is a **major, breaking version**. See TRANSITION.md for information on upgrading your code.
+
+* Rewrote the transpiler using Esprima
+* Support default exports and named exports in the same module
+  * Default export now exports to `moduleObject.default`, see TRANSITION.md for details
+* Fixed multiline export parsing
+* Added support for `module` keyword (i.e. `module foo from "foo"`)
+* Added support for `import "foo";` form
+* fixed the `--anonymous` flag with the CLI for recursive transpiling (#20)
+* Removed relative pathing for AMD resolution & direct CoffeeScript transpilation (see TRANSITION.md)
+
+## v0.2.0 <span class="release-date">(2013-07-08)</span>
+
+* added support for default export (`export default jQuery`)
+* added support for default import (`import $ from "jquery"`)
+* added support for re-exporting properties (`export { ajax } from "jquery"`)
+* removed support for `export =` syntax (use `export default`)
+* removed support for `import "jquery" as $` (use `import $ from "jquery"`)
+
+## v0.1.3 <span class="release-date">(2013-06-21)</span>
+
+* fixed: import/export statements within block comments are now ignored
+* added support for `export var foo = …`
+* added support for `export function foo() { … }`
+
+## v0.1.2 <span class="release-date">(2013-03-07)</span>
+
+* use Grunt for building the project
+* fixed: use local variables for imports
+
+## v0.1.1 <span class="release-date">(2013-02-24)</span>
+
+* fixed: add missing `--global` option to CLI
+* documentation and clarifications of examples
+
+## v0.1.0 <span class="release-date">(2013-02-11)</span>
+
+* initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..cabe814
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2014 Square Inc.
+ 
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ 
+    http://www.apache.org/licenses/LICENSE-2.0
+ 
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8657243
--- /dev/null
+++ b/README.md
@@ -0,0 +1,192 @@
+# ES6 Module Transpiler [![Build Status](https://travis-ci.org/esnext/es6-module-transpiler.png)](https://travis-ci.org/esnext/es6-module-transpiler)
+
+ES6 Module Transpiler is an experimental compiler that allows you to write your
+JavaScript using a subset of the ES6 module syntax, and compile it into
+AMD or CommonJS modules.
+
+This compiler provides a way to experiment with ES6 syntax in real world
+scenarios to see how the syntax holds up. It also provides a nicer, more
+declarative way to write AMD (or CommonJS) modules.
+
+See the [CHANGELOG](./CHANGELOG.md) for the latest updates.
+
+## Usage
+
+### Build tools
+
+The easiest way to use the transpiler is from an existing build tool. There
+several plugins developed for different build tools:
+
+* **Grunt:** [grunt-es6-module-transpiler](https://github.com/joefiorini/grunt-es6-module-transpiler), maintained by @joefiorini (not yet compatible with v0.5.x)
+* **Gulp:** [gulp-es6-module-transpiler](https://github.com/ryanseddon/gulp-es6-module-transpiler), maintained by @ryanseddon
+* **Brunch:** [es6-module-transpiler-brunch](https://github.com/gcollazo/es6-module-transpiler-brunch), maintained by @gcollazo *(CommonJS only)* (not yet compatible with v0.5.x)
+* **Broccoli:** [broccoli-es6-concatenator](https://github.com/joliss/broccoli-es6-concatenator), maintained by @joliss (not yet compatible with v0.5.x)
+* **Mimosa:** [mimosa-es6-module-transpiler](https://github.com/dbashford/mimosa-es6-module-transpiler), maintained by @dbashford (not yet compatible with v0.5.x)
+* **AMD Formatter:** [es6-module-transpiler-amd-formatter](https://github.com/caridy/es6-module-transpiler-amd-formatter), maintained by @caridy (compatible with v0.5.x+ only)
+
+### Executable
+
+The transpiler can be used directly from the command line:
+
+```
+$ npm install -g es6-module-transpiler
+$ compile-modules convert foo.js
+```
+
+Here is the basic usage:
+
+```
+compile-modules convert -I lib -o out FILE [FILE…]
+```
+
+### Library
+
+You can also use the transpiler as a library:
+
+```javascript
+var transpiler = require('es6-module-transpiler');
+var Container = transpiler.Container;
+var FileResolver = transpiler.FileResolver;
+var BundleFormatter = transpiler.formatters.bundle;
+
+var container = new Container({
+  resolvers: [new FileResolver(['lib/'])],
+  formatter: new BundleFormatter()
+});
+
+container.getModule('index');
+container.write('out/mylib.js');
+```
+
+## Supported ES6 Module Syntax
+
+### Named Exports
+
+There are two types of exports. *Named exports* like the following:
+
+```javascript
+// foobar.js
+var foo = 'foo', bar = 'bar';
+
+export { foo, bar };
+```
+
+This module has two named exports, `foo` and `bar`.
+
+You can also write this form as:
+
+```javascript
+// foobar.js
+export var foo = 'foo';
+export var bar = 'bar';
+```
+
+Either way, another module can then import your exports like so:
+
+```js
+import { foo, bar } from 'foobar';
+
+console.log(foo);  // 'foo'
+```
+
+### Default Exports
+
+You can also export a *default* export. For example, an ES6ified jQuery might
+look like this:
+
+```javascript
+// jquery.js
+var jQuery = function() {};
+
+jQuery.prototype = {
+  // ...
+};
+
+export default jQuery;
+```
+
+Then, an app that uses jQuery could import it with:
+
+```javascript
+import $ from 'jquery';
+```
+
+The default export of the "jquery" module is now aliased to `$`.
+
+A default export makes the most sense as a module's "main" export, like the
+`jQuery` object in jQuery. You can use default and named exports in parallel.
+
+### Other Syntax
+
+#### `import "foo";`
+
+A "bare import" that doesn't import any identifiers is useful for executing
+side effects in a module. For example:
+
+```js
+// alerter.js
+alert("alert! alert!");
+
+// alertee.js
+import "alerter";  // will pop up alert box
+```
+
+## Compiled Output
+
+### Default Exports
+
+This is super important:
+
+**Default exports bind to an identifier on the module called `default`!**
+
+Internally, the transpiler will use this default identifer when importing, but
+any outside consumer needs to be aware that it should use the `default` key and
+not the module itself. For example, a CommonJS consumer should look like this:
+
+```js
+var $ = require('jquery')['default'];
+```
+
+## Installation
+
+Add this project to your application's package.json by running this:
+
+    $ npm install --save es6-module-transpiler
+
+Or install it globally:
+
+    $ npm install -g es6-module-transpiler
+
+## Acknowledgements
+
+Thanks to [Yehuda Katz](https://twitter.com/wycats) for
+[js_module_transpiler](https://github.com/wycats/js_module_transpiler), the
+library on which this one is based. Thanks to [Dave
+Herman](https://twitter.com/littlecalculist) for his work on ES6 modules.
+Thanks to [Erik Bryn](https://twitter.com/ebryn) for providing the initial push
+to write this library. Thanks to [Domenic
+Denicola](https://twitter.com/domenic), [Jo Liss](https://twitter.com/jo_liss),
+& [Thomas Boyt](https://twitter.com/thomasaboyt) for their efforts to make this
+project even better. And finally thanks to the JavaScript community at Square
+for helping to write and release this library.
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Add some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
+
+Any contributors to the master es6-module-transpiler repository must sign the
+[Individual Contributor License Agreement (CLA)][cla].  It's a short form that
+covers our bases and makes sure you're eligible to contribute.
+
+[cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
+
+When you have a change you'd like to see in the master repository, [send a pull
+request](https://github.com/esnext/es6-module-transpiler/pulls). Before we merge
+your request, we'll make sure you're in the list of people who have signed a
+CLA.
+
+Thanks, and enjoy living in the ES6 future!
diff --git a/TRANSITION.md b/TRANSITION.md
new file mode 100644
index 0000000..0a6ebc8
--- /dev/null
+++ b/TRANSITION.md
@@ -0,0 +1,168 @@
+# Transitioning from 0.4.x to 0.5.x
+
+## API Changes
+
+The API has completely changed. We still allow transpiling to CommonJS, but any
+of the other previously supported formats have been removed. Each output format
+is handled by a "formatter", and 3rd-party formatters may be used by using the
+`--format` option in the CLI or initialing a `Container` with a particular
+`formatter` when using as a library.
+
+## Command-line changes
+
+The transpiler still has a CLI, but it is structured completely differently.
+See `compile-modules -h` for details.
+
+## Spec compilance
+
+### Bindings
+
+In order to comply with the spec, this project now supports mutable bindings.
+For example, given this:
+
+```js
+export var count = 0;
+export function incr() { count++; }
+```
+
+And when it's imported, this will work:
+
+```js
+import { count, incr } from './count';
+assert.equal(count, 0);
+incr();
+assert.equal(count, 1);
+```
+
+### Circular References
+
+Cycles now work properly. Note that not all cases of cycles can be properly
+handled - this is simply the nature of cycles. For example, this works:
+
+```js
+// a.js
+import { b } from './b';
+
+export function a(n) {
+  if (n % 2 === 0) {
+    return b(n);
+  } else {
+    return n + 1;
+  }
+}
+
+// b.js
+import { a } from './a';
+
+export function b(n) {
+  if (n % 2 === 0) {
+    return n;
+  } else {
+    return a(n);
+  }
+}
+```
+
+This works because neither `a` nor `b` uses the other until sometime "later".
+This second example will not work:
+
+```js
+// a.js
+import { b } from './b';
+export var a = b;
+
+// b.js
+import { a } from './a';
+export var b = a;
+```
+
+This is a contrived example, obviously, but many more complicated examples
+boil down to this same thing.
+
+# Transitioning from 0.2.x to 0.3.x
+
+## default export changes
+
+### Spec changes
+
+`export default foo;` has been removed in favor of `export default = foo`.
+
+### Internal changes
+
+In 0.2.x, the default export was the module's only export. Now, it's internally a named export called `default`:
+
+```js
+// es6
+export default bar;
+
+//cjs
+exports.default = bar;
+
+// es6
+import bar from "bar";
+
+// cjs
+var bar = require("bar").default;
+```
+
+This means that your "entry point" - anywhere you're importing the transpiled output from AMD or CJS - needs to explicitly import `default`. For example, if your AMD app was using ES6-built Handlebars, you would need to do:
+
+```js
+define("my-module",
+  ["handlebars"],
+  function (handlebars) {
+    var handlebars = handlebars.default;
+  })
+```
+
+## New features you should use
+
+* Multi line exports!
+
+```js
+export default = {
+  foo: "\n to your heart's content!"
+};
+```
+
+* Module keyword!
+
+```js
+module foo from "foo";
+var namedExport = foo.namedExport;
+```
+
+* Mixed default/named exports!
+
+```js
+export default = "foo";
+export var bar = "bar";
+```
+
+* Bare imports!
+
+```js
+// executes side effects in "foo" but doesn't import anything
+import "foo";
+```
+
+## Old features you can no longer use
+
+* Relative pathing in AMD modules has been removed, as it was broken, hacky, and made people sad.
+
+* CoffeeScript support is removed, sort of.
+
+If you're using **the original CoffeeScript compiler**, you can use JS passthrough:
+
+```coffeescript
+foo = "look at this elegant coffeescript!"
+
+`
+// now we're in sad curly-brace land
+export default = foo;
+`
+```
+
+You'll then want to transpile your modules using the compiled JS as a base.
+
+Unfortunately, **this doesn't work with CoffeeScript Redux (or EmberScript)**, because that compiler wraps pass-through in an `eval` call.
diff --git a/bin/compile-modules b/bin/compile-modules
new file mode 100755
index 0000000..9c3f3d0
--- /dev/null
+++ b/bin/compile-modules
@@ -0,0 +1,84 @@
+#!/usr/bin/env node
+
+var Path = require('path');
+var exe = Path.basename(process.argv[1]);
+var getopt = require('posix-getopt');
+var parser = new getopt.BasicParser('h(help)v(version)', process.argv);
+var option;
+
+function usage(puts) {
+  puts(exe + ' [--help] [--version] <command> [<args>]');
+  puts();
+  puts('Commands');
+  puts();
+  puts('  convert  Converts modules from `import`/`export` to an ES5 equivalent.');
+  puts('  help     Display help for a given command.');
+}
+
+function makeWriteLine(stream) {
+  return function(line) {
+    if (!line || line[line.length - 1] !== '\n') {
+      line = (line || '') + '\n';
+    }
+    stream.write(line);
+  };
+}
+
+var puts = makeWriteLine(process.stdout);
+var eputs = makeWriteLine(process.stderr);
+
+while ((option = parser.getopt()) !== undefined) {
+  if (option.error) {
+    usage(eputs);
+    process.exit(1);
+  }
+
+  switch (option.option) {
+    case 'h':
+      usage(puts);
+      process.exit(0);
+      break;
+
+    case 'v':
+      puts(exe + ' v' + require(Path.join(__dirname, '../package.json')).version);
+      process.exit(0);
+      break;
+  }
+}
+
+var args = process.argv;
+var offset = parser.optind();
+
+var commandName = args[offset];
+if (commandName === 'help') {
+  commandName = args[offset + 1];
+  args = ['--help'].concat(args.slice(offset + 2 /* skip 'help' and command name */));
+} else {
+  args = args.slice(offset + 1);
+}
+
+if (typeof commandName !== 'string') {
+  usage(puts);
+  process.exit(1);
+}
+
+var command;
+
+try {
+  command = require(Path.join('../lib/cli', commandName));
+} catch (ex) {
+  usage(eputs);
+  process.exit(1);
+}
+
+try {
+  var exitCode = command.run(args, puts, eputs);
+  process.exit(exitCode);
+} catch (ex) {
+  if (ex.constructor.name === 'AssertionError') {
+    eputs('error: ' + exe + ' ' + commandName + ' -- ' + ex.message);
+    process.exit(1);
+  } else {
+    throw ex;
+  }
+}
diff --git a/lib/browser/capture_stack_trace_polyfill.js b/lib/browser/capture_stack_trace_polyfill.js
new file mode 100644
index 0000000..f8b2a74
--- /dev/null
+++ b/lib/browser/capture_stack_trace_polyfill.js
@@ -0,0 +1,75 @@
+var DEFAULT_STACK_TRACE_LIMIT = 10;
+
+// Polyfill Error.captureStackTrace, which exists only in v8 (Chrome). This is
+// used in depd, which is used by ast-types.
+if (!Error.captureStackTrace) {
+  /**
+   * Adds a 'stack' property to the given object with stack info.
+   *
+   * @param obj
+   * @returns {Error.stack|*}
+   */
+  Error.captureStackTrace = function(obj) {
+    var stack = new Error().stack;
+    var prepare = Error.prepareStackTrace;
+
+    if (prepare) {
+      stack = prepare(stack, parseStack(stack));
+    }
+
+    obj.stack = stack;
+  };
+}
+
+if (typeof Error.stackTraceLimit === 'undefined') {
+  Error.stackTraceLimit = DEFAULT_STACK_TRACE_LIMIT;
+}
+
+/**
+ * Parse a formatted stack string into an array of call sites.
+ *
+ * @param {string} stack
+ * @returns {Array.<CallSite>}
+ */
+function parseStack(stack) {
+  return stack.split('\n').slice(0, Error.stackTraceLimit).map(CallSite.parse);
+}
+
+/**
+ * Represents a call site in a stack trace.
+ *
+ * @param {string} fnName
+ * @param {string} fileName
+ * @param {number} lineNumber
+ * @param {number} columnNumber
+ * @param {boolean} isEval
+ * @param {string} evalOrigin
+ * @constructor
+ */
+function CallSite(fnName, fileName, lineNumber, columnNumber, isEval, evalOrigin) {
+  this.getFunctionName = function() { return fnName; };
+  this.getFileName = function() { return fileName; };
+  this.getLineNumber = function() { return lineNumber; };
+  this.getColumnNumber = function() { return columnNumber; };
+  this.isEval = function() { return isEval; };
+  this.getEvalOrigin = function() { return evalOrigin; };
+}
+
+/**
+ * Parses a line in a formatted stack trace and returns call site info.
+ *
+ * @param {string} stackTraceLine
+ * @returns {CallSite}
+ */
+CallSite.parse = function(stackTraceLine) {
+  var fnNameAndLocation = stackTraceLine.split('@');
+  var fnName = fnNameAndLocation[0];
+  var location = fnNameAndLocation[1];
+
+  var fileAndLineAndColumn = location ? location.split(':') : [];
+  var fileName = fileAndLineAndColumn[0];
+  var lineNumber = parseInt(fileAndLineAndColumn[1], 10);
+  var columnNumber = parseInt(fileAndLineAndColumn[2], 10);
+
+  return new CallSite(fnName, fileName, lineNumber, columnNumber, fnName === 'eval', '');
+};
diff --git a/lib/browser/fs.js b/lib/browser/fs.js
new file mode 100644
index 0000000..7492fff
--- /dev/null
+++ b/lib/browser/fs.js
@@ -0,0 +1,4 @@
+var Fs = require('fake-fs');
+var fs = new Fs();
+fs.patch();
+module.exports = fs;
diff --git a/lib/browser/index.js b/lib/browser/index.js
new file mode 100644
index 0000000..9bac1f7
--- /dev/null
+++ b/lib/browser/index.js
@@ -0,0 +1,20 @@
+// Polyfill process.umask() since browserify doesn't add it (yet).
+// Remove it once https://github.com/defunctzombie/node-process/pull/22 is
+// merged and included in browserify.
+process.umask = function() { return 0; };
+
+require('./capture_stack_trace_polyfill');
+
+var Container = require('../container');
+var FileResolver = require('../file_resolver');
+var formatters = require('../formatters');
+var Module = require('../module');
+
+exports.FileResolver = FileResolver;
+exports.Container = Container;
+exports.formatters = formatters;
+exports.Module = Module;
+
+// Export the fake filesystem so someone can get stuff in and out.
+exports.fs = require('./fs');
+
diff --git a/lib/cli/convert.js b/lib/cli/convert.js
new file mode 100644
index 0000000..d264587
--- /dev/null
+++ b/lib/cli/convert.js
@@ -0,0 +1,152 @@
+/* jshint node:true, undef:true, unused:true */
+
+var fs = require('fs');
+var assert = require('assert');
+var Path = require('path');
+var recast = require('recast');
+
+var formatters = require('../formatters');
+var Container = require('../container');
+var FileResolver = require('../file_resolver');
+
+var getopt = require('posix-getopt');
+var exe = Path.basename(process.argv[1]);
+
+exports.run = function(args, puts, eputs) {
+  var offset = 0;
+
+  var files = [];
+  var includePaths = [process.cwd()];
+  var output;
+  var formatter = formatters[formatters.DEFAULT];
+  var resolverClasses = [FileResolver];
+
+  while (offset < args.length) {
+    var parser = new getopt.BasicParser('h(help)o:(output)I:(include)f:(format)r:(resolver)', ['', ''].concat(args.slice(offset)));
+    var option;
+
+    while ((option = parser.getopt()) !== undefined) {
+      if (option.error) {
+        usage(eputs);
+        return 1;
+      }
+
+      switch (option.option) {
+        case 'h':
+          usage(puts);
+          return 0;
+
+        case 'o':
+          output = option.optarg;
+          break;
+
+        case 'I':
+          includePaths.push(option.optarg);
+          break;
+
+        case 'f':
+          formatter = formatters[option.optarg];
+          if (!formatter) {
+            try { formatter = require(option.optarg); }
+            catch (ex) {}
+          }
+          if (!formatter) {
+            eputs('Cannot find formatter for ' + option.optarg);
+            usage(eputs);
+            return 1;
+          }
+          break;
+
+        case 'r':
+          try {
+            var resolverPath = option.optarg;
+            if (fs.existsSync(resolverPath)) {
+              resolverClasses.push(require(Path.join(process.cwd(), resolverPath)));
+            } else {
+              resolverClasses.push(require(option.optarg));
+            }
+          }
+          catch (ex) {
+            eputs('Error reading resolver ' + option.optarg + ': ' + ex);
+            usage(eputs);
+            return 1;
+          }
+          break;
+      }
+    }
+
+    for (offset += parser.optind() - 2; args[offset] && args[offset][0] !== '-'; offset++) {
+      files.push(args[offset]);
+    }
+  }
+
+  assert.ok(
+    files.length > 0,
+    'Please provide at least one file to convert.'
+  );
+
+  if (typeof formatter === 'function') {
+    formatter = new formatter();
+  }
+
+  var resolvers = resolverClasses.map(function(resolverClass) {
+    return new resolverClass(includePaths);
+  });
+  var container = new Container({
+    formatter: formatter,
+    resolvers: resolvers
+  });
+
+  files.forEach(function(file) {
+    container.getModule(file);
+  });
+
+  if (output) {
+    container.write(output);
+  } else {
+    var outputs = container.convert();
+    assert.equal(
+      outputs.length, 1,
+      'Cannot output ' + outputs.length + ' files to stdout. ' +
+      'Please use the --output flag to specify where to put the ' +
+      'files or choose a formatter that concatenates.'
+    );
+    process.stdout.write(recast.print(outputs[0]).code);
+  }
+
+};
+
+function bold(string) {
+  return '\x1b[01m' + string + '\x1b[0m';
+}
+
+function usage(puts) {
+  puts(exe + ' convert [-I <path>] [-o <path>] [-f <path|name>] [-r <path>] <path> [<path> ...]');
+  puts();
+  puts(bold('Description'));
+  puts();
+  puts('  Converts the given modules by changing `import`/`export` statements to an ES5 equivalent.');
+  puts();
+  puts(bold('Options'));
+  puts();
+  puts('  -I, --include <path>      Check the given path for imported modules (usable multiple times).');
+  puts('  -o, --output <path>       File or directory to output converted files.');
+  puts('  -f, --format <path|name>  Path to custom formatter or choose from built-in formats.');
+  puts('  -r, --resolver <path>     Path to custom resolver (usable multiple times).');
+  puts('  -h, --help                Show this help message.');
+  puts();
+  puts(bold('Formats'));
+  puts();
+  puts('  commonjs - convert modules to files using CommonJS `require` and `exports` objects.');
+  puts('  bundle - concatenate modules into a single file.');
+  puts();
+  puts('  You may provide custom a formatter by passing the path to your module to the `--format` option. See the');
+  puts('  source of any of the built-in formatters for details on how to build your own.');
+  puts();
+  puts(bold('Resolvers'));
+  puts();
+  puts('  Resolvers resolve import paths to modules. The default resolver will search the include paths provided');
+  puts('  by `--include` arguments and the current working directory. To provide custom resolver logic, pass the');
+  puts('  path to your resolver module providing a `resolveModule` function or class with an instance method with');
+  puts('  this signature: `resolveModule(importedPath:String, fromModule:?Module, container:Container): Module`.');
+}
diff --git a/lib/container.js b/lib/container.js
new file mode 100644
index 0000000..48bf0eb
--- /dev/null
+++ b/lib/container.js
@@ -0,0 +1,255 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var Path = require('path');
+
+var Rewriter = require('./rewriter');
+var Writer = require('./writer');
+var recast = require('recast');
+
+/** @typedef {{resolveModule: function(string, Module, Container): Module}} */
+var Resolver;
+
+/**
+ * Represents a container of modules for the given options.
+ *
+ * @constructor
+ * @param {{resolvers: Resolver[], formatter: Formatter}} options
+ */
+function Container(options) {
+  options = options || {};
+
+  var formatter = options.formatter;
+  if (typeof formatter === 'function') {
+    formatter = new formatter();
+  }
+
+  assert.ok(
+    formatter,
+    'missing required option `formatter`'
+  );
+
+  var resolvers = options.resolvers;
+
+  assert.ok(
+    resolvers && resolvers.length > 0,
+    'at least one resolver is required'
+  );
+  resolvers.forEach(function(resolver) {
+    assert.equal(
+      typeof resolver.resolveModule, 'function',
+      '`resolver` must have `resolveModule` function: ' + resolver
+    );
+  });
+
+  Object.defineProperties(this, {
+    modules: {
+      value: Object.create(null)
+    },
+
+    formatter: {
+      value: formatter
+    },
+
+    resolvers: {
+      value: resolvers
+    },
+
+    options: {
+      value: options
+    },
+
+    basePath: {
+      value: options.basePath || process.cwd()
+    },
+
+    sourceRoot: {
+      value: options.sourceRoot || '/'
+    }
+  });
+}
+
+/**
+ * Gets a module by resolving `path`. If `path` is resolved to the same path
+ * as a previous call, the same object will be returned.
+ *
+ * @param {string} importedPath
+ * @param {Module=} fromModule
+ * @return {Module}
+ */
+Container.prototype.getModule = function(importedPath, fromModule) {
+  for (var i = 0, length = this.resolvers.length; i < length; i++) {
+    var resolvedModule = this.resolvers[i].resolveModule(
+      importedPath,
+      fromModule,
+      this
+    );
+
+    if (resolvedModule) {
+      this.addModule(resolvedModule);
+      return resolvedModule;
+    }
+  }
+
+  throw new Error(
+    'missing module import' +
+    (fromModule ? ' from ' + fromModule.relativePath : '') +
+    ' for path: ' + importedPath
+  );
+};
+
+/**
+ * Adds a module to the internal cache and gives it a unique name.
+ *
+ * @private
+ * @param {Module} mod
+ */
+Container.prototype.addModule = function(mod) {
+  if (mod.path in this.modules) {
+    return;
+  }
+
+  if (this._convertResult) {
+    throw new Error(
+      'container has already converted contained modules ' +
+      'and cannot add new module: ' + mod.path
+    );
+  }
+
+  // We have not seen this module before, so let's give it a unique name.
+  var modules = this.getModules();
+  var name = mod.name;
+  var baseName = name;
+  var counter = 0;
+  var nameExists;
+
+  while (true) {
+    nameExists = modules.some(function(existingModule) {
+      return existingModule.name === name;
+    });
+
+    if (!nameExists) {
+      break;
+    } else {
+      counter++;
+      name = baseName + counter;
+    }
+  }
+
+  mod.name = name;
+  delete mod.id;
+  this.modules[mod.path] = mod;
+};
+
+/**
+ * Get a cached module by a resolved path.
+ *
+ * @param {string} resolvedPath
+ * @return {?Module}
+ */
+Container.prototype.getCachedModule = function(resolvedPath) {
+  return this.modules[resolvedPath];
+};
+
+/**
+ * Writes the contents of this container to the given path.
+ *
+ * @param {string} target
+ */
+Container.prototype.write = function(target) {
+  if (!this._convertResult) {
+    this._convertResult = this.convert();
+  }
+  var files = this._convertResult;
+  var writer = new Writer(target, {
+    sourceRoot: this.sourceRoot,
+    basePath: this.basePath
+  });
+  writer.write(files);
+};
+
+/**
+ * Translate and return the contents of this container.
+ *
+ * @return {{filename: string, code: string, map: object}[]}
+ */
+Container.prototype.transform = function() {
+  if (!this._convertResult) {
+    this._convertResult = this.convert();
+  }
+
+  var files = this._convertResult;
+  var codes = [];
+
+  files.forEach(function(file) {
+    var rendered = recast.print(file, {
+      sourceMapName: Path.relative(this.basePath, file.filename),
+      sourceRoot: this.sourceRoot
+    });
+    var code = rendered.code;
+    var map = rendered.map;
+
+    codes.push({
+      filename: file.filename,
+      code: code,
+      map: map
+    });
+  }, this);
+
+  return codes;
+};
+
+/**
+ * Converts the contents of this container using the current formatter.
+ *
+ * @return {File[]}
+ */
+Container.prototype.convert = function() {
+  if (this.formatter.beforeConvert) {
+    this.formatter.beforeConvert(this);
+  }
+
+  var modules = this.getModules();
+
+  var rewriter = new Rewriter(this.formatter);
+  rewriter.rewrite(modules);
+
+  var formatter = this.formatter;
+  return formatter.build(modules);
+};
+
+/**
+ * Follows all imports/exports looking for new modules to add to this container.
+ */
+Container.prototype.findImportedModules = function() {
+  var knownModules;
+  var lastModuleCount = 0;
+
+  while ((knownModules = this.getModules()).length !== lastModuleCount) {
+    lastModuleCount = knownModules.length;
+    for (var i = 0; i < lastModuleCount; i++) {
+      // Force loading of imported modules.
+      noop(knownModules[i].imports.modules);
+    }
+  }
+};
+
+/**
+ * Gets the modules in this container in no particular order.
+ *
+ * @return {Module[]}
+ */
+Container.prototype.getModules = function() {
+  var modules = this.modules;
+  return Object.keys(modules).map(function(key) {
+    return modules[key];
+  });
+};
+
+/**
+ * Does nothing. This is only here to make JSHint/other static analysis
+ * tools happy about using getters for side effects.
+ */
+function noop() {}
+
+module.exports = Container;
diff --git a/lib/declaration_info.js b/lib/declaration_info.js
new file mode 100644
index 0000000..c69b415
--- /dev/null
+++ b/lib/declaration_info.js
@@ -0,0 +1,82 @@
+/* jshint node:true, undef:true, unused:true */
+
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+
+/**
+ * Represents information about a declaration that creates a local binding
+ * represented by `identifier`. For example, given that `declaration` is the
+ * following variable declaration:
+ *
+ *   var a = 1;
+ *
+ * Then `identifier` references the `a` node in the variable declaration's
+ * first declarator. Likewise, given that `declaration` is this function
+ * declaration:
+ *
+ *   function add(a, b) {}
+ *
+ * Then `identifier` references the `add` node, the declaration's `id`.
+ *
+ * @constructor
+ * @param {Node} declaration
+ * @param {Identifier} identifier
+ */
+function DeclarationInfo(declaration, identifier) {
+  /**
+   * @type {Node}
+   * @name DeclarationInfo#declaration
+   */
+  this.declaration = declaration;
+  /**
+   * @type {Identifier}
+   * @name DeclarationInfo#identifier
+   */
+  this.identifier = identifier;
+}
+
+/**
+ * Get the declaration info for the given identifier path, if the identifier is
+ * actually part of a declaration.
+ *
+ * @param {NodePath} identifierPath
+ * @return {?DeclarationInfo}
+ */
+DeclarationInfo.forIdentifierPath = function(identifierPath) {
+  if (n.VariableDeclarator.check(identifierPath.parent.node)) {
+    return new DeclarationInfo(
+      identifierPath.parent.parent.node,
+      identifierPath.node
+    );
+  } else if (n.ClassDeclaration.check(identifierPath.parent.node)) {
+    return new DeclarationInfo(
+      identifierPath.parent.node,
+      identifierPath.node
+    );
+  } else if (n.FunctionDeclaration.check(identifierPath.parent.node)) {
+    return new DeclarationInfo(
+      identifierPath.parent.node,
+      identifierPath.node
+    );
+  } else if (n.ImportSpecifier.check(identifierPath.parent.node)) {
+    return new DeclarationInfo(
+      identifierPath.parent.parent.node,
+      identifierPath.node
+    );
+  } else if (n.ImportNamespaceSpecifier.check(identifierPath.parent.node)) {
+    return new DeclarationInfo(
+      identifierPath.parent.parent.node,
+      identifierPath.node
+    );
+  } else if (n.ImportDefaultSpecifier.check(identifierPath.parent.node)) {
+    return new DeclarationInfo(
+      identifierPath.parent.parent.node,
+      identifierPath.node
+    );
+  } else {
+    return null;
+  }
+};
+
+module.exports = DeclarationInfo;
diff --git a/lib/exports.js b/lib/exports.js
new file mode 100644
index 0000000..b35e9c0
--- /dev/null
+++ b/lib/exports.js
@@ -0,0 +1,365 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+
+var ModuleBindingList = require('./module_binding_list');
+var ModuleBindingDeclaration = require('./module_binding_declaration');
+var ModuleBindingSpecifier = require('./module_binding_specifier');
+var DeclarationInfo = require('./declaration_info');
+
+var utils = require('./utils');
+var memo = utils.memo;
+var extend = utils.extend;
+var sourcePosition = utils.sourcePosition;
+
+/**
+ * Represents a list of the exports for the given module.
+ *
+ * @constructor
+ * @extends ModuleBindingList
+ * @param {Module} mod
+ */
+function ExportDeclarationList(mod) {
+  ModuleBindingList.call(this, mod);
+}
+extend(ExportDeclarationList, ModuleBindingList);
+
+/**
+ * @private
+ * @param {AST.Declaration} node
+ * @return {boolean}
+ */
+ExportDeclarationList.prototype.isMatchingBinding = function(node) {
+  return n.ExportDeclaration.check(node);
+};
+
+/**
+ * Gets an export declaration for the given `node`.
+ *
+ * @private
+ * @param {AST.ExportDeclaration} node
+ * @return {ExportDeclaration}
+ */
+ExportDeclarationList.prototype.declarationForNode = function(node) {
+  if (node.default) {
+    return new DefaultExportDeclaration(this.module, node);
+  } else if (n.VariableDeclaration.check(node.declaration)) {
+    return new VariableExportDeclaration(this.module, node);
+  } else if (n.FunctionDeclaration.check(node.declaration)) {
+    return new FunctionExportDeclaration(this.module, node);
+  } else if (n.ClassDeclaration.check(node.declaration)) {
+    return new ClassExportDeclaration(this.module, node);
+  } else if (n.ExportBatchSpecifier.check(node.specifiers[0])) {
+    throw new Error(
+      '`export *` found at ' + sourcePosition(this.module, node) +
+      ' is not supported, please use `export { … }` instead'
+    );
+  } else {
+    return new NamedExportDeclaration(this.module, node);
+  }
+};
+
+/**
+ * @param {NodePath} referencePath
+ * @return {?ExportSpecifier}
+ */
+ExportDeclarationList.prototype.findSpecifierForReference = function(referencePath) {
+  if (n.ExportSpecifier.check(referencePath.parent.node) && referencePath.parent.parent.node.source) {
+    // This is a direct export from another module, e.g. `export { foo } from 'foo'`.
+    return /** @type {ExportSpecifier} */this.findSpecifierByIdentifier(referencePath.node);
+  }
+
+  var declaration = this.findDeclarationForReference(referencePath);
+
+  if (!declaration) {
+    return null;
+  }
+
+  var specifier = /** @type {ExportSpecifier} */this.findSpecifierByName(declaration.node.name);
+  assert.ok(
+    specifier,
+    'no specifier found for `' + referencePath.node.name + '`! this should not happen!'
+  );
+  return specifier;
+};
+
+/**
+ * Contains information about an export declaration.
+ *
+ * @constructor
+ * @abstract
+ * @extends ModuleBindingDeclaration
+ * @param {Module} mod
+ * @param {ExportDeclaration} node
+ */
+function ExportDeclaration(mod, node) {
+  assert.ok(
+    n.ExportDeclaration.check(node),
+    'expected an export declaration, got ' + (node && node.type)
+  );
+
+  ModuleBindingDeclaration.call(this, mod, node);
+}
+extend(ExportDeclaration, ModuleBindingDeclaration);
+
+/**
+ * Returns a string description suitable for debugging.
+ *
+ * @return {string}
+ */
+ExportDeclaration.prototype.inspect = function() {
+  return recast.print(this.node).code;
+};
+
+/**
+ * @see ExportDeclaration#inspect
+ */
+ExportDeclaration.prototype.toString = ExportDeclaration.prototype.inspect;
+
+/**
+ * Represents an export declaration of the form:
+ *
+ *   export default foo;
+ *
+ * @constructor
+ * @extends ExportDeclaration
+ * @param {Module} mod
+ * @param {AST.ExportDeclaration} node
+ */
+function DefaultExportDeclaration(mod, node) {
+  ExportDeclaration.call(this, mod, node);
+}
+extend(DefaultExportDeclaration, ExportDeclaration);
+
+/**
+ * Contains a list of specifier name information for this export.
+ *
+ * @type {ExportSpecifier[]}
+ * @name DefaultExportSpecifier#specifiers
+ */
+memo(DefaultExportDeclaration.prototype, 'specifiers', /** @this DefaultExportDeclaration */function() {
+  var specifier = new DefaultExportSpecifier(this, this.node.declaration);
+  return [specifier];
+});
+
+/**
+ * Represents an export declaration of the form:
+ *
+ *   export { foo, bar };
+ *
+ * @constructor
+ * @extends ExportDeclaration
+ * @param {Module} mod
+ * @param {AST.ExportDeclaration} node
+ */
+function NamedExportDeclaration(mod, node) {
+  ExportDeclaration.call(this, mod, node);
+}
+extend(NamedExportDeclaration, ExportDeclaration);
+
+/**
+ * Contains a list of specifier name information for this export.
+ *
+ * @type {ExportSpecifier[]}
+ * @name NamedExportDeclaration#specifiers
+ */
+memo(NamedExportDeclaration.prototype, 'specifiers', /** @this NamedExportDeclaration */function() {
+  var self = this;
+  return this.node.specifiers.map(function(specifier) {
+    return new ExportSpecifier(self, specifier);
+  });
+});
+
+/**
+ * Represents an export declaration of the form:
+ *
+ *   export var foo = 1;
+ *
+ * @constructor
+ * @extends ExportDeclaration
+ * @param {Module} mod
+ * @param {AST.ExportDeclaration} node
+ */
+function VariableExportDeclaration(mod, node) {
+  ExportDeclaration.call(this, mod, node);
+}
+extend(VariableExportDeclaration, ExportDeclaration);
+
+/**
+ * Gets the list of export specifiers for this declaration.
+ *
+ * @type {ExportSpecifier[]}
+ * @name VariableExportDeclaration#specifiers
+ */
+memo(VariableExportDeclaration.prototype, 'specifiers', /** @this VariableExportDeclaration */function() {
+  var self = this;
+  return this.node.declaration.declarations.map(function(declarator) {
+    return new ExportSpecifier(self, declarator);
+  });
+});
+
+/**
+ * Represents an export declaration of the form:
+ *
+ *   export class Foo {}
+ *
+ * @constructor
+ * @extends ExportDeclaration
+ * @param {Module} mod
+ * @param {AST.ExportDeclaration} node
+ */
+function ClassExportDeclaration(mod, node) {
+  ExportDeclaration.call(this, mod, node);
+}
+extend(ClassExportDeclaration, ExportDeclaration);
+
+/**
+ * Gets the list of export specifiers for this declaration.
+ *
+ * @type {ExportSpecifier[]}
+ * @name ClassExportDeclaration#specifiers
+ */
+memo(ClassExportDeclaration.prototype, 'specifiers', /** @this ClassExportDeclaration */function() {
+  return [new ExportSpecifier(this, this.node.declaration)];
+});
+
+/**
+ * Represents an export declaration of the form:
+ *
+ *   export function foo() {}
+ *
+ * @constructor
+ * @extends ExportDeclaration
+ * @param {Module} mod
+ * @param {AST.ExportDeclaration} node
+ */
+function FunctionExportDeclaration(mod, node) {
+  ExportDeclaration.call(this, mod, node);
+}
+extend(FunctionExportDeclaration, ExportDeclaration);
+
+/**
+ * Gets the list of export specifiers for this declaration.
+ *
+ * @type {ExportSpecifier[]}
+ * @name FunctionExportDeclaration#specifiers
+ */
+memo(FunctionExportDeclaration.prototype, 'specifiers', /** @this FunctionExportDeclaration */function() {
+  return [new ExportSpecifier(this, this.node.declaration)];
+});
+
+/**
+ * Represents an export specifier in an export declaration.
+ *
+ * @constructor
+ * @extends ModuleBindingSpecifier
+ * @param {ExportDeclaration} declaration
+ * @param {AST.Node} node
+ */
+function ExportSpecifier(declaration, node) {
+  ModuleBindingSpecifier.call(this, declaration, node);
+}
+extend(ExportSpecifier, ModuleBindingSpecifier);
+
+/**
+ * Contains the local declaration info for this export specifier. For example,
+ * in this module:
+ *
+ *   var a = 1;
+ *   export { a };
+ *
+ * The module declaration info for the `a` export specifier is the variable
+ * declaration plus the `a` identifier in its first declarator.
+ *
+ * @type {?DeclarationInfo}
+ * @name ExportSpecifier#moduleDeclaration
+ */
+memo(ExportSpecifier.prototype, 'moduleDeclaration', /** @this ExportSpecifier */function() {
+  if (this.declaration.source) {
+    // This is part of a direct export, e.g. `export { ... } from '...'`, so
+    // there is no declaration as part of this module.
+    return null;
+  }
+
+  var bindings = this.moduleScope.getBindings();
+  var identifierPaths = bindings[this.from];
+  assert.ok(
+    identifierPaths && identifierPaths.length === 1,
+    'expected exactly one declaration for export `' +
+    this.from + '` at ' + sourcePosition(this.module, this.node) +
+    ', found ' + (identifierPaths ? identifierPaths.length : 'none')
+  );
+
+  var identifierPath = identifierPaths[0];
+  var declarationInfo = DeclarationInfo.forIdentifierPath(identifierPath);
+  assert.ok(
+    declarationInfo,
+    'cannot detect declaration for `' +
+    identifierPath.node.name + '`, found parent.type `' +
+    identifierPath.parent.node.type + '`'
+  );
+
+  return declarationInfo;
+});
+
+/**
+ * Represents an export specifier in a default export declaration.
+ *
+ * @constructor
+ * @extends ExportSpecifier
+ * @param {ExportDeclaration} declaration
+ * @param {AST.Expression} node
+ */
+function DefaultExportSpecifier(declaration, node) {
+  ExportSpecifier.call(this, declaration, node);
+}
+extend(DefaultExportSpecifier, ExportSpecifier);
+
+/**
+ * The node of a default export specifier is an expression, not a specifier.
+ *
+ * @type {AST.Expression}
+ */
+DefaultExportSpecifier.prototype.node = null;
+
+/**
+ * Default export specifier names are always "default".
+ *
+ * @type {string}
+ * @name DefaultExportSpecifier#name
+ * @default "default"
+ */
+DefaultExportSpecifier.prototype.name = 'default';
+
+/**
+ * Default export specifiers do not bind to a local identifier.
+ *
+ * @type {?Identifier}
+ * @name DefaultExportSpecifier#identifier
+ * @default null
+ */
+DefaultExportSpecifier.prototype.identifier = null;
+
+/**
+ * Default export specifiers do not have a local bound name.
+ *
+ * @type {?string}
+ * @name DefaultExportSpecifier#from
+ * @default null
+ */
+DefaultExportSpecifier.prototype.from = null;
+
+/**
+ * Default export specifiers do not have a local declaration.
+ *
+ * @type {?DeclarationInfo}
+ * @name DefaultExportSpecifier#moduleDeclaration
+ * @default null
+ */
+DefaultExportSpecifier.prototype.moduleDeclaration = null;
+
+module.exports = ExportDeclarationList;
diff --git a/lib/file_resolver.js b/lib/file_resolver.js
new file mode 100644
index 0000000..d2b697d
--- /dev/null
+++ b/lib/file_resolver.js
@@ -0,0 +1,89 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var Path = require('path');
+var fs = require('fs');
+
+var Module = require('./module');
+
+/**
+ * Provides resolution of absolute paths from module import sources.
+ *
+ * @constructor
+ */
+function FileResolver(paths) {
+  assert.ok(
+    paths && paths.length > 0,
+    'missing required argument `paths`'
+  );
+
+  this.paths = paths.map(function(path) {
+    return Path.resolve(path);
+  });
+}
+
+/**
+ * Resolves `importedPath` imported by the given module `fromModule` to a
+ * module.
+ *
+ * @param {string} importedPath
+ * @param {?Module} fromModule
+ * @param {Container} container
+ * @return {?Module}
+ */
+FileResolver.prototype.resolveModule = function(importedPath, fromModule, container) {
+  var resolvedPath = this.resolvePath(importedPath, fromModule);
+  if (resolvedPath) {
+    var cachedModule = container.getCachedModule(resolvedPath);
+    if (cachedModule) {
+      return cachedModule;
+    } else {
+      if (!Path.extname(importedPath)) {
+        importedPath += Path.extname(resolvedPath);
+      }
+      return new Module(resolvedPath, importedPath, container);
+    }
+  } else {
+    return null;
+  }
+};
+
+/**
+ * Resolves `importedPath` against the importing module `fromModule`, if given,
+ * within this resolver's paths.
+ *
+ * @private
+ * @param {string} importedPath
+ * @param {?Module} fromModule
+ * @return {string}
+ */
+FileResolver.prototype.resolvePath = function(importedPath, fromModule) {
+  var paths = this.paths;
+
+  if (importedPath[0] === '.' && fromModule) {
+    paths = [Path.dirname(fromModule.path)];
+  }
+
+  for (var i = 0, length = paths.length; i < length; i++) {
+    var includePath = paths[i];
+    var resolved = Path.resolve(includePath, importedPath);
+    if (!~resolved.lastIndexOf('.')) {
+      resolved += '.js';
+    }
+    
+    if (fs.existsSync(resolved)) {
+      return resolved;
+    }
+
+    // edge cases when a module may have dotted filename, i.e. jquery.min.js
+    // and the module name is passed without the extension
+    resolved += '.js';
+    if (fs.existsSync(resolved)) {
+      return resolved;
+    }
+  }
+
+  return null;
+};
+
+module.exports = FileResolver;
diff --git a/lib/formatters.js b/lib/formatters.js
new file mode 100644
index 0000000..01839b1
--- /dev/null
+++ b/lib/formatters.js
@@ -0,0 +1,5 @@
+/* jshint node:true, undef:true, unused:true */
+
+exports.DEFAULT = 'bundle';
+exports.bundle = require('./formatters/bundle_formatter');
+exports.commonjs = require('./formatters/commonjs_formatter');
diff --git a/lib/formatters/bundle_formatter.js b/lib/formatters/bundle_formatter.js
new file mode 100644
index 0000000..589bd23
--- /dev/null
+++ b/lib/formatters/bundle_formatter.js
@@ -0,0 +1,497 @@
+/* jshint node:true, undef:true, unused:true */
+
+var recast = require('recast');
+var types = recast.types;
+var b = types.builders;
+var n = types.namedTypes;
+
+var Replacement = require('../replacement');
+var utils = require('../utils');
+var IIFE = utils.IIFE;
+var extend = utils.extend;
+var sort = require('../sorting').sort;
+var Formatter = require('./formatter');
+
+
+/**
+ * The 'bundle' formatter aims to increase the compressibility of the generated
+ * source, especially by tools such as Google Closure Compiler or UglifyJS.
+ * For example, given these modules:
+ *
+ *   ```js
+ *   // a.js
+ *   import { b } from './b';
+ *   console.log(b);
+ *
+ *   // b.js
+ *   export var b = 3;
+ *   export var b2 = 6;
+ *   ```
+ *
+ * The final output will be a single file looking something like this:
+ *
+ *   ```js
+ *   (function() {
+ *     "use strict";
+ *     // b.js
+ *     var b$$b = 3;
+ *     var b$$b2 = 6;
+ *
+ *     // a.js
+ *     console.log(b$$b);
+ *   }).call(this);
+ *   ```
+ *
+ * @constructor
+ */
+function BundleFormatter() {
+  Formatter.call(this);
+}
+extend(BundleFormatter, Formatter);
+
+/**
+ * This hook is called by the container before it converts its modules. We use
+ * it to ensure all of the imports are included because we need to know about
+ * them at compile time.
+ *
+ * @param {Container} container
+ */
+BundleFormatter.prototype.beforeConvert = function(container) {
+  container.findImportedModules();
+
+  // Cache all the import and export specifier names.
+  container.getModules().forEach(function(mod) {
+    [mod.imports, mod.exports].forEach(function(bindingList) {
+      bindingList.declarations.forEach(function (declaration) {
+        declaration.specifiers.forEach(function (specifier) {
+          specifier.name;
+        });
+      });
+    });
+  });
+};
+
+/**
+ * @override
+ */
+BundleFormatter.prototype.build = function(modules) {
+  modules = sort(modules);
+  return [b.file(b.program([b.expressionStatement(IIFE(
+    b.expressionStatement(b.literal('use strict')),
+      this.buildNamespaceImportObjects(modules),
+      modules.length === 1 ?
+      modules[0].ast.program.body :
+      modules.reduce(function(statements, mod) {
+        return statements.concat(mod.ast.program.body);
+      }, [])
+  ))]))];
+};
+
+/**
+ * Builds a variable declaration that contains declarations of all the namespace
+ * objects required by `import * as foo from 'foo'` statements.
+ *
+ * @private
+ * @param {Module[]} modules
+ * @return {?AST.VariableDeclaration}
+ */
+BundleFormatter.prototype.buildNamespaceImportObjects = function(modules) {
+  var self = this;
+  var namespaceImportedModules = [];
+
+  // Collect all the modules imported using a namespace import declaration.
+  modules.forEach(function(mod) {
+    mod.imports.namespaceImports.forEach(function(namespaceImportDeclaration) {
+      var namespaceImportedModule = namespaceImportDeclaration.source;
+      if (namespaceImportedModules.indexOf(namespaceImportedModule) < 0) {
+        namespaceImportedModules.push(namespaceImportedModule);
+      }
+    });
+  });
+
+  if (namespaceImportedModules.length === 0) {
+    return null;
+  }
+
+  /**
+   * Builds a variable declarator for the given module whose initial value is an
+   * object with properties for each export from the module being imported. For
+   * example, given a module "foo" with exports "a" and "b" this object will be
+   * created:
+   *
+   *   foo$$ = {
+   *     get a() {
+   *       return foo$$a;
+   *     },
+   *
+   *     get b() {
+   *       return foo$$b;
+   *     }
+   *   }
+   *
+   * @param {Module} mod
+   * @returns {AST.VariableDeclarator}
+   */
+  function createDeclaratorForModule(mod) {
+    return b.variableDeclarator(
+      b.identifier(mod.id),
+      b.objectExpression(
+        mod.exports.declarations.reduce(function(props, exportDeclaration) {
+          exportDeclaration.specifiers.forEach(function(specifier) {
+            props.push(createGetterForSpecifier(mod, specifier));
+          });
+
+          return props;
+        }, [])
+      )
+    );
+  }
+
+  /**
+   * Builds a getter property for retrieving the value of the given export
+   * specifier at the time it is accessed. For example, given a module "foo"
+   * with export specifier "a" this property will be created:
+   *
+   *   get a() {
+   *     return foo$$a;
+   *   }
+   *
+   * @param {Module} mod
+   * @param {ExportSpecifier} specifier
+   * @returns {AST.Property}
+   */
+  function createGetterForSpecifier(mod, specifier) {
+    return b.property(
+      'get',
+      b.identifier(specifier.name),
+      b.functionExpression(
+        null,
+        [],
+        b.blockStatement([
+          b.returnStatement(self.reference(mod, specifier.name))
+        ])
+      )
+    );
+  }
+
+  return b.variableDeclaration(
+    'var',
+    namespaceImportedModules.map(createDeclaratorForModule)
+  );
+};
+
+/**
+ * @override
+ *
+ *   ```js
+ *   export default <FunctionDeclaration|ClassDeclaration>
+ *   // or
+ *   export default <declaration|expression>;
+ *   ```
+ */
+BundleFormatter.prototype.defaultExport = function(mod, declaration) {
+  if (n.FunctionDeclaration.check(declaration) ||
+      n.ClassDeclaration.check(declaration)) {
+    // export default function foo () {}
+    // -> becomes:
+    // function <moduleName>foo () {}
+    // var <moduleName>default = <moduleName>foo;
+    var renamedDeclaration = Object.create(declaration);
+    renamedDeclaration.id = this.reference(mod, declaration.id);
+    return [
+      renamedDeclaration,
+      b.variableDeclaration(
+        'var',
+        [b.variableDeclarator(
+          this.reference(mod, 'default'),
+          this.reference(mod, declaration.id)
+        )]
+      )
+    ];
+  }
+  return b.variableDeclaration(
+    'var',
+    [b.variableDeclarator(
+      this.reference(mod, 'default'),
+      declaration
+    )]
+  );
+};
+
+/**
+ * Get a reference to the original exported value referenced in `mod` at
+ * `referencePath`. If the given reference path does not correspond to an
+ * export, we do not need to rewrite the reference. For example, since `value`
+ * is not exported it does not need to be rewritten:
+ *
+ *   ```js
+ *   // a.js
+ *   var value = 99;
+ *   console.log(value);
+ *   ```
+ *
+ * If `value` was exported then we would need to rewrite it:
+ *
+ *   ```js
+ *   // a.js
+ *   export var value = 3;
+ *   console.log(value);
+ *   ```
+ *
+ * In this case we re-write both `value` references to something like
+ * `a$$value`. The tricky part happens when we re-export an imported binding:
+ *
+ *   ```js
+ *   // a.js
+ *   export var value = 11;
+ *
+ *   // b.js
+ *   import { value } from './a';
+ *   export { value };
+ *
+ *   // c.js
+ *   import { value } from './b';
+ *   console.log(value);
+ *   ```
+ *
+ * The `value` reference in a.js will be rewritten as something like `a$$value`
+ * as expected. The `value` reference in c.js will not be rewritten as
+ * `b$$value` despite the fact that it is imported from b.js. This is because
+ * we must follow the binding through to its import from a.js. Thus, our
+ * `value` references will both be rewritten to `a$$value` to ensure they
+ * match.
+ *
+ * @override
+ */
+BundleFormatter.prototype.exportedReference = function(mod, referencePath) {
+  var specifier = mod.exports.findSpecifierForReference(referencePath);
+  if (specifier) {
+    specifier = specifier.terminalExportSpecifier;
+    return this.reference(specifier.module, specifier.name);
+  } else {
+    return null;
+  }
+};
+
+/**
+ * Get a reference to the original exported value referenced in `mod` at
+ * `referencePath`. This is very similar to BundleFormatter#exportedReference
+ * in its approach.
+ *
+ * @override
+ */
+BundleFormatter.prototype.importedReference = function(mod, referencePath) {
+  var specifier = mod.imports.findSpecifierForReference(referencePath);
+
+  if (!specifier) {
+    return null;
+  }
+
+  if (specifier.from) {
+    specifier = specifier.terminalExportSpecifier;
+    if (n.ImportNamespaceSpecifier.check(specifier.node)) {
+      // Reference the built namespace object, e.g. mod$$.
+      return b.identifier(specifier.declaration.source.id);
+    }
+    return this.reference(specifier.module, specifier.name);
+  } else {
+    return b.identifier(specifier.declaration.source.id);
+  }
+};
+
+/**
+ * If the given reference has a local declaration at the top-level then we must
+ * rewrite that reference to have a module-scoped name.
+ *
+ * @param {Module} mod
+ * @param {NodePath} referencePath
+ * @return {?Node}
+ */
+BundleFormatter.prototype.localReference = function(mod, referencePath) {
+  var scope = referencePath.scope.lookup(referencePath.node.name);
+  if (scope && scope.isGlobal) {
+    return this.reference(mod, referencePath.node);
+  } else {
+    return null;
+  }
+};
+
+/**
+ * Replaces non-default exports. Exported bindings do not need to be
+ * replaced with actual statements since they only control how local references
+ * are renamed within the module.
+ *
+ * @override
+ */
+BundleFormatter.prototype.processExportDeclaration = function(mod, nodePath) {
+  var node = nodePath.node;
+  if (n.FunctionDeclaration.check(node.declaration)) {
+    return Replacement.swaps(
+      // drop `export`
+      nodePath, node.declaration
+    ).and(
+      // transform the function
+      this.processFunctionDeclaration(mod, nodePath.get('declaration'))
+    );
+  } else if (n.ClassDeclaration.check(node.declaration)) {
+    return Replacement.swaps(
+      // drop `export`
+      nodePath, node.declaration
+    ).and(
+      // transform the class
+      this.processClassDeclaration(mod, nodePath.get('declaration'))
+    );
+  } else if (n.VariableDeclaration.check(node.declaration)) {
+    return Replacement.swaps(
+      // drop `export`
+      nodePath, node.declaration
+    ).and(
+      // transform the variables
+      this.processVariableDeclaration(mod, nodePath.get('declaration'))
+    );
+  } else if (node.declaration) {
+    throw new Error(
+        'unexpected export style, found a declaration of type: ' +
+        node.declaration.type
+    );
+  } else {
+    /**
+     * This node looks like this:
+     *
+     *   ```js
+     *   export { foo, bar };
+     *   ```
+     *
+     * Which means that it has no value in the generated code as its only
+     * function is to control how imports are rewritten.
+     */
+    return Replacement.removes(nodePath);
+  }
+};
+
+/**
+ * Since named export reassignment is just a local variable, we can ignore it.
+ * e.g.
+ *
+ *   ```js
+ *   export var foo = 1;
+ *   foo = 2;
+ *   ```
+ *
+ * @override
+ */
+BundleFormatter.prototype.processExportReassignment = function(mod, nodePath) {
+  return null;
+};
+
+/**
+ * Rename the top-level function declaration to a unique name.
+ *
+ *   ```js
+ *   function foo() {}
+ *   ```
+ *
+ * Becomes e.g.
+ *
+ *   ```js
+ *   function mod$$foo() {}
+ *   ```
+ *
+ * @override
+ */
+BundleFormatter.prototype.processFunctionDeclaration = function(mod, nodePath) {
+  return Replacement.swaps(
+    nodePath.get('id'),
+    this.reference(mod, nodePath.node.id)
+  );
+};
+
+/**
+ * Rename the top-level class declaration to a unique name.
+ *
+ *   ```js
+ *   class Foo {}
+ *   ```
+ *
+ * Becomes e.g.
+ *
+ *   ```js
+ *   class mod$$Foo {}
+ *   ```
+ *
+ * @override
+ */
+BundleFormatter.prototype.processClassDeclaration = function(mod, nodePath) {
+  return Replacement.swaps(
+    nodePath.get('id'),
+    this.reference(mod, nodePath.node.id)
+  );
+};
+
+/**
+ * Since import declarations only control how we rewrite references we can just
+ * remove them -- they don't turn into any actual statements.
+ *
+ * @override
+ */
+BundleFormatter.prototype.processImportDeclaration = function(mod, nodePath) {
+  return Replacement.removes(nodePath);
+};
+
+/**
+ * Process a variable declaration found at the top level of the module. We need
+ * to ensure that exported variables are rewritten appropriately, so we may
+ * need to rewrite some or all of this variable declaration. For example:
+ *
+ *   ```js
+ *   var a = 1, b, c = 3;
+ *   ...
+ *   export { a, b };
+ *   ```
+ *
+ * We turn those being exported into assignments as needed, e.g.
+ *
+ *   ```js
+ *   var c = 3;
+ *   mod$$a = 1;
+ *   ```
+ *
+ * @override
+ */
+BundleFormatter.prototype.processVariableDeclaration = function(mod, nodePath) {
+  var self = this;
+  return Replacement.map(
+    nodePath.get('declarations'),
+    function(declaratorPath) {
+      return Replacement.swaps(
+        declaratorPath.get('id'),
+        self.reference(mod, declaratorPath.get('id').node)
+      );
+    }
+  );
+};
+
+/**
+ * Returns an expression which globally references the export named by
+ * `identifier` for the given module `mod`. For example:
+ *
+ *   ```js
+ *   // rsvp/defer.js, export default
+ *   rsvp$defer$$default
+ *
+ *   // rsvp/utils.js, export function isFunction
+ *   rsvp$utils$$isFunction
+ *   ```
+ *
+ * @param {Module} mod
+ * @param {Identifier|string} identifier
+ * @return {Identifier}
+ * @private
+ */
+BundleFormatter.prototype.reference = function(mod, identifier) {
+  return b.identifier(
+    mod.id + (n.Identifier.check(identifier) ? identifier.name : identifier)
+  );
+};
+
+module.exports = BundleFormatter;
diff --git a/lib/formatters/commonjs_formatter.js b/lib/formatters/commonjs_formatter.js
new file mode 100644
index 0000000..5c6a983
--- /dev/null
+++ b/lib/formatters/commonjs_formatter.js
@@ -0,0 +1,537 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+var util = require('ast-util');
+
+var extend = require('../utils').extend;
+var compatMemberExpression = require('../utils').compatMemberExpression;
+var Replacement = require('../replacement');
+var Formatter = require('./formatter');
+
+/**
+ * The 'commonjs' setting for referencing exports aims to produce code that can
+ * be used in environments using the CommonJS module system, such as Node.js.
+ *
+ * @constructor
+ */
+function CommonJSFormatter() {
+  Formatter.call(this);
+}
+extend(CommonJSFormatter, Formatter);
+
+/**
+ * Convert a list of ordered modules into a list of files.
+ *
+ * @param {Module[]} modules Modules in execution order.
+ * @return {File[]}
+ */
+CommonJSFormatter.prototype.build = function(modules) {
+  var self = this;
+  return modules.map(function(mod) {
+    var body = mod.ast.program.body;
+
+    var requiresDeclaration = self.buildRequires(mod);
+    var earlyExportsStatement = self.buildEarlyExports(mod);
+    var lateExports = self.buildLateExports(mod);
+
+    if (requiresDeclaration) {
+      body.unshift(requiresDeclaration);
+    }
+
+    if (earlyExportsStatement) {
+      body.unshift(earlyExportsStatement);
+    }
+
+    body.unshift(b.expressionStatement(b.literal('use strict')));
+
+    if (lateExports) {
+      body.push(lateExports);
+    }
+
+    mod.ast.filename = mod.relativePath;
+    return mod.ast;
+  });
+};
+
+/**
+ * Process all export bindings which may be exported before any module code is
+ * actually run, i.e. function declarations.
+ *
+ * @param {Module} mod
+ * @returns {?AST.Statement}
+ * @private
+ */
+CommonJSFormatter.prototype.buildEarlyExports = function(mod) {
+  var assignments = [];
+  var exportObject = b.identifier('exports');
+
+  this.forEachExportBinding(mod, function(specifier, name) {
+    if (!n.FunctionDeclaration.check(specifier.declaration.node.declaration)) {
+      // Only function declarations are handled as early exports.
+      return;
+    }
+
+    assignments.push(b.assignmentExpression(
+      '=',
+      compatMemberExpression(
+        exportObject,
+        name
+      ),
+      b.identifier(specifier.from)
+    ));
+  });
+
+  if (assignments.length > 0) {
+    return b.expressionStatement(
+      b.sequenceExpression(assignments)
+    );
+  } else {
+    return null;
+  }
+};
+
+/**
+ * Process all export bindings which were not exported at the beginning of the
+ * module, i.e. everything except function declarations.
+ *
+ * @param {Module} mod
+ * @returns {?AST.Statement}
+ * @private
+ */
+CommonJSFormatter.prototype.buildLateExports = function(mod) {
+  var self = this;
+  var assignments = [];
+  var exportObject = b.identifier('exports');
+
+  this.forEachExportBinding(mod, function(specifier, name) {
+    if (n.FunctionDeclaration.check(specifier.declaration.node.declaration)) {
+      // Function declarations are handled as early exports.
+      return;
+    }
+
+    var from;
+
+    if (specifier.importSpecifier) {
+      if (n.ImportNamespaceSpecifier.check(specifier.importSpecifier.node)) {
+        from = b.identifier(specifier.importSpecifier.declaration.source.id)
+      } else {
+        from = self.reference(
+          specifier.importSpecifier.declaration.source,
+          specifier.importSpecifier.from
+        );
+      }
+    } else if (specifier.declaration.source) {
+      from = self.reference(
+        specifier.declaration.source,
+        specifier.name
+      );
+    } else {
+      from = b.identifier(specifier.from);
+    }
+
+    assignments.push(b.assignmentExpression(
+      '=',
+      compatMemberExpression(
+        exportObject,
+        name
+      ),
+      from
+    ));
+  });
+
+  if (assignments.length > 0) {
+    return b.expressionStatement(
+      b.sequenceExpression(assignments)
+    );
+  } else {
+    return null;
+  }
+};
+
+/**
+ * Iterates over each exported binding and calls `iterator` with its specifier.
+ *
+ * @param {Module} mod
+ * @param {function(ModuleBindingSpecifier, string)} iterator
+ * @private
+ */
+CommonJSFormatter.prototype.forEachExportBinding = function(mod, iterator) {
+  mod.exports.names.forEach(function(name) {
+    var specifier = mod.exports.findSpecifierByName(name);
+
+    assert.ok(
+      specifier,
+        'no export specifier found for export name `' +
+        name + '` from ' + mod.relativePath
+    );
+
+    if (!specifier.from) {
+      // Default exports are handled elsewhere.
+      return;
+    }
+
+    iterator(specifier, name);
+  });
+};
+
+/**
+ * Build a series of requires based on the imports (and exports with sources)
+ * in the given module.
+ *
+ * @private
+ * @param {Module} mod
+ * @return {?AST.VariableDeclaration}
+ */
+CommonJSFormatter.prototype.buildRequires = function(mod) {
+  var declarators = [];
+  var requiredModules = [];
+
+  [mod.imports, mod.exports].forEach(function(declarations) {
+    declarations.modules.forEach(function(sourceModule) {
+      if (requiredModules.indexOf(sourceModule) >= 0) {
+        return;
+      }
+      requiredModules.push(sourceModule);
+
+      var matchingDeclaration;
+      declarations.declarations.some(function(declaration) {
+        if (declaration.source === sourceModule) {
+          matchingDeclaration = declaration;
+          return true;
+        }
+      });
+
+      assert.ok(
+        matchingDeclaration,
+          'no matching declaration for source module: ' + sourceModule.relativePath
+      );
+
+      // `(import|export) { ... } from 'math'` -> `math$$ = require('math')`
+      declarators.push(b.variableDeclarator(
+        b.identifier(sourceModule.id),
+        b.callExpression(
+          b.identifier('require'),
+          [b.literal(matchingDeclaration.sourcePath)]
+        )
+      ));
+    });
+  });
+
+  if (declarators.length > 0) {
+    return b.variableDeclaration('var', declarators);
+  } else {
+    return null;
+  }
+};
+
+/**
+ * @override
+ *
+ *   ```js
+ *   export default <FunctionDeclaration|ClassDeclaration>
+ *   // or
+ *   export default <declaration|expression>;
+ *   ```
+ */
+CommonJSFormatter.prototype.defaultExport = function(mod, declaration) {
+  if (n.FunctionDeclaration.check(declaration) ||
+      n.ClassDeclaration.check(declaration)) {
+    // export default function foo () {}
+    // -> becomes:
+    // function foo () {}
+    // export['default'] = foo;
+    return [
+      declaration,
+      b.expressionStatement(b.assignmentExpression(
+        '=',
+        b.memberExpression(
+          b.identifier('exports'),
+          b.literal('default'),
+          true
+        ),
+        declaration.id
+      ))
+    ];
+  }
+  return b.expressionStatement(b.assignmentExpression(
+    '=',
+    b.memberExpression(
+      b.identifier('exports'),
+      b.literal('default'),
+      true
+    ),
+    declaration
+  ));
+};
+
+/**
+ * Because exported references are captured via a closure as part of a getter
+ * on the `exports` object, there's no need to rewrite local references to
+ * exported values. For example, `value` in this example can stay as is:
+ *
+ *   // a.js
+ *   export var value = 1;
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.exportedReference = function(mod, referencePath) {
+  return null;
+};
+
+/**
+ * Gets a reference to an imported binding by getting the value from the
+ * required module on demand. For example, this module:
+ *
+ *   // b.js
+ *   import { value } from './a';
+ *   console.log(value);
+ *
+ * Would be rewritten to look something like this:
+ *
+ *   var a$$ = require('./a');
+ *   console.log(a$$.value):
+ *
+ * If the given reference does not refer to an imported binding then no
+ * rewriting is required and `null` will be returned.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.importedReference = function(mod, referencePath) {
+  var specifier = mod.imports.findSpecifierForReference(referencePath);
+
+  if (!specifier) {
+    return null;
+  }
+
+  if (specifier.from) {
+    // import { value } from './a';
+    // import a from './a';
+    return this.reference(
+      specifier.declaration.source,
+      specifier.from
+    );
+  } else {
+    // import * as a from './a'
+    return b.identifier(specifier.declaration.source.id);
+  }
+};
+
+/**
+ * We do not need to rewrite references to local declarations.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.localReference = function(mod, referencePath) {
+  return null;
+};
+
+/**
+ * Replaces non-default exports. For declarations we simply remove the `export`
+ * keyword. For export declarations that just specify bindings, e.g.
+ *
+ *   export { a, b };
+ *
+ * we remove them entirely since they'll be handled when we define properties on
+ * the `exports` object.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.processExportDeclaration = function(mod, nodePath) {
+  var node = nodePath.node;
+
+  if (n.FunctionDeclaration.check(node.declaration)) {
+    return Replacement.swaps(nodePath, node.declaration);
+  } else if (n.VariableDeclaration.check(node.declaration)) {
+    return Replacement.swaps(nodePath, node.declaration);
+  } else if (n.ClassDeclaration.check(node.declaration)) {
+    return Replacement.swaps(nodePath, node.declaration);
+  } else if (node.declaration) {
+    throw new Error('unexpected export style, found a declaration of type: ' + node.declaration.type);
+  } else {
+    return Replacement.removes(nodePath);
+  }
+};
+
+/**
+ * We explicitly disallow reassignment because we cannot propagate changes to
+ * importing modules as we would in ES5, e.g. these are both disallowed:
+ *
+ * export var foo = 1;
+ * foo = 2;
+ *
+ * export var bar = 1;
+ * bar++;
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.processExportReassignment = function(mod, nodePath) {
+  var node = nodePath.node;
+  var exportName;
+
+  if (n.AssignmentExpression.check(node)) {
+    exportName = node.left.name;
+  } else if (n.UpdateExpression.check(node)) {
+    exportName = node.argument.name;
+  } else {
+    throw new Error('unexpected export reassignment type: ' + node.type);
+  }
+
+
+  if (n.UpdateExpression.check(node) && !node.prefix) {
+    /**
+     * The result of `a++` is the value of `a` before it is incremented, so we
+     * can't just use the result as the new value for `exports.a`. The question
+     * is whether we need to preserve the result of `a++` or not. In this case,
+     * we do:
+     *
+     *   ```js
+     *   foo(a++);
+     *   ```
+     *
+     * But in this case we don't, since the result is ignored:
+     *
+     *   ```js
+     *   a++;
+     *   ```
+     */
+
+    if (n.ExpressionStatement.check(nodePath.parent.node)) {
+      // The result is ignored here, so `a++` can become `a++, exports.a = a`.
+      return Replacement.swaps(
+        nodePath,
+        b.sequenceExpression([
+          node,
+          b.assignmentExpression(
+            '=',
+            compatMemberExpression(
+              b.identifier('exports'),
+              exportName
+            ),
+            b.identifier(exportName)
+          )
+        ])
+      );
+    } else {
+      // The result is used here, so we need a temporary variable to store it.
+      var result = util.injectVariable(nodePath.scope, util.uniqueIdentifier(nodePath.scope));
+
+      return Replacement.swaps(
+        nodePath,
+        b.sequenceExpression([
+          b.assignmentExpression(
+            '=',
+            result,
+            node
+          ),
+          b.assignmentExpression(
+            '=',
+            compatMemberExpression(
+              b.identifier('exports'),
+              exportName
+            ),
+            b.identifier(exportName)
+          ),
+          result
+        ])
+      );
+    }
+  }
+
+  /**
+   * We can use the result of the update/assignment as-is in this case, e.g.
+   *
+   *   ```js
+   *   foo(++a);
+   *   b = 9;
+   *   ```
+   *
+   * Can become:
+   *
+   *   ```js
+   *   foo(exports.a = ++a);
+   *   exports.b = b = 9;
+   *   ```
+   */
+  return Replacement.swaps(
+    nodePath,
+    b.assignmentExpression(
+      '=',
+      compatMemberExpression(
+        b.identifier('exports'),
+        exportName
+      ),
+      node
+    )
+  );
+};
+
+/**
+ * Process a function declaration found at the top level of the module. Since
+ * we do not need to rewrite exported functions, we can leave function
+ * declarations alone.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.processFunctionDeclaration = function(mod, nodePath) {
+  return null;
+};
+
+/**
+ * Process a class declaration found at the top level of the module. Since
+ * we do not need to rewrite exported classes, we can leave class
+ * declarations alone.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.processClassDeclaration = function(mod, nodePath) {
+  return null;
+};
+
+/**
+ * Since import declarations only control how we rewrite references we can just
+ * remove them -- they don't turn into any actual statements.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.processImportDeclaration = function(mod, nodePath) {
+  return Replacement.removes(nodePath);
+};
+
+/**
+ * Process a variable declaration found at the top level of the module. Since
+ * we do not need to rewrite exported variables, we can leave variable
+ * declarations alone.
+ *
+ * @override
+ */
+CommonJSFormatter.prototype.processVariableDeclaration = function(mod, nodePath) {
+  return null;
+};
+
+/**
+ * Returns an expression which globally references the export named by
+ * `identifier` for the given module `mod`. For example:
+ *
+ *    // rsvp/defer.js, export default
+ *    rsvp$defer$$['default']
+ *
+ *    // rsvp/utils.js, export function isFunction
+ *    rsvp$utils$$.isFunction
+ *
+ * @param {Module} mod
+ * @param {Identifier} identifier
+ * @return {MemberExpression}
+ * @private
+ */
+CommonJSFormatter.prototype.reference = function(mod, identifier) {
+  return compatMemberExpression(
+    b.identifier(mod.id),
+    identifier
+  );
+};
+
+module.exports = CommonJSFormatter;
diff --git a/lib/formatters/formatter.js b/lib/formatters/formatter.js
new file mode 100644
index 0000000..36c206c
--- /dev/null
+++ b/lib/formatters/formatter.js
@@ -0,0 +1,198 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+
+var Replacement = require('../replacement');
+
+/**
+ * This class provides a base for any concrete formatter classes. Subclasses
+ * of this class will be used by Rewriter to determine how to replace various
+ * parts of an AST while walking it to achieve conversion from ES6 modules to
+ * another format.
+ *
+ * @constructor
+ * @abstract
+ */
+function Formatter() {}
+
+/**
+ * Convert a list of ordered modules into a list of files.
+ *
+ * @param {Module[]} modules Modules in execution order.
+ * @return {File[]}
+ */
+Formatter.prototype.build = function(modules) {
+  throw new Error('#build must be implemented in subclasses');
+};
+
+/**
+ * Replaces default export declarations with something else. Subclasses will
+ * generally return a statement that takes the declaration value and stashes
+ * it somewhere appropriate for the transpiled format, e.g. creates a local
+ * variable, assigns the value to something, or calls a function with it.
+ *
+ * Given an export statement like so:
+ *
+ *   ```js
+ *   export default foo(bar);
+ *   ```
+ *
+ * This method will be called with the module containing the statement and
+ * the AST node corresponding to `foo(bar)`.
+ *
+ * @param {Module} mod
+ * @param {Expression} declaration
+ * @return {Statement}
+ */
+Formatter.prototype.defaultExport = function(mod, declaration) {
+  throw new Error('#defaultExport must be implemented in subclasses');
+};
+
+/**
+ * Resolves references to exported bindings. In the example below, if we refer
+ * to `value` elsewhere in the module then that reference may need to be
+ * rewritten. This method allows us to configure what it is rewritten to.
+ *
+ *   ```js
+ *   // a.js
+ *   export var value = 1;
+ *   ```
+ *
+ * Subclasses should return null if the original reference should be left
+ * intact.
+ *
+ * @param {Module} mod
+ * @param {NodePath} referencePath
+ * @return {?Expression}
+ */
+Formatter.prototype.exportedReference = function(mod, referencePath) {
+  throw new Error('#exportedReference must be implemented in subclasses');
+};
+
+/**
+ * Gets a reference to an imported binding. In this example, we will be called
+ * with the NodePath for `value` in `console.log(value)`:
+ *
+ *   ```js
+ *   // b.js
+ *   import { value } from './a';
+ *   console.log(value);
+ *   ```
+ *
+ * If the given reference does not refer to an imported binding then no
+ * rewriting is required and `null` should be returned.
+ *
+ * @param {Module} mod
+ * @param {NodePath} referencePath
+ * @return {?Expression}
+ */
+Formatter.prototype.importedReference = function(mod, referencePath) {
+  throw new Error('#importedReference must be implemented in subclasses');
+};
+
+/**
+ * Determines what the given reference should be rewritten to, if anything.
+ * Subclasses should override this only if they wish to rename bindings not
+ * associated with imports and exports.
+ *
+ * This is used by the bundle formatter, for example, to ensure that bindings
+ * at module scope are rewritten with unique names to prevent collisions with
+ * bindings from other modules.
+ *
+ * @param {Module} mod
+ * @param {NodePath} referencePath
+ * @return {?Node}
+ */
+Formatter.prototype.localReference = function(mod, referencePath) {
+  return null;
+};
+
+/**
+ * Process a function declaration found at the top level of the module.
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @return {?Node[]}
+ */
+Formatter.prototype.processFunctionDeclaration = function(mod, nodePath) {
+  throw new Error('#processFunctionDeclaration must be implemented in subclasses');
+};
+
+/**
+ * Process a class declaration found at the top level of the module.
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @return {?Node[]}
+ */
+Formatter.prototype.processClassDeclaration = function(mod, nodePath) {
+  throw new Error('#processClassDeclaration must be implemented in subclasses');
+};
+
+/**
+ * Process a variable declaration found at the top level of the module.
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @return {?Node[]}
+ */
+Formatter.prototype.processVariableDeclaration = function(mod, nodePath) {
+  throw new Error('#processVariableDeclaration must be implemented in subclasses');
+};
+
+/**
+ * Replaces non-default exports. These exports are of one of the following
+ * forms:
+ *
+ *   ```js
+ *   export var a = 1;
+ *   export function a() {}
+ *   export class a {}
+ *   export { a };
+ *   ```
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @return {?Replacement}
+ */
+Formatter.prototype.processExportDeclaration = function(mod, nodePath) {
+  throw new Error('#processExportDeclaration must be implemented in subclasses');
+};
+
+/**
+ * Process and optionally replace an update to an exported binding. This can
+ * either be an assignment expression or an update expression, i.e.
+ *
+ *   ```js
+ *   export var foo = 1;
+ *   foo = 2;
+ *   foo++;
+ *   ```
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @return {?Replacement}
+ */
+Formatter.prototype.processExportReassignment = function(mod, nodePath) {
+  throw new Error('#processExportReassignment must be implemented in subclasses');
+};
+
+/**
+ * Optionally replace an import declaration. Subclasses should almost always
+ * replace import declarations. It may be replaced with a dependency lookup, or
+ * perhaps with nothing.
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @return {?Replacement}
+ */
+Formatter.prototype.processImportDeclaration = function(mod, nodePath) {
+  throw new Error('#processImportDeclaration must be implemented in subclasses');
+};
+
+module.exports = Formatter;
diff --git a/lib/imports.js b/lib/imports.js
new file mode 100644
index 0000000..47931e3
--- /dev/null
+++ b/lib/imports.js
@@ -0,0 +1,221 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+
+var ModuleBindingList = require('./module_binding_list');
+var ModuleBindingDeclaration = require('./module_binding_declaration');
+var ModuleBindingSpecifier = require('./module_binding_specifier');
+
+var utils = require('./utils');
+var memo = utils.memo;
+var extend = utils.extend;
+var sourcePosition = utils.sourcePosition;
+
+/**
+ * Represents a list of the imports for the given module.
+ *
+ * @constructor
+ * @param {Module} mod
+ * @extends ModuleBindingList
+ */
+function ImportDeclarationList(mod) {
+  ModuleBindingList.call(this, mod);
+}
+extend(ImportDeclarationList, ModuleBindingList);
+
+/**
+ * @private
+ * @param {AST.Node} node
+ * @return {boolean}
+ */
+ImportDeclarationList.prototype.isMatchingBinding = function(node) {
+  return n.ImportDeclaration.check(node);
+};
+
+/**
+ * Gets an import declaration for the given `node`.
+ *
+ * @private
+ * @param {AST.ImportDeclaration} node
+ * @return {ImportDeclaration}
+ */
+ImportDeclarationList.prototype.declarationForNode = function(node) {
+  return new ImportDeclaration(this.module, node);
+};
+
+/**
+ * Gets the namespace imports from the list of imports.
+ *
+ * @private
+ * @type {ImportDeclaration[]}
+ * @name ImportDeclaration#namespaceImports
+ */
+memo(ImportDeclarationList.prototype, 'namespaceImports', /** @this ImportDeclarationList */function() {
+  return this.declarations.filter(function(declaration) {
+    return declaration.hasNamespaceImport;
+  });
+});
+
+/**
+ * Contains information about an import declaration.
+ *
+ *   ```js
+ *   import foo from 'math';
+ *   import { sin, cos } from 'math';
+ *   import * as bar from 'math';
+ *   import foo, { sin, cos } from 'math';
+ *   import foo, * as bar from 'math';
+ *   ```
+ *
+ * @constructor
+ * @abstract
+ * @param {Module} mod
+ * @param {AST.ImportDeclaration} node
+ * @extends ModuleBindingDeclaration
+ */
+function ImportDeclaration(mod, node) {
+  assert.ok(
+    n.ImportDeclaration.check(node),
+    'expected an import declaration, got ' + (node && node.type)
+  );
+
+  ModuleBindingDeclaration.call(this, mod, node);
+}
+extend(ImportDeclaration, ModuleBindingDeclaration);
+
+/**
+ * Contains a list of specifier name information for this import.
+ *
+ * @type {ImportSpecifier[]}
+ * @name ImportDeclaration#specifiers
+ */
+memo(ImportDeclaration.prototype, 'specifiers', /** @this ImportDeclaration */function() {
+  var self = this;
+  return this.node.specifiers.map(function(specifier) {
+    if (n.ImportDefaultSpecifier.check(specifier)) {
+      return new ImportDefaultSpecifier(self, specifier);
+    } else if (n.ImportNamespaceSpecifier.check(specifier)) {
+      return new ImportNamespaceSpecifier(self, specifier);
+    }
+    return new ImportNamedSpecifier(self, specifier);
+  });
+});
+
+/**
+ * @type {boolean}
+ * @name ImportDeclaration#hasNamespaceImport
+ */
+memo(ImportDeclaration.prototype, 'hasNamespaceImport', /** @this ImportDeclaration */function() {
+  return this.specifiers.some(function(specifier) {
+    return specifier instanceof ImportNamespaceSpecifier;
+  });
+});
+
+/**
+ * Represents an import specifier. The "a" and "b as c" are both import
+ * specifiers in the following import statement.
+ *
+ *   import { a, b as c } from "a";
+ *
+ * @constructor
+ * @extends ModuleBindingSpecifier
+ * @param {ImportDeclaration} declaration
+ * @param {AST.ImportNamedSpecifier} node
+ */
+function ImportNamedSpecifier(declaration, node) {
+  assert.ok(
+    declaration instanceof ImportDeclaration,
+    'expected an instance of ImportDeclaration'
+  );
+  ModuleBindingSpecifier.call(this, declaration, node);
+}
+extend(ImportNamedSpecifier, ModuleBindingSpecifier);
+
+/**
+ * @type {ExportSpecifier}
+ * @name ImportNamedSpecifier#exportSpecifier
+ */
+memo(ImportNamedSpecifier.prototype, 'exportSpecifier', /** @this ImportNamedSpecifier */function() {
+  var source = this.declaration.source;
+  assert.ok(source, 'import specifiers must have a valid source');
+  var exportSpecifier = source.exports.findSpecifierByName(this.from);
+  assert.ok(
+    exportSpecifier,
+    'import `' + this.from + '` at ' +
+    sourcePosition(this.module, this.node) +
+    ' has no matching export in ' + source.relativePath
+  );
+  return exportSpecifier;
+});
+
+
+/**
+ * Represents a default import specifier. The "a" in the following import statement.
+ *
+ *   import a from "a";
+ *
+ * @constructor
+ * @extends ModuleBindingSpecifier
+ * @param {ImportDeclaration} declaration
+ * @param {AST.ImportDefaultSpecifier} node
+ */
+function ImportDefaultSpecifier(declaration, node) {
+  assert.ok(
+    declaration instanceof ImportDeclaration,
+    'expected an instance of ImportDeclaration'
+  );
+  ModuleBindingSpecifier.call(this, declaration, node);
+}
+extend(ImportDefaultSpecifier, ModuleBindingSpecifier);
+
+memo(ImportDefaultSpecifier.prototype, 'exportSpecifier', /** @this ImportSpecifier */function() {
+  var source = this.declaration.source;
+  assert.ok(source, 'import specifiers must have a valid source');
+  var exportSpecifier = source.exports.findSpecifierByName(this.from);
+  assert.ok(
+    exportSpecifier,
+    'import `default` at ' +
+    sourcePosition(this.module, this.node) +
+    ' has no matching export in ' + source.relativePath
+  );
+  return exportSpecifier;
+});
+
+memo(ImportDefaultSpecifier.prototype, 'from', function() {
+  return 'default';
+});
+
+/**
+ * Represents a namespace import specifier. The "a" in the following import
+ * statement.
+ *
+ *   import * as a from "a";
+ *
+ * @constructor
+ * @extends ModuleBindingSpecifier
+ * @param {ImportDeclaration} declaration
+ * @param {AST.ImportNamespaceSpecifier} node
+ */
+function ImportNamespaceSpecifier(declaration, node) {
+  assert.ok(
+    declaration instanceof ImportDeclaration,
+    'expected an instance of ImportDeclaration'
+  );
+  ModuleBindingSpecifier.call(this, declaration, node);
+}
+extend(ImportNamespaceSpecifier, ModuleBindingSpecifier);
+
+memo(ImportNamespaceSpecifier.prototype, 'exportSpecifier', /** @this ImportNamespaceSpecifier */function() {
+  var source = this.declaration.source;
+  assert.ok(source, 'import specifiers must have a valid source');
+  return null;
+});
+
+memo(ImportNamespaceSpecifier.prototype, 'from', function() {
+  return null;
+});
+
+module.exports = ImportDeclarationList;
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..cf92f6a
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,11 @@
+/* jshint node:true, undef:true, unused:true */
+
+var Container = require('./container');
+var FileResolver = require('./file_resolver');
+var formatters = require('./formatters');
+var Module = require('./module');
+
+exports.FileResolver = FileResolver;
+exports.Container = Container;
+exports.formatters = formatters;
+exports.Module = Module;
diff --git a/lib/module.js b/lib/module.js
new file mode 100644
index 0000000..96529c5
--- /dev/null
+++ b/lib/module.js
@@ -0,0 +1,202 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var fs = require('fs');
+var Path = require('path');
+
+var esprima = require('esprima-fb');
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+var NodePath = recast.types.NodePath;
+
+var ImportDeclarationList = require('./imports');
+var ExportDeclarationList = require('./exports');
+var utils = require('./utils');
+var memo = utils.memo;
+var endsWith = utils.endsWith;
+
+/**
+ * Represents a JavaScript module at a particular location on disk.
+ *
+ * @param {string} path
+ * @param {string} relativePath
+ * @param {Container} container
+ * @constructor
+ */
+function Module(path, relativePath, container) {
+  Object.defineProperties(this, {
+    /**
+     * @type {string}
+     * @name Module#path
+     */
+    path: {
+      value: path,
+      enumerable: true,
+      writable: false
+    },
+
+    /**
+     * @type {string}
+     * @name Module#relativePath
+     */
+    relativePath: {
+      value: relativePath,
+      enumerable: true,
+      writable: false
+    },
+
+    /**
+     * @type {string}
+     * @name Module#sourceFileName
+     */
+    sourceFileName: {
+      value: Path.relative(container.basePath, path),
+      enumerable: true,
+      writable: false
+    },
+
+    /**
+     * @type {Container}
+     * @name Module#container
+     */
+    container: {
+      value: container,
+      enumerable: true,
+      writable: false
+    }
+  });
+}
+
+/**
+ * Clears the cached data for this module.
+ */
+Module.prototype.reload = function() {
+  delete this.src;
+  delete this.ast;
+  delete this.imports;
+  delete this.exports;
+  delete this.scope;
+};
+
+/**
+ * The list of imports declared by this module.
+ *
+ * @type {ImportDeclarationList}
+ * @name Module#imports
+ */
+memo(Module.prototype, 'imports', /** @this Module */function() {
+  var result = new ImportDeclarationList(this);
+  result.readProgram(this.ast.program);
+  return result;
+});
+
+/**
+ * The list of exports declared by this module.
+ *
+ * @type {ExportDeclarationList}
+ * @name Module#exports
+ */
+memo(Module.prototype, 'exports', /** @this Module */function() {
+  var result = new ExportDeclarationList(this);
+  result.readProgram(this.ast.program);
+  return result;
+});
+
+/**
+ * This module's scope.
+ *
+ * @type {Scope}
+ * @name Module#scope
+ */
+memo(Module.prototype, 'scope', /** @this Module */function() {
+  return new NodePath(this.ast).get('program').get('body').scope;
+});
+
+/**
+ * This module's source code represented as an abstract syntax tree.
+ *
+ * @type {File}
+ * @name Module#ast
+ */
+memo(Module.prototype, 'ast', /** @this Module */function() {
+  return recast.parse(
+    this.src, {
+      esprima: esprima,
+      sourceFileName: this.sourceFileName
+    }
+  );
+});
+
+/**
+ * This module's source code.
+ *
+ * @type {String}
+ * @name Module#src
+ */
+memo(Module.prototype, 'src', /** @this Module */function() {
+  return fs.readFileSync(this.path).toString();
+});
+
+/**
+ * A reference to the options from this module's container.
+ *
+ * @type {object}
+ * @name Module#options
+ */
+memo(Module.prototype, 'options', /** @this Module */function() {
+  return this.container.options;
+});
+
+/**
+ * This module's relative name, like {#relativePath} but without the extension.
+ * This may be modified by a Container if this Module is part of a Container.
+ *
+ * @type {string}
+ * @name Module#name
+ */
+memo(Module.prototype, 'name', /** @this Module */function() {
+  var relativePath = this.relativePath;
+  if (endsWith(relativePath, '.js')) {
+    return relativePath.slice(0, -3);
+  } else {
+    return relativePath;
+  }
+});
+
+/**
+ * A string suitable for a JavaScript identifier named for this module.
+ *
+ * @type {string}
+ * @name Module#id
+ */
+memo(Module.prototype, 'id', /** @this Module */function() {
+  return this.name.replace(/[^\w$_]/g, '$') + '$$';
+});
+
+/**
+ * Gets a Module by path relative to this module.
+ *
+ * @param {string} sourcePath
+ * @return {Module}
+ */
+Module.prototype.getModule = function(sourcePath) {
+  return this.container.getModule(sourcePath, this);
+};
+
+/**
+ * Generate a descriptive string suitable for debugging.
+ *
+ * @return {string}
+ */
+Module.prototype.inspect = function() {
+  return '#<' + this.constructor.name + ' ' + this.relativePath + '>';
+};
+
+/**
+ * @see Module#inspect
+ */
+Module.prototype.toString = Module.prototype.inspect;
+
+module.exports = Module;
diff --git a/lib/module_binding_declaration.js b/lib/module_binding_declaration.js
new file mode 100644
index 0000000..0db6442
--- /dev/null
+++ b/lib/module_binding_declaration.js
@@ -0,0 +1,133 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+
+var utils = require('./utils');
+var memo = utils.memo;
+
+/**
+ * Contains information about a module binding declaration. This corresponds to
+ * the shared functionality of `ExportDeclaration` and `ImportDeclaration` in
+ * the ES6 spec.
+ *
+ * @constructor
+ * @abstract
+ * @param {Module} mod
+ * @param {AST.ImportDeclaration|AST.ExportDeclaration} node
+ */
+function ModuleBindingDeclaration(mod, node) {
+  assert.ok(
+    n.ImportDeclaration.check(node) || n.ExportDeclaration.check(node),
+    'expected an import or export declaration, got ' + (node && node.type)
+  );
+
+  Object.defineProperties(this, {
+    /**
+     * @name ModuleBindingDeclaration#node
+     * @type {AST.ImportDeclaration|AST.ExportDeclaration}
+     */
+    node: {
+      value: node
+    },
+
+    /**
+     * @name ModuleBindingDeclaration#module
+     * @type {Module}
+     */
+    module: {
+      value: mod
+    }
+  });
+}
+
+/**
+ * Finds the specifier that creates the local binding given by `name`, if one
+ * exists. Otherwise `null` is returned.
+ *
+ * @param {string} name
+ * @return {?ModuleBindingSpecifier}
+ */
+ModuleBindingDeclaration.prototype.findSpecifierByName = function(name) {
+  var specifiers = this.specifiers;
+
+  for (var i = 0, length = specifiers.length; i < length; i++) {
+    var specifier = specifiers[i];
+    if (specifier.name === name) {
+      return specifier;
+    }
+  }
+
+  return null;
+};
+
+/**
+ * @param {AST.Identifier} identifier
+ * @return {?ModuleBindingSpecifier}
+ */
+ModuleBindingDeclaration.prototype.findSpecifierByIdentifier = function(identifier) {
+  for (var i = 0, length = this.specifiers.length; i < length; i++) {
+    var specifier = this.specifiers[i];
+    if (specifier.identifier === identifier) {
+      return specifier;
+    }
+  }
+
+  return null;
+};
+
+/**
+ * Gets the raw path of the `from` part of the declaration, if present. For
+ * example:
+ *
+ *   ```js
+ *   import { map } from "array";
+ *   ```
+ *
+ * The source path for the above declaration is "array".
+ *
+ * @type {?string}
+ * @name ModuleBindingDeclaration#sourcePath
+ */
+memo(ModuleBindingDeclaration.prototype, 'sourcePath', /** @this ModuleBindingDeclaration */function() {
+  return this.node.source ? this.node.source.value : null;
+});
+
+/**
+ * Gets a reference to the module referenced by this declaration.
+ *
+ * @type {Module}
+ * @name ModuleBindingDeclaration#source
+ */
+memo(ModuleBindingDeclaration.prototype, 'source', /** @this ModuleBindingDeclaration */function() {
+  return this.sourcePath ? this.module.getModule(this.sourcePath) : null;
+});
+
+/**
+ * Gets the containing module's scope.
+ *
+ * @type {Scope}
+ * @name ModuleBindingDeclaration#moduleScope
+ */
+memo(ModuleBindingDeclaration.prototype, 'moduleScope', /** @this ModuleBindingDeclaration */function() {
+  return this.module.scope;
+});
+
+/**
+ * Generate a string representing this object to aid debugging.
+ *
+ * @return {string}
+ */
+ModuleBindingDeclaration.prototype.inspect = function() {
+  return recast.print(this.node).code;
+};
+
+/**
+ * @see ModuleBindingDeclaration#inspect
+ */
+ModuleBindingDeclaration.prototype.toString = ModuleBindingDeclaration.prototype.inspect;
+
+module.exports = ModuleBindingDeclaration;
diff --git a/lib/module_binding_list.js b/lib/module_binding_list.js
new file mode 100644
index 0000000..e778dcc
--- /dev/null
+++ b/lib/module_binding_list.js
@@ -0,0 +1,244 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+
+var utils = require('./utils');
+var memo = utils.memo;
+var sourcePosition = utils.sourcePosition;
+
+/**
+ * Represents a list of bindings for the given module. This corresponds to the
+ * shared functionality from `ExportsList` and `ImportsList` from the ES6 spec.
+ *
+ * @abstract
+ * @constructor
+ * @param {Module} mod
+ */
+function ModuleBindingList(mod) {
+  Object.defineProperties(this, {
+    /**
+     * @name ModuleBindingList#_nodes
+     * @type {AST.ImportDeclaration[]|AST.ExportDeclaration[]}
+     * @private
+     */
+    _nodes: {
+      value: []
+    },
+
+    /**
+     * @name ModuleBindingList#module
+     * @type {Module}
+     */
+    module: {
+      value: mod
+    }
+  });
+}
+
+/**
+ * Add all the binding declarations from the given scope body. Generally this
+ * should be the Program node's `body` property, an array of statements.
+ *
+ * @param {AST.Program} program
+ */
+ModuleBindingList.prototype.readProgram = function(program) {
+  var body = program.body;
+  for (var i = 0; i < body.length; i++) {
+    if (this.isMatchingBinding(body[i])) {
+      this.addDeclaration(body[i]);
+    }
+  }
+};
+
+/**
+ * Adds a declaration to the list.
+ *
+ * @private
+ * @param {AST.ImportDeclaration|AST.ExportDeclaration} node
+ */
+ModuleBindingList.prototype.addDeclaration = function(node) {
+  assert.ok(
+    this.isMatchingBinding(node),
+    'expected node to be an declaration, but got ' +
+    (node && node.type)
+  );
+  this._nodes.push(node);
+
+  // reset the cache
+  delete this.declarations;
+  delete this.specifiers;
+  delete this.modules;
+};
+
+/**
+ * Gets the associated module's scope.
+ *
+ * @type {Scope}
+ * @name ModuleBindingList#moduleScope
+ */
+memo(ModuleBindingList.prototype, 'moduleScope', /** @this ModuleBindingList */function() {
+  return this.module.scope;
+});
+
+/**
+ * Gets all the modules referenced by the declarations in this list.
+ *
+ * @type {Module[]}
+ * @name ModuleBindingList#modules
+ */
+memo(ModuleBindingList.prototype, 'modules', /** @this ModuleBindingList */function() {
+  var modules = [];
+
+  this.declarations.forEach(function(declaration) {
+    if (declaration.source && modules.indexOf(declaration.source) < 0) {
+      modules.push(declaration.source);
+    }
+  });
+
+  return modules;
+});
+
+/**
+ * Finds the specifier that creates the local binding given by `name`, if one
+ * exists. Otherwise `null` is returned.
+ *
+ * @private
+ * @param {string} name
+ * @return {?ModuleBindingSpecifier}
+ */
+ModuleBindingList.prototype.findSpecifierByName = function(name) {
+  for (var i = 0, length = this.declarations.length; i < length; i++) {
+    var specifier = this.declarations[i].findSpecifierByName(name);
+    if (specifier) { return specifier; }
+  }
+
+  return null;
+};
+
+/**
+ * Finds the specifier whose identifier is the given identifier, if one exists.
+ * Otherwise `null` is returned.
+ *
+ * @private
+ * @param {AST.Identifier} identifier
+ * @return {?ModuleBindingSpecifier}
+ */
+ModuleBindingList.prototype.findSpecifierByIdentifier = function(identifier) {
+  for (var i = 0, length = this.declarations.length; i < length; i++) {
+    var specifier = this.declarations[i].findSpecifierByIdentifier(identifier);
+    if (specifier && specifier.identifier === identifier) {
+      return specifier;
+    }
+  }
+
+  return null;
+};
+
+/**
+ * @param {NodePath} referencePath
+ * @return {?ModuleBindingSpecifier}
+ */
+ModuleBindingList.prototype.findSpecifierForReference = function(referencePath) {
+  var declaration = this.findDeclarationForReference(referencePath);
+
+  if (!declaration) {
+    return null;
+  }
+
+  var specifier = this.findSpecifierByIdentifier(declaration.node);
+  assert.ok(
+    specifier,
+    'no specifier found for `' + referencePath.node.name + '`! this should not happen!'
+  );
+  return specifier;
+};
+
+/**
+ * @private
+ */
+ModuleBindingList.prototype.findDeclarationForReference = function(referencePath) {
+  // Check names to avoid traversing scopes for all references.
+  if (this.names.indexOf(referencePath.node.name) < 0) {
+    return null;
+  }
+
+  var node = referencePath.node;
+  var declaringScope = referencePath.scope.lookup(node.name);
+  assert.ok(
+    declaringScope,
+    '`' + node.name + '` at ' + sourcePosition(this.module, node) +
+    ' cannot be bound if it is not declared'
+  );
+
+  // Bindings are at the top level, so if this isn't then it's shadowing.
+  if (!declaringScope.isGlobal) {
+    return null;
+  }
+
+  var declarations = declaringScope.getBindings()[node.name];
+  if (!declarations || declarations.length !== 1) {
+    throw new SyntaxError(
+      'expected one declaration for `' + node.name +
+      '`, at ' + sourcePosition(this.module, node) +
+      ' but found ' + (declarations ? declarations.length : 'none')
+    );
+  }
+
+  return declarations[0];
+};
+
+/**
+ * Generate a string representing this object to aid debugging.
+ *
+ * @return {string}
+ */
+ModuleBindingList.prototype.inspect = function() {
+  var result = '#<' + this.constructor.name;
+
+  result += ' module=' + this.module.relativePath;
+
+  if (this.declarations.length > 0) {
+    result += ' declarations=' + this.declarations.map(function(imp) {
+      return imp.inspect();
+    }).join(', ');
+  }
+
+  result += '>';
+  return result;
+};
+
+/**
+ * @see ModuleBindingList#inspect
+ */
+ModuleBindingList.prototype.toString = ModuleBindingList.prototype.inspect;
+
+/**
+ * Contains a list of declarations.
+ *
+ * @type {(ImportDeclaration[]|ExportDeclaration[])}
+ * @name ModuleBindingList#declarations
+ */
+memo(ModuleBindingList.prototype, 'declarations', /** @this ModuleBindingList */function() {
+  var self = this;
+
+  return this._nodes.map(function(child) {
+    return self.declarationForNode(child);
+  });
+});
+
+/**
+ * Contains a combined list of names for all the declarations contained in this
+ * list.
+ *
+ * @type {string[]}
+ * @name ModuleBindingList#names
+ */
+memo(ModuleBindingList.prototype, 'names', /** @this ModuleBindingList */function() {
+  return this.declarations.reduce(function(names, decl) {
+    return names.concat(decl.specifiers.map(function(specifier) {
+      return specifier.name;
+    }));
+  }, []);
+});
+
+module.exports = ModuleBindingList;
diff --git a/lib/module_binding_specifier.js b/lib/module_binding_specifier.js
new file mode 100644
index 0000000..5551e8f
--- /dev/null
+++ b/lib/module_binding_specifier.js
@@ -0,0 +1,244 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+
+var utils = require('./utils');
+var memo = utils.memo;
+var sourcePosition = utils.sourcePosition;
+
+/**
+ * A module binding specifier provides the shared functionality of
+ * ImportSpecifiers and ExportSpecifiers in the ES6 spec.
+ *
+ * @constructor
+ * @param {ModuleBindingDeclaration} declaration
+ * @param {AST.NamedSpecifier} node
+ */
+function ModuleBindingSpecifier(declaration, node) {
+  Object.defineProperties(this, {
+    /**
+     * @name ModuleBindingSpecifier#declaration
+     * @type {ModuleBindingDeclaration}
+     */
+    declaration: {
+      value: declaration
+    },
+
+    /**
+     * @name ModuleBindingSpecifier#node
+     * @type {AST.NamedSpecifier}
+     */
+    node: {
+      value: node
+    }
+  });
+}
+
+/**
+ * Gets the module this specifier is declared in.
+ *
+ * @type Module
+ * @name ModuleBindingSpecifier#module
+ */
+memo(ModuleBindingSpecifier.prototype, 'module', /** @this ModuleBindingSpecifier */function() {
+  return this.declaration.module;
+});
+
+/**
+ * Gets the scope at the top level of the module.
+ *
+ * @type {Scope}
+ * @name ModuleBindingSpecifier#moduleScope
+ */
+memo(ModuleBindingSpecifier.prototype, 'moduleScope', /** @this ModuleBindingSpecifier */function() {
+  return this.declaration.moduleScope;
+});
+
+/**
+ * Gets the name of this specifier. For import specifiers this is the name of
+ * the binding this specifier will create locally, i.e. "foo" in both of these
+ * import statements:
+ *
+ *   import { foo } from "util";
+ *   import { bar as foo } from "util";
+ *
+ * In export specifiers it is the name of the exported declaration or the alias
+ * given to an internal name, i.e. "foo" in both of these export statements:
+ *
+ *   export { bar as foo };
+ *   export var foo = 1;
+ *
+ * @type {string}
+ * @name ModuleBindingSpecifier#name
+ */
+memo(ModuleBindingSpecifier.prototype, 'name', /** @this ModuleBindingSpecifier */function() {
+  return this.identifier.name;
+});
+
+/**
+ * Gets the name of the identifier this specifier comes from as distinct from
+ * `name`. This value will only be set if the local name and the
+ * imported/exported name differ, i.e. it will be "foo" in these statements:
+ *
+ *   import { foo as bar } from "util";
+ *   export { foo as bar };
+ *
+ * And it will be undefined in these statements:
+ *
+ *   import { foo } from "util";
+ *   export { foo };
+ *
+ * @type {string}
+ * @name ModuleBindingSpecifier#from
+ */
+memo(ModuleBindingSpecifier.prototype, 'from', /** @this ModuleBindingSpecifier */function() {
+  return this.node.id.name;
+});
+
+/**
+ * Gets the node that gives this specifier its name as it would be imported,
+ * i.e. "foo" in these statements:
+ *
+ *   import { foo } from "utils";
+ *   import { bar as foo } from "utils";
+ *   export { foo };
+ *   export { bar as foo };
+ *
+ * @type {AST.Identifier}
+ * @name ModuleBindingSpecifier#identifier
+ */
+memo(ModuleBindingSpecifier.prototype, 'identifier', /** @this ModuleBindingSpecifier */function() {
+  return this.node.name || this.node.id;
+});
+
+/**
+ * Gets the export specifier corresponding to this specifier. This can be from
+ * either an import or export declaration, since both can have a "from" part:
+ *
+ *   import { map } from "array";
+ *   export { map } from "array";
+ *
+ * In both of the above examples, the export specifier of `map` would be part
+ * of the export statement in the "array" module that exports it.
+ *
+ * @type {?ExportSpecifier}
+ * @name ModuleBindingSpecifier#exportSpecifier
+ */
+memo(ModuleBindingSpecifier.prototype, 'exportSpecifier', /** @this ModuleBindingSpecifier */function() {
+  var source = this.declaration.source;
+  if (source) {
+    var exports = source.exports;
+    return exports.findSpecifierByName(this.from);
+  } else {
+    return null;
+  }
+});
+
+/**
+ * Gets the import specifier corresponding to this specifier. This should only
+ * happen when exporting a binding that is imported in the same module, like so:
+ *
+ *   import { map } from "array";
+ *   export { map };
+ *
+ * The `map` export specifier has the `map` import specifier as its
+ * `importSpecifier` property value. The `map` import specifier has no
+ * `importSpecifier` property value.
+ *
+ * @type {?ImportSpecifier}
+ * @name ModuleBindingSpecifier#importSpecifier
+ */
+memo(ModuleBindingSpecifier.prototype, 'importSpecifier', /** @this ModuleBindingSpecifier */function() {
+  // This may be an export from this module, so find the declaration.
+  var localExportDeclarationInfo = this.moduleDeclaration;
+
+  if (localExportDeclarationInfo && n.ImportDeclaration.check(localExportDeclarationInfo.declaration)) {
+    // It was imported then exported with two separate declarations.
+    var exportModule = this.module;
+    return exportModule.imports.findSpecifierByIdentifier(localExportDeclarationInfo.identifier);
+  } else {
+    return null;
+  }
+});
+
+/**
+ * Gets the original export value by following chains of export/import
+ * statements. For example:
+ *
+ *   // a.js
+ *   export var a = 1;
+ *
+ *   // b.js
+ *   export { a } from "./a";
+ *
+ *   // c.js
+ *   import { a } from "./b";
+ *   export { a };
+ *
+ *   // d.js
+ *   import { a } from "./c";
+ *
+ * The terminal export specifier for all of these specifiers is the export in
+ * a.js, since all of them can be traced back to that one.
+ *
+ * @type {ExportSpecifier}
+ * @name ModuleBindingSpecifier#terminalExportSpecifier
+ */
+memo(ModuleBindingSpecifier.prototype, 'terminalExportSpecifier', /** @this ModuleBindingSpecifier */function() {
+  if (this.exportSpecifier) {
+    // This is true for both imports and exports with a source, e.g.
+    // `import { foo } from 'foo'` or `export { foo } from 'foo'`.
+    return this.exportSpecifier.terminalExportSpecifier;
+  }
+
+  // This is an export from this module, so find the declaration.
+  var importSpecifier = this.importSpecifier;
+  if (importSpecifier) {
+    if (n.ImportNamespaceSpecifier.check(importSpecifier.node)) {
+      // Namespace imports create a local binding, so they are the terminal.
+      return importSpecifier;
+    }
+
+    var nextExportSpecifier = importSpecifier.exportSpecifier;
+    assert.ok(
+      nextExportSpecifier,
+      'expected matching export in ' + importSpecifier.declaration.source.relativePath +
+      ' for import of `' + importSpecifier.name + '` at ' +
+      sourcePosition(this.module, this.moduleDeclaration.identifier)
+    );
+    return nextExportSpecifier.terminalExportSpecifier;
+  } else {
+    // It was declared in this module, so we are the terminal export specifier.
+    return this;
+  }
+});
+
+/**
+ * @type {?DeclarationInfo}
+ */
+ModuleBindingSpecifier.prototype.moduleDeclaration = null;
+
+/**
+ * Gets a string representation of this module binding specifier suitable for
+ * debugging.
+ *
+ * @return {string}
+ */
+ModuleBindingSpecifier.prototype.inspect = function() {
+  return '#<' + this.constructor.name +
+    ' module=' + this.declaration.module.relativePath +
+    ' name=' + this.name +
+    ' from=' + this.from +
+    '>';
+};
+
+/**
+ * @see ModuleBindingSpecifier#inspect
+ */
+ModuleBindingSpecifier.prototype.toString = ModuleBindingSpecifier.prototype.inspect;
+
+module.exports = ModuleBindingSpecifier;
diff --git a/lib/replacement.js b/lib/replacement.js
new file mode 100644
index 0000000..ffffa69
--- /dev/null
+++ b/lib/replacement.js
@@ -0,0 +1,95 @@
+/* jshint node:true, undef:true, unused:true */
+
+var recast = require('recast');
+
+/** @typedef [NodePath, AST.Node[]] */
+var ReplacementPair;
+
+/**
+ * Represents a replacement of a node path with zero or more nodes.
+ *
+ * @constructor
+ * @param {NodePath=} nodePath
+ * @param {AST.Node[]=} nodes
+ */
+function Replacement(nodePath, nodes) {
+  /**
+   * @private
+   * @type {ReplacementPair[]}
+   */
+  this.queue = [];
+  if (nodePath && nodes) {
+    this.queue.push([nodePath, nodes]);
+  }
+}
+
+/**
+ * Performs the replacement.
+ */
+Replacement.prototype.replace = function() {
+  for (var i = 0, length = this.queue.length; i < length; i++) {
+    var item = this.queue[i];
+    item[0].replace.apply(item[0], item[1]);
+  }
+};
+
+/**
+ * Incorporates the replacements from the given Replacement into this one.
+ *
+ * @param {Replacement} anotherReplacement
+ */
+Replacement.prototype.and = function(anotherReplacement) {
+  this.queue.push.apply(this.queue, anotherReplacement.queue);
+  return this;
+};
+
+/**
+ * Constructs a Replacement that, when run, will remove the node from the AST.
+ *
+ * @param {NodePath} nodePath
+ * @return {Replacement}
+ */
+Replacement.removes = function(nodePath) {
+  return new Replacement(nodePath, []);
+};
+
+/**
+ * Constructs a Replacement that, when run, will insert the given nodes after
+ * the one in nodePath.
+ *
+ * @param {NodePath} nodePath
+ * @param {AST.Node[]} nodes
+ * @return {Replacement}
+ */
+Replacement.adds = function(nodePath, nodes) {
+  return new Replacement(nodePath, [nodePath.node].concat(nodes));
+};
+
+/**
+ * Constructs a Replacement that, when run, swaps the node in nodePath with the
+ * given node or nodes.
+ *
+ * @param {NodePath} nodePath
+ * @param {AST.Node|AST.Node[]} nodes
+ */
+Replacement.swaps = function(nodePath, nodes) {
+  if (!Array.isArray(nodes)) {
+    nodes = [nodes];
+  }
+  return new Replacement(nodePath, nodes);
+};
+
+Replacement.map = function(nodePaths, callback) {
+  var result = new Replacement();
+
+  nodePaths.each(function(nodePath) {
+    var replacement = callback(nodePath);
+    if (replacement) {
+      result.and(replacement);
+    }
+  });
+
+  return result;
+};
+
+module.exports = Replacement;
diff --git a/lib/rewriter.js b/lib/rewriter.js
new file mode 100644
index 0000000..3a9466b
--- /dev/null
+++ b/lib/rewriter.js
@@ -0,0 +1,416 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var recast = require('recast');
+var types = recast.types;
+var n = types.namedTypes;
+var b = types.builders;
+var astUtil = require('ast-util');
+
+var utils = require('./utils');
+var extend = utils.extend;
+var sourcePosition = utils.sourcePosition;
+var Replacement = require('./replacement');
+
+/**
+ * Replaces references to local bindings created by `mod`'s imports
+ * with references to the original value in the source module.
+ *
+ * @constructor
+ * @param {Formatter} formatter
+ * @extends types.PathVisitor
+ */
+function Rewriter(formatter) {
+  types.PathVisitor.call(this);
+
+  Object.defineProperties(this, {
+    formatter: {
+      value: formatter
+    }
+  });
+}
+extend(Rewriter, types.PathVisitor);
+
+/**
+ * Rewrites references to all imported and exported bindings according to the
+ * rules from this rewriter's formatter. For example, this module:
+ *
+ *   ```js
+ *   import { sin, cos } from './math';
+ *   import fib from './math/fib';
+ *
+ *   assert.equal(sin(0), 0);
+ *   assert.equal(cos(0), 1);
+ *   assert.equal(fib(1), 1);
+ *   ```
+ *
+ * has its references to the imported bindings `sin`, `cos`, and `fib`
+ * rewritten to reference the source module:
+ *
+ *   ```js
+ *   assert.equal(math$$.sin(0), 0);
+ *   assert.equal(math$$.cos(0), 1);
+ *   assert.equal(math$fib$$.fib(1), 1);
+ *   ```
+ *
+ * @param {Module[]} modules
+ */
+Rewriter.prototype.rewrite = function(modules) {
+  var replacements = [];
+
+  // FIXME: This is just here to ensure that all imports and exports know where
+  // they came from. We need this because after we re-write the declarations
+  // will not be there anymore and we'll need to ensure they're cached up front.
+  modules.forEach(function(mod) {
+    [mod.exports, mod.imports].forEach(function(declarations) {
+      declarations.declarations.forEach(function(declaration) {
+        declaration.specifiers.forEach(function(specifier) {
+          return specifier.importSpecifier;
+        });
+      });
+    });
+  });
+
+  this.replacements = replacements;
+  for (var i = 0, length = modules.length; i < length; i++) {
+    var mod = modules[i];
+    if (mod.exports.declarations.length > 0 || mod.imports.declarations.length > 0) {
+      this.currentModule = mod;
+      types.visit(mod.ast.program, this);
+    } else {
+      types.visit(mod.ast.program, new DeclarationLinterVisitor(mod));
+    }
+  }
+  this.currentModule = null;
+  this.replacements = null;
+
+  replacements.forEach(function(replacement) {
+    if (replacement.replace) {
+      replacement.replace();
+    } else {
+      var path = replacement.shift();
+      path.replace.apply(path, replacement);
+    }
+  });
+};
+
+/**
+ * Process all identifiers looking for references to variables in scope.
+ *
+ * @param {NodePath} nodePath
+ * @return {boolean}
+ * @private
+ */
+Rewriter.prototype.visitIdentifier = function(nodePath) {
+  if (astUtil.isReference(nodePath)) {
+    var exportReference = this.getExportReferenceForReference(this.currentModule, nodePath);
+    if (exportReference) {
+      this.replacements.push(Replacement.swaps(nodePath, exportReference));
+    }
+  }
+
+  return false;
+};
+
+/**
+ * Process all variable declarations looking for top-level exported variables.
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitVariableDeclaration = function(nodePath) {
+  if (nodePath.scope.isGlobal) {
+    var replacement = this.formatter.processVariableDeclaration(this.currentModule, nodePath);
+    if (replacement) {
+      this.replacements.push(replacement);
+    }
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * We need to ensure that the LHS of this assignment is not an imported binding.
+ * If it is, we throw a "compile"-time error since this is not allowed by the
+ * spec (see section 12.14.1, Assignment Operators / Static Semantics: Early
+ * Errors).
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitAssignmentExpression = function(nodePath) {
+  this.assertImportIsNotReassigned(this.currentModule, nodePath.get('left'));
+  if (this.currentModule.exports.findDeclarationForReference(nodePath.get('left'))) {
+    var replacement = this.formatter.processExportReassignment(this.currentModule, nodePath);
+    if (replacement) {
+      this.replacements.push(replacement);
+    }
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * Process all top-level function declarations in case they need to be processed.
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitFunctionDeclaration = function(nodePath) {
+  if (n.Program.check(nodePath.parent.node)) {
+    var replacement = this.formatter.processFunctionDeclaration(this.currentModule, nodePath);
+    if (replacement) {
+      this.replacements.push(replacement);
+    }
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * Process all top-level class declarations in case they need to be processed.
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitClassDeclaration = function(nodePath) {
+  if (n.Program.check(nodePath.parent.node)) {
+    var replacement = this.formatter.processClassDeclaration(this.currentModule, nodePath);
+    if (replacement) {
+      this.replacements.push(replacement);
+    }
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * Look for all export declarations so we can rewrite them.
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitExportDeclaration = function(nodePath) {
+  assertStatementIsTopLevel(this.currentModule, nodePath);
+
+  var replacement;
+  if (nodePath.node.default) {
+    /**
+     * Default exports do not create bindings, so we can safely turn these
+     * into expressions that do something with the exported value.
+     *
+     * Make sure that the exported value is replaced if it is a reference
+     * to an imported binding. For example:
+     *
+     *   import { foo } from './foo';
+     *   export default foo;
+     *
+     * Might become:
+     *
+     *   mod$$.default = foo$$.foo;
+     */
+    var declaration = nodePath.node.declaration;
+    var declarationPath = nodePath.get('declaration');
+    if (astUtil.isReference(declarationPath)) {
+      var exportReference = this.getExportReferenceForReference(this.currentModule, declarationPath);
+      if (exportReference) {
+        declaration = exportReference;
+      }
+    }
+    replacement = Replacement.swaps(nodePath, this.formatter.defaultExport(this.currentModule, declaration));
+  } else {
+    replacement = this.formatter.processExportDeclaration(this.currentModule, nodePath);
+  }
+
+  if (replacement) {
+    this.replacements.push(replacement);
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * Process import declarations so they can be rewritten.
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitImportDeclaration = function(nodePath) {
+  assertStatementIsTopLevel(this.currentModule, nodePath);
+  var replacement = this.formatter.processImportDeclaration(this.currentModule, nodePath);
+  if (replacement) {
+    this.replacements.push(replacement);
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * Process update expressions (e.g. `a++`) so we can re-write modifications to
+ * exported variables.
+ *
+ * @param {NodePath} nodePath
+ * @private
+ */
+Rewriter.prototype.visitUpdateExpression = function(nodePath) {
+  this.assertImportIsNotReassigned(this.currentModule, nodePath.get('argument'));
+  if (this.currentModule.exports.findDeclarationForReference(nodePath.get('argument'))) {
+    var replacement = this.formatter.processExportReassignment(this.currentModule, nodePath);
+    if (replacement) {
+      this.replacements.push(replacement);
+    }
+  }
+
+  this.traverse(nodePath);
+};
+
+/**
+ * We need to ensure that reference updates (i.e. `ref =`, `ref++`) are not
+ * allowed for imported bindings. If it is, we throw a "compile"-time error
+ * since this is not allowed by the spec (see section 12.14.1, Assignment
+ * Operators / Static Semantics: Early Errors).
+ *
+ * @private
+ * @param {Module} mod
+ * @param {Identifier} nodePath
+ */
+Rewriter.prototype.assertImportIsNotReassigned = function(mod, nodePath) {
+  var declarationPath;
+  var identifierPath;
+  var bindingDescription;
+
+  if (n.Identifier.check(nodePath.node)) {
+    // Do we have a named import…
+    //
+    //   import { foo } from 'foo';
+    //
+    // …that we then try to assign or update?
+    //
+    //   foo++;
+    //   foo = 1;
+    //
+    declarationPath = mod.imports.findDeclarationForReference(nodePath);
+    if (!declarationPath || !n.ImportSpecifier.check(declarationPath.parent.node)) {
+      return;
+    }
+
+    bindingDescription = '`' + declarationPath.node.name + '`';
+  } else if (n.MemberExpression.check(nodePath.node)) {
+    // Do we have a namespace import…
+    //
+    //   import * as foo from 'foo';
+    //
+    // …with a property that we then try to assign or update?
+    //
+    //   foo.a++;
+    //   foo.a = 1;
+    //   foo['a'] = 1;
+    //
+    var objectPath = nodePath.get('object');
+
+    if (!n.Identifier.check(objectPath.node)) {
+      return;
+    }
+
+    declarationPath = mod.imports.findDeclarationForReference(objectPath);
+    if (!declarationPath || !n.ImportNamespaceSpecifier.check(declarationPath.parent.node)) {
+      return;
+    }
+
+    var propertyPath = nodePath.get('property');
+    if (n.Identifier.check(propertyPath.node)) {
+      bindingDescription = '`' + propertyPath.node.name + '`';
+    } else {
+      bindingDescription = 'of namespace `' + objectPath.node.name + '`';
+    }
+  } else {
+    return;
+  }
+
+  throw new SyntaxError(
+    'Cannot reassign imported binding ' + bindingDescription +
+    ' at ' + sourcePosition(mod, nodePath.node)
+  );
+};
+
+/**
+ * @private
+ */
+Rewriter.prototype.getExportReferenceForReference = function(mod, referencePath) {
+  if (n.ExportSpecifier.check(referencePath.parent.node) && !referencePath.parent.node.default) {
+    // Do not rewrite non-default export specifiers.
+    return null;
+  }
+
+  /**
+   * We need to replace references to variables that are imported or
+   * exported with the correct export expression. The export expression
+   * should be named for the original export for a variable.
+   *
+   * That is, imports must be followed to their export. If that exported
+   * value came from an import then repeat the process until you find a
+   * declaration of the exported value.
+   */
+  var exportSpecifier = mod.exports.findSpecifierByName(referencePath.node.name);
+  if (exportSpecifier && exportSpecifier.declaration.source && exportSpecifier.node !== referencePath.parent.node) {
+    // This is a direct export from another module, e.g. `export { foo } from
+    // 'foo'`. There are no local bindings created by this, so there is no
+    // associated export for this reference and no need to rewrite it.
+    return null;
+  }
+
+  return this.formatter.exportedReference(mod, referencePath) ||
+    this.formatter.importedReference(mod, referencePath) ||
+    this.formatter.localReference(mod, referencePath);
+};
+
+/**
+ * Traverses ASTs only checking for invalid import/export declaration semantics.
+ *
+ * @constructor
+ * @extends types.PathVisitor
+ * @param {Module} mod
+ * @private
+ */
+function DeclarationLinterVisitor(mod) {
+  this.module = mod;
+  types.PathVisitor.call(this);
+}
+extend(DeclarationLinterVisitor, types.PathVisitor);
+
+/**
+ * Checks that the import declaration is at the top level.
+ *
+ * @param {NodePath} nodePath
+ */
+DeclarationLinterVisitor.prototype.visitImportDeclaration = function(nodePath) {
+  assertStatementIsTopLevel(this.module, nodePath);
+};
+
+/**
+ * Checks that the export declaration is at the top level.
+ *
+ * @param {NodePath} nodePath
+ */
+DeclarationLinterVisitor.prototype.visitExportDeclaration = function(nodePath) {
+  assertStatementIsTopLevel(this.module, nodePath);
+};
+
+/**
+ * We need to ensure that all imports/exports are only at the top level. Esprima
+ * should perhaps take care of this for us, but it does not.
+ *
+ * @param {Module} mod
+ * @param {NodePath} nodePath
+ * @private
+ */
+function assertStatementIsTopLevel(mod, nodePath) {
+  if (!nodePath.scope.isGlobal) {
+    throw new SyntaxError(
+      'Unexpected non-top level ' + nodePath.node.type +
+      ' found at ' + sourcePosition(mod, nodePath.node)
+    );
+  }
+}
+
+module.exports = Rewriter;
diff --git a/lib/sorting.js b/lib/sorting.js
new file mode 100644
index 0000000..372e50d
--- /dev/null
+++ b/lib/sorting.js
@@ -0,0 +1,45 @@
+/**
+ * Determines the execution order of the given modules. This function resolves
+ * cycles by preserving the order in which the modules are visited.
+ *
+ * @param {Module[]} modules
+ * @return {Module[]}
+ */
+function sort(modules) {
+  var result = [];
+  var state = {};
+
+  modules.forEach(function(mod) {
+    visit(mod, result, state);
+  });
+
+  return result;
+}
+exports.sort = sort;
+
+/**
+ * Visits the given module, adding it to `result` after visiting all of the
+ * modules it imports, recursively. The `state` argument is private and maps
+ * module ids to the current visit state.
+ *
+ * @private
+ * @param {Module} mod
+ * @param {Module[]} result
+ * @param {Object.<string,string>} state
+ */
+function visit(mod, result, state) {
+  if (state[mod.id] === 'added') {
+    // already in the list, ignore it
+    return;
+  }
+  if (state[mod.id] === 'seen') {
+    // cycle found, just ignore it
+    return;
+  }
+  state[mod.id] = 'seen';
+  mod.imports.modules.forEach(function(mod) {
+    visit(mod, result, state);
+  });
+  state[mod.id] = 'added';
+  result.push(mod);
+}
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..bb58bce
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,134 @@
+/* jshint node:true, undef:true, unused:true */
+
+var recast = require('recast');
+var n = recast.types.namedTypes;
+var b = recast.types.builders;
+var reserved = require('reserved');
+var realFS = require('fs');
+var Path = require('path');
+
+var proto = '__proto__';
+
+function memo(object, property, getter) {
+  Object.defineProperty(object, property, {
+    get: function() {
+      this[property] = getter.call(this);
+      return this[property];
+    },
+
+    set: function(value) {
+      Object.defineProperty(this, property, {
+        value: value,
+        configurable: true,
+        writable: true
+      });
+    }
+  });
+}
+exports.memo = memo;
+
+function startsWith(string, substring) {
+  return string.lastIndexOf(substring, 0) === 0;
+}
+exports.startsWith = startsWith;
+
+function endsWith(string, substring) {
+  var expected = string.length - substring.length;
+  return string.indexOf(substring, expected) === expected;
+}
+exports.endsWith = endsWith;
+
+function extend(subclass, superclass) {
+  subclass[proto] = superclass;
+  subclass.prototype = Object.create(superclass.prototype);
+  subclass.prototype.constructor = subclass;
+}
+exports.extend = extend;
+
+function sourcePosition(mod, node) {
+  var loc = node && node.loc;
+  if (loc) {
+    return mod.relativePath + ':' + loc.start.line + ':' + (loc.start.column + 1);
+  } else {
+    return mod.relativePath;
+  }
+}
+exports.sourcePosition = sourcePosition;
+
+function IIFE() {
+  var body = [];
+  var args = Array.prototype.concat.apply(body, arguments);
+
+  args.forEach(function(node) {
+    if (n.Expression.check(node)) {
+      node = b.expressionStatement(node);
+    }
+    if (n.Statement.check(node)) {
+      body.push(node);
+    }
+  });
+
+  return b.callExpression(
+    b.memberExpression(
+      b.functionExpression(null, [], b.blockStatement(body)),
+      b.identifier("call"),
+      false
+    ),
+    [b.thisExpression()]
+  );
+}
+exports.IIFE = IIFE;
+
+/**
+ * Create a member express that is compatible with ES3. Which means
+ * reserved words will be treated as computed props. E.g.:
+ *
+ *    foo["default"] // instead of `foo.default`
+ *
+ * while still supporting identifiers for non-reserved words:
+ *
+ *    foo.bar        // since bar is not reserved
+ *
+ * @param {Identifier} obj Identifier for the object reference
+ * @param {Identifier|String} prop Identifier or string name for the member property
+ * @return {b.memberExpression} AST for the member expression
+ */
+function compatMemberExpression(obj, prop) {
+  var isIdentifier = n.Identifier.check(prop);
+  var name = isIdentifier ? prop.name : prop;
+  var computed = reserved.indexOf(name) >= 0;
+  return b.memberExpression(
+    obj,
+    computed ? b.literal(name) : (isIdentifier ? prop : b.identifier(prop)),
+    computed
+  );
+}
+
+exports.compatMemberExpression = compatMemberExpression;
+
+/**
+ * Create a hierarchy of directories of it does not already exist.
+ *
+ * @param {string} path
+ * @param {{fs: object=}} options
+ */
+function mkdirpSync(path, options) {
+  var fs = options && options.fs || realFS;
+
+  var ancestors = [];
+  var ancestor = path;
+
+  while (true) {
+    var nextAncestor = Path.dirname(ancestor);
+    if (nextAncestor === ancestor) { break; }
+    ancestors.unshift(ancestor);
+    ancestor = nextAncestor;
+  }
+
+  ancestors.forEach(function(dir) {
+    if (!fs.existsSync(dir)) {
+      fs.mkdirSync(dir);
+    }
+  });
+}
+exports.mkdirpSync = mkdirpSync;
diff --git a/lib/writer.js b/lib/writer.js
new file mode 100644
index 0000000..e9a4d70
--- /dev/null
+++ b/lib/writer.js
@@ -0,0 +1,78 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+var recast = require('recast');
+var fs = require('fs');
+var Path = require('path');
+var mkdirpSync = require('./utils').mkdirpSync;
+
+function Writer(target, options) {
+  options = options || {};
+  this.target = target;
+  this.basePath = options.basePath || process.cwd();
+  this.sourceRoot = options.sourceRoot;
+}
+
+Writer.prototype.write = function(files) {
+  var target = this.target;
+
+  switch (files.length) {
+    case 0:
+      throw new Error('expected at least one file to write, got zero');
+
+    case 1:
+      // We got a single file, so `target` should refer to either a file or a
+      // directory, but only if the file has a name.
+      var isDirectory = false;
+      try {
+        isDirectory = fs.statSync(target).isDirectory();
+      } catch (ex) {}
+
+      assert.ok(
+        !isDirectory || files[0].filename,
+        'unable to determine filename for output to directory: ' + target
+      );
+      this.writeFile(
+        files[0],
+        isDirectory ? Path.resolve(target, files[0].filename) : target
+      );
+      break;
+
+    default:
+      // We got multiple files to output, so `target` should be a directory or
+      // not exist (so we can create it).
+      var self = this;
+      files.forEach(function(file) {
+        self.writeFile(file, Path.resolve(target, file.filename));
+      });
+      break;
+  }
+};
+
+Writer.prototype.writeFile = function(file, filename) {
+  var sourceMapFilename = filename + '.map';
+
+  var rendered = recast.print(file, {
+    sourceMapName: Path.relative(this.basePath, filename),
+    sourceRoot: this.sourceRoot
+  });
+
+  var code = rendered.code;
+  assert.ok(filename, 'missing filename for file: ' + code);
+
+  mkdirpSync(Path.dirname(filename));
+
+  if (rendered.map) {
+    code += '\n\n//# sourceMappingURL=' + Path.basename(sourceMapFilename);
+
+    fs.writeFileSync(
+      sourceMapFilename,
+      JSON.stringify(rendered.map),
+      'utf8'
+    );
+  }
+
+  fs.writeFileSync(filename, code, 'utf8');
+};
+
+module.exports = Writer;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..521c227
--- /dev/null
+++ b/package.json
@@ -0,0 +1,60 @@
+{
+  "name": "es6-module-transpiler",
+  "version": "0.10.0",
+  "description": "es6-module-transpiler is an experimental compiler that allows you to write your JavaScript using a subset of the current ES6 module syntax, and compile it into various formats.",
+  "homepage": "http://esnext.github.io/es6-module-transpiler",
+  "keywords": [
+    "es6",
+    "module",
+    "transpile",
+    "amd",
+    "commonjs"
+  ],
+  "bugs": "https://github.com/square/es6-module-transpiler/issues",
+  "bin": {
+    "compile-modules": "./bin/compile-modules"
+  },
+  "files": [
+    "bin",
+    "lib",
+    "LICENSE",
+    "README.md"
+  ],
+  "directories": {
+    "lib": "./lib",
+    "test": "test"
+  },
+  "main": "lib/index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/square/es6-module-transpiler.git"
+  },
+  "scripts": {
+    "test": "npm run test-bundle && npm run test-commonjs && npm run test-unit",
+    "test-bundle": "node test/runner.js -f bundle",
+    "test-commonjs": "node test/runner.js -f commonjs",
+    "test-unit": "mocha -R spec test/unit",
+    "build-standalone": "browserify -s ModuleTranspiler -e lib/index.js -o es6-module-transpiler.js"
+  },
+  "author": "Square, Inc.",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "ast-util": "^0.5.1",
+    "esprima-fb": "^7001.1.0-dev-harmony-fb",
+    "posix-getopt": "^1.0.0",
+    "recast": "^0.9.5",
+    "reserved": "^0.1.2"
+  },
+  "devDependencies": {
+    "browserify": "^6.3.2",
+    "es6-class": "^0.9.2",
+    "example-runner": "^0.2.0",
+    "fake-fs": "^0.5.0",
+    "mocha": "^2.0.1",
+    "tmp": "0.0.24"
+  },
+  "browser": {
+    "fs": "./lib/browser/fs.js",
+    "./lib/index.js": "./lib/browser/index.js"
+  }
+}
\ No newline at end of file
diff --git a/test/examples/bare-import/exporter.js b/test/examples/bare-import/exporter.js
new file mode 100644
index 0000000..89cf73f
--- /dev/null
+++ b/test/examples/bare-import/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+global.sideEffectyValue = 99;
diff --git a/test/examples/bare-import/importer.js b/test/examples/bare-import/importer.js
new file mode 100644
index 0000000..d9c51a0
--- /dev/null
+++ b/test/examples/bare-import/importer.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+import './exporter';
+
+assert.equal(global.sideEffectyValue, 99);
diff --git a/test/examples/bindings/exporter.js b/test/examples/bindings/exporter.js
new file mode 100644
index 0000000..a635466
--- /dev/null
+++ b/test/examples/bindings/exporter.js
@@ -0,0 +1,7 @@
+/* jshint esnext:true */
+
+export var count = 0;
+
+export function incr() {
+  count++;
+}
diff --git a/test/examples/bindings/importer.js b/test/examples/bindings/importer.js
new file mode 100644
index 0000000..df69785
--- /dev/null
+++ b/test/examples/bindings/importer.js
@@ -0,0 +1,7 @@
+/* jshint esnext:true */
+
+import { count, incr } from './exporter';
+
+assert.equal(count, 0);
+incr();
+assert.equal(count, 1);
diff --git a/test/examples/cycles-defaults/a.js b/test/examples/cycles-defaults/a.js
new file mode 100644
index 0000000..4e0289b
--- /dev/null
+++ b/test/examples/cycles-defaults/a.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+import b from './b';
+
+export default { a: 1, get b() { return b.b; } };
diff --git a/test/examples/cycles-defaults/b.js b/test/examples/cycles-defaults/b.js
new file mode 100644
index 0000000..3ce87b6
--- /dev/null
+++ b/test/examples/cycles-defaults/b.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+import a from './a';
+
+export default { b: 2, get a() { return a.a; } };
diff --git a/test/examples/cycles-defaults/importer.js b/test/examples/cycles-defaults/importer.js
new file mode 100644
index 0000000..7b5708c
--- /dev/null
+++ b/test/examples/cycles-defaults/importer.js
@@ -0,0 +1,9 @@
+/* jshint esnext:true */
+
+import a from './a';
+import b from './b';
+
+assert.equal(a.a, 1);
+assert.equal(a.b, 2);
+assert.equal(b.a, 1);
+assert.equal(b.b, 2);
diff --git a/test/examples/cycles-immediate/evens.js b/test/examples/cycles-immediate/evens.js
new file mode 100644
index 0000000..7626849
--- /dev/null
+++ b/test/examples/cycles-immediate/evens.js
@@ -0,0 +1,20 @@
+/* jshint esnext:true */
+
+import { nextOdd } from './odds';
+
+/**
+ * We go through these gymnastics to eager-bind to nextOdd. This is done to
+ * ensure that both this module and the 'odds' module eagerly use something
+ * from the other.
+ */
+export var nextEven = (function() {
+  return function(n) {
+    var no = nextOdd(n);
+    return (no === n + 2) ?
+      no - 1 : no;
+  };
+})(nextOdd);
+
+export function isEven(n) {
+  return n % 2 === 0;
+}
diff --git a/test/examples/cycles-immediate/main.js b/test/examples/cycles-immediate/main.js
new file mode 100644
index 0000000..cfe2fcd
--- /dev/null
+++ b/test/examples/cycles-immediate/main.js
@@ -0,0 +1,24 @@
+/* jshint esnext:true */
+
+/**
+ * The 'evens' and 'odds' modules are configured in such a way that they both
+ * have two exported functions: isEven, nextEven, isOdd, and nextOdd. Normally
+ * these four functions could be in any order regardless of which depends on
+ * which because of JavaScript function hoisting.
+ *
+ * For the purposes of our test we need to prevent function hoisting, so it has
+ * been arranged that two of them will be function expressions assigned to
+ * variables. Specifically, isOdd and nextEven both eagerly evaluate their
+ * dependencies (i.e. isEven and nextOdd). This allows us to test that exported
+ * function declarations are available before what would be a module's
+ * "execute" step, per the spec.
+ */
+import { nextEven, isEven } from './evens';
+import { nextOdd, isOdd } from './odds';
+
+assert.equal(nextEven(1), 2);
+assert.equal(nextOdd(1), 3);
+assert.ok(isOdd(1));
+assert.ok(!isOdd(0));
+assert.ok(isEven(0));
+assert.ok(!isEven(1));
diff --git a/test/examples/cycles-immediate/odds.js b/test/examples/cycles-immediate/odds.js
new file mode 100644
index 0000000..618bf87
--- /dev/null
+++ b/test/examples/cycles-immediate/odds.js
@@ -0,0 +1,18 @@
+/* jshint esnext:true */
+
+import { isEven } from './evens';
+
+export function nextOdd(n) {
+  return isEven(n) ? n + 1 : n + 2;
+}
+
+/**
+ * We go through these gymnastics to eager-bind to isEven. This is done to
+ * ensure that both this module and the 'evens' module eagerly use something
+ * from the other.
+ */
+export var isOdd = (function(isEven) {
+  return function(n) {
+    return !isEven(n);
+  };
+})(isEven);
diff --git a/test/examples/cycles/a.js b/test/examples/cycles/a.js
new file mode 100644
index 0000000..511370c
--- /dev/null
+++ b/test/examples/cycles/a.js
@@ -0,0 +1,9 @@
+/* jshint esnext:true */
+
+import { b } from './b';
+
+export function getb() {
+  return b;
+}
+
+export var a = 1;
diff --git a/test/examples/cycles/b.js b/test/examples/cycles/b.js
new file mode 100644
index 0000000..5a66f9b
--- /dev/null
+++ b/test/examples/cycles/b.js
@@ -0,0 +1,9 @@
+/* jshint esnext:true */
+
+import { a } from './a';
+
+export function geta() {
+  return a;
+}
+
+export var b = 2;
diff --git a/test/examples/cycles/c.js b/test/examples/cycles/c.js
new file mode 100644
index 0000000..c9a5044
--- /dev/null
+++ b/test/examples/cycles/c.js
@@ -0,0 +1,9 @@
+/* jshint esnext:true */
+
+import { a, getb } from './a';
+import { b, geta } from './b';
+
+assert.equal(geta(), 1);
+assert.equal(a, 1);
+assert.equal(getb(), 2);
+assert.equal(b, 2);
diff --git a/test/examples/duplicate-import-fails/exporter.js b/test/examples/duplicate-import-fails/exporter.js
new file mode 100644
index 0000000..6c0b10d
--- /dev/null
+++ b/test/examples/duplicate-import-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var a = 1;
\ No newline at end of file
diff --git a/test/examples/duplicate-import-fails/importer.js b/test/examples/duplicate-import-fails/importer.js
new file mode 100644
index 0000000..0436e34
--- /dev/null
+++ b/test/examples/duplicate-import-fails/importer.js
@@ -0,0 +1,7 @@
+/* jshint esnext:true */
+
+/* error: type=SyntaxError message="expected one declaration for `a`, at importer.js:7:14 but found 2" */
+import { a } from './exporter';
+import { a } from './exporter';
+
+assert.equal(a, 1);
\ No newline at end of file
diff --git a/test/examples/duplicate-import-specifier-fails/exporter.js b/test/examples/duplicate-import-specifier-fails/exporter.js
new file mode 100644
index 0000000..6c0b10d
--- /dev/null
+++ b/test/examples/duplicate-import-specifier-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var a = 1;
\ No newline at end of file
diff --git a/test/examples/duplicate-import-specifier-fails/importer.js b/test/examples/duplicate-import-specifier-fails/importer.js
new file mode 100644
index 0000000..7a46f0f
--- /dev/null
+++ b/test/examples/duplicate-import-specifier-fails/importer.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+/* error: type=SyntaxError message="expected one declaration for `a`, at importer.js:5:14 but found 2" */
+import { a, a } from './exporter';
+assert.equal(a, 1);
\ No newline at end of file
diff --git a/test/examples/export-and-import-reference-share-var/first.js b/test/examples/export-and-import-reference-share-var/first.js
new file mode 100644
index 0000000..56ee87e
--- /dev/null
+++ b/test/examples/export-and-import-reference-share-var/first.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+export var a = 1;
+assert.equal(a, 1);
\ No newline at end of file
diff --git a/test/examples/export-and-import-reference-share-var/second.js b/test/examples/export-and-import-reference-share-var/second.js
new file mode 100644
index 0000000..f1677a0
--- /dev/null
+++ b/test/examples/export-and-import-reference-share-var/second.js
@@ -0,0 +1,15 @@
+/* jshint esnext:true */
+
+import { a } from './first';
+
+// This variable declaration is going to be altered because `b` needs to be
+// re-written. We need to make sure that the `a` re-writing and the unaffected
+// `c` declarator are not being clobbered by that alteration.
+var a_ = a, b = 9, c = 'c';
+
+assert.equal(a, 1);
+assert.equal(a_, 1);
+assert.equal(b, 9);
+assert.equal(c, 'c');
+
+export { b };
diff --git a/test/examples/export-class-expression/exporter.js b/test/examples/export-class-expression/exporter.js
new file mode 100644
index 0000000..08eef6f
--- /dev/null
+++ b/test/examples/export-class-expression/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export default class {};
diff --git a/test/examples/export-class-expression/importer.js b/test/examples/export-class-expression/importer.js
new file mode 100644
index 0000000..0056bf4
--- /dev/null
+++ b/test/examples/export-class-expression/importer.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+import Foo from './exporter';
+
+assert.strictEqual(new Foo().constructor, Foo);
diff --git a/test/examples/export-class/exporter.js b/test/examples/export-class/exporter.js
new file mode 100644
index 0000000..871c7f9
--- /dev/null
+++ b/test/examples/export-class/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export class Foo {}
diff --git a/test/examples/export-class/importer.js b/test/examples/export-class/importer.js
new file mode 100644
index 0000000..d4d1752
--- /dev/null
+++ b/test/examples/export-class/importer.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+import { Foo } from './exporter';
+
+assert.strictEqual(new Foo().constructor, Foo);
diff --git a/test/examples/export-default-class/exporter.js b/test/examples/export-default-class/exporter.js
new file mode 100644
index 0000000..0342727
--- /dev/null
+++ b/test/examples/export-default-class/exporter.js
@@ -0,0 +1,8 @@
+/* jshint esnext:true */
+
+export default class Point {
+  constructor(x, y) {
+    this.x = x;
+    this.y = y;
+  }
+}
diff --git a/test/examples/export-default-class/importer.js b/test/examples/export-default-class/importer.js
new file mode 100644
index 0000000..ff40016
--- /dev/null
+++ b/test/examples/export-default-class/importer.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+import Point from './exporter';
+
+assert.strictEqual(new Point(1, 2).x, 1);
+assert.strictEqual(new Point(1, 2).y, 2);
diff --git a/test/examples/export-default-function/exporter.js b/test/examples/export-default-function/exporter.js
new file mode 100644
index 0000000..7203b31
--- /dev/null
+++ b/test/examples/export-default-function/exporter.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+export default function () {
+  return 1;
+}
diff --git a/test/examples/export-default-function/importer.js b/test/examples/export-default-function/importer.js
new file mode 100644
index 0000000..7886323
--- /dev/null
+++ b/test/examples/export-default-function/importer.js
@@ -0,0 +1,8 @@
+/* jshint esnext:true */
+
+import fn1 from './exporter';
+
+import { default as fn2 } from './exporter';
+
+assert.equal(fn1(), 1);
+assert.equal(fn2(), 1);
diff --git a/test/examples/export-default-named-function/exporter.js b/test/examples/export-default-named-function/exporter.js
new file mode 100644
index 0000000..5e4bb63
--- /dev/null
+++ b/test/examples/export-default-named-function/exporter.js
@@ -0,0 +1,7 @@
+export default function foo() {
+  return 1;
+}
+
+export function callsFoo() {
+  return foo();
+}
\ No newline at end of file
diff --git a/test/examples/export-default-named-function/importer.js b/test/examples/export-default-named-function/importer.js
new file mode 100644
index 0000000..d6f83dc
--- /dev/null
+++ b/test/examples/export-default-named-function/importer.js
@@ -0,0 +1,4 @@
+import foo, { callsFoo } from './exporter';
+
+assert.strictEqual(foo(), 1);
+assert.strictEqual(callsFoo(), 1);
\ No newline at end of file
diff --git a/test/examples/export-default/exporter.js b/test/examples/export-default/exporter.js
new file mode 100644
index 0000000..39138f4
--- /dev/null
+++ b/test/examples/export-default/exporter.js
@@ -0,0 +1,16 @@
+/* jshint esnext:true */
+
+var a = 42;
+
+export function change() {
+  a++;
+}
+
+assert.equal(a, 42);
+export default a;
+
+// Any replacement for the `export default` above needs to happen in the same
+// location. It cannot be done, say, at the end of the file. Otherwise the new
+// value of `a` will be used and will be incorrect.
+a = 0;
+assert.equal(a, 0);
diff --git a/test/examples/export-default/importer.js b/test/examples/export-default/importer.js
new file mode 100644
index 0000000..e368b11
--- /dev/null
+++ b/test/examples/export-default/importer.js
@@ -0,0 +1,12 @@
+/* jshint esnext:true */
+
+import value from './exporter';
+import { change } from './exporter';
+assert.equal(value, 42);
+
+change();
+assert.equal(
+  value,
+  42,
+  'default export should not be bound'
+);
diff --git a/test/examples/export-from-default/first.js b/test/examples/export-from-default/first.js
new file mode 100644
index 0000000..4f74980
--- /dev/null
+++ b/test/examples/export-from-default/first.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export default 1;
diff --git a/test/examples/export-from-default/second.js b/test/examples/export-from-default/second.js
new file mode 100644
index 0000000..ee70dcc
--- /dev/null
+++ b/test/examples/export-from-default/second.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export { default } from './first';
diff --git a/test/examples/export-from-default/third.js b/test/examples/export-from-default/third.js
new file mode 100644
index 0000000..df0fb26
--- /dev/null
+++ b/test/examples/export-from-default/third.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import a from './second';
+assert.equal(a, 1);
diff --git a/test/examples/export-from/first.js b/test/examples/export-from/first.js
new file mode 100644
index 0000000..c17a6e9
--- /dev/null
+++ b/test/examples/export-from/first.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var a = 1;
diff --git a/test/examples/export-from/second.js b/test/examples/export-from/second.js
new file mode 100644
index 0000000..3d0b980
--- /dev/null
+++ b/test/examples/export-from/second.js
@@ -0,0 +1,7 @@
+/* jshint esnext:true */
+
+export { a } from './first';
+
+// This `a` reference should not be re-written because this export is not
+// creating a local binding.
+assert.equal(typeof a, 'undefined');
diff --git a/test/examples/export-from/third.js b/test/examples/export-from/third.js
new file mode 100644
index 0000000..95314fc
--- /dev/null
+++ b/test/examples/export-from/third.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import { a } from './second';
+assert.equal(a, 1);
diff --git a/test/examples/export-function/exporter.js b/test/examples/export-function/exporter.js
new file mode 100644
index 0000000..f74c2ee
--- /dev/null
+++ b/test/examples/export-function/exporter.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+export function foo() {
+  return 121;
+}
+assert.equal(foo(), 121);
diff --git a/test/examples/export-function/importer.js b/test/examples/export-function/importer.js
new file mode 100644
index 0000000..c2bd539
--- /dev/null
+++ b/test/examples/export-function/importer.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import { foo } from './exporter';
+assert.equal(foo(), 121);
diff --git a/test/examples/export-list/exporter.js b/test/examples/export-list/exporter.js
new file mode 100644
index 0000000..d4a462e
--- /dev/null
+++ b/test/examples/export-list/exporter.js
@@ -0,0 +1,11 @@
+/* jshint esnext:true */
+
+var a = 1;
+var b = 2;
+
+function incr() {
+  var c = a++; // Capture `a++` to force us to use a temporary variable.
+  b++;
+}
+
+export { a, b, incr };
diff --git a/test/examples/export-list/importer.js b/test/examples/export-list/importer.js
new file mode 100644
index 0000000..569d434
--- /dev/null
+++ b/test/examples/export-list/importer.js
@@ -0,0 +1,9 @@
+/* jshint esnext:true */
+
+import { a, b, incr } from './exporter';
+
+assert.equal(a, 1);
+assert.equal(b, 2);
+incr();
+assert.equal(a, 2);
+assert.equal(b, 3);
diff --git a/test/examples/export-mixins/exporter.js b/test/examples/export-mixins/exporter.js
new file mode 100644
index 0000000..2621773
--- /dev/null
+++ b/test/examples/export-mixins/exporter.js
@@ -0,0 +1,2 @@
+export default 1;
+export var bar = 2;
diff --git a/test/examples/export-mixins/importer.js b/test/examples/export-mixins/importer.js
new file mode 100644
index 0000000..537592e
--- /dev/null
+++ b/test/examples/export-mixins/importer.js
@@ -0,0 +1,4 @@
+import foo, { bar } from './exporter';
+
+assert.equal(foo, 1);
+assert.equal(bar, 2);
diff --git a/test/examples/export-named-class/exporter.js b/test/examples/export-named-class/exporter.js
new file mode 100644
index 0000000..5ffed58
--- /dev/null
+++ b/test/examples/export-named-class/exporter.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+class Foo {}
+export { Foo };
diff --git a/test/examples/export-named-class/importer.js b/test/examples/export-named-class/importer.js
new file mode 100644
index 0000000..d4d1752
--- /dev/null
+++ b/test/examples/export-named-class/importer.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+import { Foo } from './exporter';
+
+assert.strictEqual(new Foo().constructor, Foo);
diff --git a/test/examples/export-not-at-top-level-fails/index.js b/test/examples/export-not-at-top-level-fails/index.js
new file mode 100644
index 0000000..a018958
--- /dev/null
+++ b/test/examples/export-not-at-top-level-fails/index.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+function foo() {
+  /* error: type=Error message="Line 5: Unexpected reserved word" */
+  export { foo };
+}
diff --git a/test/examples/export-var/exporter.js b/test/examples/export-var/exporter.js
new file mode 100644
index 0000000..e35920b
--- /dev/null
+++ b/test/examples/export-var/exporter.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+export var a = 1;
+assert.equal(a, 1);
diff --git a/test/examples/export-var/importer.js b/test/examples/export-var/importer.js
new file mode 100644
index 0000000..e23ff4b
--- /dev/null
+++ b/test/examples/export-var/importer.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import { a } from './exporter';
+assert.equal(a, 1);
diff --git a/test/examples/import-as/exporter.js b/test/examples/import-as/exporter.js
new file mode 100644
index 0000000..6c6f1bd
--- /dev/null
+++ b/test/examples/import-as/exporter.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+export var a = 'a';
+export var b = 'b';
+export default 'DEF';
diff --git a/test/examples/import-as/importer.js b/test/examples/import-as/importer.js
new file mode 100644
index 0000000..e9cc6b9
--- /dev/null
+++ b/test/examples/import-as/importer.js
@@ -0,0 +1,7 @@
+/* jshint esnext:true */
+
+import { a as b, b as a, default as def } from './exporter';
+
+assert.equal(b, 'a');
+assert.equal(a, 'b');
+assert.equal(def, 'DEF');
diff --git a/test/examples/import-chain/first.js b/test/examples/import-chain/first.js
new file mode 100644
index 0000000..0cc3795
--- /dev/null
+++ b/test/examples/import-chain/first.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var value = 42;
diff --git a/test/examples/import-chain/second.js b/test/examples/import-chain/second.js
new file mode 100644
index 0000000..e111ea3
--- /dev/null
+++ b/test/examples/import-chain/second.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import { value } from './first';
+export { value };
diff --git a/test/examples/import-chain/third.js b/test/examples/import-chain/third.js
new file mode 100644
index 0000000..c608ea4
--- /dev/null
+++ b/test/examples/import-chain/third.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import { value } from './second';
+assert.equal(value, 42);
diff --git a/test/examples/import-not-at-top-level-fails/index.js b/test/examples/import-not-at-top-level-fails/index.js
new file mode 100644
index 0000000..3b5a406
--- /dev/null
+++ b/test/examples/import-not-at-top-level-fails/index.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+function foo() {
+  /* error: type=Error message="Line 5: Unexpected reserved word" */
+  import foo from './index';
+}
diff --git a/test/examples/module-level-declarations/mod.js b/test/examples/module-level-declarations/mod.js
new file mode 100644
index 0000000..6160767
--- /dev/null
+++ b/test/examples/module-level-declarations/mod.js
@@ -0,0 +1,8 @@
+var a = 1;
+
+assert.equal(a, 1);
+assert.equal(getA(), 1);
+
+function getA() {
+  return a;
+}
\ No newline at end of file
diff --git a/test/examples/named-function-expression/exporter.js b/test/examples/named-function-expression/exporter.js
new file mode 100644
index 0000000..1f3f644
--- /dev/null
+++ b/test/examples/named-function-expression/exporter.js
@@ -0,0 +1 @@
+export var a = 1;
\ No newline at end of file
diff --git a/test/examples/named-function-expression/importer.js b/test/examples/named-function-expression/importer.js
new file mode 100644
index 0000000..2cff8e0
--- /dev/null
+++ b/test/examples/named-function-expression/importer.js
@@ -0,0 +1,9 @@
+import { a } from './exporter';
+
+var getA = function getA() {
+  var a = 2;
+  return a;
+};
+
+assert.strictEqual(a, 1);
+assert.strictEqual(getA(), 2);
\ No newline at end of file
diff --git a/test/examples/namespace-reassign-import-fails/exporter.js b/test/examples/namespace-reassign-import-fails/exporter.js
new file mode 100644
index 0000000..73091db
--- /dev/null
+++ b/test/examples/namespace-reassign-import-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var foo = 1;
\ No newline at end of file
diff --git a/test/examples/namespace-reassign-import-fails/importer.js b/test/examples/namespace-reassign-import-fails/importer.js
new file mode 100644
index 0000000..db72f9e
--- /dev/null
+++ b/test/examples/namespace-reassign-import-fails/importer.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+import * as exp from './exporter';
+
+/* error: type=SyntaxError message="Cannot reassign imported binding `foo` at importer.js:6:1" */
+exp.foo = 2;
\ No newline at end of file
diff --git a/test/examples/namespace-update-import-fails/exporter.js b/test/examples/namespace-update-import-fails/exporter.js
new file mode 100644
index 0000000..73091db
--- /dev/null
+++ b/test/examples/namespace-update-import-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var foo = 1;
\ No newline at end of file
diff --git a/test/examples/namespace-update-import-fails/importer.js b/test/examples/namespace-update-import-fails/importer.js
new file mode 100644
index 0000000..f815834
--- /dev/null
+++ b/test/examples/namespace-update-import-fails/importer.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+import * as exp from './exporter';
+
+/* error: type=SyntaxError message="Cannot reassign imported binding of namespace `exp` at importer.js:6:1" */
+exp['foo']++;
\ No newline at end of file
diff --git a/test/examples/namespaces/exporter.js b/test/examples/namespaces/exporter.js
new file mode 100644
index 0000000..6c6f1bd
--- /dev/null
+++ b/test/examples/namespaces/exporter.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+export var a = 'a';
+export var b = 'b';
+export default 'DEF';
diff --git a/test/examples/namespaces/importer.js b/test/examples/namespaces/importer.js
new file mode 100644
index 0000000..3d4a3bd
--- /dev/null
+++ b/test/examples/namespaces/importer.js
@@ -0,0 +1,13 @@
+/* jshint esnext:true */
+
+import * as foo from './exporter';
+
+assert.equal(foo['default'], 'DEF');
+assert.equal(foo.b, 'b');
+assert.equal(foo.a, 'a');
+
+var keys = [];
+for (var key in foo) {
+  keys.push(key);
+}
+assert.deepEqual(keys.sort(), ['a', 'b', 'default']);
diff --git a/test/examples/re-export-default-import/first.js b/test/examples/re-export-default-import/first.js
new file mode 100644
index 0000000..81b78df
--- /dev/null
+++ b/test/examples/re-export-default-import/first.js
@@ -0,0 +1,5 @@
+/* jshint esnext:true */
+
+export default function hi() {
+  return 'hi';
+}
diff --git a/test/examples/re-export-default-import/second.js b/test/examples/re-export-default-import/second.js
new file mode 100644
index 0000000..62b7c25
--- /dev/null
+++ b/test/examples/re-export-default-import/second.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+import hi from './first';
+export { hi };
diff --git a/test/examples/re-export-default-import/third.js b/test/examples/re-export-default-import/third.js
new file mode 100644
index 0000000..1e0a5a4
--- /dev/null
+++ b/test/examples/re-export-default-import/third.js
@@ -0,0 +1,4 @@
+/* jshint esnext:true */
+
+import { hi } from './second';
+assert.equal(hi(), 'hi');
diff --git a/test/examples/re-export-namespace-import/first.js b/test/examples/re-export-namespace-import/first.js
new file mode 100644
index 0000000..0b0c330
--- /dev/null
+++ b/test/examples/re-export-namespace-import/first.js
@@ -0,0 +1,2 @@
+export var a = 1;
+export var b = 2;
diff --git a/test/examples/re-export-namespace-import/second.js b/test/examples/re-export-namespace-import/second.js
new file mode 100644
index 0000000..d6c8404
--- /dev/null
+++ b/test/examples/re-export-namespace-import/second.js
@@ -0,0 +1,2 @@
+import * as mod from './first';
+export { mod };
diff --git a/test/examples/re-export-namespace-import/third.js b/test/examples/re-export-namespace-import/third.js
new file mode 100644
index 0000000..10a562f
--- /dev/null
+++ b/test/examples/re-export-namespace-import/third.js
@@ -0,0 +1,4 @@
+import { mod } from './second';
+
+assert.strictEqual(mod.a, 1);
+assert.strictEqual(mod.b, 2);
diff --git a/test/examples/reassign-import-fails/exporter.js b/test/examples/reassign-import-fails/exporter.js
new file mode 100644
index 0000000..2fbf0f7
--- /dev/null
+++ b/test/examples/reassign-import-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var x = 1;
\ No newline at end of file
diff --git a/test/examples/reassign-import-fails/importer.js b/test/examples/reassign-import-fails/importer.js
new file mode 100644
index 0000000..35ce0f1
--- /dev/null
+++ b/test/examples/reassign-import-fails/importer.js
@@ -0,0 +1,11 @@
+/* jshint esnext:true */
+
+import { x } from './exporter';
+
+(function() {
+    for(var x = 0; x < 1; x++){}
+    for(var x = 0; x < 1; x++){}
+});
+
+/* error: type=SyntaxError message="Cannot reassign imported binding `x` at importer.js:11:1" */
+x = 10;
diff --git a/test/examples/reassign-import-not-at-top-level-fails/exporter.js b/test/examples/reassign-import-not-at-top-level-fails/exporter.js
new file mode 100644
index 0000000..a56f2ec
--- /dev/null
+++ b/test/examples/reassign-import-not-at-top-level-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var x = 1;
diff --git a/test/examples/reassign-import-not-at-top-level-fails/importer.js b/test/examples/reassign-import-not-at-top-level-fails/importer.js
new file mode 100644
index 0000000..8b5bbf9
--- /dev/null
+++ b/test/examples/reassign-import-not-at-top-level-fails/importer.js
@@ -0,0 +1,11 @@
+/* jshint esnext:true */
+
+import { x } from './exporter';
+
+export function foo () {
+  var x = 1;
+}
+export function bar () {
+  /* error: type=SyntaxError message="Cannot reassign imported binding `x` at importer.js:10:3" */
+  x = 1;
+}
diff --git a/test/examples/this-is-global/mod.js b/test/examples/this-is-global/mod.js
new file mode 100644
index 0000000..c6f4da9
--- /dev/null
+++ b/test/examples/this-is-global/mod.js
@@ -0,0 +1,8 @@
+/* jshint esnext:true */
+
+assert.strictEqual(
+  this,
+  global,
+  '`this` (keys=' + Object.keys(this) + ') does not equal ' +
+  '`global` (keys=' + Object.keys(global) + ')'
+);
\ No newline at end of file
diff --git a/test/examples/update-expression-of-import-fails/exporter.js b/test/examples/update-expression-of-import-fails/exporter.js
new file mode 100644
index 0000000..e0f2ffc
--- /dev/null
+++ b/test/examples/update-expression-of-import-fails/exporter.js
@@ -0,0 +1,3 @@
+/* jshint esnext:true */
+
+export var a = 0;
\ No newline at end of file
diff --git a/test/examples/update-expression-of-import-fails/importer.js b/test/examples/update-expression-of-import-fails/importer.js
new file mode 100644
index 0000000..cb81604
--- /dev/null
+++ b/test/examples/update-expression-of-import-fails/importer.js
@@ -0,0 +1,6 @@
+/* jshint esnext:true */
+
+import { a } from './exporter';
+
+/* error: type=SyntaxError message="Cannot reassign imported binding `a` at importer.js:6:1" */
+a++;
\ No newline at end of file
diff --git a/test/runner.js b/test/runner.js
new file mode 100644
index 0000000..d5fcf3d
--- /dev/null
+++ b/test/runner.js
@@ -0,0 +1,250 @@
+/* jshint node:true, undef:true, unused:true */
+
+Error.stackTraceLimit = 50;
+
+var fs = require('fs');
+var Path = require('path');
+var vm = require('vm');
+var assert = require('assert');
+var es6class = require('es6-class');
+
+var modules = require('../lib');
+var utils = require('../lib/utils');
+var endsWith = utils.endsWith;
+var ExpectedError = require('./support/expected_error');
+
+var examples = Path.join(__dirname, 'examples');
+
+var paths = [];
+var formatters = require('../lib/formatters');
+var formatterNames = Object.keys(formatters).filter(function(formatter) {
+  return formatter !== 'DEFAULT';
+});
+var formatter = formatters.DEFAULT;
+
+var getopt = require('posix-getopt');
+var parser = new getopt.BasicParser('h(help)f:(format)', process.argv);
+var option;
+
+while ((option = parser.getopt()) !== undefined) {
+  if (option.error) {
+    usage();
+    process.exit(1);
+  }
+
+  switch (option.option) {
+    case 'f':
+      formatter = option.optarg;
+      if (formatterNames.indexOf(formatter) < 0) {
+        usage();
+        process.exit(1);
+      }
+      break;
+
+    case 'h':
+      usage();
+      process.exit(0);
+      break;
+  }
+}
+
+paths.push.apply(paths, process.argv.slice(parser.optind()));
+
+if (paths.length === 0) {
+  paths = fs.readdirSync(examples).map(function(example) {
+    return Path.join(examples, example);
+  });
+} else {
+  var cwd = process.cwd();
+  paths = paths.map(function(example) {
+    return Path.resolve(cwd, example);
+  });
+}
+
+var results = Path.join(__dirname, 'results');
+if (fs.existsSync(results)) {
+  rmrf(results);
+}
+fs.mkdirSync(results);
+runTests(paths);
+
+function runTests(paths) {
+  var passed = 0, failed = 0;
+  paths.forEach(function(path) {
+    if (runTestDir(path)) {
+      passed++;
+    } else {
+      failed++;
+    }
+  });
+
+  console.log();
+  console.log('%d passed, %s failed.', passed, failed);
+  process.exit(
+    (passed + failed === 0) ? 1 : // no tests, fail
+              failed === 0  ? 0 : // no failed, pass
+                              1); // some failed, fail
+}
+
+function runTestDir(testDir) {
+  var passed = false;
+  var testName = Path.basename(testDir);
+
+  var options = {
+    resolvers: [new modules.FileResolver([testDir])],
+    formatter: formatters[formatter]
+  };
+  var container = new modules.Container(options);
+
+  var expectedError;
+  try {
+    fs.readdirSync(testDir).forEach(function(child) {
+      var mod = container.getModule(child);
+      var contents = fs.readFileSync(mod.path).toString();
+      var newExpectedError = ExpectedError.getFromSource(contents);
+
+      assert.ok(
+        !newExpectedError || !expectedError,
+        'found more than one error comment!'
+      );
+
+      expectedError = newExpectedError;
+    });
+
+    var resultPath = Path.join(results, testName + '.js');
+    container.write(resultPath);
+
+    var testAssert = wrappedAssert();
+    if (fs.statSync(resultPath).isDirectory()) {
+      fs.readdirSync(resultPath).forEach(function(child) {
+        if (Path.extname(child) === '.js') {
+          requireTestFile('./' + child, resultPath, testAssert);
+        }
+      });
+    } else {
+      requireTestFile(resultPath, process.cwd(), testAssert);
+    }
+
+    assert.ok(
+      expectedError || testAssert.count > 0,
+      'expected at least one assertion'
+    );
+
+    if (expectedError) {
+      expectedError.assertMatch(null);
+    }
+
+    passed = true;
+    printSuccess(testName);
+  } catch (ex) {
+    if (!(ex instanceof assert.AssertionError) && expectedError) {
+      ex = expectedError.matchError(ex);
+    }
+
+    if (ex) {
+      printFailure(testName, ex);
+      console.log();
+    } else {
+      printSuccess(testName);
+      passed = true;
+    }
+  }
+
+  return passed;
+}
+
+// TODO: Just use the real node require system with proxyquire?
+var testFileCache;
+var testFileGlobal;
+function requireTestFile(path, relativeTo, assert) {
+  if (path[0] === '.') {
+    path = Path.resolve(relativeTo, path);
+  }
+
+  if (!testFileCache) { testFileCache = {}; }
+
+  if (path in testFileCache) {
+    return testFileCache[path];
+  } else if (!fs.existsSync(path) && !endsWith(path, '.js')) {
+    return requireTestFile(path + '.js');
+  }
+
+  var code = fs.readFileSync(path);
+  var mod = {exports: {}};
+  testFileCache[path] = mod.exports;
+
+  if (!testFileGlobal) { testFileGlobal = {}; }
+
+  testFileGlobal.assert = assert;
+  testFileGlobal.global = testFileGlobal;
+  testFileGlobal.module = mod;
+  testFileGlobal.exports = mod.exports;
+  testFileGlobal.require = function(requiredPath) {
+    return requireTestFile(requiredPath, Path.dirname(path), assert);
+  };
+
+  // Hack to work around an issue where vm does not set `this` to the context.
+  code = '(function(){' + es6class.compile(code).code + '\n}).call(global);';
+  vm.runInNewContext(code, testFileGlobal, path);
+
+  testFileCache[path] = mod.exports;
+  return mod.exports;
+}
+
+function wrappedAssert() {
+  var result = {count: 0};
+
+  Object.getOwnPropertyNames(assert).forEach(function(property) {
+    result[property] = function() {
+      result.count++;
+      return assert[property].apply(assert, arguments);
+    };
+  });
+
+  return result;
+}
+
+function rmrf(path) {
+  var stat = fs.statSync(path);
+  if (stat.isDirectory()) {
+    fs.readdirSync(path).forEach(function(child) {
+      rmrf(Path.join(path, child));
+    });
+    fs.rmdirSync(path);
+  } else if (stat.isFile()) {
+    fs.unlinkSync(path);
+  }
+}
+
+/**
+ * Prints a line to stdout for the given test indicating that it passed.
+ *
+ * @param {string} testName
+ */
+function printSuccess(testName) {
+  console.log('\x1b[32m✓ \x1b[0m' + testName);
+}
+
+/**
+ * Prints a line to stdout for the given test indicating that it failed. In
+ * addition, prints any additional information indented one level.
+ *
+ * @param {string} testName
+ * @param {Error} error
+ */
+function printFailure(testName, error) {
+  console.log('\x1b[31m✘ ' + testName + '\x1b[0m');
+  console.log();
+  if (error.stack) {
+    console.log(error.stack);
+  } else {
+    console.log(error.message);
+  }
+}
+
+function usage() {
+  console.log('node test/runner.js [OPTIONS] [EXAMPLE1 [EXAMPLE2 ...]]');
+  console.log();
+  console.log('  -f, --format <name>  Choose from: %s.', formatterNames.join(', '));
+  console.log('  -h, --help           Show this help message.');
+}
diff --git a/test/support/expected_error.js b/test/support/expected_error.js
new file mode 100644
index 0000000..9a1b596
--- /dev/null
+++ b/test/support/expected_error.js
@@ -0,0 +1,116 @@
+/* jshint node:true, undef:true, unused:true */
+
+var assert = require('assert');
+
+/**
+ * @param {string|Error=} type
+ * @param {string=} message
+ * @constructor
+ */
+function ExpectedError(type, message) {
+  this.type = type;
+  this.message = message;
+}
+
+/**
+ * Builds an ExpectedError from the given source code.
+ *
+ * @param {string} source
+ * @return {?ExpectedError}
+ */
+ExpectedError.getFromSource = function(source) {
+  var errorMatch = source.match(/\/\*\s*error:\s*(.+?)\*\//);
+  if (!errorMatch) {
+    return null;
+  }
+
+  var errorInfo = errorMatch[1];
+  var expectedTypeMatch = errorInfo.match(/type=([a-zA-Z]+)/);
+  var expectedMessageMatch = errorInfo.match(/message="([^"]+)"/);
+
+  assert.ok(
+    expectedTypeMatch || expectedMessageMatch,
+    'expected error comment contains neither a type or a message: ' +
+    errorInfo
+  );
+
+  return new ExpectedError(
+    expectedTypeMatch && expectedTypeMatch[1],
+    expectedMessageMatch && expectedMessageMatch[1]
+  );
+};
+
+/**
+ * Determines whether the given error matches the expected error type.
+ *
+ * @param {!Error} error
+ * @return {boolean}
+ */
+ExpectedError.prototype.matchesType = function(error) {
+  return !this.type ||
+    (typeof this.type === 'function' && error instanceof this.type) ||
+    (this.type === error.constructor) ||
+    (this.type === error.constructor.name);
+};
+
+/**
+ * Determines whether the given error matches the expected error message.
+ *
+ * @param {!Error} error
+ * @return {boolean}
+ */
+ExpectedError.prototype.matchesMessage = function(error) {
+  return !this.message ||
+    (this.message === error.message) ||
+    (this.message.test && this.message.test(error.message));
+};
+
+/**
+ * Asserts that the given error matches the expected error info.
+ *
+ * @param {?Error} error
+ */
+ExpectedError.prototype.assertMatch = function(error) {
+  var matchError = this.matchError(error);
+  if (matchError) {
+    throw matchError;
+  }
+};
+
+/**
+ * Gets the error to throw if the given error does not match.
+ *
+ * @param {?Error} error
+ * @return {?AssertionError}
+ */
+ExpectedError.prototype.matchError = function(error) {
+  var matchesType = error && this.matchesType(error);
+  var matchesMessage = error && this.matchesMessage(error);
+
+  if (matchesType && matchesMessage) {
+    return null;
+  }
+
+  var assertMessage = 'expected error';
+
+  if (!matchesType && this.type) {
+    assertMessage += ' type to equal ' + this.type;
+    if (!matchesMessage && this.message) {
+      assertMessage += ' and';
+    }
+  }
+  if (!matchesMessage && this.message) {
+    assertMessage += ' message to match ' +
+      (typeof this.message === 'string' ? '"' + this.message + '"' : this.message);
+  }
+
+  if (error) {
+    assertMessage += ', but got ' + error;
+  } else {
+    assertMessage += ', but no error was thrown'
+  }
+
+  return new assert.AssertionError({ message: assertMessage });
+};
+
+module.exports = ExpectedError;
diff --git a/test/support/test_formatter.js b/test/support/test_formatter.js
new file mode 100644
index 0000000..e5837b9
--- /dev/null
+++ b/test/support/test_formatter.js
@@ -0,0 +1,72 @@
+var Formatter = require('../../lib/formatters/formatter');
+var extend = require('../../lib/utils').extend;
+
+/**
+ * This basic formatter does not alter the AST at all, and only exists
+ * help write unit tests for things that depend on formatters.
+ *
+ * @class
+ * @extends Formatter
+ */
+function TestFormatter() {
+  Formatter.call(this);
+  this.processedExportDeclarationCount = 0;
+  this.processedExportReassignmentCount = 0;
+  this.processedImportDeclarationCount = 0;
+  this.processedFunctionDeclarationCount = 0;
+  this.processedVariableDeclarationCount = 0;
+}
+extend(TestFormatter, Formatter);
+
+/**
+ * @override
+ */
+TestFormatter.prototype.build = function(modules) {
+  return modules.map(function(mod) {
+    var ast = mod.ast;
+    ast.filename = mod.relativePath;
+    return ast;
+  });
+};
+
+/**
+ * @override
+ */
+TestFormatter.prototype.processExportDeclaration = function() {
+  this.processedExportDeclarationCount++;
+  return null;
+};
+
+/**
+ * @override
+ */
+TestFormatter.prototype.processExportReassignment = function() {
+  this.processedExportReassignmentCount++;
+  return null;
+};
+
+/**
+ * @override
+ */
+TestFormatter.prototype.processImportDeclaration = function() {
+  this.processedImportDeclarationCount++;
+  return null;
+};
+
+/**
+ * @override
+ */
+TestFormatter.prototype.processFunctionDeclaration = function() {
+  this.processedFunctionDeclarationCount++;
+  return null;
+};
+
+/**
+ * @override
+ */
+TestFormatter.prototype.processVariableDeclaration = function() {
+  this.processedVariableDeclarationCount++;
+  return null;
+};
+
+exports.TestFormatter = TestFormatter;
diff --git a/test/support/test_resolver.js b/test/support/test_resolver.js
new file mode 100644
index 0000000..f6112e5
--- /dev/null
+++ b/test/support/test_resolver.js
@@ -0,0 +1,36 @@
+var Module = require('../../lib/module');
+var Path = require('path');
+
+/**
+ * This basic resolver just returns a module whose #src is set to an
+ * empty string to prevent an attempt to read from the file system.
+ *
+ * @class
+ * @param {Object.<string,string>=} sources
+ */
+function TestResolver(sources) {
+  this.sources = sources || {};
+}
+
+/**
+ * @param {string} path
+ * @param {Module} mod
+ * @param {Container} container
+ * @returns {Module}
+ */
+TestResolver.prototype.resolveModule = function(path, mod, container) {
+  if (mod) {
+    path = Path.normalize(Path.join(mod.relativePath, '..', path));
+  }
+
+  var cachedModule = container.getCachedModule(path);
+  if (cachedModule) {
+    return cachedModule;
+  }
+
+  var resolved = new Module(path, path, container);
+  resolved.src = this.sources[path] || '';
+  return resolved;
+};
+
+exports.TestResolver = TestResolver;
diff --git a/test/unit/container_test.js b/test/unit/container_test.js
new file mode 100644
index 0000000..d0f0a1a
--- /dev/null
+++ b/test/unit/container_test.js
@@ -0,0 +1,120 @@
+var Container = require('../../lib/container');
+var Module = require('../../lib/module');
+var Path = require('path');
+var TestFormatter = require('../support/test_formatter').TestFormatter;
+var TestResolver = require('../support/test_resolver').TestResolver;
+var assert = require('assert');
+var fs = require('fs');
+var tmp = require('tmp');
+
+describe('Container', function() {
+  describe('#write', function() {
+    it('allows multiple calls but only converts once', function(done) {
+      var buildCallCount = 0;
+      var source = 'var a = 1;';
+      var formatter = new TestFormatter();
+
+      /**
+       * This formatter only exists to count the number of times #build is
+       * called and create a result of the right data structure.
+       *
+       * @param {Module[]} modules
+       * @returns {File[]}
+       */
+      formatter.build = function(modules) {
+        buildCallCount++;
+        return TestFormatter.prototype.build.call(this, modules);
+      };
+
+      var container = new Container({
+        formatter: formatter,
+        resolvers: [new TestResolver({
+          'a.js': source
+        })]
+      });
+
+      // Ensure we have a module to write at all.
+      container.getModule('a.js');
+
+      tmp.dir(function(err, path) {
+        if (err) { return done(err); }
+
+        // Write the contents to a temporary directory.
+        container.write(path);
+        assert.strictEqual(buildCallCount, 1);
+
+        // Ensure that the written file contains the original code.
+        var a1 = fs.readFileSync(Path.join(path, 'a.js'), 'utf8');
+        assert.ok(
+          a1.indexOf(source) === 0,
+          'expected written source to start with original source, but was: ' + a1
+        );
+
+        tmp.dir(function(err, path2) {
+          if (err) { return done(err); }
+          assert.notStrictEqual(path, path2);
+
+          // Write to yet another temporary directory with the same container.
+          container.write(path2);
+          assert.strictEqual(buildCallCount, 1);
+
+          // Ensure that the written file contains the original code.
+          var a2 = fs.readFileSync(Path.join(path2, 'a.js'), 'utf8');
+          assert.ok(
+            a2.indexOf(source) === 0,
+            'expected written source to start with original source, but was: ' + a2
+          );
+
+          done();
+        });
+      });
+    });
+
+    it('freezes the container, effectively preventing adding new modules', function(done) {
+      var container = new Container({
+        formatter: new TestFormatter(),
+        resolvers: [new TestResolver()]
+      });
+
+      container.getModule('a.js');
+
+      tmp.dir(function(err, path) {
+        if (err) { return done(err); }
+
+        container.write(path);
+
+        try {
+          container.getModule('b.js');
+          assert.fail('expected an exception');
+        } catch (ex) {
+          assert.strictEqual(
+            'container has already converted contained modules and cannot add new module: b.js',
+            ex.message
+          );
+        }
+
+        done();
+      });
+    });
+  });
+
+  describe('#transform', function() {
+    var formatter = new TestFormatter();
+    var source = 'export var a = 1;';
+    var container = new Container({
+      formatter: formatter,
+      resolvers: [new TestResolver({
+        'a.js': source
+      })]
+    });
+
+    // Ensure we have a module to write at all.
+    container.getModule('a.js');
+
+    var files = container.transform();
+    assert.strictEqual(files.length, 1);
+    assert.strictEqual(files[0].filename, 'a.js');
+    assert.strictEqual(files[0].code, source);
+    assert.strictEqual(typeof files[0].map, 'object');
+  });
+});
diff --git a/test/unit/mkdirp_test.js b/test/unit/mkdirp_test.js
new file mode 100644
index 0000000..9d989ee
--- /dev/null
+++ b/test/unit/mkdirp_test.js
@@ -0,0 +1,56 @@
+var assert = require('assert');
+var mkdirpSync = require('../../lib/utils').mkdirpSync;
+
+describe('mkdirpSync', function() {
+  var calls;
+  var fs = {
+    existsSync: function(path) {
+      calls.push(['existsSync', path]);
+      return path === '/' || path === '/path' || path === 'path';
+    },
+
+    mkdirSync: function(path) {
+      calls.push(['mkdirSync', path]);
+    }
+  };
+
+  beforeEach(function() {
+    calls = [];
+  });
+
+  context('given absolute paths', function() {
+    it('checks each path component, making the ones that do not exist', function() {
+      mkdirpSync('/path/to/some/dir', { fs: fs });
+      assert.deepEqual(
+        calls,
+        [
+          ['existsSync', '/path'],
+          ['existsSync', '/path/to'],
+          ['mkdirSync', '/path/to'],
+          ['existsSync', '/path/to/some'],
+          ['mkdirSync', '/path/to/some'],
+          ['existsSync', '/path/to/some/dir'],
+          ['mkdirSync', '/path/to/some/dir']
+        ]
+      );
+    });
+  });
+
+  context('given relative paths', function() {
+    it('checks each path component, making the ones that do not exist', function() {
+      mkdirpSync('path/to/some/dir', { fs: fs });
+      assert.deepEqual(
+        calls,
+        [
+          ['existsSync', 'path'],
+          ['existsSync', 'path/to'],
+          ['mkdirSync', 'path/to'],
+          ['existsSync', 'path/to/some'],
+          ['mkdirSync', 'path/to/some'],
+          ['existsSync', 'path/to/some/dir'],
+          ['mkdirSync', 'path/to/some/dir']
+        ]
+      );
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/unit/module_binding_specifier_test.js b/test/unit/module_binding_specifier_test.js
new file mode 100644
index 0000000..d2134ea
--- /dev/null
+++ b/test/unit/module_binding_specifier_test.js
@@ -0,0 +1,110 @@
+var assert = require('assert');
+var Container = require('../../lib/container');
+var Module = require('../../lib/module');
+var TestFormatter = require('../support/test_formatter').TestFormatter;
+var TestResolver = require('../support/test_resolver').TestResolver;
+
+describe('ModuleBindingSpecifier', function() {
+  describe('#terminalExportSpecifier', function() {
+    var sources;
+    var container;
+
+    beforeEach(function() {
+      container = new Container({
+        formatter: new TestFormatter(),
+        resolvers: [new TestResolver(sources)]
+      });
+    });
+
+    function getExportSpecifier(modulePath, exportedName) {
+      var mod = container.getModule(modulePath);
+      var specifier = mod.exports.findSpecifierByName(exportedName);
+      if (!specifier) {
+        throw new Error('unable to find export `' + exportedName + '` in module: ' + modulePath);
+      }
+      return specifier;
+    }
+
+    context('when the export is a variable declaration', function() {
+      before(function() {
+        sources = { 'index.js': 'export var a = 1;' };
+      });
+
+      it('is the export itself', function() {
+        var specifier = getExportSpecifier('index.js', 'a');
+        assert.strictEqual(specifier.terminalExportSpecifier, specifier);
+      });
+    });
+
+    context('when the export is a function declaration', function() {
+      before(function() {
+        sources = { 'index.js': 'export function a() {}' };
+      });
+
+      it('is the export itself', function() {
+        var specifier = getExportSpecifier('index.js', 'a');
+        assert.strictEqual(specifier.terminalExportSpecifier, specifier);
+      });
+    });
+
+    context('when the export binds an import by name from another module', function() {
+      before(function() {
+        sources = {
+          'index.js': 'import { a } from "middle.js";\nexport { a };',
+          'middle.js': 'import { a } from "root.js";\nexport { a };',
+          'root.js': 'export var a = 1;'
+        };
+      });
+
+      it('follows the trail of imports until it finds the original', function() {
+        var rootA = getExportSpecifier('root.js', 'a');
+        var specifier = getExportSpecifier('index.js', 'a');
+        assert.strictEqual(
+          specifier.terminalExportSpecifier,
+          rootA,
+          'expected ' + specifier.terminalExportSpecifier + ' to equal ' + rootA
+        );
+      });
+    });
+
+    context('when the export directly re-exports a binding by name from another module', function() {
+      before(function() {
+        sources = {
+          'index.js': 'import { a } from "middle.js";\nexport { a };',
+          'middle.js': 'export { a } from "root.js";',
+          'root.js': 'export var a = 1;'
+        };
+      });
+
+      it('follows the trail of imports until it finds the original', function() {
+        var rootA = getExportSpecifier('root.js', 'a');
+        var specifier = getExportSpecifier('index.js', 'a');
+        assert.strictEqual(
+          specifier.terminalExportSpecifier,
+          rootA,
+          'expected ' + specifier.terminalExportSpecifier + ' to equal ' + rootA
+        );
+      });
+    });
+
+    xcontext('when the export binds an import through a batch export', function() {
+      before(function() {
+        sources = {
+          'index.js': 'import { a } from "middle.js";\nexport { a };',
+          'middle.js': 'export * from "root.js";',
+          'root.js': 'export var a = 1;'
+        };
+      });
+
+      it('follows the trail of imports until it finds the original', function() {
+        var rootA = getExportSpecifier('root.js', 'a');
+        var specifier = getExportSpecifier('index.js', 'a');
+        assert.strictEqual(
+          getExportSpecifier('index.js', 'a').terminalExportSpecifier,
+          rootA,
+          'expected ' + specifier.terminalExportSpecifier + ' to equal ' + rootA
+        );
+      });
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/unit/rewriter_test.js b/test/unit/rewriter_test.js
new file mode 100644
index 0000000..0d99548
--- /dev/null
+++ b/test/unit/rewriter_test.js
@@ -0,0 +1,30 @@
+var Container = require('../../lib/container');
+var Module = require('../../lib/module');
+var Rewriter = require('../../lib/rewriter');
+var TestFormatter = require('../support/test_formatter').TestFormatter;
+var TestResolver = require('../support/test_resolver').TestResolver;
+var assert = require('assert');
+
+describe('Rewriter', function() {
+  describe('#rewrite', function() {
+    context('when a module has no imports or exports', function() {
+      it('does not traverse the module at all', function() {
+        var formatter = new TestFormatter();
+        var container = new Container({
+          formatter: formatter,
+          resolvers: [new TestResolver({
+            'a.js': 'var foo = 1\nfunction bar() {}'
+          })]
+        });
+
+        var a = container.getModule('a.js');
+        var rewriter = new Rewriter(formatter);
+        rewriter.rewrite(container.getModules());
+
+        assert.strictEqual(formatter.processedExportDeclarationCount, 0);
+        assert.strictEqual(formatter.processedFunctionDeclarationCount, 0);
+        assert.strictEqual(formatter.processedVariableDeclarationCount, 0);
+      });
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/unit/sorting_test.js b/test/unit/sorting_test.js
new file mode 100644
index 0000000..3a1e034
--- /dev/null
+++ b/test/unit/sorting_test.js
@@ -0,0 +1,77 @@
+var sort = require('../../lib/sorting').sort;
+var assert = require('assert');
+var util = require('util');
+
+function assertArraysEqual(a, b) {
+  var message = 'expected ' + util.inspect(a) + ' to equal ' + util.inspect(b);
+  assert.ok(a.length === b.length, message);
+
+  for (var i = 0, length = a.length; i < length; i++) {
+    assert.ok(a[i] === b[i], message);
+  }
+}
+
+describe('sort', function() {
+  it('returns an empty list given an empty list', function() {
+    assertArraysEqual(sort([]), []);
+  });
+
+  it('returns a list of a single module given a single module', function() {
+    var mod = { id: 'testmod', imports: { modules: [] } };
+    assertArraysEqual(sort([mod]), [mod]);
+  });
+
+  it('properly orders a linear set of modules', function() {
+    var a = { id: 'a', imports: { modules: [] } };
+    var b = { id: 'b', imports: { modules: [a] } };
+    var c = { id: 'c', imports: { modules: [b] } };
+
+    assertArraysEqual(sort([b, a, c]), [a, b, c]);
+  });
+
+  it('properly orders a tree of modules', function() {
+    var b = { id: 'b', imports: { modules: [] } };
+    var c = { id: 'c', imports: { modules: [] } };
+    var a = { id: 'a', imports: { modules: [b, c] } };
+
+    assertArraysEqual(sort([b, a, c]), [b, c, a]);
+    assertArraysEqual(sort([c, a, b]), [c, b, a]);
+  });
+
+  it('properly orders a DAG of modules', function() {
+    var a = { id: 'a', imports: { modules: [] } };
+    var b = { id: 'b', imports: { modules: [] } };
+    var c = { id: 'c', imports: { modules: [] } };
+
+    a.imports.modules.push(b, c);
+    b.imports.modules.push(c);
+
+    assertArraysEqual(sort([a, b, c]), [c, b, a]);
+    assertArraysEqual(sort([b, a, c]), [c, b, a]);
+  });
+
+  it('orders a simple cyclic graph by last-required', function() {
+    var a = { id: 'a', imports: { modules: [] } };
+    var b = { id: 'b', imports: { modules: [] } };
+
+    a.imports.modules.push(b);
+    b.imports.modules.push(a);
+
+    assertArraysEqual(sort([a, b]), [b, a]);
+    assertArraysEqual(sort([b, a]), [a, b]);
+  });
+
+  it('orders a complex cyclic graph by last-required', function() {
+    var a = { id: 'a', imports: { modules: [] } };
+    var b = { id: 'b', imports: { modules: [a] } };
+    var c = { id: 'c', imports: { modules: [b] } };
+    var d = { id: 'd', imports: { modules: [c] } };
+
+    a.imports.modules.push(d);
+
+    assertArraysEqual(sort([b, c, d, a]), [c, d, a, b]);
+    assertArraysEqual(sort([b, c, a, d]), [c, d, a, b]);
+    assertArraysEqual(sort([c, d, a, b]), [d, a, b, c]);
+    assertArraysEqual(sort([c, b, a, d]), [d, a, b, c]);
+  });
+});
\ No newline at end of file

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



More information about the Pkg-javascript-commits mailing list