├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TRANSITION.md ├── bin └── compile-modules ├── lib ├── browser │ ├── capture_stack_trace_polyfill.js │ ├── fs.js │ └── index.js ├── cli │ └── convert.js ├── container.js ├── declaration_info.js ├── exports.js ├── file_resolver.js ├── formatters.js ├── formatters │ ├── bundle_formatter.js │ ├── commonjs_formatter.js │ └── formatter.js ├── imports.js ├── index.js ├── module.js ├── module_binding_declaration.js ├── module_binding_list.js ├── module_binding_specifier.js ├── replacement.js ├── rewriter.js ├── sorting.js ├── utils.js └── writer.js ├── package.json └── test ├── examples ├── bare-import │ ├── exporter.js │ └── importer.js ├── bindings │ ├── exporter.js │ └── importer.js ├── cycles-defaults │ ├── a.js │ ├── b.js │ └── importer.js ├── cycles-immediate │ ├── evens.js │ ├── main.js │ └── odds.js ├── cycles │ ├── a.js │ ├── b.js │ └── c.js ├── duplicate-import-fails │ ├── exporter.js │ └── importer.js ├── duplicate-import-specifier-fails │ ├── exporter.js │ └── importer.js ├── export-and-import-reference-share-var │ ├── first.js │ └── second.js ├── export-class-expression │ ├── exporter.js │ └── importer.js ├── export-class │ ├── exporter.js │ └── importer.js ├── export-default-class │ ├── exporter.js │ └── importer.js ├── export-default-function │ ├── exporter.js │ └── importer.js ├── export-default-named-function │ ├── exporter.js │ └── importer.js ├── export-default │ ├── exporter.js │ └── importer.js ├── export-from-default │ ├── first.js │ ├── second.js │ └── third.js ├── export-from │ ├── first.js │ ├── second.js │ └── third.js ├── export-function │ ├── exporter.js │ └── importer.js ├── export-list │ ├── exporter.js │ └── importer.js ├── export-mixins │ ├── exporter.js │ └── importer.js ├── export-named-class │ ├── exporter.js │ └── importer.js ├── export-not-at-top-level-fails │ └── index.js ├── export-var │ ├── exporter.js │ └── importer.js ├── import-as │ ├── exporter.js │ └── importer.js ├── import-chain │ ├── first.js │ ├── second.js │ └── third.js ├── import-not-at-top-level-fails │ └── index.js ├── module-level-declarations │ └── mod.js ├── named-function-expression │ ├── exporter.js │ └── importer.js ├── namespace-reassign-import-fails │ ├── exporter.js │ └── importer.js ├── namespace-update-import-fails │ ├── exporter.js │ └── importer.js ├── namespaces │ ├── exporter.js │ └── importer.js ├── re-export-default-import │ ├── first.js │ ├── second.js │ └── third.js ├── re-export-namespace-import │ ├── first.js │ ├── second.js │ └── third.js ├── reassign-import-fails │ ├── exporter.js │ └── importer.js ├── reassign-import-not-at-top-level-fails │ ├── exporter.js │ └── importer.js ├── this-is-global │ └── mod.js └── update-expression-of-import-fails │ ├── exporter.js │ └── importer.js ├── runner.js ├── support ├── expected_error.js ├── test_formatter.js └── test_resolver.js └── unit ├── container_test.js ├── mkdirp_test.js ├── module_binding_specifier_test.js ├── rewriter_test.js └── sorting_test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 2 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/results 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | "browser" : false, 4 | "boss" : true, 5 | "curly": false, 6 | "debug": false, 7 | "devel": false, 8 | "eqeqeq": true, 9 | "evil": true, 10 | "forin": false, 11 | "immed": false, 12 | "laxbreak": false, 13 | "newcap": true, 14 | "noarg": true, 15 | "noempty": false, 16 | "nonew": false, 17 | "nomen": false, 18 | "onevar": false, 19 | "plusplus": false, 20 | "regexp": false, 21 | "undef": false, 22 | "sub": true, 23 | "strict": false, 24 | "white": false, 25 | "eqnull": true, 26 | "esnext": true 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_install: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Brian Donovan 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v0.9.5 (2014-11-14) 4 | 5 | * Fix source maps to include directory names as well as file names. 6 | 7 | ## v0.9.4 (2014-11-14) 8 | 9 | * Fix re-exporting module objects created by namespace imports (e.g. `import * as foo from 'foo'; export { foo }`). 10 | 11 | ## v0.9.3 (2014-11-03) 12 | 13 | * Fix bundle format support for named exports of class declarations. 14 | 15 | ## v0.9.2 (2014-11-03) 16 | 17 | * Add support for exporting ES6 class declarations and expressions. 18 | 19 | ## v0.9.1 (2014-11-03) 20 | 21 | * Clarify README to indicate that ES6 module syntax is now stable. 22 | * Add `Container#transform` to get transformed code without writing to the file system. 23 | 24 | ## v0.9.0 (2014-10-22) 25 | 26 | * Add support for namespace imports (e.g. `import * as foo from 'foo'`). 27 | 28 | ## v0.8.3 (2014-10-17) 29 | 30 | * Fixed internal helper `mkdirpSync` that could cause `Container#write` to fail on some machines. 31 | 32 | ## v0.8.2 (2014-10-15) 33 | 34 | * Fixed bundle formatter renaming of function or class declarations in export default. Previously they were not properly renamed to prevent collision with other modules. 35 | 36 | ## v0.8.1 (2014-10-15) 37 | 38 | * 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). 39 | 40 | ```js 41 | // mod.js 42 | var a; 43 | var fn = function f() { var a; return a; }; 44 | ``` 45 | 46 | ## v0.8.0 (2014-09-30) 47 | 48 | * Simplify the CommonJS formatter output to use ES3. 49 | 50 | ## v0.7.0 (2014-09-30) 51 | 52 | * Use a common base class for all built-in formatters. 53 | * Various internal cleanups. 54 | 55 | ## v0.6.2 (2014-08-20) 56 | 57 | * Prevent all instances of export reassignment, not just those at the top level. 58 | 59 | ## v0.6.1 (2014-08-20) 60 | 61 | * Reference a custom fork of esprima. 62 | * Allow resolvers to set the `Module#src` property to prevent direct file system access. 63 | 64 | ## v0.6.0 (2014-07-28) 65 | 66 | * Add support for source maps. 67 | * Allow formatters to handle export reassignment. 68 | * Update recast to v0.6.6. 69 | 70 | ## v0.5.1 (2014-06-25) 71 | 72 | * Fix the file list to be included in npm. 73 | 74 | ## v0.5.0 (2014-06-24) 75 | 76 | * Completely re-written using [recast](https://github.com/benjamn/recast). 77 | * Removed YUI support, to be re-added as a plugin. 78 | * Removed AMD support, to be re-added as a plugin. 79 | * Added "bundle" formatter for concatenating all modules together. 80 | * Assert on various invalid module syntax usage. 81 | 82 | ## v0.4.0 (2014-04-03) 83 | 84 | * Remove trailing whitespace after AMD define (#93) 85 | * (**breaking**) Default to anonymous modules for AMD output (use `--infer-name` flag for old behavior). Replaces `--anonymous` flag. 86 | 87 | ## v0.3.6 (2013-12-16) 88 | 89 | * Rebuilt & republished to fix regression on quoting `static` property (sorry!) 90 | 91 | ## v0.3.5 (2013-12-01) 92 | 93 | * Fixed incorrect module path strings in CJS output (#82) 94 | 95 | ## v0.3.4 (2013-11-19) 96 | 97 | * CJS: Build a module object when using `module foo from "foo"` for better forward-compatibility. 98 | * Added YUI transpiler, lovingly created & maintained by the YUI team 99 | * Fixed `'static'` keyword not being quoted in Esprima, causing issues in some JS runtimes 100 | 101 | ## v0.3.3 (2013-10-25) 102 | 103 | * Fix syntax error in CommonJS output with default imports and `compatFix` option 104 | 105 | ## v0.3.2 (2013-10-18) 106 | 107 | * Fixes path resolution on the command line (thanks rpflorence!) 108 | 109 | ## v0.3.1 (2013-10-17) 110 | 111 | * Use a working commit for Esprima 112 | 113 | ## v0.3.0 (2013-10-16) 114 | 115 | This is a **major, breaking version**. See TRANSITION.md for information on upgrading your code. 116 | 117 | * Rewrote the transpiler using Esprima 118 | * Support default exports and named exports in the same module 119 | * Default export now exports to `moduleObject.default`, see TRANSITION.md for details 120 | * Fixed multiline export parsing 121 | * Added support for `module` keyword (i.e. `module foo from "foo"`) 122 | * Added support for `import "foo";` form 123 | * fixed the `--anonymous` flag with the CLI for recursive transpiling (#20) 124 | * Removed relative pathing for AMD resolution & direct CoffeeScript transpilation (see TRANSITION.md) 125 | 126 | ## v0.2.0 (2013-07-08) 127 | 128 | * added support for default export (`export default jQuery`) 129 | * added support for default import (`import $ from "jquery"`) 130 | * added support for re-exporting properties (`export { ajax } from "jquery"`) 131 | * removed support for `export =` syntax (use `export default`) 132 | * removed support for `import "jquery" as $` (use `import $ from "jquery"`) 133 | 134 | ## v0.1.3 (2013-06-21) 135 | 136 | * fixed: import/export statements within block comments are now ignored 137 | * added support for `export var foo = …` 138 | * added support for `export function foo() { … }` 139 | 140 | ## v0.1.2 (2013-03-07) 141 | 142 | * use Grunt for building the project 143 | * fixed: use local variables for imports 144 | 145 | ## v0.1.1 (2013-02-24) 146 | 147 | * fixed: add missing `--global` option to CLI 148 | * documentation and clarifications of examples 149 | 150 | ## v0.1.0 (2013-02-11) 151 | 152 | * initial release 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Square Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ES6 Module Transpiler [![Build Status](https://travis-ci.org/esnext/es6-module-transpiler.png)](https://travis-ci.org/esnext/es6-module-transpiler) 2 | 3 | ## NOTE: This project been deprecated in favor of Babel & Rollup. 4 | 5 | This project is part of esnext, which has merged with [Babel]. All the features 6 | of esnext are supported by Babel, and more. All the tests from esnext have been 7 | ported over to Babel to ensure that switchers will have minimal code changes to 8 | make. The maintainers of esnext will continue working on Babel to bring better 9 | spec compliance, ES6 feature support, and performance. If you want a fast tool 10 | with bundling support as found in this project, you should check out [Rollup]. 11 | 12 | --- 13 | 14 | ES6 Module Transpiler is an experimental compiler that allows you to write your 15 | JavaScript using a subset of the ES6 module syntax, and compile it into AMD or 16 | CommonJS modules. 17 | 18 | This compiler provides a way to experiment with ES6 syntax in real world 19 | scenarios to see how the syntax holds up. It also provides a nicer, more 20 | declarative way to write AMD (or CommonJS) modules. 21 | 22 | See the [CHANGELOG](./CHANGELOG.md) for the latest updates. 23 | 24 | ## Usage 25 | 26 | ### Build tools 27 | 28 | The easiest way to use the transpiler is from an existing build tool. There 29 | several plugins developed for different build tools: 30 | 31 | * **Grunt:** [grunt-es6-module-transpiler](https://github.com/joefiorini/grunt-es6-module-transpiler), maintained by @joefiorini (not yet compatible with v0.5.x) 32 | * **Gulp:** [gulp-es6-module-transpiler](https://github.com/ryanseddon/gulp-es6-module-transpiler), maintained by @ryanseddon 33 | * **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) 34 | * **Broccoli:** [broccoli-es6-concatenator](https://github.com/joliss/broccoli-es6-concatenator), maintained by @joliss (not yet compatible with v0.5.x) 35 | * **Mimosa:** [mimosa-es6-module-transpiler](https://github.com/dbashford/mimosa-es6-module-transpiler), maintained by @dbashford (not yet compatible with v0.5.x) 36 | * **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) 37 | 38 | ### Executable 39 | 40 | The transpiler can be used directly from the command line: 41 | 42 | ``` 43 | $ npm install -g es6-module-transpiler 44 | $ compile-modules convert foo.js 45 | ``` 46 | 47 | Here is the basic usage: 48 | 49 | ``` 50 | compile-modules convert -I lib -o out FILE [FILE…] 51 | ``` 52 | 53 | ### Library 54 | 55 | You can also use the transpiler as a library: 56 | 57 | ```javascript 58 | var transpiler = require('es6-module-transpiler'); 59 | var Container = transpiler.Container; 60 | var FileResolver = transpiler.FileResolver; 61 | var BundleFormatter = transpiler.formatters.bundle; 62 | 63 | var container = new Container({ 64 | resolvers: [new FileResolver(['lib/'])], 65 | formatter: new BundleFormatter() 66 | }); 67 | 68 | container.getModule('index'); 69 | container.write('out/mylib.js'); 70 | ``` 71 | 72 | ## Supported ES6 Module Syntax 73 | 74 | ### Named Exports 75 | 76 | There are two types of exports. *Named exports* like the following: 77 | 78 | ```javascript 79 | // foobar.js 80 | var foo = 'foo', bar = 'bar'; 81 | 82 | export { foo, bar }; 83 | ``` 84 | 85 | This module has two named exports, `foo` and `bar`. 86 | 87 | You can also write this form as: 88 | 89 | ```javascript 90 | // foobar.js 91 | export var foo = 'foo'; 92 | export var bar = 'bar'; 93 | ``` 94 | 95 | Either way, another module can then import your exports like so: 96 | 97 | ```js 98 | import { foo, bar } from 'foobar'; 99 | 100 | console.log(foo); // 'foo' 101 | ``` 102 | 103 | ### Default Exports 104 | 105 | You can also export a *default* export. For example, an ES6ified jQuery might 106 | look like this: 107 | 108 | ```javascript 109 | // jquery.js 110 | var jQuery = function() {}; 111 | 112 | jQuery.prototype = { 113 | // ... 114 | }; 115 | 116 | export default jQuery; 117 | ``` 118 | 119 | Then, an app that uses jQuery could import it with: 120 | 121 | ```javascript 122 | import $ from 'jquery'; 123 | ``` 124 | 125 | The default export of the "jquery" module is now aliased to `$`. 126 | 127 | A default export makes the most sense as a module's "main" export, like the 128 | `jQuery` object in jQuery. You can use default and named exports in parallel. 129 | 130 | ### Other Syntax 131 | 132 | #### `import "foo";` 133 | 134 | A "bare import" that doesn't import any identifiers is useful for executing 135 | side effects in a module. For example: 136 | 137 | ```js 138 | // alerter.js 139 | alert("alert! alert!"); 140 | 141 | // alertee.js 142 | import "alerter"; // will pop up alert box 143 | ``` 144 | 145 | ## Compiled Output 146 | 147 | ### Default Exports 148 | 149 | This is super important: 150 | 151 | **Default exports bind to an identifier on the module called `default`!** 152 | 153 | Internally, the transpiler will use this default identifer when importing, but 154 | any outside consumer needs to be aware that it should use the `default` key and 155 | not the module itself. For example, a CommonJS consumer should look like this: 156 | 157 | ```js 158 | var $ = require('jquery')['default']; 159 | ``` 160 | 161 | ## Installation 162 | 163 | Add this project to your application's package.json by running this: 164 | 165 | $ npm install --save es6-module-transpiler 166 | 167 | Or install it globally: 168 | 169 | $ npm install -g es6-module-transpiler 170 | 171 | ## Acknowledgements 172 | 173 | Thanks to [Yehuda Katz](https://twitter.com/wycats) for 174 | [js_module_transpiler](https://github.com/wycats/js_module_transpiler), the 175 | library on which this one is based. Thanks to [Dave 176 | Herman](https://twitter.com/littlecalculist) for his work on ES6 modules. 177 | Thanks to [Erik Bryn](https://twitter.com/ebryn) for providing the initial push 178 | to write this library. Thanks to [Domenic 179 | Denicola](https://twitter.com/domenic), [Jo Liss](https://twitter.com/jo_liss), 180 | & [Thomas Boyt](https://twitter.com/thomasaboyt) for their efforts to make this 181 | project even better. And finally thanks to the JavaScript community at Square 182 | for helping to write and release this library. 183 | 184 | ## Contributing 185 | 186 | 1. Fork it 187 | 2. Create your feature branch (`git checkout -b my-new-feature`) 188 | 3. Commit your changes (`git commit -am 'Add some feature'`) 189 | 4. Push to the branch (`git push origin my-new-feature`) 190 | 5. Create new Pull Request 191 | 192 | Any contributors to the master es6-module-transpiler repository must sign the 193 | [Individual Contributor License Agreement (CLA)][cla]. It's a short form that 194 | covers our bases and makes sure you're eligible to contribute. 195 | 196 | [cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 197 | 198 | When you have a change you'd like to see in the master repository, [send a pull 199 | request](https://github.com/esnext/es6-module-transpiler/pulls). Before we merge 200 | your request, we'll make sure you're in the list of people who have signed a 201 | CLA. 202 | 203 | Thanks, and enjoy living in the ES6 future! 204 | 205 | [Babel]: https://babeljs.io/ 206 | [Rollup]: https://github.com/rollup/rollup 207 | -------------------------------------------------------------------------------- /TRANSITION.md: -------------------------------------------------------------------------------- 1 | # Transitioning from 0.4.x to 0.5.x 2 | 3 | ## API Changes 4 | 5 | The API has completely changed. We still allow transpiling to CommonJS, but any 6 | of the other previously supported formats have been removed. Each output format 7 | is handled by a "formatter", and 3rd-party formatters may be used by using the 8 | `--format` option in the CLI or initialing a `Container` with a particular 9 | `formatter` when using as a library. 10 | 11 | ## Command-line changes 12 | 13 | The transpiler still has a CLI, but it is structured completely differently. 14 | See `compile-modules -h` for details. 15 | 16 | ## Spec compilance 17 | 18 | ### Bindings 19 | 20 | In order to comply with the spec, this project now supports mutable bindings. 21 | For example, given this: 22 | 23 | ```js 24 | export var count = 0; 25 | export function incr() { count++; } 26 | ``` 27 | 28 | And when it's imported, this will work: 29 | 30 | ```js 31 | import { count, incr } from './count'; 32 | assert.equal(count, 0); 33 | incr(); 34 | assert.equal(count, 1); 35 | ``` 36 | 37 | ### Circular References 38 | 39 | Cycles now work properly. Note that not all cases of cycles can be properly 40 | handled - this is simply the nature of cycles. For example, this works: 41 | 42 | ```js 43 | // a.js 44 | import { b } from './b'; 45 | 46 | export function a(n) { 47 | if (n % 2 === 0) { 48 | return b(n); 49 | } else { 50 | return n + 1; 51 | } 52 | } 53 | 54 | // b.js 55 | import { a } from './a'; 56 | 57 | export function b(n) { 58 | if (n % 2 === 0) { 59 | return n; 60 | } else { 61 | return a(n); 62 | } 63 | } 64 | ``` 65 | 66 | This works because neither `a` nor `b` uses the other until sometime "later". 67 | This second example will not work: 68 | 69 | ```js 70 | // a.js 71 | import { b } from './b'; 72 | export var a = b; 73 | 74 | // b.js 75 | import { a } from './a'; 76 | export var b = a; 77 | ``` 78 | 79 | This is a contrived example, obviously, but many more complicated examples 80 | boil down to this same thing. 81 | 82 | # Transitioning from 0.2.x to 0.3.x 83 | 84 | ## default export changes 85 | 86 | ### Spec changes 87 | 88 | `export default foo;` has been removed in favor of `export default = foo`. 89 | 90 | ### Internal changes 91 | 92 | In 0.2.x, the default export was the module's only export. Now, it's internally a named export called `default`: 93 | 94 | ```js 95 | // es6 96 | export default bar; 97 | 98 | //cjs 99 | exports.default = bar; 100 | 101 | // es6 102 | import bar from "bar"; 103 | 104 | // cjs 105 | var bar = require("bar").default; 106 | ``` 107 | 108 | 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: 109 | 110 | ```js 111 | define("my-module", 112 | ["handlebars"], 113 | function (handlebars) { 114 | var handlebars = handlebars.default; 115 | }) 116 | ``` 117 | 118 | ## New features you should use 119 | 120 | * Multi line exports! 121 | 122 | ```js 123 | export default = { 124 | foo: "\n to your heart's content!" 125 | }; 126 | ``` 127 | 128 | * Module keyword! 129 | 130 | ```js 131 | module foo from "foo"; 132 | var namedExport = foo.namedExport; 133 | ``` 134 | 135 | * Mixed default/named exports! 136 | 137 | ```js 138 | export default = "foo"; 139 | export var bar = "bar"; 140 | ``` 141 | 142 | * Bare imports! 143 | 144 | ```js 145 | // executes side effects in "foo" but doesn't import anything 146 | import "foo"; 147 | ``` 148 | 149 | ## Old features you can no longer use 150 | 151 | * Relative pathing in AMD modules has been removed, as it was broken, hacky, and made people sad. 152 | 153 | * CoffeeScript support is removed, sort of. 154 | 155 | If you're using **the original CoffeeScript compiler**, you can use JS passthrough: 156 | 157 | ```coffeescript 158 | foo = "look at this elegant coffeescript!" 159 | 160 | ` 161 | // now we're in sad curly-brace land 162 | export default = foo; 163 | ` 164 | ``` 165 | 166 | You'll then want to transpile your modules using the compiled JS as a base. 167 | 168 | Unfortunately, **this doesn't work with CoffeeScript Redux (or EmberScript)**, because that compiler wraps pass-through in an `eval` call. 169 | -------------------------------------------------------------------------------- /bin/compile-modules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Path = require('path'); 4 | var exe = Path.basename(process.argv[1]); 5 | var getopt = require('posix-getopt'); 6 | var parser = new getopt.BasicParser('h(help)v(version)', process.argv); 7 | var option; 8 | 9 | function usage(puts) { 10 | puts(exe + ' [--help] [--version] []'); 11 | puts(); 12 | puts('Commands'); 13 | puts(); 14 | puts(' convert Converts modules from `import`/`export` to an ES5 equivalent.'); 15 | puts(' help Display help for a given command.'); 16 | } 17 | 18 | function makeWriteLine(stream) { 19 | return function(line) { 20 | if (!line || line[line.length - 1] !== '\n') { 21 | line = (line || '') + '\n'; 22 | } 23 | stream.write(line); 24 | }; 25 | } 26 | 27 | var puts = makeWriteLine(process.stdout); 28 | var eputs = makeWriteLine(process.stderr); 29 | 30 | while ((option = parser.getopt()) !== undefined) { 31 | if (option.error) { 32 | usage(eputs); 33 | process.exit(1); 34 | } 35 | 36 | switch (option.option) { 37 | case 'h': 38 | usage(puts); 39 | process.exit(0); 40 | break; 41 | 42 | case 'v': 43 | puts(exe + ' v' + require(Path.join(__dirname, '../package.json')).version); 44 | process.exit(0); 45 | break; 46 | } 47 | } 48 | 49 | var args = process.argv; 50 | var offset = parser.optind(); 51 | 52 | var commandName = args[offset]; 53 | if (commandName === 'help') { 54 | commandName = args[offset + 1]; 55 | args = ['--help'].concat(args.slice(offset + 2 /* skip 'help' and command name */)); 56 | } else { 57 | args = args.slice(offset + 1); 58 | } 59 | 60 | if (typeof commandName !== 'string') { 61 | usage(puts); 62 | process.exit(1); 63 | } 64 | 65 | var command; 66 | 67 | try { 68 | command = require(Path.join('../lib/cli', commandName)); 69 | } catch (ex) { 70 | usage(eputs); 71 | process.exit(1); 72 | } 73 | 74 | try { 75 | var exitCode = command.run(args, puts, eputs); 76 | process.exit(exitCode); 77 | } catch (ex) { 78 | if (ex.constructor.name === 'AssertionError') { 79 | eputs('error: ' + exe + ' ' + commandName + ' -- ' + ex.message); 80 | process.exit(1); 81 | } else { 82 | throw ex; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/browser/capture_stack_trace_polyfill.js: -------------------------------------------------------------------------------- 1 | var DEFAULT_STACK_TRACE_LIMIT = 10; 2 | 3 | // Polyfill Error.captureStackTrace, which exists only in v8 (Chrome). This is 4 | // used in depd, which is used by ast-types. 5 | if (!Error.captureStackTrace) { 6 | /** 7 | * Adds a 'stack' property to the given object with stack info. 8 | * 9 | * @param obj 10 | * @returns {Error.stack|*} 11 | */ 12 | Error.captureStackTrace = function(obj) { 13 | var stack = new Error().stack; 14 | var prepare = Error.prepareStackTrace; 15 | 16 | if (prepare) { 17 | stack = prepare(stack, parseStack(stack)); 18 | } 19 | 20 | obj.stack = stack; 21 | }; 22 | } 23 | 24 | if (typeof Error.stackTraceLimit === 'undefined') { 25 | Error.stackTraceLimit = DEFAULT_STACK_TRACE_LIMIT; 26 | } 27 | 28 | /** 29 | * Parse a formatted stack string into an array of call sites. 30 | * 31 | * @param {string} stack 32 | * @returns {Array.} 33 | */ 34 | function parseStack(stack) { 35 | return stack.split('\n').slice(0, Error.stackTraceLimit).map(CallSite.parse); 36 | } 37 | 38 | /** 39 | * Represents a call site in a stack trace. 40 | * 41 | * @param {string} fnName 42 | * @param {string} fileName 43 | * @param {number} lineNumber 44 | * @param {number} columnNumber 45 | * @param {boolean} isEval 46 | * @param {string} evalOrigin 47 | * @constructor 48 | */ 49 | function CallSite(fnName, fileName, lineNumber, columnNumber, isEval, evalOrigin) { 50 | this.getFunctionName = function() { return fnName; }; 51 | this.getFileName = function() { return fileName; }; 52 | this.getLineNumber = function() { return lineNumber; }; 53 | this.getColumnNumber = function() { return columnNumber; }; 54 | this.isEval = function() { return isEval; }; 55 | this.getEvalOrigin = function() { return evalOrigin; }; 56 | } 57 | 58 | /** 59 | * Parses a line in a formatted stack trace and returns call site info. 60 | * 61 | * @param {string} stackTraceLine 62 | * @returns {CallSite} 63 | */ 64 | CallSite.parse = function(stackTraceLine) { 65 | var fnNameAndLocation = stackTraceLine.split('@'); 66 | var fnName = fnNameAndLocation[0]; 67 | var location = fnNameAndLocation[1]; 68 | 69 | var fileAndLineAndColumn = location ? location.split(':') : []; 70 | var fileName = fileAndLineAndColumn[0]; 71 | var lineNumber = parseInt(fileAndLineAndColumn[1], 10); 72 | var columnNumber = parseInt(fileAndLineAndColumn[2], 10); 73 | 74 | return new CallSite(fnName, fileName, lineNumber, columnNumber, fnName === 'eval', ''); 75 | }; 76 | -------------------------------------------------------------------------------- /lib/browser/fs.js: -------------------------------------------------------------------------------- 1 | var Fs = require('fake-fs'); 2 | var fs = new Fs(); 3 | fs.patch(); 4 | module.exports = fs; 5 | -------------------------------------------------------------------------------- /lib/browser/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill process.umask() since browserify doesn't add it (yet). 2 | // Remove it once https://github.com/defunctzombie/node-process/pull/22 is 3 | // merged and included in browserify. 4 | process.umask = function() { return 0; }; 5 | 6 | require('./capture_stack_trace_polyfill'); 7 | 8 | var Container = require('../container'); 9 | var FileResolver = require('../file_resolver'); 10 | var formatters = require('../formatters'); 11 | var Module = require('../module'); 12 | 13 | exports.FileResolver = FileResolver; 14 | exports.Container = Container; 15 | exports.formatters = formatters; 16 | exports.Module = Module; 17 | 18 | // Export the fake filesystem so someone can get stuff in and out. 19 | exports.fs = require('./fs'); 20 | 21 | -------------------------------------------------------------------------------- /lib/cli/convert.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var fs = require('fs'); 4 | var assert = require('assert'); 5 | var Path = require('path'); 6 | var recast = require('recast'); 7 | 8 | var formatters = require('../formatters'); 9 | var Container = require('../container'); 10 | var FileResolver = require('../file_resolver'); 11 | 12 | var getopt = require('posix-getopt'); 13 | var exe = Path.basename(process.argv[1]); 14 | 15 | exports.run = function(args, puts, eputs) { 16 | var offset = 0; 17 | 18 | var files = []; 19 | var includePaths = [process.cwd()]; 20 | var output; 21 | var formatter = formatters[formatters.DEFAULT]; 22 | var resolverClasses = [FileResolver]; 23 | 24 | while (offset < args.length) { 25 | var parser = new getopt.BasicParser('h(help)o:(output)I:(include)f:(format)r:(resolver)', ['', ''].concat(args.slice(offset))); 26 | var option; 27 | 28 | while ((option = parser.getopt()) !== undefined) { 29 | if (option.error) { 30 | usage(eputs); 31 | return 1; 32 | } 33 | 34 | switch (option.option) { 35 | case 'h': 36 | usage(puts); 37 | return 0; 38 | 39 | case 'o': 40 | output = option.optarg; 41 | break; 42 | 43 | case 'I': 44 | includePaths.push(option.optarg); 45 | break; 46 | 47 | case 'f': 48 | formatter = formatters[option.optarg]; 49 | if (!formatter) { 50 | try { formatter = require(option.optarg); } 51 | catch (ex) {} 52 | } 53 | if (!formatter) { 54 | eputs('Cannot find formatter for ' + option.optarg); 55 | usage(eputs); 56 | return 1; 57 | } 58 | break; 59 | 60 | case 'r': 61 | try { 62 | var resolverPath = option.optarg; 63 | if (fs.existsSync(resolverPath)) { 64 | resolverClasses.push(require(Path.join(process.cwd(), resolverPath))); 65 | } else { 66 | resolverClasses.push(require(option.optarg)); 67 | } 68 | } 69 | catch (ex) { 70 | eputs('Error reading resolver ' + option.optarg + ': ' + ex); 71 | usage(eputs); 72 | return 1; 73 | } 74 | break; 75 | } 76 | } 77 | 78 | for (offset += parser.optind() - 2; args[offset] && args[offset][0] !== '-'; offset++) { 79 | files.push(args[offset]); 80 | } 81 | } 82 | 83 | assert.ok( 84 | files.length > 0, 85 | 'Please provide at least one file to convert.' 86 | ); 87 | 88 | if (typeof formatter === 'function') { 89 | formatter = new formatter(); 90 | } 91 | 92 | var resolvers = resolverClasses.map(function(resolverClass) { 93 | return new resolverClass(includePaths); 94 | }); 95 | var container = new Container({ 96 | formatter: formatter, 97 | resolvers: resolvers 98 | }); 99 | 100 | files.forEach(function(file) { 101 | container.getModule(file); 102 | }); 103 | 104 | if (output) { 105 | container.write(output); 106 | } else { 107 | var outputs = container.convert(); 108 | assert.equal( 109 | outputs.length, 1, 110 | 'Cannot output ' + outputs.length + ' files to stdout. ' + 111 | 'Please use the --output flag to specify where to put the ' + 112 | 'files or choose a formatter that concatenates.' 113 | ); 114 | process.stdout.write(recast.print(outputs[0]).code); 115 | } 116 | 117 | }; 118 | 119 | function bold(string) { 120 | return '\x1b[01m' + string + '\x1b[0m'; 121 | } 122 | 123 | function usage(puts) { 124 | puts(exe + ' convert [-I ] [-o ] [-f ] [-r ] [ ...]'); 125 | puts(); 126 | puts(bold('Description')); 127 | puts(); 128 | puts(' Converts the given modules by changing `import`/`export` statements to an ES5 equivalent.'); 129 | puts(); 130 | puts(bold('Options')); 131 | puts(); 132 | puts(' -I, --include Check the given path for imported modules (usable multiple times).'); 133 | puts(' -o, --output File or directory to output converted files.'); 134 | puts(' -f, --format Path to custom formatter or choose from built-in formats.'); 135 | puts(' -r, --resolver Path to custom resolver (usable multiple times).'); 136 | puts(' -h, --help Show this help message.'); 137 | puts(); 138 | puts(bold('Formats')); 139 | puts(); 140 | puts(' commonjs - convert modules to files using CommonJS `require` and `exports` objects.'); 141 | puts(' bundle - concatenate modules into a single file.'); 142 | puts(); 143 | puts(' You may provide custom a formatter by passing the path to your module to the `--format` option. See the'); 144 | puts(' source of any of the built-in formatters for details on how to build your own.'); 145 | puts(); 146 | puts(bold('Resolvers')); 147 | puts(); 148 | puts(' Resolvers resolve import paths to modules. The default resolver will search the include paths provided'); 149 | puts(' by `--include` arguments and the current working directory. To provide custom resolver logic, pass the'); 150 | puts(' path to your resolver module providing a `resolveModule` function or class with an instance method with'); 151 | puts(' this signature: `resolveModule(importedPath:String, fromModule:?Module, container:Container): Module`.'); 152 | } 153 | -------------------------------------------------------------------------------- /lib/container.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var Path = require('path'); 5 | 6 | var Rewriter = require('./rewriter'); 7 | var Writer = require('./writer'); 8 | var recast = require('recast'); 9 | 10 | /** @typedef {{resolveModule: function(string, Module, Container): Module}} */ 11 | var Resolver; 12 | 13 | /** 14 | * Represents a container of modules for the given options. 15 | * 16 | * @constructor 17 | * @param {{resolvers: Resolver[], formatter: Formatter}} options 18 | */ 19 | function Container(options) { 20 | options = options || {}; 21 | 22 | var formatter = options.formatter; 23 | if (typeof formatter === 'function') { 24 | formatter = new formatter(); 25 | } 26 | 27 | assert.ok( 28 | formatter, 29 | 'missing required option `formatter`' 30 | ); 31 | 32 | var resolvers = options.resolvers; 33 | 34 | assert.ok( 35 | resolvers && resolvers.length > 0, 36 | 'at least one resolver is required' 37 | ); 38 | resolvers.forEach(function(resolver) { 39 | assert.equal( 40 | typeof resolver.resolveModule, 'function', 41 | '`resolver` must have `resolveModule` function: ' + resolver 42 | ); 43 | }); 44 | 45 | Object.defineProperties(this, { 46 | modules: { 47 | value: Object.create(null) 48 | }, 49 | 50 | formatter: { 51 | value: formatter 52 | }, 53 | 54 | resolvers: { 55 | value: resolvers 56 | }, 57 | 58 | options: { 59 | value: options 60 | }, 61 | 62 | basePath: { 63 | value: options.basePath || process.cwd() 64 | }, 65 | 66 | sourceRoot: { 67 | value: options.sourceRoot || '/' 68 | } 69 | }); 70 | } 71 | 72 | /** 73 | * Gets a module by resolving `path`. If `path` is resolved to the same path 74 | * as a previous call, the same object will be returned. 75 | * 76 | * @param {string} importedPath 77 | * @param {Module=} fromModule 78 | * @return {Module} 79 | */ 80 | Container.prototype.getModule = function(importedPath, fromModule) { 81 | for (var i = 0, length = this.resolvers.length; i < length; i++) { 82 | var resolvedModule = this.resolvers[i].resolveModule( 83 | importedPath, 84 | fromModule, 85 | this 86 | ); 87 | 88 | if (resolvedModule) { 89 | this.addModule(resolvedModule); 90 | return resolvedModule; 91 | } 92 | } 93 | 94 | throw new Error( 95 | 'missing module import' + 96 | (fromModule ? ' from ' + fromModule.relativePath : '') + 97 | ' for path: ' + importedPath 98 | ); 99 | }; 100 | 101 | /** 102 | * Adds a module to the internal cache and gives it a unique name. 103 | * 104 | * @private 105 | * @param {Module} mod 106 | */ 107 | Container.prototype.addModule = function(mod) { 108 | if (mod.path in this.modules) { 109 | return; 110 | } 111 | 112 | if (this._convertResult) { 113 | throw new Error( 114 | 'container has already converted contained modules ' + 115 | 'and cannot add new module: ' + mod.path 116 | ); 117 | } 118 | 119 | // We have not seen this module before, so let's give it a unique name. 120 | var modules = this.getModules(); 121 | var name = mod.name; 122 | var baseName = name; 123 | var counter = 0; 124 | var nameExists; 125 | 126 | while (true) { 127 | nameExists = modules.some(function(existingModule) { 128 | return existingModule.name === name; 129 | }); 130 | 131 | if (!nameExists) { 132 | break; 133 | } else { 134 | counter++; 135 | name = baseName + counter; 136 | } 137 | } 138 | 139 | mod.name = name; 140 | delete mod.id; 141 | this.modules[mod.path] = mod; 142 | }; 143 | 144 | /** 145 | * Get a cached module by a resolved path. 146 | * 147 | * @param {string} resolvedPath 148 | * @return {?Module} 149 | */ 150 | Container.prototype.getCachedModule = function(resolvedPath) { 151 | return this.modules[resolvedPath]; 152 | }; 153 | 154 | /** 155 | * Writes the contents of this container to the given path. 156 | * 157 | * @param {string} target 158 | */ 159 | Container.prototype.write = function(target) { 160 | if (!this._convertResult) { 161 | this._convertResult = this.convert(); 162 | } 163 | var files = this._convertResult; 164 | var writer = new Writer(target, { 165 | sourceRoot: this.sourceRoot, 166 | basePath: this.basePath 167 | }); 168 | writer.write(files); 169 | }; 170 | 171 | /** 172 | * Translate and return the contents of this container. 173 | * 174 | * @return {{filename: string, code: string, map: object}[]} 175 | */ 176 | Container.prototype.transform = function() { 177 | if (!this._convertResult) { 178 | this._convertResult = this.convert(); 179 | } 180 | 181 | var files = this._convertResult; 182 | var codes = []; 183 | 184 | files.forEach(function(file) { 185 | var rendered = recast.print(file, { 186 | sourceMapName: Path.relative(this.basePath, file.filename), 187 | sourceRoot: this.sourceRoot 188 | }); 189 | var code = rendered.code; 190 | var map = rendered.map; 191 | 192 | codes.push({ 193 | filename: file.filename, 194 | code: code, 195 | map: map 196 | }); 197 | }, this); 198 | 199 | return codes; 200 | }; 201 | 202 | /** 203 | * Converts the contents of this container using the current formatter. 204 | * 205 | * @return {File[]} 206 | */ 207 | Container.prototype.convert = function() { 208 | if (this.formatter.beforeConvert) { 209 | this.formatter.beforeConvert(this); 210 | } 211 | 212 | var modules = this.getModules(); 213 | 214 | var rewriter = new Rewriter(this.formatter); 215 | rewriter.rewrite(modules); 216 | 217 | var formatter = this.formatter; 218 | return formatter.build(modules); 219 | }; 220 | 221 | /** 222 | * Follows all imports/exports looking for new modules to add to this container. 223 | */ 224 | Container.prototype.findImportedModules = function() { 225 | var knownModules; 226 | var lastModuleCount = 0; 227 | 228 | while ((knownModules = this.getModules()).length !== lastModuleCount) { 229 | lastModuleCount = knownModules.length; 230 | for (var i = 0; i < lastModuleCount; i++) { 231 | // Force loading of imported modules. 232 | noop(knownModules[i].imports.modules); 233 | } 234 | } 235 | }; 236 | 237 | /** 238 | * Gets the modules in this container in no particular order. 239 | * 240 | * @return {Module[]} 241 | */ 242 | Container.prototype.getModules = function() { 243 | var modules = this.modules; 244 | return Object.keys(modules).map(function(key) { 245 | return modules[key]; 246 | }); 247 | }; 248 | 249 | /** 250 | * Does nothing. This is only here to make JSHint/other static analysis 251 | * tools happy about using getters for side effects. 252 | */ 253 | function noop() {} 254 | 255 | module.exports = Container; 256 | -------------------------------------------------------------------------------- /lib/declaration_info.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var recast = require('recast'); 4 | var types = recast.types; 5 | var n = types.namedTypes; 6 | 7 | /** 8 | * Represents information about a declaration that creates a local binding 9 | * represented by `identifier`. For example, given that `declaration` is the 10 | * following variable declaration: 11 | * 12 | * var a = 1; 13 | * 14 | * Then `identifier` references the `a` node in the variable declaration's 15 | * first declarator. Likewise, given that `declaration` is this function 16 | * declaration: 17 | * 18 | * function add(a, b) {} 19 | * 20 | * Then `identifier` references the `add` node, the declaration's `id`. 21 | * 22 | * @constructor 23 | * @param {Node} declaration 24 | * @param {Identifier} identifier 25 | */ 26 | function DeclarationInfo(declaration, identifier) { 27 | /** 28 | * @type {Node} 29 | * @name DeclarationInfo#declaration 30 | */ 31 | this.declaration = declaration; 32 | /** 33 | * @type {Identifier} 34 | * @name DeclarationInfo#identifier 35 | */ 36 | this.identifier = identifier; 37 | } 38 | 39 | /** 40 | * Get the declaration info for the given identifier path, if the identifier is 41 | * actually part of a declaration. 42 | * 43 | * @param {NodePath} identifierPath 44 | * @return {?DeclarationInfo} 45 | */ 46 | DeclarationInfo.forIdentifierPath = function(identifierPath) { 47 | if (n.VariableDeclarator.check(identifierPath.parent.node)) { 48 | return new DeclarationInfo( 49 | identifierPath.parent.parent.node, 50 | identifierPath.node 51 | ); 52 | } else if (n.ClassDeclaration.check(identifierPath.parent.node)) { 53 | return new DeclarationInfo( 54 | identifierPath.parent.node, 55 | identifierPath.node 56 | ); 57 | } else if (n.FunctionDeclaration.check(identifierPath.parent.node)) { 58 | return new DeclarationInfo( 59 | identifierPath.parent.node, 60 | identifierPath.node 61 | ); 62 | } else if (n.ImportSpecifier.check(identifierPath.parent.node)) { 63 | return new DeclarationInfo( 64 | identifierPath.parent.parent.node, 65 | identifierPath.node 66 | ); 67 | } else if (n.ImportNamespaceSpecifier.check(identifierPath.parent.node)) { 68 | return new DeclarationInfo( 69 | identifierPath.parent.parent.node, 70 | identifierPath.node 71 | ); 72 | } else if (n.ImportDefaultSpecifier.check(identifierPath.parent.node)) { 73 | return new DeclarationInfo( 74 | identifierPath.parent.parent.node, 75 | identifierPath.node 76 | ); 77 | } else { 78 | return null; 79 | } 80 | }; 81 | 82 | module.exports = DeclarationInfo; 83 | -------------------------------------------------------------------------------- /lib/exports.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | var recast = require('recast'); 6 | var types = recast.types; 7 | var n = types.namedTypes; 8 | 9 | var ModuleBindingList = require('./module_binding_list'); 10 | var ModuleBindingDeclaration = require('./module_binding_declaration'); 11 | var ModuleBindingSpecifier = require('./module_binding_specifier'); 12 | var DeclarationInfo = require('./declaration_info'); 13 | 14 | var utils = require('./utils'); 15 | var memo = utils.memo; 16 | var extend = utils.extend; 17 | var sourcePosition = utils.sourcePosition; 18 | 19 | /** 20 | * Represents a list of the exports for the given module. 21 | * 22 | * @constructor 23 | * @extends ModuleBindingList 24 | * @param {Module} mod 25 | */ 26 | function ExportDeclarationList(mod) { 27 | ModuleBindingList.call(this, mod); 28 | } 29 | extend(ExportDeclarationList, ModuleBindingList); 30 | 31 | /** 32 | * @private 33 | * @param {AST.Declaration} node 34 | * @return {boolean} 35 | */ 36 | ExportDeclarationList.prototype.isMatchingBinding = function(node) { 37 | return n.ExportDeclaration.check(node); 38 | }; 39 | 40 | /** 41 | * Gets an export declaration for the given `node`. 42 | * 43 | * @private 44 | * @param {AST.ExportDeclaration} node 45 | * @return {ExportDeclaration} 46 | */ 47 | ExportDeclarationList.prototype.declarationForNode = function(node) { 48 | if (node.default) { 49 | return new DefaultExportDeclaration(this.module, node); 50 | } else if (n.VariableDeclaration.check(node.declaration)) { 51 | return new VariableExportDeclaration(this.module, node); 52 | } else if (n.FunctionDeclaration.check(node.declaration)) { 53 | return new FunctionExportDeclaration(this.module, node); 54 | } else if (n.ClassDeclaration.check(node.declaration)) { 55 | return new ClassExportDeclaration(this.module, node); 56 | } else if (n.ExportBatchSpecifier.check(node.specifiers[0])) { 57 | throw new Error( 58 | '`export *` found at ' + sourcePosition(this.module, node) + 59 | ' is not supported, please use `export { … }` instead' 60 | ); 61 | } else { 62 | return new NamedExportDeclaration(this.module, node); 63 | } 64 | }; 65 | 66 | /** 67 | * @param {NodePath} referencePath 68 | * @return {?ExportSpecifier} 69 | */ 70 | ExportDeclarationList.prototype.findSpecifierForReference = function(referencePath) { 71 | if (n.ExportSpecifier.check(referencePath.parent.node) && referencePath.parent.parent.node.source) { 72 | // This is a direct export from another module, e.g. `export { foo } from 'foo'`. 73 | return /** @type {ExportSpecifier} */this.findSpecifierByIdentifier(referencePath.node); 74 | } 75 | 76 | var declaration = this.findDeclarationForReference(referencePath); 77 | 78 | if (!declaration) { 79 | return null; 80 | } 81 | 82 | var specifier = /** @type {ExportSpecifier} */this.findSpecifierByName(declaration.node.name); 83 | assert.ok( 84 | specifier, 85 | 'no specifier found for `' + referencePath.node.name + '`! this should not happen!' 86 | ); 87 | return specifier; 88 | }; 89 | 90 | /** 91 | * Contains information about an export declaration. 92 | * 93 | * @constructor 94 | * @abstract 95 | * @extends ModuleBindingDeclaration 96 | * @param {Module} mod 97 | * @param {ExportDeclaration} node 98 | */ 99 | function ExportDeclaration(mod, node) { 100 | assert.ok( 101 | n.ExportDeclaration.check(node), 102 | 'expected an export declaration, got ' + (node && node.type) 103 | ); 104 | 105 | ModuleBindingDeclaration.call(this, mod, node); 106 | } 107 | extend(ExportDeclaration, ModuleBindingDeclaration); 108 | 109 | /** 110 | * Returns a string description suitable for debugging. 111 | * 112 | * @return {string} 113 | */ 114 | ExportDeclaration.prototype.inspect = function() { 115 | return recast.print(this.node).code; 116 | }; 117 | 118 | /** 119 | * @see ExportDeclaration#inspect 120 | */ 121 | ExportDeclaration.prototype.toString = ExportDeclaration.prototype.inspect; 122 | 123 | /** 124 | * Represents an export declaration of the form: 125 | * 126 | * export default foo; 127 | * 128 | * @constructor 129 | * @extends ExportDeclaration 130 | * @param {Module} mod 131 | * @param {AST.ExportDeclaration} node 132 | */ 133 | function DefaultExportDeclaration(mod, node) { 134 | ExportDeclaration.call(this, mod, node); 135 | } 136 | extend(DefaultExportDeclaration, ExportDeclaration); 137 | 138 | /** 139 | * Contains a list of specifier name information for this export. 140 | * 141 | * @type {ExportSpecifier[]} 142 | * @name DefaultExportSpecifier#specifiers 143 | */ 144 | memo(DefaultExportDeclaration.prototype, 'specifiers', /** @this DefaultExportDeclaration */function() { 145 | var specifier = new DefaultExportSpecifier(this, this.node.declaration); 146 | return [specifier]; 147 | }); 148 | 149 | /** 150 | * Represents an export declaration of the form: 151 | * 152 | * export { foo, bar }; 153 | * 154 | * @constructor 155 | * @extends ExportDeclaration 156 | * @param {Module} mod 157 | * @param {AST.ExportDeclaration} node 158 | */ 159 | function NamedExportDeclaration(mod, node) { 160 | ExportDeclaration.call(this, mod, node); 161 | } 162 | extend(NamedExportDeclaration, ExportDeclaration); 163 | 164 | /** 165 | * Contains a list of specifier name information for this export. 166 | * 167 | * @type {ExportSpecifier[]} 168 | * @name NamedExportDeclaration#specifiers 169 | */ 170 | memo(NamedExportDeclaration.prototype, 'specifiers', /** @this NamedExportDeclaration */function() { 171 | var self = this; 172 | return this.node.specifiers.map(function(specifier) { 173 | return new ExportSpecifier(self, specifier); 174 | }); 175 | }); 176 | 177 | /** 178 | * Represents an export declaration of the form: 179 | * 180 | * export var foo = 1; 181 | * 182 | * @constructor 183 | * @extends ExportDeclaration 184 | * @param {Module} mod 185 | * @param {AST.ExportDeclaration} node 186 | */ 187 | function VariableExportDeclaration(mod, node) { 188 | ExportDeclaration.call(this, mod, node); 189 | } 190 | extend(VariableExportDeclaration, ExportDeclaration); 191 | 192 | /** 193 | * Gets the list of export specifiers for this declaration. 194 | * 195 | * @type {ExportSpecifier[]} 196 | * @name VariableExportDeclaration#specifiers 197 | */ 198 | memo(VariableExportDeclaration.prototype, 'specifiers', /** @this VariableExportDeclaration */function() { 199 | var self = this; 200 | return this.node.declaration.declarations.map(function(declarator) { 201 | return new ExportSpecifier(self, declarator); 202 | }); 203 | }); 204 | 205 | /** 206 | * Represents an export declaration of the form: 207 | * 208 | * export class Foo {} 209 | * 210 | * @constructor 211 | * @extends ExportDeclaration 212 | * @param {Module} mod 213 | * @param {AST.ExportDeclaration} node 214 | */ 215 | function ClassExportDeclaration(mod, node) { 216 | ExportDeclaration.call(this, mod, node); 217 | } 218 | extend(ClassExportDeclaration, ExportDeclaration); 219 | 220 | /** 221 | * Gets the list of export specifiers for this declaration. 222 | * 223 | * @type {ExportSpecifier[]} 224 | * @name ClassExportDeclaration#specifiers 225 | */ 226 | memo(ClassExportDeclaration.prototype, 'specifiers', /** @this ClassExportDeclaration */function() { 227 | return [new ExportSpecifier(this, this.node.declaration)]; 228 | }); 229 | 230 | /** 231 | * Represents an export declaration of the form: 232 | * 233 | * export function foo() {} 234 | * 235 | * @constructor 236 | * @extends ExportDeclaration 237 | * @param {Module} mod 238 | * @param {AST.ExportDeclaration} node 239 | */ 240 | function FunctionExportDeclaration(mod, node) { 241 | ExportDeclaration.call(this, mod, node); 242 | } 243 | extend(FunctionExportDeclaration, ExportDeclaration); 244 | 245 | /** 246 | * Gets the list of export specifiers for this declaration. 247 | * 248 | * @type {ExportSpecifier[]} 249 | * @name FunctionExportDeclaration#specifiers 250 | */ 251 | memo(FunctionExportDeclaration.prototype, 'specifiers', /** @this FunctionExportDeclaration */function() { 252 | return [new ExportSpecifier(this, this.node.declaration)]; 253 | }); 254 | 255 | /** 256 | * Represents an export specifier in an export declaration. 257 | * 258 | * @constructor 259 | * @extends ModuleBindingSpecifier 260 | * @param {ExportDeclaration} declaration 261 | * @param {AST.Node} node 262 | */ 263 | function ExportSpecifier(declaration, node) { 264 | ModuleBindingSpecifier.call(this, declaration, node); 265 | } 266 | extend(ExportSpecifier, ModuleBindingSpecifier); 267 | 268 | /** 269 | * Contains the local declaration info for this export specifier. For example, 270 | * in this module: 271 | * 272 | * var a = 1; 273 | * export { a }; 274 | * 275 | * The module declaration info for the `a` export specifier is the variable 276 | * declaration plus the `a` identifier in its first declarator. 277 | * 278 | * @type {?DeclarationInfo} 279 | * @name ExportSpecifier#moduleDeclaration 280 | */ 281 | memo(ExportSpecifier.prototype, 'moduleDeclaration', /** @this ExportSpecifier */function() { 282 | if (this.declaration.source) { 283 | // This is part of a direct export, e.g. `export { ... } from '...'`, so 284 | // there is no declaration as part of this module. 285 | return null; 286 | } 287 | 288 | var bindings = this.moduleScope.getBindings(); 289 | var identifierPaths = bindings[this.from]; 290 | assert.ok( 291 | identifierPaths && identifierPaths.length === 1, 292 | 'expected exactly one declaration for export `' + 293 | this.from + '` at ' + sourcePosition(this.module, this.node) + 294 | ', found ' + (identifierPaths ? identifierPaths.length : 'none') 295 | ); 296 | 297 | var identifierPath = identifierPaths[0]; 298 | var declarationInfo = DeclarationInfo.forIdentifierPath(identifierPath); 299 | assert.ok( 300 | declarationInfo, 301 | 'cannot detect declaration for `' + 302 | identifierPath.node.name + '`, found parent.type `' + 303 | identifierPath.parent.node.type + '`' 304 | ); 305 | 306 | return declarationInfo; 307 | }); 308 | 309 | /** 310 | * Represents an export specifier in a default export declaration. 311 | * 312 | * @constructor 313 | * @extends ExportSpecifier 314 | * @param {ExportDeclaration} declaration 315 | * @param {AST.Expression} node 316 | */ 317 | function DefaultExportSpecifier(declaration, node) { 318 | ExportSpecifier.call(this, declaration, node); 319 | } 320 | extend(DefaultExportSpecifier, ExportSpecifier); 321 | 322 | /** 323 | * The node of a default export specifier is an expression, not a specifier. 324 | * 325 | * @type {AST.Expression} 326 | */ 327 | DefaultExportSpecifier.prototype.node = null; 328 | 329 | /** 330 | * Default export specifier names are always "default". 331 | * 332 | * @type {string} 333 | * @name DefaultExportSpecifier#name 334 | * @default "default" 335 | */ 336 | DefaultExportSpecifier.prototype.name = 'default'; 337 | 338 | /** 339 | * Default export specifiers do not bind to a local identifier. 340 | * 341 | * @type {?Identifier} 342 | * @name DefaultExportSpecifier#identifier 343 | * @default null 344 | */ 345 | DefaultExportSpecifier.prototype.identifier = null; 346 | 347 | /** 348 | * Default export specifiers do not have a local bound name. 349 | * 350 | * @type {?string} 351 | * @name DefaultExportSpecifier#from 352 | * @default null 353 | */ 354 | DefaultExportSpecifier.prototype.from = null; 355 | 356 | /** 357 | * Default export specifiers do not have a local declaration. 358 | * 359 | * @type {?DeclarationInfo} 360 | * @name DefaultExportSpecifier#moduleDeclaration 361 | * @default null 362 | */ 363 | DefaultExportSpecifier.prototype.moduleDeclaration = null; 364 | 365 | module.exports = ExportDeclarationList; 366 | -------------------------------------------------------------------------------- /lib/file_resolver.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var Path = require('path'); 5 | var fs = require('fs'); 6 | 7 | var Module = require('./module'); 8 | 9 | /** 10 | * Provides resolution of absolute paths from module import sources. 11 | * 12 | * @constructor 13 | */ 14 | function FileResolver(paths) { 15 | assert.ok( 16 | paths && paths.length > 0, 17 | 'missing required argument `paths`' 18 | ); 19 | 20 | this.paths = paths.map(function(path) { 21 | return Path.resolve(path); 22 | }); 23 | } 24 | 25 | /** 26 | * Resolves `importedPath` imported by the given module `fromModule` to a 27 | * module. 28 | * 29 | * @param {string} importedPath 30 | * @param {?Module} fromModule 31 | * @param {Container} container 32 | * @return {?Module} 33 | */ 34 | FileResolver.prototype.resolveModule = function(importedPath, fromModule, container) { 35 | var resolvedPath = this.resolvePath(importedPath, fromModule); 36 | if (resolvedPath) { 37 | var cachedModule = container.getCachedModule(resolvedPath); 38 | if (cachedModule) { 39 | return cachedModule; 40 | } else { 41 | if (!Path.extname(importedPath)) { 42 | importedPath += Path.extname(resolvedPath); 43 | } 44 | return new Module(resolvedPath, importedPath, container); 45 | } 46 | } else { 47 | return null; 48 | } 49 | }; 50 | 51 | /** 52 | * Resolves `importedPath` against the importing module `fromModule`, if given, 53 | * within this resolver's paths. 54 | * 55 | * @private 56 | * @param {string} importedPath 57 | * @param {?Module} fromModule 58 | * @return {string} 59 | */ 60 | FileResolver.prototype.resolvePath = function(importedPath, fromModule) { 61 | var paths = this.paths; 62 | 63 | if (importedPath[0] === '.' && fromModule) { 64 | paths = [Path.dirname(fromModule.path)]; 65 | } 66 | 67 | for (var i = 0, length = paths.length; i < length; i++) { 68 | var includePath = paths[i]; 69 | var resolved = Path.resolve(includePath, importedPath); 70 | if (!~resolved.lastIndexOf('.')) { 71 | resolved += '.js'; 72 | } 73 | 74 | if (fs.existsSync(resolved)) { 75 | return resolved; 76 | } 77 | 78 | // edge cases when a module may have dotted filename, i.e. jquery.min.js 79 | // and the module name is passed without the extension 80 | resolved += '.js'; 81 | if (fs.existsSync(resolved)) { 82 | return resolved; 83 | } 84 | } 85 | 86 | return null; 87 | }; 88 | 89 | module.exports = FileResolver; 90 | -------------------------------------------------------------------------------- /lib/formatters.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | exports.DEFAULT = 'bundle'; 4 | exports.bundle = require('./formatters/bundle_formatter'); 5 | exports.commonjs = require('./formatters/commonjs_formatter'); 6 | -------------------------------------------------------------------------------- /lib/formatters/bundle_formatter.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var recast = require('recast'); 4 | var types = recast.types; 5 | var b = types.builders; 6 | var n = types.namedTypes; 7 | 8 | var Replacement = require('../replacement'); 9 | var utils = require('../utils'); 10 | var IIFE = utils.IIFE; 11 | var extend = utils.extend; 12 | var sort = require('../sorting').sort; 13 | var Formatter = require('./formatter'); 14 | 15 | 16 | /** 17 | * The 'bundle' formatter aims to increase the compressibility of the generated 18 | * source, especially by tools such as Google Closure Compiler or UglifyJS. 19 | * For example, given these modules: 20 | * 21 | * ```js 22 | * // a.js 23 | * import { b } from './b'; 24 | * console.log(b); 25 | * 26 | * // b.js 27 | * export var b = 3; 28 | * export var b2 = 6; 29 | * ``` 30 | * 31 | * The final output will be a single file looking something like this: 32 | * 33 | * ```js 34 | * (function() { 35 | * "use strict"; 36 | * // b.js 37 | * var b$$b = 3; 38 | * var b$$b2 = 6; 39 | * 40 | * // a.js 41 | * console.log(b$$b); 42 | * }).call(this); 43 | * ``` 44 | * 45 | * @constructor 46 | */ 47 | function BundleFormatter() { 48 | Formatter.call(this); 49 | } 50 | extend(BundleFormatter, Formatter); 51 | 52 | /** 53 | * This hook is called by the container before it converts its modules. We use 54 | * it to ensure all of the imports are included because we need to know about 55 | * them at compile time. 56 | * 57 | * @param {Container} container 58 | */ 59 | BundleFormatter.prototype.beforeConvert = function(container) { 60 | container.findImportedModules(); 61 | 62 | // Cache all the import and export specifier names. 63 | container.getModules().forEach(function(mod) { 64 | [mod.imports, mod.exports].forEach(function(bindingList) { 65 | bindingList.declarations.forEach(function (declaration) { 66 | declaration.specifiers.forEach(function (specifier) { 67 | specifier.name; 68 | }); 69 | }); 70 | }); 71 | }); 72 | }; 73 | 74 | /** 75 | * @override 76 | */ 77 | BundleFormatter.prototype.build = function(modules) { 78 | modules = sort(modules); 79 | return [b.file(b.program([b.expressionStatement(IIFE( 80 | b.expressionStatement(b.literal('use strict')), 81 | this.buildNamespaceImportObjects(modules), 82 | modules.length === 1 ? 83 | modules[0].ast.program.body : 84 | modules.reduce(function(statements, mod) { 85 | return statements.concat(mod.ast.program.body); 86 | }, []) 87 | ))]))]; 88 | }; 89 | 90 | /** 91 | * Builds a variable declaration that contains declarations of all the namespace 92 | * objects required by `import * as foo from 'foo'` statements. 93 | * 94 | * @private 95 | * @param {Module[]} modules 96 | * @return {?AST.VariableDeclaration} 97 | */ 98 | BundleFormatter.prototype.buildNamespaceImportObjects = function(modules) { 99 | var self = this; 100 | var namespaceImportedModules = []; 101 | 102 | // Collect all the modules imported using a namespace import declaration. 103 | modules.forEach(function(mod) { 104 | mod.imports.namespaceImports.forEach(function(namespaceImportDeclaration) { 105 | var namespaceImportedModule = namespaceImportDeclaration.source; 106 | if (namespaceImportedModules.indexOf(namespaceImportedModule) < 0) { 107 | namespaceImportedModules.push(namespaceImportedModule); 108 | } 109 | }); 110 | }); 111 | 112 | if (namespaceImportedModules.length === 0) { 113 | return null; 114 | } 115 | 116 | /** 117 | * Builds a variable declarator for the given module whose initial value is an 118 | * object with properties for each export from the module being imported. For 119 | * example, given a module "foo" with exports "a" and "b" this object will be 120 | * created: 121 | * 122 | * foo$$ = { 123 | * get a() { 124 | * return foo$$a; 125 | * }, 126 | * 127 | * get b() { 128 | * return foo$$b; 129 | * } 130 | * } 131 | * 132 | * @param {Module} mod 133 | * @returns {AST.VariableDeclarator} 134 | */ 135 | function createDeclaratorForModule(mod) { 136 | return b.variableDeclarator( 137 | b.identifier(mod.id), 138 | b.objectExpression( 139 | mod.exports.declarations.reduce(function(props, exportDeclaration) { 140 | exportDeclaration.specifiers.forEach(function(specifier) { 141 | props.push(createGetterForSpecifier(mod, specifier)); 142 | }); 143 | 144 | return props; 145 | }, []) 146 | ) 147 | ); 148 | } 149 | 150 | /** 151 | * Builds a getter property for retrieving the value of the given export 152 | * specifier at the time it is accessed. For example, given a module "foo" 153 | * with export specifier "a" this property will be created: 154 | * 155 | * get a() { 156 | * return foo$$a; 157 | * } 158 | * 159 | * @param {Module} mod 160 | * @param {ExportSpecifier} specifier 161 | * @returns {AST.Property} 162 | */ 163 | function createGetterForSpecifier(mod, specifier) { 164 | return b.property( 165 | 'get', 166 | b.identifier(specifier.name), 167 | b.functionExpression( 168 | null, 169 | [], 170 | b.blockStatement([ 171 | b.returnStatement(self.reference(mod, specifier.name)) 172 | ]) 173 | ) 174 | ); 175 | } 176 | 177 | return b.variableDeclaration( 178 | 'var', 179 | namespaceImportedModules.map(createDeclaratorForModule) 180 | ); 181 | }; 182 | 183 | /** 184 | * @override 185 | * 186 | * ```js 187 | * export default 188 | * // or 189 | * export default ; 190 | * ``` 191 | */ 192 | BundleFormatter.prototype.defaultExport = function(mod, declaration) { 193 | if (n.FunctionDeclaration.check(declaration) || 194 | n.ClassDeclaration.check(declaration)) { 195 | // export default function foo () {} 196 | // -> becomes: 197 | // function foo () {} 198 | // var default = foo; 199 | var renamedDeclaration = Object.create(declaration); 200 | renamedDeclaration.id = this.reference(mod, declaration.id); 201 | return [ 202 | renamedDeclaration, 203 | b.variableDeclaration( 204 | 'var', 205 | [b.variableDeclarator( 206 | this.reference(mod, 'default'), 207 | this.reference(mod, declaration.id) 208 | )] 209 | ) 210 | ]; 211 | } 212 | return b.variableDeclaration( 213 | 'var', 214 | [b.variableDeclarator( 215 | this.reference(mod, 'default'), 216 | declaration 217 | )] 218 | ); 219 | }; 220 | 221 | /** 222 | * Get a reference to the original exported value referenced in `mod` at 223 | * `referencePath`. If the given reference path does not correspond to an 224 | * export, we do not need to rewrite the reference. For example, since `value` 225 | * is not exported it does not need to be rewritten: 226 | * 227 | * ```js 228 | * // a.js 229 | * var value = 99; 230 | * console.log(value); 231 | * ``` 232 | * 233 | * If `value` was exported then we would need to rewrite it: 234 | * 235 | * ```js 236 | * // a.js 237 | * export var value = 3; 238 | * console.log(value); 239 | * ``` 240 | * 241 | * In this case we re-write both `value` references to something like 242 | * `a$$value`. The tricky part happens when we re-export an imported binding: 243 | * 244 | * ```js 245 | * // a.js 246 | * export var value = 11; 247 | * 248 | * // b.js 249 | * import { value } from './a'; 250 | * export { value }; 251 | * 252 | * // c.js 253 | * import { value } from './b'; 254 | * console.log(value); 255 | * ``` 256 | * 257 | * The `value` reference in a.js will be rewritten as something like `a$$value` 258 | * as expected. The `value` reference in c.js will not be rewritten as 259 | * `b$$value` despite the fact that it is imported from b.js. This is because 260 | * we must follow the binding through to its import from a.js. Thus, our 261 | * `value` references will both be rewritten to `a$$value` to ensure they 262 | * match. 263 | * 264 | * @override 265 | */ 266 | BundleFormatter.prototype.exportedReference = function(mod, referencePath) { 267 | var specifier = mod.exports.findSpecifierForReference(referencePath); 268 | if (specifier) { 269 | specifier = specifier.terminalExportSpecifier; 270 | return this.reference(specifier.module, specifier.name); 271 | } else { 272 | return null; 273 | } 274 | }; 275 | 276 | /** 277 | * Get a reference to the original exported value referenced in `mod` at 278 | * `referencePath`. This is very similar to BundleFormatter#exportedReference 279 | * in its approach. 280 | * 281 | * @override 282 | */ 283 | BundleFormatter.prototype.importedReference = function(mod, referencePath) { 284 | var specifier = mod.imports.findSpecifierForReference(referencePath); 285 | 286 | if (!specifier) { 287 | return null; 288 | } 289 | 290 | if (specifier.from) { 291 | specifier = specifier.terminalExportSpecifier; 292 | if (n.ImportNamespaceSpecifier.check(specifier.node)) { 293 | // Reference the built namespace object, e.g. mod$$. 294 | return b.identifier(specifier.declaration.source.id); 295 | } 296 | return this.reference(specifier.module, specifier.name); 297 | } else { 298 | return b.identifier(specifier.declaration.source.id); 299 | } 300 | }; 301 | 302 | /** 303 | * If the given reference has a local declaration at the top-level then we must 304 | * rewrite that reference to have a module-scoped name. 305 | * 306 | * @param {Module} mod 307 | * @param {NodePath} referencePath 308 | * @return {?Node} 309 | */ 310 | BundleFormatter.prototype.localReference = function(mod, referencePath) { 311 | var scope = referencePath.scope.lookup(referencePath.node.name); 312 | if (scope && scope.isGlobal) { 313 | return this.reference(mod, referencePath.node); 314 | } else { 315 | return null; 316 | } 317 | }; 318 | 319 | /** 320 | * Replaces non-default exports. Exported bindings do not need to be 321 | * replaced with actual statements since they only control how local references 322 | * are renamed within the module. 323 | * 324 | * @override 325 | */ 326 | BundleFormatter.prototype.processExportDeclaration = function(mod, nodePath) { 327 | var node = nodePath.node; 328 | if (n.FunctionDeclaration.check(node.declaration)) { 329 | return Replacement.swaps( 330 | // drop `export` 331 | nodePath, node.declaration 332 | ).and( 333 | // transform the function 334 | this.processFunctionDeclaration(mod, nodePath.get('declaration')) 335 | ); 336 | } else if (n.ClassDeclaration.check(node.declaration)) { 337 | return Replacement.swaps( 338 | // drop `export` 339 | nodePath, node.declaration 340 | ).and( 341 | // transform the class 342 | this.processClassDeclaration(mod, nodePath.get('declaration')) 343 | ); 344 | } else if (n.VariableDeclaration.check(node.declaration)) { 345 | return Replacement.swaps( 346 | // drop `export` 347 | nodePath, node.declaration 348 | ).and( 349 | // transform the variables 350 | this.processVariableDeclaration(mod, nodePath.get('declaration')) 351 | ); 352 | } else if (node.declaration) { 353 | throw new Error( 354 | 'unexpected export style, found a declaration of type: ' + 355 | node.declaration.type 356 | ); 357 | } else { 358 | /** 359 | * This node looks like this: 360 | * 361 | * ```js 362 | * export { foo, bar }; 363 | * ``` 364 | * 365 | * Which means that it has no value in the generated code as its only 366 | * function is to control how imports are rewritten. 367 | */ 368 | return Replacement.removes(nodePath); 369 | } 370 | }; 371 | 372 | /** 373 | * Since named export reassignment is just a local variable, we can ignore it. 374 | * e.g. 375 | * 376 | * ```js 377 | * export var foo = 1; 378 | * foo = 2; 379 | * ``` 380 | * 381 | * @override 382 | */ 383 | BundleFormatter.prototype.processExportReassignment = function(mod, nodePath) { 384 | return null; 385 | }; 386 | 387 | /** 388 | * Rename the top-level function declaration to a unique name. 389 | * 390 | * ```js 391 | * function foo() {} 392 | * ``` 393 | * 394 | * Becomes e.g. 395 | * 396 | * ```js 397 | * function mod$$foo() {} 398 | * ``` 399 | * 400 | * @override 401 | */ 402 | BundleFormatter.prototype.processFunctionDeclaration = function(mod, nodePath) { 403 | return Replacement.swaps( 404 | nodePath.get('id'), 405 | this.reference(mod, nodePath.node.id) 406 | ); 407 | }; 408 | 409 | /** 410 | * Rename the top-level class declaration to a unique name. 411 | * 412 | * ```js 413 | * class Foo {} 414 | * ``` 415 | * 416 | * Becomes e.g. 417 | * 418 | * ```js 419 | * class mod$$Foo {} 420 | * ``` 421 | * 422 | * @override 423 | */ 424 | BundleFormatter.prototype.processClassDeclaration = function(mod, nodePath) { 425 | return Replacement.swaps( 426 | nodePath.get('id'), 427 | this.reference(mod, nodePath.node.id) 428 | ); 429 | }; 430 | 431 | /** 432 | * Since import declarations only control how we rewrite references we can just 433 | * remove them -- they don't turn into any actual statements. 434 | * 435 | * @override 436 | */ 437 | BundleFormatter.prototype.processImportDeclaration = function(mod, nodePath) { 438 | return Replacement.removes(nodePath); 439 | }; 440 | 441 | /** 442 | * Process a variable declaration found at the top level of the module. We need 443 | * to ensure that exported variables are rewritten appropriately, so we may 444 | * need to rewrite some or all of this variable declaration. For example: 445 | * 446 | * ```js 447 | * var a = 1, b, c = 3; 448 | * ... 449 | * export { a, b }; 450 | * ``` 451 | * 452 | * We turn those being exported into assignments as needed, e.g. 453 | * 454 | * ```js 455 | * var c = 3; 456 | * mod$$a = 1; 457 | * ``` 458 | * 459 | * @override 460 | */ 461 | BundleFormatter.prototype.processVariableDeclaration = function(mod, nodePath) { 462 | var self = this; 463 | return Replacement.map( 464 | nodePath.get('declarations'), 465 | function(declaratorPath) { 466 | return Replacement.swaps( 467 | declaratorPath.get('id'), 468 | self.reference(mod, declaratorPath.get('id').node) 469 | ); 470 | } 471 | ); 472 | }; 473 | 474 | /** 475 | * Returns an expression which globally references the export named by 476 | * `identifier` for the given module `mod`. For example: 477 | * 478 | * ```js 479 | * // rsvp/defer.js, export default 480 | * rsvp$defer$$default 481 | * 482 | * // rsvp/utils.js, export function isFunction 483 | * rsvp$utils$$isFunction 484 | * ``` 485 | * 486 | * @param {Module} mod 487 | * @param {Identifier|string} identifier 488 | * @return {Identifier} 489 | * @private 490 | */ 491 | BundleFormatter.prototype.reference = function(mod, identifier) { 492 | return b.identifier( 493 | mod.id + (n.Identifier.check(identifier) ? identifier.name : identifier) 494 | ); 495 | }; 496 | 497 | module.exports = BundleFormatter; 498 | -------------------------------------------------------------------------------- /lib/formatters/commonjs_formatter.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var recast = require('recast'); 5 | var types = recast.types; 6 | var n = types.namedTypes; 7 | var b = types.builders; 8 | var util = require('ast-util'); 9 | 10 | var extend = require('../utils').extend; 11 | var compatMemberExpression = require('../utils').compatMemberExpression; 12 | var Replacement = require('../replacement'); 13 | var Formatter = require('./formatter'); 14 | 15 | /** 16 | * The 'commonjs' setting for referencing exports aims to produce code that can 17 | * be used in environments using the CommonJS module system, such as Node.js. 18 | * 19 | * @constructor 20 | */ 21 | function CommonJSFormatter() { 22 | Formatter.call(this); 23 | } 24 | extend(CommonJSFormatter, Formatter); 25 | 26 | /** 27 | * Convert a list of ordered modules into a list of files. 28 | * 29 | * @param {Module[]} modules Modules in execution order. 30 | * @return {File[]} 31 | */ 32 | CommonJSFormatter.prototype.build = function(modules) { 33 | var self = this; 34 | return modules.map(function(mod) { 35 | var body = mod.ast.program.body; 36 | 37 | var requiresDeclaration = self.buildRequires(mod); 38 | var earlyExportsStatement = self.buildEarlyExports(mod); 39 | var lateExports = self.buildLateExports(mod); 40 | 41 | if (requiresDeclaration) { 42 | body.unshift(requiresDeclaration); 43 | } 44 | 45 | if (earlyExportsStatement) { 46 | body.unshift(earlyExportsStatement); 47 | } 48 | 49 | body.unshift(b.expressionStatement(b.literal('use strict'))); 50 | 51 | if (lateExports) { 52 | body.push(lateExports); 53 | } 54 | 55 | mod.ast.filename = mod.relativePath; 56 | return mod.ast; 57 | }); 58 | }; 59 | 60 | /** 61 | * Process all export bindings which may be exported before any module code is 62 | * actually run, i.e. function declarations. 63 | * 64 | * @param {Module} mod 65 | * @returns {?AST.Statement} 66 | * @private 67 | */ 68 | CommonJSFormatter.prototype.buildEarlyExports = function(mod) { 69 | var assignments = []; 70 | var exportObject = b.identifier('exports'); 71 | 72 | this.forEachExportBinding(mod, function(specifier, name) { 73 | if (!n.FunctionDeclaration.check(specifier.declaration.node.declaration)) { 74 | // Only function declarations are handled as early exports. 75 | return; 76 | } 77 | 78 | assignments.push(b.assignmentExpression( 79 | '=', 80 | compatMemberExpression( 81 | exportObject, 82 | name 83 | ), 84 | b.identifier(specifier.from) 85 | )); 86 | }); 87 | 88 | if (assignments.length > 0) { 89 | return b.expressionStatement( 90 | b.sequenceExpression(assignments) 91 | ); 92 | } else { 93 | return null; 94 | } 95 | }; 96 | 97 | /** 98 | * Process all export bindings which were not exported at the beginning of the 99 | * module, i.e. everything except function declarations. 100 | * 101 | * @param {Module} mod 102 | * @returns {?AST.Statement} 103 | * @private 104 | */ 105 | CommonJSFormatter.prototype.buildLateExports = function(mod) { 106 | var self = this; 107 | var assignments = []; 108 | var exportObject = b.identifier('exports'); 109 | 110 | this.forEachExportBinding(mod, function(specifier, name) { 111 | if (n.FunctionDeclaration.check(specifier.declaration.node.declaration)) { 112 | // Function declarations are handled as early exports. 113 | return; 114 | } 115 | 116 | var from; 117 | 118 | if (specifier.importSpecifier) { 119 | if (n.ImportNamespaceSpecifier.check(specifier.importSpecifier.node)) { 120 | from = b.identifier(specifier.importSpecifier.declaration.source.id) 121 | } else { 122 | from = self.reference( 123 | specifier.importSpecifier.declaration.source, 124 | specifier.importSpecifier.from 125 | ); 126 | } 127 | } else if (specifier.declaration.source) { 128 | from = self.reference( 129 | specifier.declaration.source, 130 | specifier.name 131 | ); 132 | } else { 133 | from = b.identifier(specifier.from); 134 | } 135 | 136 | assignments.push(b.assignmentExpression( 137 | '=', 138 | compatMemberExpression( 139 | exportObject, 140 | name 141 | ), 142 | from 143 | )); 144 | }); 145 | 146 | if (assignments.length > 0) { 147 | return b.expressionStatement( 148 | b.sequenceExpression(assignments) 149 | ); 150 | } else { 151 | return null; 152 | } 153 | }; 154 | 155 | /** 156 | * Iterates over each exported binding and calls `iterator` with its specifier. 157 | * 158 | * @param {Module} mod 159 | * @param {function(ModuleBindingSpecifier, string)} iterator 160 | * @private 161 | */ 162 | CommonJSFormatter.prototype.forEachExportBinding = function(mod, iterator) { 163 | mod.exports.names.forEach(function(name) { 164 | var specifier = mod.exports.findSpecifierByName(name); 165 | 166 | assert.ok( 167 | specifier, 168 | 'no export specifier found for export name `' + 169 | name + '` from ' + mod.relativePath 170 | ); 171 | 172 | if (!specifier.from) { 173 | // Default exports are handled elsewhere. 174 | return; 175 | } 176 | 177 | iterator(specifier, name); 178 | }); 179 | }; 180 | 181 | /** 182 | * Build a series of requires based on the imports (and exports with sources) 183 | * in the given module. 184 | * 185 | * @private 186 | * @param {Module} mod 187 | * @return {?AST.VariableDeclaration} 188 | */ 189 | CommonJSFormatter.prototype.buildRequires = function(mod) { 190 | var declarators = []; 191 | var requiredModules = []; 192 | 193 | [mod.imports, mod.exports].forEach(function(declarations) { 194 | declarations.modules.forEach(function(sourceModule) { 195 | if (requiredModules.indexOf(sourceModule) >= 0) { 196 | return; 197 | } 198 | requiredModules.push(sourceModule); 199 | 200 | var matchingDeclaration; 201 | declarations.declarations.some(function(declaration) { 202 | if (declaration.source === sourceModule) { 203 | matchingDeclaration = declaration; 204 | return true; 205 | } 206 | }); 207 | 208 | assert.ok( 209 | matchingDeclaration, 210 | 'no matching declaration for source module: ' + sourceModule.relativePath 211 | ); 212 | 213 | // `(import|export) { ... } from 'math'` -> `math$$ = require('math')` 214 | declarators.push(b.variableDeclarator( 215 | b.identifier(sourceModule.id), 216 | b.callExpression( 217 | b.identifier('require'), 218 | [b.literal(matchingDeclaration.sourcePath)] 219 | ) 220 | )); 221 | }); 222 | }); 223 | 224 | if (declarators.length > 0) { 225 | return b.variableDeclaration('var', declarators); 226 | } else { 227 | return null; 228 | } 229 | }; 230 | 231 | /** 232 | * @override 233 | * 234 | * ```js 235 | * export default 236 | * // or 237 | * export default ; 238 | * ``` 239 | */ 240 | CommonJSFormatter.prototype.defaultExport = function(mod, declaration) { 241 | if (n.FunctionDeclaration.check(declaration) || 242 | n.ClassDeclaration.check(declaration)) { 243 | // export default function foo () {} 244 | // -> becomes: 245 | // function foo () {} 246 | // export['default'] = foo; 247 | return [ 248 | declaration, 249 | b.expressionStatement(b.assignmentExpression( 250 | '=', 251 | b.memberExpression( 252 | b.identifier('exports'), 253 | b.literal('default'), 254 | true 255 | ), 256 | declaration.id 257 | )) 258 | ]; 259 | } 260 | return b.expressionStatement(b.assignmentExpression( 261 | '=', 262 | b.memberExpression( 263 | b.identifier('exports'), 264 | b.literal('default'), 265 | true 266 | ), 267 | declaration 268 | )); 269 | }; 270 | 271 | /** 272 | * Because exported references are captured via a closure as part of a getter 273 | * on the `exports` object, there's no need to rewrite local references to 274 | * exported values. For example, `value` in this example can stay as is: 275 | * 276 | * // a.js 277 | * export var value = 1; 278 | * 279 | * @override 280 | */ 281 | CommonJSFormatter.prototype.exportedReference = function(mod, referencePath) { 282 | return null; 283 | }; 284 | 285 | /** 286 | * Gets a reference to an imported binding by getting the value from the 287 | * required module on demand. For example, this module: 288 | * 289 | * // b.js 290 | * import { value } from './a'; 291 | * console.log(value); 292 | * 293 | * Would be rewritten to look something like this: 294 | * 295 | * var a$$ = require('./a'); 296 | * console.log(a$$.value): 297 | * 298 | * If the given reference does not refer to an imported binding then no 299 | * rewriting is required and `null` will be returned. 300 | * 301 | * @override 302 | */ 303 | CommonJSFormatter.prototype.importedReference = function(mod, referencePath) { 304 | var specifier = mod.imports.findSpecifierForReference(referencePath); 305 | 306 | if (!specifier) { 307 | return null; 308 | } 309 | 310 | if (specifier.from) { 311 | // import { value } from './a'; 312 | // import a from './a'; 313 | return this.reference( 314 | specifier.declaration.source, 315 | specifier.from 316 | ); 317 | } else { 318 | // import * as a from './a' 319 | return b.identifier(specifier.declaration.source.id); 320 | } 321 | }; 322 | 323 | /** 324 | * We do not need to rewrite references to local declarations. 325 | * 326 | * @override 327 | */ 328 | CommonJSFormatter.prototype.localReference = function(mod, referencePath) { 329 | return null; 330 | }; 331 | 332 | /** 333 | * Replaces non-default exports. For declarations we simply remove the `export` 334 | * keyword. For export declarations that just specify bindings, e.g. 335 | * 336 | * export { a, b }; 337 | * 338 | * we remove them entirely since they'll be handled when we define properties on 339 | * the `exports` object. 340 | * 341 | * @override 342 | */ 343 | CommonJSFormatter.prototype.processExportDeclaration = function(mod, nodePath) { 344 | var node = nodePath.node; 345 | 346 | if (n.FunctionDeclaration.check(node.declaration)) { 347 | return Replacement.swaps(nodePath, node.declaration); 348 | } else if (n.VariableDeclaration.check(node.declaration)) { 349 | return Replacement.swaps(nodePath, node.declaration); 350 | } else if (n.ClassDeclaration.check(node.declaration)) { 351 | return Replacement.swaps(nodePath, node.declaration); 352 | } else if (node.declaration) { 353 | throw new Error('unexpected export style, found a declaration of type: ' + node.declaration.type); 354 | } else { 355 | return Replacement.removes(nodePath); 356 | } 357 | }; 358 | 359 | /** 360 | * We explicitly disallow reassignment because we cannot propagate changes to 361 | * importing modules as we would in ES5, e.g. these are both disallowed: 362 | * 363 | * export var foo = 1; 364 | * foo = 2; 365 | * 366 | * export var bar = 1; 367 | * bar++; 368 | * 369 | * @override 370 | */ 371 | CommonJSFormatter.prototype.processExportReassignment = function(mod, nodePath) { 372 | var node = nodePath.node; 373 | var exportName; 374 | 375 | if (n.AssignmentExpression.check(node)) { 376 | exportName = node.left.name; 377 | } else if (n.UpdateExpression.check(node)) { 378 | exportName = node.argument.name; 379 | } else { 380 | throw new Error('unexpected export reassignment type: ' + node.type); 381 | } 382 | 383 | 384 | if (n.UpdateExpression.check(node) && !node.prefix) { 385 | /** 386 | * The result of `a++` is the value of `a` before it is incremented, so we 387 | * can't just use the result as the new value for `exports.a`. The question 388 | * is whether we need to preserve the result of `a++` or not. In this case, 389 | * we do: 390 | * 391 | * ```js 392 | * foo(a++); 393 | * ``` 394 | * 395 | * But in this case we don't, since the result is ignored: 396 | * 397 | * ```js 398 | * a++; 399 | * ``` 400 | */ 401 | 402 | if (n.ExpressionStatement.check(nodePath.parent.node)) { 403 | // The result is ignored here, so `a++` can become `a++, exports.a = a`. 404 | return Replacement.swaps( 405 | nodePath, 406 | b.sequenceExpression([ 407 | node, 408 | b.assignmentExpression( 409 | '=', 410 | compatMemberExpression( 411 | b.identifier('exports'), 412 | exportName 413 | ), 414 | b.identifier(exportName) 415 | ) 416 | ]) 417 | ); 418 | } else { 419 | // The result is used here, so we need a temporary variable to store it. 420 | var result = util.injectVariable(nodePath.scope, util.uniqueIdentifier(nodePath.scope)); 421 | 422 | return Replacement.swaps( 423 | nodePath, 424 | b.sequenceExpression([ 425 | b.assignmentExpression( 426 | '=', 427 | result, 428 | node 429 | ), 430 | b.assignmentExpression( 431 | '=', 432 | compatMemberExpression( 433 | b.identifier('exports'), 434 | exportName 435 | ), 436 | b.identifier(exportName) 437 | ), 438 | result 439 | ]) 440 | ); 441 | } 442 | } 443 | 444 | /** 445 | * We can use the result of the update/assignment as-is in this case, e.g. 446 | * 447 | * ```js 448 | * foo(++a); 449 | * b = 9; 450 | * ``` 451 | * 452 | * Can become: 453 | * 454 | * ```js 455 | * foo(exports.a = ++a); 456 | * exports.b = b = 9; 457 | * ``` 458 | */ 459 | return Replacement.swaps( 460 | nodePath, 461 | b.assignmentExpression( 462 | '=', 463 | compatMemberExpression( 464 | b.identifier('exports'), 465 | exportName 466 | ), 467 | node 468 | ) 469 | ); 470 | }; 471 | 472 | /** 473 | * Process a function declaration found at the top level of the module. Since 474 | * we do not need to rewrite exported functions, we can leave function 475 | * declarations alone. 476 | * 477 | * @override 478 | */ 479 | CommonJSFormatter.prototype.processFunctionDeclaration = function(mod, nodePath) { 480 | return null; 481 | }; 482 | 483 | /** 484 | * Process a class declaration found at the top level of the module. Since 485 | * we do not need to rewrite exported classes, we can leave class 486 | * declarations alone. 487 | * 488 | * @override 489 | */ 490 | CommonJSFormatter.prototype.processClassDeclaration = function(mod, nodePath) { 491 | return null; 492 | }; 493 | 494 | /** 495 | * Since import declarations only control how we rewrite references we can just 496 | * remove them -- they don't turn into any actual statements. 497 | * 498 | * @override 499 | */ 500 | CommonJSFormatter.prototype.processImportDeclaration = function(mod, nodePath) { 501 | return Replacement.removes(nodePath); 502 | }; 503 | 504 | /** 505 | * Process a variable declaration found at the top level of the module. Since 506 | * we do not need to rewrite exported variables, we can leave variable 507 | * declarations alone. 508 | * 509 | * @override 510 | */ 511 | CommonJSFormatter.prototype.processVariableDeclaration = function(mod, nodePath) { 512 | return null; 513 | }; 514 | 515 | /** 516 | * Returns an expression which globally references the export named by 517 | * `identifier` for the given module `mod`. For example: 518 | * 519 | * // rsvp/defer.js, export default 520 | * rsvp$defer$$['default'] 521 | * 522 | * // rsvp/utils.js, export function isFunction 523 | * rsvp$utils$$.isFunction 524 | * 525 | * @param {Module} mod 526 | * @param {Identifier} identifier 527 | * @return {MemberExpression} 528 | * @private 529 | */ 530 | CommonJSFormatter.prototype.reference = function(mod, identifier) { 531 | return compatMemberExpression( 532 | b.identifier(mod.id), 533 | identifier 534 | ); 535 | }; 536 | 537 | module.exports = CommonJSFormatter; 538 | -------------------------------------------------------------------------------- /lib/formatters/formatter.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | var recast = require('recast'); 6 | var types = recast.types; 7 | var n = types.namedTypes; 8 | var b = types.builders; 9 | 10 | var Replacement = require('../replacement'); 11 | 12 | /** 13 | * This class provides a base for any concrete formatter classes. Subclasses 14 | * of this class will be used by Rewriter to determine how to replace various 15 | * parts of an AST while walking it to achieve conversion from ES6 modules to 16 | * another format. 17 | * 18 | * @constructor 19 | * @abstract 20 | */ 21 | function Formatter() {} 22 | 23 | /** 24 | * Convert a list of ordered modules into a list of files. 25 | * 26 | * @param {Module[]} modules Modules in execution order. 27 | * @return {File[]} 28 | */ 29 | Formatter.prototype.build = function(modules) { 30 | throw new Error('#build must be implemented in subclasses'); 31 | }; 32 | 33 | /** 34 | * Replaces default export declarations with something else. Subclasses will 35 | * generally return a statement that takes the declaration value and stashes 36 | * it somewhere appropriate for the transpiled format, e.g. creates a local 37 | * variable, assigns the value to something, or calls a function with it. 38 | * 39 | * Given an export statement like so: 40 | * 41 | * ```js 42 | * export default foo(bar); 43 | * ``` 44 | * 45 | * This method will be called with the module containing the statement and 46 | * the AST node corresponding to `foo(bar)`. 47 | * 48 | * @param {Module} mod 49 | * @param {Expression} declaration 50 | * @return {Statement} 51 | */ 52 | Formatter.prototype.defaultExport = function(mod, declaration) { 53 | throw new Error('#defaultExport must be implemented in subclasses'); 54 | }; 55 | 56 | /** 57 | * Resolves references to exported bindings. In the example below, if we refer 58 | * to `value` elsewhere in the module then that reference may need to be 59 | * rewritten. This method allows us to configure what it is rewritten to. 60 | * 61 | * ```js 62 | * // a.js 63 | * export var value = 1; 64 | * ``` 65 | * 66 | * Subclasses should return null if the original reference should be left 67 | * intact. 68 | * 69 | * @param {Module} mod 70 | * @param {NodePath} referencePath 71 | * @return {?Expression} 72 | */ 73 | Formatter.prototype.exportedReference = function(mod, referencePath) { 74 | throw new Error('#exportedReference must be implemented in subclasses'); 75 | }; 76 | 77 | /** 78 | * Gets a reference to an imported binding. In this example, we will be called 79 | * with the NodePath for `value` in `console.log(value)`: 80 | * 81 | * ```js 82 | * // b.js 83 | * import { value } from './a'; 84 | * console.log(value); 85 | * ``` 86 | * 87 | * If the given reference does not refer to an imported binding then no 88 | * rewriting is required and `null` should be returned. 89 | * 90 | * @param {Module} mod 91 | * @param {NodePath} referencePath 92 | * @return {?Expression} 93 | */ 94 | Formatter.prototype.importedReference = function(mod, referencePath) { 95 | throw new Error('#importedReference must be implemented in subclasses'); 96 | }; 97 | 98 | /** 99 | * Determines what the given reference should be rewritten to, if anything. 100 | * Subclasses should override this only if they wish to rename bindings not 101 | * associated with imports and exports. 102 | * 103 | * This is used by the bundle formatter, for example, to ensure that bindings 104 | * at module scope are rewritten with unique names to prevent collisions with 105 | * bindings from other modules. 106 | * 107 | * @param {Module} mod 108 | * @param {NodePath} referencePath 109 | * @return {?Node} 110 | */ 111 | Formatter.prototype.localReference = function(mod, referencePath) { 112 | return null; 113 | }; 114 | 115 | /** 116 | * Process a function declaration found at the top level of the module. 117 | * 118 | * @param {Module} mod 119 | * @param {NodePath} nodePath 120 | * @return {?Node[]} 121 | */ 122 | Formatter.prototype.processFunctionDeclaration = function(mod, nodePath) { 123 | throw new Error('#processFunctionDeclaration must be implemented in subclasses'); 124 | }; 125 | 126 | /** 127 | * Process a class declaration found at the top level of the module. 128 | * 129 | * @param {Module} mod 130 | * @param {NodePath} nodePath 131 | * @return {?Node[]} 132 | */ 133 | Formatter.prototype.processClassDeclaration = function(mod, nodePath) { 134 | throw new Error('#processClassDeclaration must be implemented in subclasses'); 135 | }; 136 | 137 | /** 138 | * Process a variable declaration found at the top level of the module. 139 | * 140 | * @param {Module} mod 141 | * @param {NodePath} nodePath 142 | * @return {?Node[]} 143 | */ 144 | Formatter.prototype.processVariableDeclaration = function(mod, nodePath) { 145 | throw new Error('#processVariableDeclaration must be implemented in subclasses'); 146 | }; 147 | 148 | /** 149 | * Replaces non-default exports. These exports are of one of the following 150 | * forms: 151 | * 152 | * ```js 153 | * export var a = 1; 154 | * export function a() {} 155 | * export class a {} 156 | * export { a }; 157 | * ``` 158 | * 159 | * @param {Module} mod 160 | * @param {NodePath} nodePath 161 | * @return {?Replacement} 162 | */ 163 | Formatter.prototype.processExportDeclaration = function(mod, nodePath) { 164 | throw new Error('#processExportDeclaration must be implemented in subclasses'); 165 | }; 166 | 167 | /** 168 | * Process and optionally replace an update to an exported binding. This can 169 | * either be an assignment expression or an update expression, i.e. 170 | * 171 | * ```js 172 | * export var foo = 1; 173 | * foo = 2; 174 | * foo++; 175 | * ``` 176 | * 177 | * @param {Module} mod 178 | * @param {NodePath} nodePath 179 | * @return {?Replacement} 180 | */ 181 | Formatter.prototype.processExportReassignment = function(mod, nodePath) { 182 | throw new Error('#processExportReassignment must be implemented in subclasses'); 183 | }; 184 | 185 | /** 186 | * Optionally replace an import declaration. Subclasses should almost always 187 | * replace import declarations. It may be replaced with a dependency lookup, or 188 | * perhaps with nothing. 189 | * 190 | * @param {Module} mod 191 | * @param {NodePath} nodePath 192 | * @return {?Replacement} 193 | */ 194 | Formatter.prototype.processImportDeclaration = function(mod, nodePath) { 195 | throw new Error('#processImportDeclaration must be implemented in subclasses'); 196 | }; 197 | 198 | module.exports = Formatter; 199 | -------------------------------------------------------------------------------- /lib/imports.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var recast = require('recast'); 5 | var types = recast.types; 6 | var n = types.namedTypes; 7 | 8 | var ModuleBindingList = require('./module_binding_list'); 9 | var ModuleBindingDeclaration = require('./module_binding_declaration'); 10 | var ModuleBindingSpecifier = require('./module_binding_specifier'); 11 | 12 | var utils = require('./utils'); 13 | var memo = utils.memo; 14 | var extend = utils.extend; 15 | var sourcePosition = utils.sourcePosition; 16 | 17 | /** 18 | * Represents a list of the imports for the given module. 19 | * 20 | * @constructor 21 | * @param {Module} mod 22 | * @extends ModuleBindingList 23 | */ 24 | function ImportDeclarationList(mod) { 25 | ModuleBindingList.call(this, mod); 26 | } 27 | extend(ImportDeclarationList, ModuleBindingList); 28 | 29 | /** 30 | * @private 31 | * @param {AST.Node} node 32 | * @return {boolean} 33 | */ 34 | ImportDeclarationList.prototype.isMatchingBinding = function(node) { 35 | return n.ImportDeclaration.check(node); 36 | }; 37 | 38 | /** 39 | * Gets an import declaration for the given `node`. 40 | * 41 | * @private 42 | * @param {AST.ImportDeclaration} node 43 | * @return {ImportDeclaration} 44 | */ 45 | ImportDeclarationList.prototype.declarationForNode = function(node) { 46 | return new ImportDeclaration(this.module, node); 47 | }; 48 | 49 | /** 50 | * Gets the namespace imports from the list of imports. 51 | * 52 | * @private 53 | * @type {ImportDeclaration[]} 54 | * @name ImportDeclaration#namespaceImports 55 | */ 56 | memo(ImportDeclarationList.prototype, 'namespaceImports', /** @this ImportDeclarationList */function() { 57 | return this.declarations.filter(function(declaration) { 58 | return declaration.hasNamespaceImport; 59 | }); 60 | }); 61 | 62 | /** 63 | * Contains information about an import declaration. 64 | * 65 | * ```js 66 | * import foo from 'math'; 67 | * import { sin, cos } from 'math'; 68 | * import * as bar from 'math'; 69 | * import foo, { sin, cos } from 'math'; 70 | * import foo, * as bar from 'math'; 71 | * ``` 72 | * 73 | * @constructor 74 | * @abstract 75 | * @param {Module} mod 76 | * @param {AST.ImportDeclaration} node 77 | * @extends ModuleBindingDeclaration 78 | */ 79 | function ImportDeclaration(mod, node) { 80 | assert.ok( 81 | n.ImportDeclaration.check(node), 82 | 'expected an import declaration, got ' + (node && node.type) 83 | ); 84 | 85 | ModuleBindingDeclaration.call(this, mod, node); 86 | } 87 | extend(ImportDeclaration, ModuleBindingDeclaration); 88 | 89 | /** 90 | * Contains a list of specifier name information for this import. 91 | * 92 | * @type {ImportSpecifier[]} 93 | * @name ImportDeclaration#specifiers 94 | */ 95 | memo(ImportDeclaration.prototype, 'specifiers', /** @this ImportDeclaration */function() { 96 | var self = this; 97 | return this.node.specifiers.map(function(specifier) { 98 | if (n.ImportDefaultSpecifier.check(specifier)) { 99 | return new ImportDefaultSpecifier(self, specifier); 100 | } else if (n.ImportNamespaceSpecifier.check(specifier)) { 101 | return new ImportNamespaceSpecifier(self, specifier); 102 | } 103 | return new ImportNamedSpecifier(self, specifier); 104 | }); 105 | }); 106 | 107 | /** 108 | * @type {boolean} 109 | * @name ImportDeclaration#hasNamespaceImport 110 | */ 111 | memo(ImportDeclaration.prototype, 'hasNamespaceImport', /** @this ImportDeclaration */function() { 112 | return this.specifiers.some(function(specifier) { 113 | return specifier instanceof ImportNamespaceSpecifier; 114 | }); 115 | }); 116 | 117 | /** 118 | * Represents an import specifier. The "a" and "b as c" are both import 119 | * specifiers in the following import statement. 120 | * 121 | * import { a, b as c } from "a"; 122 | * 123 | * @constructor 124 | * @extends ModuleBindingSpecifier 125 | * @param {ImportDeclaration} declaration 126 | * @param {AST.ImportNamedSpecifier} node 127 | */ 128 | function ImportNamedSpecifier(declaration, node) { 129 | assert.ok( 130 | declaration instanceof ImportDeclaration, 131 | 'expected an instance of ImportDeclaration' 132 | ); 133 | ModuleBindingSpecifier.call(this, declaration, node); 134 | } 135 | extend(ImportNamedSpecifier, ModuleBindingSpecifier); 136 | 137 | /** 138 | * @type {ExportSpecifier} 139 | * @name ImportNamedSpecifier#exportSpecifier 140 | */ 141 | memo(ImportNamedSpecifier.prototype, 'exportSpecifier', /** @this ImportNamedSpecifier */function() { 142 | var source = this.declaration.source; 143 | assert.ok(source, 'import specifiers must have a valid source'); 144 | var exportSpecifier = source.exports.findSpecifierByName(this.from); 145 | assert.ok( 146 | exportSpecifier, 147 | 'import `' + this.from + '` at ' + 148 | sourcePosition(this.module, this.node) + 149 | ' has no matching export in ' + source.relativePath 150 | ); 151 | return exportSpecifier; 152 | }); 153 | 154 | 155 | /** 156 | * Represents a default import specifier. The "a" in the following import statement. 157 | * 158 | * import a from "a"; 159 | * 160 | * @constructor 161 | * @extends ModuleBindingSpecifier 162 | * @param {ImportDeclaration} declaration 163 | * @param {AST.ImportDefaultSpecifier} node 164 | */ 165 | function ImportDefaultSpecifier(declaration, node) { 166 | assert.ok( 167 | declaration instanceof ImportDeclaration, 168 | 'expected an instance of ImportDeclaration' 169 | ); 170 | ModuleBindingSpecifier.call(this, declaration, node); 171 | } 172 | extend(ImportDefaultSpecifier, ModuleBindingSpecifier); 173 | 174 | memo(ImportDefaultSpecifier.prototype, 'exportSpecifier', /** @this ImportSpecifier */function() { 175 | var source = this.declaration.source; 176 | assert.ok(source, 'import specifiers must have a valid source'); 177 | var exportSpecifier = source.exports.findSpecifierByName(this.from); 178 | assert.ok( 179 | exportSpecifier, 180 | 'import `default` at ' + 181 | sourcePosition(this.module, this.node) + 182 | ' has no matching export in ' + source.relativePath 183 | ); 184 | return exportSpecifier; 185 | }); 186 | 187 | memo(ImportDefaultSpecifier.prototype, 'from', function() { 188 | return 'default'; 189 | }); 190 | 191 | /** 192 | * Represents a namespace import specifier. The "a" in the following import 193 | * statement. 194 | * 195 | * import * as a from "a"; 196 | * 197 | * @constructor 198 | * @extends ModuleBindingSpecifier 199 | * @param {ImportDeclaration} declaration 200 | * @param {AST.ImportNamespaceSpecifier} node 201 | */ 202 | function ImportNamespaceSpecifier(declaration, node) { 203 | assert.ok( 204 | declaration instanceof ImportDeclaration, 205 | 'expected an instance of ImportDeclaration' 206 | ); 207 | ModuleBindingSpecifier.call(this, declaration, node); 208 | } 209 | extend(ImportNamespaceSpecifier, ModuleBindingSpecifier); 210 | 211 | memo(ImportNamespaceSpecifier.prototype, 'exportSpecifier', /** @this ImportNamespaceSpecifier */function() { 212 | var source = this.declaration.source; 213 | assert.ok(source, 'import specifiers must have a valid source'); 214 | return null; 215 | }); 216 | 217 | memo(ImportNamespaceSpecifier.prototype, 'from', function() { 218 | return null; 219 | }); 220 | 221 | module.exports = ImportDeclarationList; 222 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var Container = require('./container'); 4 | var FileResolver = require('./file_resolver'); 5 | var formatters = require('./formatters'); 6 | var Module = require('./module'); 7 | 8 | exports.FileResolver = FileResolver; 9 | exports.Container = Container; 10 | exports.formatters = formatters; 11 | exports.Module = Module; 12 | -------------------------------------------------------------------------------- /lib/module.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var Path = require('path'); 6 | 7 | var esprima = require('esprima-fb'); 8 | var recast = require('recast'); 9 | var types = recast.types; 10 | var n = types.namedTypes; 11 | var b = types.builders; 12 | var NodePath = recast.types.NodePath; 13 | 14 | var ImportDeclarationList = require('./imports'); 15 | var ExportDeclarationList = require('./exports'); 16 | var utils = require('./utils'); 17 | var memo = utils.memo; 18 | var endsWith = utils.endsWith; 19 | 20 | /** 21 | * Represents a JavaScript module at a particular location on disk. 22 | * 23 | * @param {string} path 24 | * @param {string} relativePath 25 | * @param {Container} container 26 | * @constructor 27 | */ 28 | function Module(path, relativePath, container) { 29 | Object.defineProperties(this, { 30 | /** 31 | * @type {string} 32 | * @name Module#path 33 | */ 34 | path: { 35 | value: path, 36 | enumerable: true, 37 | writable: false 38 | }, 39 | 40 | /** 41 | * @type {string} 42 | * @name Module#relativePath 43 | */ 44 | relativePath: { 45 | value: relativePath, 46 | enumerable: true, 47 | writable: false 48 | }, 49 | 50 | /** 51 | * @type {string} 52 | * @name Module#sourceFileName 53 | */ 54 | sourceFileName: { 55 | value: Path.relative(container.basePath, path), 56 | enumerable: true, 57 | writable: false 58 | }, 59 | 60 | /** 61 | * @type {Container} 62 | * @name Module#container 63 | */ 64 | container: { 65 | value: container, 66 | enumerable: true, 67 | writable: false 68 | } 69 | }); 70 | } 71 | 72 | /** 73 | * Clears the cached data for this module. 74 | */ 75 | Module.prototype.reload = function() { 76 | delete this.src; 77 | delete this.ast; 78 | delete this.imports; 79 | delete this.exports; 80 | delete this.scope; 81 | }; 82 | 83 | /** 84 | * The list of imports declared by this module. 85 | * 86 | * @type {ImportDeclarationList} 87 | * @name Module#imports 88 | */ 89 | memo(Module.prototype, 'imports', /** @this Module */function() { 90 | var result = new ImportDeclarationList(this); 91 | result.readProgram(this.ast.program); 92 | return result; 93 | }); 94 | 95 | /** 96 | * The list of exports declared by this module. 97 | * 98 | * @type {ExportDeclarationList} 99 | * @name Module#exports 100 | */ 101 | memo(Module.prototype, 'exports', /** @this Module */function() { 102 | var result = new ExportDeclarationList(this); 103 | result.readProgram(this.ast.program); 104 | return result; 105 | }); 106 | 107 | /** 108 | * This module's scope. 109 | * 110 | * @type {Scope} 111 | * @name Module#scope 112 | */ 113 | memo(Module.prototype, 'scope', /** @this Module */function() { 114 | return new NodePath(this.ast).get('program').get('body').scope; 115 | }); 116 | 117 | /** 118 | * This module's source code represented as an abstract syntax tree. 119 | * 120 | * @type {File} 121 | * @name Module#ast 122 | */ 123 | memo(Module.prototype, 'ast', /** @this Module */function() { 124 | return recast.parse( 125 | this.src, { 126 | esprima: esprima, 127 | sourceFileName: this.sourceFileName 128 | } 129 | ); 130 | }); 131 | 132 | /** 133 | * This module's source code. 134 | * 135 | * @type {String} 136 | * @name Module#src 137 | */ 138 | memo(Module.prototype, 'src', /** @this Module */function() { 139 | return fs.readFileSync(this.path).toString(); 140 | }); 141 | 142 | /** 143 | * A reference to the options from this module's container. 144 | * 145 | * @type {object} 146 | * @name Module#options 147 | */ 148 | memo(Module.prototype, 'options', /** @this Module */function() { 149 | return this.container.options; 150 | }); 151 | 152 | /** 153 | * This module's relative name, like {#relativePath} but without the extension. 154 | * This may be modified by a Container if this Module is part of a Container. 155 | * 156 | * @type {string} 157 | * @name Module#name 158 | */ 159 | memo(Module.prototype, 'name', /** @this Module */function() { 160 | var relativePath = this.relativePath; 161 | if (endsWith(relativePath, '.js')) { 162 | return relativePath.slice(0, -3); 163 | } else { 164 | return relativePath; 165 | } 166 | }); 167 | 168 | /** 169 | * A string suitable for a JavaScript identifier named for this module. 170 | * 171 | * @type {string} 172 | * @name Module#id 173 | */ 174 | memo(Module.prototype, 'id', /** @this Module */function() { 175 | return this.name.replace(/[^\w$_]/g, '$') + '$$'; 176 | }); 177 | 178 | /** 179 | * Gets a Module by path relative to this module. 180 | * 181 | * @param {string} sourcePath 182 | * @return {Module} 183 | */ 184 | Module.prototype.getModule = function(sourcePath) { 185 | return this.container.getModule(sourcePath, this); 186 | }; 187 | 188 | /** 189 | * Generate a descriptive string suitable for debugging. 190 | * 191 | * @return {string} 192 | */ 193 | Module.prototype.inspect = function() { 194 | return '#<' + this.constructor.name + ' ' + this.relativePath + '>'; 195 | }; 196 | 197 | /** 198 | * @see Module#inspect 199 | */ 200 | Module.prototype.toString = Module.prototype.inspect; 201 | 202 | module.exports = Module; 203 | -------------------------------------------------------------------------------- /lib/module_binding_declaration.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | var recast = require('recast'); 6 | var types = recast.types; 7 | var n = types.namedTypes; 8 | 9 | var utils = require('./utils'); 10 | var memo = utils.memo; 11 | 12 | /** 13 | * Contains information about a module binding declaration. This corresponds to 14 | * the shared functionality of `ExportDeclaration` and `ImportDeclaration` in 15 | * the ES6 spec. 16 | * 17 | * @constructor 18 | * @abstract 19 | * @param {Module} mod 20 | * @param {AST.ImportDeclaration|AST.ExportDeclaration} node 21 | */ 22 | function ModuleBindingDeclaration(mod, node) { 23 | assert.ok( 24 | n.ImportDeclaration.check(node) || n.ExportDeclaration.check(node), 25 | 'expected an import or export declaration, got ' + (node && node.type) 26 | ); 27 | 28 | Object.defineProperties(this, { 29 | /** 30 | * @name ModuleBindingDeclaration#node 31 | * @type {AST.ImportDeclaration|AST.ExportDeclaration} 32 | */ 33 | node: { 34 | value: node 35 | }, 36 | 37 | /** 38 | * @name ModuleBindingDeclaration#module 39 | * @type {Module} 40 | */ 41 | module: { 42 | value: mod 43 | } 44 | }); 45 | } 46 | 47 | /** 48 | * Finds the specifier that creates the local binding given by `name`, if one 49 | * exists. Otherwise `null` is returned. 50 | * 51 | * @param {string} name 52 | * @return {?ModuleBindingSpecifier} 53 | */ 54 | ModuleBindingDeclaration.prototype.findSpecifierByName = function(name) { 55 | var specifiers = this.specifiers; 56 | 57 | for (var i = 0, length = specifiers.length; i < length; i++) { 58 | var specifier = specifiers[i]; 59 | if (specifier.name === name) { 60 | return specifier; 61 | } 62 | } 63 | 64 | return null; 65 | }; 66 | 67 | /** 68 | * @param {AST.Identifier} identifier 69 | * @return {?ModuleBindingSpecifier} 70 | */ 71 | ModuleBindingDeclaration.prototype.findSpecifierByIdentifier = function(identifier) { 72 | for (var i = 0, length = this.specifiers.length; i < length; i++) { 73 | var specifier = this.specifiers[i]; 74 | if (specifier.identifier === identifier) { 75 | return specifier; 76 | } 77 | } 78 | 79 | return null; 80 | }; 81 | 82 | /** 83 | * Gets the raw path of the `from` part of the declaration, if present. For 84 | * example: 85 | * 86 | * ```js 87 | * import { map } from "array"; 88 | * ``` 89 | * 90 | * The source path for the above declaration is "array". 91 | * 92 | * @type {?string} 93 | * @name ModuleBindingDeclaration#sourcePath 94 | */ 95 | memo(ModuleBindingDeclaration.prototype, 'sourcePath', /** @this ModuleBindingDeclaration */function() { 96 | return this.node.source ? this.node.source.value : null; 97 | }); 98 | 99 | /** 100 | * Gets a reference to the module referenced by this declaration. 101 | * 102 | * @type {Module} 103 | * @name ModuleBindingDeclaration#source 104 | */ 105 | memo(ModuleBindingDeclaration.prototype, 'source', /** @this ModuleBindingDeclaration */function() { 106 | return this.sourcePath ? this.module.getModule(this.sourcePath) : null; 107 | }); 108 | 109 | /** 110 | * Gets the containing module's scope. 111 | * 112 | * @type {Scope} 113 | * @name ModuleBindingDeclaration#moduleScope 114 | */ 115 | memo(ModuleBindingDeclaration.prototype, 'moduleScope', /** @this ModuleBindingDeclaration */function() { 116 | return this.module.scope; 117 | }); 118 | 119 | /** 120 | * Generate a string representing this object to aid debugging. 121 | * 122 | * @return {string} 123 | */ 124 | ModuleBindingDeclaration.prototype.inspect = function() { 125 | return recast.print(this.node).code; 126 | }; 127 | 128 | /** 129 | * @see ModuleBindingDeclaration#inspect 130 | */ 131 | ModuleBindingDeclaration.prototype.toString = ModuleBindingDeclaration.prototype.inspect; 132 | 133 | module.exports = ModuleBindingDeclaration; 134 | -------------------------------------------------------------------------------- /lib/module_binding_list.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | var utils = require('./utils'); 6 | var memo = utils.memo; 7 | var sourcePosition = utils.sourcePosition; 8 | 9 | /** 10 | * Represents a list of bindings for the given module. This corresponds to the 11 | * shared functionality from `ExportsList` and `ImportsList` from the ES6 spec. 12 | * 13 | * @abstract 14 | * @constructor 15 | * @param {Module} mod 16 | */ 17 | function ModuleBindingList(mod) { 18 | Object.defineProperties(this, { 19 | /** 20 | * @name ModuleBindingList#_nodes 21 | * @type {AST.ImportDeclaration[]|AST.ExportDeclaration[]} 22 | * @private 23 | */ 24 | _nodes: { 25 | value: [] 26 | }, 27 | 28 | /** 29 | * @name ModuleBindingList#module 30 | * @type {Module} 31 | */ 32 | module: { 33 | value: mod 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Add all the binding declarations from the given scope body. Generally this 40 | * should be the Program node's `body` property, an array of statements. 41 | * 42 | * @param {AST.Program} program 43 | */ 44 | ModuleBindingList.prototype.readProgram = function(program) { 45 | var body = program.body; 46 | for (var i = 0; i < body.length; i++) { 47 | if (this.isMatchingBinding(body[i])) { 48 | this.addDeclaration(body[i]); 49 | } 50 | } 51 | }; 52 | 53 | /** 54 | * Adds a declaration to the list. 55 | * 56 | * @private 57 | * @param {AST.ImportDeclaration|AST.ExportDeclaration} node 58 | */ 59 | ModuleBindingList.prototype.addDeclaration = function(node) { 60 | assert.ok( 61 | this.isMatchingBinding(node), 62 | 'expected node to be an declaration, but got ' + 63 | (node && node.type) 64 | ); 65 | this._nodes.push(node); 66 | 67 | // reset the cache 68 | delete this.declarations; 69 | delete this.specifiers; 70 | delete this.modules; 71 | }; 72 | 73 | /** 74 | * Gets the associated module's scope. 75 | * 76 | * @type {Scope} 77 | * @name ModuleBindingList#moduleScope 78 | */ 79 | memo(ModuleBindingList.prototype, 'moduleScope', /** @this ModuleBindingList */function() { 80 | return this.module.scope; 81 | }); 82 | 83 | /** 84 | * Gets all the modules referenced by the declarations in this list. 85 | * 86 | * @type {Module[]} 87 | * @name ModuleBindingList#modules 88 | */ 89 | memo(ModuleBindingList.prototype, 'modules', /** @this ModuleBindingList */function() { 90 | var modules = []; 91 | 92 | this.declarations.forEach(function(declaration) { 93 | if (declaration.source && modules.indexOf(declaration.source) < 0) { 94 | modules.push(declaration.source); 95 | } 96 | }); 97 | 98 | return modules; 99 | }); 100 | 101 | /** 102 | * Finds the specifier that creates the local binding given by `name`, if one 103 | * exists. Otherwise `null` is returned. 104 | * 105 | * @private 106 | * @param {string} name 107 | * @return {?ModuleBindingSpecifier} 108 | */ 109 | ModuleBindingList.prototype.findSpecifierByName = function(name) { 110 | for (var i = 0, length = this.declarations.length; i < length; i++) { 111 | var specifier = this.declarations[i].findSpecifierByName(name); 112 | if (specifier) { return specifier; } 113 | } 114 | 115 | return null; 116 | }; 117 | 118 | /** 119 | * Finds the specifier whose identifier is the given identifier, if one exists. 120 | * Otherwise `null` is returned. 121 | * 122 | * @private 123 | * @param {AST.Identifier} identifier 124 | * @return {?ModuleBindingSpecifier} 125 | */ 126 | ModuleBindingList.prototype.findSpecifierByIdentifier = function(identifier) { 127 | for (var i = 0, length = this.declarations.length; i < length; i++) { 128 | var specifier = this.declarations[i].findSpecifierByIdentifier(identifier); 129 | if (specifier && specifier.identifier === identifier) { 130 | return specifier; 131 | } 132 | } 133 | 134 | return null; 135 | }; 136 | 137 | /** 138 | * @param {NodePath} referencePath 139 | * @return {?ModuleBindingSpecifier} 140 | */ 141 | ModuleBindingList.prototype.findSpecifierForReference = function(referencePath) { 142 | var declaration = this.findDeclarationForReference(referencePath); 143 | 144 | if (!declaration) { 145 | return null; 146 | } 147 | 148 | var specifier = this.findSpecifierByIdentifier(declaration.node); 149 | assert.ok( 150 | specifier, 151 | 'no specifier found for `' + referencePath.node.name + '`! this should not happen!' 152 | ); 153 | return specifier; 154 | }; 155 | 156 | /** 157 | * @private 158 | */ 159 | ModuleBindingList.prototype.findDeclarationForReference = function(referencePath) { 160 | // Check names to avoid traversing scopes for all references. 161 | if (this.names.indexOf(referencePath.node.name) < 0) { 162 | return null; 163 | } 164 | 165 | var node = referencePath.node; 166 | var declaringScope = referencePath.scope.lookup(node.name); 167 | assert.ok( 168 | declaringScope, 169 | '`' + node.name + '` at ' + sourcePosition(this.module, node) + 170 | ' cannot be bound if it is not declared' 171 | ); 172 | 173 | // Bindings are at the top level, so if this isn't then it's shadowing. 174 | if (!declaringScope.isGlobal) { 175 | return null; 176 | } 177 | 178 | var declarations = declaringScope.getBindings()[node.name]; 179 | if (!declarations || declarations.length !== 1) { 180 | throw new SyntaxError( 181 | 'expected one declaration for `' + node.name + 182 | '`, at ' + sourcePosition(this.module, node) + 183 | ' but found ' + (declarations ? declarations.length : 'none') 184 | ); 185 | } 186 | 187 | return declarations[0]; 188 | }; 189 | 190 | /** 191 | * Generate a string representing this object to aid debugging. 192 | * 193 | * @return {string} 194 | */ 195 | ModuleBindingList.prototype.inspect = function() { 196 | var result = '#<' + this.constructor.name; 197 | 198 | result += ' module=' + this.module.relativePath; 199 | 200 | if (this.declarations.length > 0) { 201 | result += ' declarations=' + this.declarations.map(function(imp) { 202 | return imp.inspect(); 203 | }).join(', '); 204 | } 205 | 206 | result += '>'; 207 | return result; 208 | }; 209 | 210 | /** 211 | * @see ModuleBindingList#inspect 212 | */ 213 | ModuleBindingList.prototype.toString = ModuleBindingList.prototype.inspect; 214 | 215 | /** 216 | * Contains a list of declarations. 217 | * 218 | * @type {(ImportDeclaration[]|ExportDeclaration[])} 219 | * @name ModuleBindingList#declarations 220 | */ 221 | memo(ModuleBindingList.prototype, 'declarations', /** @this ModuleBindingList */function() { 222 | var self = this; 223 | 224 | return this._nodes.map(function(child) { 225 | return self.declarationForNode(child); 226 | }); 227 | }); 228 | 229 | /** 230 | * Contains a combined list of names for all the declarations contained in this 231 | * list. 232 | * 233 | * @type {string[]} 234 | * @name ModuleBindingList#names 235 | */ 236 | memo(ModuleBindingList.prototype, 'names', /** @this ModuleBindingList */function() { 237 | return this.declarations.reduce(function(names, decl) { 238 | return names.concat(decl.specifiers.map(function(specifier) { 239 | return specifier.name; 240 | })); 241 | }, []); 242 | }); 243 | 244 | module.exports = ModuleBindingList; 245 | -------------------------------------------------------------------------------- /lib/module_binding_specifier.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | var recast = require('recast'); 6 | var types = recast.types; 7 | var n = types.namedTypes; 8 | 9 | var utils = require('./utils'); 10 | var memo = utils.memo; 11 | var sourcePosition = utils.sourcePosition; 12 | 13 | /** 14 | * A module binding specifier provides the shared functionality of 15 | * ImportSpecifiers and ExportSpecifiers in the ES6 spec. 16 | * 17 | * @constructor 18 | * @param {ModuleBindingDeclaration} declaration 19 | * @param {AST.NamedSpecifier} node 20 | */ 21 | function ModuleBindingSpecifier(declaration, node) { 22 | Object.defineProperties(this, { 23 | /** 24 | * @name ModuleBindingSpecifier#declaration 25 | * @type {ModuleBindingDeclaration} 26 | */ 27 | declaration: { 28 | value: declaration 29 | }, 30 | 31 | /** 32 | * @name ModuleBindingSpecifier#node 33 | * @type {AST.NamedSpecifier} 34 | */ 35 | node: { 36 | value: node 37 | } 38 | }); 39 | } 40 | 41 | /** 42 | * Gets the module this specifier is declared in. 43 | * 44 | * @type Module 45 | * @name ModuleBindingSpecifier#module 46 | */ 47 | memo(ModuleBindingSpecifier.prototype, 'module', /** @this ModuleBindingSpecifier */function() { 48 | return this.declaration.module; 49 | }); 50 | 51 | /** 52 | * Gets the scope at the top level of the module. 53 | * 54 | * @type {Scope} 55 | * @name ModuleBindingSpecifier#moduleScope 56 | */ 57 | memo(ModuleBindingSpecifier.prototype, 'moduleScope', /** @this ModuleBindingSpecifier */function() { 58 | return this.declaration.moduleScope; 59 | }); 60 | 61 | /** 62 | * Gets the name of this specifier. For import specifiers this is the name of 63 | * the binding this specifier will create locally, i.e. "foo" in both of these 64 | * import statements: 65 | * 66 | * import { foo } from "util"; 67 | * import { bar as foo } from "util"; 68 | * 69 | * In export specifiers it is the name of the exported declaration or the alias 70 | * given to an internal name, i.e. "foo" in both of these export statements: 71 | * 72 | * export { bar as foo }; 73 | * export var foo = 1; 74 | * 75 | * @type {string} 76 | * @name ModuleBindingSpecifier#name 77 | */ 78 | memo(ModuleBindingSpecifier.prototype, 'name', /** @this ModuleBindingSpecifier */function() { 79 | return this.identifier.name; 80 | }); 81 | 82 | /** 83 | * Gets the name of the identifier this specifier comes from as distinct from 84 | * `name`. This value will only be set if the local name and the 85 | * imported/exported name differ, i.e. it will be "foo" in these statements: 86 | * 87 | * import { foo as bar } from "util"; 88 | * export { foo as bar }; 89 | * 90 | * And it will be undefined in these statements: 91 | * 92 | * import { foo } from "util"; 93 | * export { foo }; 94 | * 95 | * @type {string} 96 | * @name ModuleBindingSpecifier#from 97 | */ 98 | memo(ModuleBindingSpecifier.prototype, 'from', /** @this ModuleBindingSpecifier */function() { 99 | return this.node.id.name; 100 | }); 101 | 102 | /** 103 | * Gets the node that gives this specifier its name as it would be imported, 104 | * i.e. "foo" in these statements: 105 | * 106 | * import { foo } from "utils"; 107 | * import { bar as foo } from "utils"; 108 | * export { foo }; 109 | * export { bar as foo }; 110 | * 111 | * @type {AST.Identifier} 112 | * @name ModuleBindingSpecifier#identifier 113 | */ 114 | memo(ModuleBindingSpecifier.prototype, 'identifier', /** @this ModuleBindingSpecifier */function() { 115 | return this.node.name || this.node.id; 116 | }); 117 | 118 | /** 119 | * Gets the export specifier corresponding to this specifier. This can be from 120 | * either an import or export declaration, since both can have a "from" part: 121 | * 122 | * import { map } from "array"; 123 | * export { map } from "array"; 124 | * 125 | * In both of the above examples, the export specifier of `map` would be part 126 | * of the export statement in the "array" module that exports it. 127 | * 128 | * @type {?ExportSpecifier} 129 | * @name ModuleBindingSpecifier#exportSpecifier 130 | */ 131 | memo(ModuleBindingSpecifier.prototype, 'exportSpecifier', /** @this ModuleBindingSpecifier */function() { 132 | var source = this.declaration.source; 133 | if (source) { 134 | var exports = source.exports; 135 | return exports.findSpecifierByName(this.from); 136 | } else { 137 | return null; 138 | } 139 | }); 140 | 141 | /** 142 | * Gets the import specifier corresponding to this specifier. This should only 143 | * happen when exporting a binding that is imported in the same module, like so: 144 | * 145 | * import { map } from "array"; 146 | * export { map }; 147 | * 148 | * The `map` export specifier has the `map` import specifier as its 149 | * `importSpecifier` property value. The `map` import specifier has no 150 | * `importSpecifier` property value. 151 | * 152 | * @type {?ImportSpecifier} 153 | * @name ModuleBindingSpecifier#importSpecifier 154 | */ 155 | memo(ModuleBindingSpecifier.prototype, 'importSpecifier', /** @this ModuleBindingSpecifier */function() { 156 | // This may be an export from this module, so find the declaration. 157 | var localExportDeclarationInfo = this.moduleDeclaration; 158 | 159 | if (localExportDeclarationInfo && n.ImportDeclaration.check(localExportDeclarationInfo.declaration)) { 160 | // It was imported then exported with two separate declarations. 161 | var exportModule = this.module; 162 | return exportModule.imports.findSpecifierByIdentifier(localExportDeclarationInfo.identifier); 163 | } else { 164 | return null; 165 | } 166 | }); 167 | 168 | /** 169 | * Gets the original export value by following chains of export/import 170 | * statements. For example: 171 | * 172 | * // a.js 173 | * export var a = 1; 174 | * 175 | * // b.js 176 | * export { a } from "./a"; 177 | * 178 | * // c.js 179 | * import { a } from "./b"; 180 | * export { a }; 181 | * 182 | * // d.js 183 | * import { a } from "./c"; 184 | * 185 | * The terminal export specifier for all of these specifiers is the export in 186 | * a.js, since all of them can be traced back to that one. 187 | * 188 | * @type {ExportSpecifier} 189 | * @name ModuleBindingSpecifier#terminalExportSpecifier 190 | */ 191 | memo(ModuleBindingSpecifier.prototype, 'terminalExportSpecifier', /** @this ModuleBindingSpecifier */function() { 192 | if (this.exportSpecifier) { 193 | // This is true for both imports and exports with a source, e.g. 194 | // `import { foo } from 'foo'` or `export { foo } from 'foo'`. 195 | return this.exportSpecifier.terminalExportSpecifier; 196 | } 197 | 198 | // This is an export from this module, so find the declaration. 199 | var importSpecifier = this.importSpecifier; 200 | if (importSpecifier) { 201 | if (n.ImportNamespaceSpecifier.check(importSpecifier.node)) { 202 | // Namespace imports create a local binding, so they are the terminal. 203 | return importSpecifier; 204 | } 205 | 206 | var nextExportSpecifier = importSpecifier.exportSpecifier; 207 | assert.ok( 208 | nextExportSpecifier, 209 | 'expected matching export in ' + importSpecifier.declaration.source.relativePath + 210 | ' for import of `' + importSpecifier.name + '` at ' + 211 | sourcePosition(this.module, this.moduleDeclaration.identifier) 212 | ); 213 | return nextExportSpecifier.terminalExportSpecifier; 214 | } else { 215 | // It was declared in this module, so we are the terminal export specifier. 216 | return this; 217 | } 218 | }); 219 | 220 | /** 221 | * @type {?DeclarationInfo} 222 | */ 223 | ModuleBindingSpecifier.prototype.moduleDeclaration = null; 224 | 225 | /** 226 | * Gets a string representation of this module binding specifier suitable for 227 | * debugging. 228 | * 229 | * @return {string} 230 | */ 231 | ModuleBindingSpecifier.prototype.inspect = function() { 232 | return '#<' + this.constructor.name + 233 | ' module=' + this.declaration.module.relativePath + 234 | ' name=' + this.name + 235 | ' from=' + this.from + 236 | '>'; 237 | }; 238 | 239 | /** 240 | * @see ModuleBindingSpecifier#inspect 241 | */ 242 | ModuleBindingSpecifier.prototype.toString = ModuleBindingSpecifier.prototype.inspect; 243 | 244 | module.exports = ModuleBindingSpecifier; 245 | -------------------------------------------------------------------------------- /lib/replacement.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var recast = require('recast'); 4 | 5 | /** @typedef [NodePath, AST.Node[]] */ 6 | var ReplacementPair; 7 | 8 | /** 9 | * Represents a replacement of a node path with zero or more nodes. 10 | * 11 | * @constructor 12 | * @param {NodePath=} nodePath 13 | * @param {AST.Node[]=} nodes 14 | */ 15 | function Replacement(nodePath, nodes) { 16 | /** 17 | * @private 18 | * @type {ReplacementPair[]} 19 | */ 20 | this.queue = []; 21 | if (nodePath && nodes) { 22 | this.queue.push([nodePath, nodes]); 23 | } 24 | } 25 | 26 | /** 27 | * Performs the replacement. 28 | */ 29 | Replacement.prototype.replace = function() { 30 | for (var i = 0, length = this.queue.length; i < length; i++) { 31 | var item = this.queue[i]; 32 | item[0].replace.apply(item[0], item[1]); 33 | } 34 | }; 35 | 36 | /** 37 | * Incorporates the replacements from the given Replacement into this one. 38 | * 39 | * @param {Replacement} anotherReplacement 40 | */ 41 | Replacement.prototype.and = function(anotherReplacement) { 42 | this.queue.push.apply(this.queue, anotherReplacement.queue); 43 | return this; 44 | }; 45 | 46 | /** 47 | * Constructs a Replacement that, when run, will remove the node from the AST. 48 | * 49 | * @param {NodePath} nodePath 50 | * @return {Replacement} 51 | */ 52 | Replacement.removes = function(nodePath) { 53 | return new Replacement(nodePath, []); 54 | }; 55 | 56 | /** 57 | * Constructs a Replacement that, when run, will insert the given nodes after 58 | * the one in nodePath. 59 | * 60 | * @param {NodePath} nodePath 61 | * @param {AST.Node[]} nodes 62 | * @return {Replacement} 63 | */ 64 | Replacement.adds = function(nodePath, nodes) { 65 | return new Replacement(nodePath, [nodePath.node].concat(nodes)); 66 | }; 67 | 68 | /** 69 | * Constructs a Replacement that, when run, swaps the node in nodePath with the 70 | * given node or nodes. 71 | * 72 | * @param {NodePath} nodePath 73 | * @param {AST.Node|AST.Node[]} nodes 74 | */ 75 | Replacement.swaps = function(nodePath, nodes) { 76 | if (!Array.isArray(nodes)) { 77 | nodes = [nodes]; 78 | } 79 | return new Replacement(nodePath, nodes); 80 | }; 81 | 82 | Replacement.map = function(nodePaths, callback) { 83 | var result = new Replacement(); 84 | 85 | nodePaths.each(function(nodePath) { 86 | var replacement = callback(nodePath); 87 | if (replacement) { 88 | result.and(replacement); 89 | } 90 | }); 91 | 92 | return result; 93 | }; 94 | 95 | module.exports = Replacement; 96 | -------------------------------------------------------------------------------- /lib/rewriter.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var recast = require('recast'); 5 | var types = recast.types; 6 | var n = types.namedTypes; 7 | var b = types.builders; 8 | var astUtil = require('ast-util'); 9 | 10 | var utils = require('./utils'); 11 | var extend = utils.extend; 12 | var sourcePosition = utils.sourcePosition; 13 | var Replacement = require('./replacement'); 14 | 15 | /** 16 | * Replaces references to local bindings created by `mod`'s imports 17 | * with references to the original value in the source module. 18 | * 19 | * @constructor 20 | * @param {Formatter} formatter 21 | * @extends types.PathVisitor 22 | */ 23 | function Rewriter(formatter) { 24 | types.PathVisitor.call(this); 25 | 26 | Object.defineProperties(this, { 27 | formatter: { 28 | value: formatter 29 | } 30 | }); 31 | } 32 | extend(Rewriter, types.PathVisitor); 33 | 34 | /** 35 | * Rewrites references to all imported and exported bindings according to the 36 | * rules from this rewriter's formatter. For example, this module: 37 | * 38 | * ```js 39 | * import { sin, cos } from './math'; 40 | * import fib from './math/fib'; 41 | * 42 | * assert.equal(sin(0), 0); 43 | * assert.equal(cos(0), 1); 44 | * assert.equal(fib(1), 1); 45 | * ``` 46 | * 47 | * has its references to the imported bindings `sin`, `cos`, and `fib` 48 | * rewritten to reference the source module: 49 | * 50 | * ```js 51 | * assert.equal(math$$.sin(0), 0); 52 | * assert.equal(math$$.cos(0), 1); 53 | * assert.equal(math$fib$$.fib(1), 1); 54 | * ``` 55 | * 56 | * @param {Module[]} modules 57 | */ 58 | Rewriter.prototype.rewrite = function(modules) { 59 | var replacements = []; 60 | 61 | // FIXME: This is just here to ensure that all imports and exports know where 62 | // they came from. We need this because after we re-write the declarations 63 | // will not be there anymore and we'll need to ensure they're cached up front. 64 | modules.forEach(function(mod) { 65 | [mod.exports, mod.imports].forEach(function(declarations) { 66 | declarations.declarations.forEach(function(declaration) { 67 | declaration.specifiers.forEach(function(specifier) { 68 | return specifier.importSpecifier; 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | this.replacements = replacements; 75 | for (var i = 0, length = modules.length; i < length; i++) { 76 | var mod = modules[i]; 77 | if (mod.exports.declarations.length > 0 || mod.imports.declarations.length > 0) { 78 | this.currentModule = mod; 79 | types.visit(mod.ast.program, this); 80 | } else { 81 | types.visit(mod.ast.program, new DeclarationLinterVisitor(mod)); 82 | } 83 | } 84 | this.currentModule = null; 85 | this.replacements = null; 86 | 87 | replacements.forEach(function(replacement) { 88 | if (replacement.replace) { 89 | replacement.replace(); 90 | } else { 91 | var path = replacement.shift(); 92 | path.replace.apply(path, replacement); 93 | } 94 | }); 95 | }; 96 | 97 | /** 98 | * Process all identifiers looking for references to variables in scope. 99 | * 100 | * @param {NodePath} nodePath 101 | * @return {boolean} 102 | * @private 103 | */ 104 | Rewriter.prototype.visitIdentifier = function(nodePath) { 105 | if (astUtil.isReference(nodePath)) { 106 | var exportReference = this.getExportReferenceForReference(this.currentModule, nodePath); 107 | if (exportReference) { 108 | this.replacements.push(Replacement.swaps(nodePath, exportReference)); 109 | } 110 | } 111 | 112 | return false; 113 | }; 114 | 115 | /** 116 | * Process all variable declarations looking for top-level exported variables. 117 | * 118 | * @param {NodePath} nodePath 119 | * @private 120 | */ 121 | Rewriter.prototype.visitVariableDeclaration = function(nodePath) { 122 | if (nodePath.scope.isGlobal) { 123 | var replacement = this.formatter.processVariableDeclaration(this.currentModule, nodePath); 124 | if (replacement) { 125 | this.replacements.push(replacement); 126 | } 127 | } 128 | 129 | this.traverse(nodePath); 130 | }; 131 | 132 | /** 133 | * We need to ensure that the LHS of this assignment is not an imported binding. 134 | * If it is, we throw a "compile"-time error since this is not allowed by the 135 | * spec (see section 12.14.1, Assignment Operators / Static Semantics: Early 136 | * Errors). 137 | * 138 | * @param {NodePath} nodePath 139 | * @private 140 | */ 141 | Rewriter.prototype.visitAssignmentExpression = function(nodePath) { 142 | this.assertImportIsNotReassigned(this.currentModule, nodePath.get('left')); 143 | if (this.currentModule.exports.findDeclarationForReference(nodePath.get('left'))) { 144 | var replacement = this.formatter.processExportReassignment(this.currentModule, nodePath); 145 | if (replacement) { 146 | this.replacements.push(replacement); 147 | } 148 | } 149 | 150 | this.traverse(nodePath); 151 | }; 152 | 153 | /** 154 | * Process all top-level function declarations in case they need to be processed. 155 | * 156 | * @param {NodePath} nodePath 157 | * @private 158 | */ 159 | Rewriter.prototype.visitFunctionDeclaration = function(nodePath) { 160 | if (n.Program.check(nodePath.parent.node)) { 161 | var replacement = this.formatter.processFunctionDeclaration(this.currentModule, nodePath); 162 | if (replacement) { 163 | this.replacements.push(replacement); 164 | } 165 | } 166 | 167 | this.traverse(nodePath); 168 | }; 169 | 170 | /** 171 | * Process all top-level class declarations in case they need to be processed. 172 | * 173 | * @param {NodePath} nodePath 174 | * @private 175 | */ 176 | Rewriter.prototype.visitClassDeclaration = function(nodePath) { 177 | if (n.Program.check(nodePath.parent.node)) { 178 | var replacement = this.formatter.processClassDeclaration(this.currentModule, nodePath); 179 | if (replacement) { 180 | this.replacements.push(replacement); 181 | } 182 | } 183 | 184 | this.traverse(nodePath); 185 | }; 186 | 187 | /** 188 | * Look for all export declarations so we can rewrite them. 189 | * 190 | * @param {NodePath} nodePath 191 | * @private 192 | */ 193 | Rewriter.prototype.visitExportDeclaration = function(nodePath) { 194 | assertStatementIsTopLevel(this.currentModule, nodePath); 195 | 196 | var replacement; 197 | if (nodePath.node.default) { 198 | /** 199 | * Default exports do not create bindings, so we can safely turn these 200 | * into expressions that do something with the exported value. 201 | * 202 | * Make sure that the exported value is replaced if it is a reference 203 | * to an imported binding. For example: 204 | * 205 | * import { foo } from './foo'; 206 | * export default foo; 207 | * 208 | * Might become: 209 | * 210 | * mod$$.default = foo$$.foo; 211 | */ 212 | var declaration = nodePath.node.declaration; 213 | var declarationPath = nodePath.get('declaration'); 214 | if (astUtil.isReference(declarationPath)) { 215 | var exportReference = this.getExportReferenceForReference(this.currentModule, declarationPath); 216 | if (exportReference) { 217 | declaration = exportReference; 218 | } 219 | } 220 | replacement = Replacement.swaps(nodePath, this.formatter.defaultExport(this.currentModule, declaration)); 221 | } else { 222 | replacement = this.formatter.processExportDeclaration(this.currentModule, nodePath); 223 | } 224 | 225 | if (replacement) { 226 | this.replacements.push(replacement); 227 | } 228 | 229 | this.traverse(nodePath); 230 | }; 231 | 232 | /** 233 | * Process import declarations so they can be rewritten. 234 | * 235 | * @param {NodePath} nodePath 236 | * @private 237 | */ 238 | Rewriter.prototype.visitImportDeclaration = function(nodePath) { 239 | assertStatementIsTopLevel(this.currentModule, nodePath); 240 | var replacement = this.formatter.processImportDeclaration(this.currentModule, nodePath); 241 | if (replacement) { 242 | this.replacements.push(replacement); 243 | } 244 | 245 | this.traverse(nodePath); 246 | }; 247 | 248 | /** 249 | * Process update expressions (e.g. `a++`) so we can re-write modifications to 250 | * exported variables. 251 | * 252 | * @param {NodePath} nodePath 253 | * @private 254 | */ 255 | Rewriter.prototype.visitUpdateExpression = function(nodePath) { 256 | this.assertImportIsNotReassigned(this.currentModule, nodePath.get('argument')); 257 | if (this.currentModule.exports.findDeclarationForReference(nodePath.get('argument'))) { 258 | var replacement = this.formatter.processExportReassignment(this.currentModule, nodePath); 259 | if (replacement) { 260 | this.replacements.push(replacement); 261 | } 262 | } 263 | 264 | this.traverse(nodePath); 265 | }; 266 | 267 | /** 268 | * We need to ensure that reference updates (i.e. `ref =`, `ref++`) are not 269 | * allowed for imported bindings. If it is, we throw a "compile"-time error 270 | * since this is not allowed by the spec (see section 12.14.1, Assignment 271 | * Operators / Static Semantics: Early Errors). 272 | * 273 | * @private 274 | * @param {Module} mod 275 | * @param {Identifier} nodePath 276 | */ 277 | Rewriter.prototype.assertImportIsNotReassigned = function(mod, nodePath) { 278 | var declarationPath; 279 | var identifierPath; 280 | var bindingDescription; 281 | 282 | if (n.Identifier.check(nodePath.node)) { 283 | // Do we have a named import… 284 | // 285 | // import { foo } from 'foo'; 286 | // 287 | // …that we then try to assign or update? 288 | // 289 | // foo++; 290 | // foo = 1; 291 | // 292 | declarationPath = mod.imports.findDeclarationForReference(nodePath); 293 | if (!declarationPath || !n.ImportSpecifier.check(declarationPath.parent.node)) { 294 | return; 295 | } 296 | 297 | bindingDescription = '`' + declarationPath.node.name + '`'; 298 | } else if (n.MemberExpression.check(nodePath.node)) { 299 | // Do we have a namespace import… 300 | // 301 | // import * as foo from 'foo'; 302 | // 303 | // …with a property that we then try to assign or update? 304 | // 305 | // foo.a++; 306 | // foo.a = 1; 307 | // foo['a'] = 1; 308 | // 309 | var objectPath = nodePath.get('object'); 310 | 311 | if (!n.Identifier.check(objectPath.node)) { 312 | return; 313 | } 314 | 315 | declarationPath = mod.imports.findDeclarationForReference(objectPath); 316 | if (!declarationPath || !n.ImportNamespaceSpecifier.check(declarationPath.parent.node)) { 317 | return; 318 | } 319 | 320 | var propertyPath = nodePath.get('property'); 321 | if (n.Identifier.check(propertyPath.node)) { 322 | bindingDescription = '`' + propertyPath.node.name + '`'; 323 | } else { 324 | bindingDescription = 'of namespace `' + objectPath.node.name + '`'; 325 | } 326 | } else { 327 | return; 328 | } 329 | 330 | throw new SyntaxError( 331 | 'Cannot reassign imported binding ' + bindingDescription + 332 | ' at ' + sourcePosition(mod, nodePath.node) 333 | ); 334 | }; 335 | 336 | /** 337 | * @private 338 | */ 339 | Rewriter.prototype.getExportReferenceForReference = function(mod, referencePath) { 340 | if (n.ExportSpecifier.check(referencePath.parent.node) && !referencePath.parent.node.default) { 341 | // Do not rewrite non-default export specifiers. 342 | return null; 343 | } 344 | 345 | /** 346 | * We need to replace references to variables that are imported or 347 | * exported with the correct export expression. The export expression 348 | * should be named for the original export for a variable. 349 | * 350 | * That is, imports must be followed to their export. If that exported 351 | * value came from an import then repeat the process until you find a 352 | * declaration of the exported value. 353 | */ 354 | var exportSpecifier = mod.exports.findSpecifierByName(referencePath.node.name); 355 | if (exportSpecifier && exportSpecifier.declaration.source && exportSpecifier.node !== referencePath.parent.node) { 356 | // This is a direct export from another module, e.g. `export { foo } from 357 | // 'foo'`. There are no local bindings created by this, so there is no 358 | // associated export for this reference and no need to rewrite it. 359 | return null; 360 | } 361 | 362 | return this.formatter.exportedReference(mod, referencePath) || 363 | this.formatter.importedReference(mod, referencePath) || 364 | this.formatter.localReference(mod, referencePath); 365 | }; 366 | 367 | /** 368 | * Traverses ASTs only checking for invalid import/export declaration semantics. 369 | * 370 | * @constructor 371 | * @extends types.PathVisitor 372 | * @param {Module} mod 373 | * @private 374 | */ 375 | function DeclarationLinterVisitor(mod) { 376 | this.module = mod; 377 | types.PathVisitor.call(this); 378 | } 379 | extend(DeclarationLinterVisitor, types.PathVisitor); 380 | 381 | /** 382 | * Checks that the import declaration is at the top level. 383 | * 384 | * @param {NodePath} nodePath 385 | */ 386 | DeclarationLinterVisitor.prototype.visitImportDeclaration = function(nodePath) { 387 | assertStatementIsTopLevel(this.module, nodePath); 388 | }; 389 | 390 | /** 391 | * Checks that the export declaration is at the top level. 392 | * 393 | * @param {NodePath} nodePath 394 | */ 395 | DeclarationLinterVisitor.prototype.visitExportDeclaration = function(nodePath) { 396 | assertStatementIsTopLevel(this.module, nodePath); 397 | }; 398 | 399 | /** 400 | * We need to ensure that all imports/exports are only at the top level. Esprima 401 | * should perhaps take care of this for us, but it does not. 402 | * 403 | * @param {Module} mod 404 | * @param {NodePath} nodePath 405 | * @private 406 | */ 407 | function assertStatementIsTopLevel(mod, nodePath) { 408 | if (!nodePath.scope.isGlobal) { 409 | throw new SyntaxError( 410 | 'Unexpected non-top level ' + nodePath.node.type + 411 | ' found at ' + sourcePosition(mod, nodePath.node) 412 | ); 413 | } 414 | } 415 | 416 | module.exports = Rewriter; 417 | -------------------------------------------------------------------------------- /lib/sorting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines the execution order of the given modules. This function resolves 3 | * cycles by preserving the order in which the modules are visited. 4 | * 5 | * @param {Module[]} modules 6 | * @return {Module[]} 7 | */ 8 | function sort(modules) { 9 | var result = []; 10 | var state = {}; 11 | 12 | modules.forEach(function(mod) { 13 | visit(mod, result, state); 14 | }); 15 | 16 | return result; 17 | } 18 | exports.sort = sort; 19 | 20 | /** 21 | * Visits the given module, adding it to `result` after visiting all of the 22 | * modules it imports, recursively. The `state` argument is private and maps 23 | * module ids to the current visit state. 24 | * 25 | * @private 26 | * @param {Module} mod 27 | * @param {Module[]} result 28 | * @param {Object.} state 29 | */ 30 | function visit(mod, result, state) { 31 | if (state[mod.id] === 'added') { 32 | // already in the list, ignore it 33 | return; 34 | } 35 | if (state[mod.id] === 'seen') { 36 | // cycle found, just ignore it 37 | return; 38 | } 39 | state[mod.id] = 'seen'; 40 | mod.imports.modules.forEach(function(mod) { 41 | visit(mod, result, state); 42 | }); 43 | state[mod.id] = 'added'; 44 | result.push(mod); 45 | } 46 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var recast = require('recast'); 4 | var n = recast.types.namedTypes; 5 | var b = recast.types.builders; 6 | var reserved = require('reserved'); 7 | var realFS = require('fs'); 8 | var Path = require('path'); 9 | 10 | var proto = '__proto__'; 11 | 12 | function memo(object, property, getter) { 13 | Object.defineProperty(object, property, { 14 | get: function() { 15 | this[property] = getter.call(this); 16 | return this[property]; 17 | }, 18 | 19 | set: function(value) { 20 | Object.defineProperty(this, property, { 21 | value: value, 22 | configurable: true, 23 | writable: true 24 | }); 25 | } 26 | }); 27 | } 28 | exports.memo = memo; 29 | 30 | function startsWith(string, substring) { 31 | return string.lastIndexOf(substring, 0) === 0; 32 | } 33 | exports.startsWith = startsWith; 34 | 35 | function endsWith(string, substring) { 36 | var expected = string.length - substring.length; 37 | return string.indexOf(substring, expected) === expected; 38 | } 39 | exports.endsWith = endsWith; 40 | 41 | function extend(subclass, superclass) { 42 | subclass[proto] = superclass; 43 | subclass.prototype = Object.create(superclass.prototype); 44 | subclass.prototype.constructor = subclass; 45 | } 46 | exports.extend = extend; 47 | 48 | function sourcePosition(mod, node) { 49 | var loc = node && node.loc; 50 | if (loc) { 51 | return mod.relativePath + ':' + loc.start.line + ':' + (loc.start.column + 1); 52 | } else { 53 | return mod.relativePath; 54 | } 55 | } 56 | exports.sourcePosition = sourcePosition; 57 | 58 | function IIFE() { 59 | var body = []; 60 | var args = Array.prototype.concat.apply(body, arguments); 61 | 62 | args.forEach(function(node) { 63 | if (n.Expression.check(node)) { 64 | node = b.expressionStatement(node); 65 | } 66 | if (n.Statement.check(node)) { 67 | body.push(node); 68 | } 69 | }); 70 | 71 | return b.callExpression( 72 | b.memberExpression( 73 | b.functionExpression(null, [], b.blockStatement(body)), 74 | b.identifier("call"), 75 | false 76 | ), 77 | [b.thisExpression()] 78 | ); 79 | } 80 | exports.IIFE = IIFE; 81 | 82 | /** 83 | * Create a member express that is compatible with ES3. Which means 84 | * reserved words will be treated as computed props. E.g.: 85 | * 86 | * foo["default"] // instead of `foo.default` 87 | * 88 | * while still supporting identifiers for non-reserved words: 89 | * 90 | * foo.bar // since bar is not reserved 91 | * 92 | * @param {Identifier} obj Identifier for the object reference 93 | * @param {Identifier|String} prop Identifier or string name for the member property 94 | * @return {b.memberExpression} AST for the member expression 95 | */ 96 | function compatMemberExpression(obj, prop) { 97 | var isIdentifier = n.Identifier.check(prop); 98 | var name = isIdentifier ? prop.name : prop; 99 | var computed = reserved.indexOf(name) >= 0; 100 | return b.memberExpression( 101 | obj, 102 | computed ? b.literal(name) : (isIdentifier ? prop : b.identifier(prop)), 103 | computed 104 | ); 105 | } 106 | 107 | exports.compatMemberExpression = compatMemberExpression; 108 | 109 | /** 110 | * Create a hierarchy of directories of it does not already exist. 111 | * 112 | * @param {string} path 113 | * @param {{fs: object=}} options 114 | */ 115 | function mkdirpSync(path, options) { 116 | var fs = options && options.fs || realFS; 117 | 118 | var ancestors = []; 119 | var ancestor = path; 120 | 121 | while (true) { 122 | var nextAncestor = Path.dirname(ancestor); 123 | if (nextAncestor === ancestor) { break; } 124 | ancestors.unshift(ancestor); 125 | ancestor = nextAncestor; 126 | } 127 | 128 | ancestors.forEach(function(dir) { 129 | if (!fs.existsSync(dir)) { 130 | fs.mkdirSync(dir); 131 | } 132 | }); 133 | } 134 | exports.mkdirpSync = mkdirpSync; 135 | -------------------------------------------------------------------------------- /lib/writer.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var recast = require('recast'); 5 | var fs = require('fs'); 6 | var Path = require('path'); 7 | var mkdirpSync = require('./utils').mkdirpSync; 8 | 9 | function Writer(target, options) { 10 | options = options || {}; 11 | this.target = target; 12 | this.basePath = options.basePath || process.cwd(); 13 | this.sourceRoot = options.sourceRoot; 14 | } 15 | 16 | Writer.prototype.write = function(files) { 17 | var target = this.target; 18 | 19 | switch (files.length) { 20 | case 0: 21 | throw new Error('expected at least one file to write, got zero'); 22 | 23 | case 1: 24 | // We got a single file, so `target` should refer to either a file or a 25 | // directory, but only if the file has a name. 26 | var isDirectory = false; 27 | try { 28 | isDirectory = fs.statSync(target).isDirectory(); 29 | } catch (ex) {} 30 | 31 | assert.ok( 32 | !isDirectory || files[0].filename, 33 | 'unable to determine filename for output to directory: ' + target 34 | ); 35 | this.writeFile( 36 | files[0], 37 | isDirectory ? Path.resolve(target, files[0].filename) : target 38 | ); 39 | break; 40 | 41 | default: 42 | // We got multiple files to output, so `target` should be a directory or 43 | // not exist (so we can create it). 44 | var self = this; 45 | files.forEach(function(file) { 46 | self.writeFile(file, Path.resolve(target, file.filename)); 47 | }); 48 | break; 49 | } 50 | }; 51 | 52 | Writer.prototype.writeFile = function(file, filename) { 53 | var sourceMapFilename = filename + '.map'; 54 | 55 | var rendered = recast.print(file, { 56 | sourceMapName: Path.relative(this.basePath, filename), 57 | sourceRoot: this.sourceRoot 58 | }); 59 | 60 | var code = rendered.code; 61 | assert.ok(filename, 'missing filename for file: ' + code); 62 | 63 | mkdirpSync(Path.dirname(filename)); 64 | 65 | if (rendered.map) { 66 | code += '\n\n//# sourceMappingURL=' + Path.basename(sourceMapFilename); 67 | 68 | fs.writeFileSync( 69 | sourceMapFilename, 70 | JSON.stringify(rendered.map), 71 | 'utf8' 72 | ); 73 | } 74 | 75 | fs.writeFileSync(filename, code, 'utf8'); 76 | }; 77 | 78 | module.exports = Writer; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es6-module-transpiler", 3 | "version": "0.10.0", 4 | "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.", 5 | "homepage": "http://esnext.github.io/es6-module-transpiler", 6 | "keywords": [ 7 | "es6", 8 | "module", 9 | "transpile", 10 | "amd", 11 | "commonjs" 12 | ], 13 | "bugs": "https://github.com/square/es6-module-transpiler/issues", 14 | "bin": { 15 | "compile-modules": "./bin/compile-modules" 16 | }, 17 | "files": [ 18 | "bin", 19 | "lib", 20 | "LICENSE", 21 | "README.md" 22 | ], 23 | "directories": { 24 | "lib": "./lib", 25 | "test": "test" 26 | }, 27 | "main": "lib/index.js", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/square/es6-module-transpiler.git" 31 | }, 32 | "scripts": { 33 | "test": "npm run test-bundle && npm run test-commonjs && npm run test-unit", 34 | "test-bundle": "node test/runner.js -f bundle", 35 | "test-commonjs": "node test/runner.js -f commonjs", 36 | "test-unit": "mocha -R spec test/unit", 37 | "build-standalone": "browserify -s ModuleTranspiler -e lib/index.js -o es6-module-transpiler.js" 38 | }, 39 | "author": "Square, Inc.", 40 | "license": "Apache-2.0", 41 | "dependencies": { 42 | "ast-util": "^0.5.1", 43 | "esprima-fb": "^7001.1.0-dev-harmony-fb", 44 | "posix-getopt": "^1.0.0", 45 | "recast": "^0.9.5", 46 | "reserved": "^0.1.2" 47 | }, 48 | "devDependencies": { 49 | "browserify": "^6.3.2", 50 | "es6-class": "^0.9.2", 51 | "example-runner": "^0.2.0", 52 | "fake-fs": "^0.5.0", 53 | "mocha": "^2.0.1", 54 | "tmp": "0.0.24" 55 | }, 56 | "browser": { 57 | "fs": "./lib/browser/fs.js", 58 | "./lib/index.js": "./lib/browser/index.js" 59 | } 60 | } -------------------------------------------------------------------------------- /test/examples/bare-import/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | global.sideEffectyValue = 99; 4 | -------------------------------------------------------------------------------- /test/examples/bare-import/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import './exporter'; 4 | 5 | assert.equal(global.sideEffectyValue, 99); 6 | -------------------------------------------------------------------------------- /test/examples/bindings/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var count = 0; 4 | 5 | export function incr() { 6 | count++; 7 | } 8 | -------------------------------------------------------------------------------- /test/examples/bindings/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { count, incr } from './exporter'; 4 | 5 | assert.equal(count, 0); 6 | incr(); 7 | assert.equal(count, 1); 8 | -------------------------------------------------------------------------------- /test/examples/cycles-defaults/a.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import b from './b'; 4 | 5 | export default { a: 1, get b() { return b.b; } }; 6 | -------------------------------------------------------------------------------- /test/examples/cycles-defaults/b.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import a from './a'; 4 | 5 | export default { b: 2, get a() { return a.a; } }; 6 | -------------------------------------------------------------------------------- /test/examples/cycles-defaults/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import a from './a'; 4 | import b from './b'; 5 | 6 | assert.equal(a.a, 1); 7 | assert.equal(a.b, 2); 8 | assert.equal(b.a, 1); 9 | assert.equal(b.b, 2); 10 | -------------------------------------------------------------------------------- /test/examples/cycles-immediate/evens.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { nextOdd } from './odds'; 4 | 5 | /** 6 | * We go through these gymnastics to eager-bind to nextOdd. This is done to 7 | * ensure that both this module and the 'odds' module eagerly use something 8 | * from the other. 9 | */ 10 | export var nextEven = (function() { 11 | return function(n) { 12 | var no = nextOdd(n); 13 | return (no === n + 2) ? 14 | no - 1 : no; 15 | }; 16 | })(nextOdd); 17 | 18 | export function isEven(n) { 19 | return n % 2 === 0; 20 | } 21 | -------------------------------------------------------------------------------- /test/examples/cycles-immediate/main.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | /** 4 | * The 'evens' and 'odds' modules are configured in such a way that they both 5 | * have two exported functions: isEven, nextEven, isOdd, and nextOdd. Normally 6 | * these four functions could be in any order regardless of which depends on 7 | * which because of JavaScript function hoisting. 8 | * 9 | * For the purposes of our test we need to prevent function hoisting, so it has 10 | * been arranged that two of them will be function expressions assigned to 11 | * variables. Specifically, isOdd and nextEven both eagerly evaluate their 12 | * dependencies (i.e. isEven and nextOdd). This allows us to test that exported 13 | * function declarations are available before what would be a module's 14 | * "execute" step, per the spec. 15 | */ 16 | import { nextEven, isEven } from './evens'; 17 | import { nextOdd, isOdd } from './odds'; 18 | 19 | assert.equal(nextEven(1), 2); 20 | assert.equal(nextOdd(1), 3); 21 | assert.ok(isOdd(1)); 22 | assert.ok(!isOdd(0)); 23 | assert.ok(isEven(0)); 24 | assert.ok(!isEven(1)); 25 | -------------------------------------------------------------------------------- /test/examples/cycles-immediate/odds.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { isEven } from './evens'; 4 | 5 | export function nextOdd(n) { 6 | return isEven(n) ? n + 1 : n + 2; 7 | } 8 | 9 | /** 10 | * We go through these gymnastics to eager-bind to isEven. This is done to 11 | * ensure that both this module and the 'evens' module eagerly use something 12 | * from the other. 13 | */ 14 | export var isOdd = (function(isEven) { 15 | return function(n) { 16 | return !isEven(n); 17 | }; 18 | })(isEven); 19 | -------------------------------------------------------------------------------- /test/examples/cycles/a.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { b } from './b'; 4 | 5 | export function getb() { 6 | return b; 7 | } 8 | 9 | export var a = 1; 10 | -------------------------------------------------------------------------------- /test/examples/cycles/b.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a } from './a'; 4 | 5 | export function geta() { 6 | return a; 7 | } 8 | 9 | export var b = 2; 10 | -------------------------------------------------------------------------------- /test/examples/cycles/c.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a, getb } from './a'; 4 | import { b, geta } from './b'; 5 | 6 | assert.equal(geta(), 1); 7 | assert.equal(a, 1); 8 | assert.equal(getb(), 2); 9 | assert.equal(b, 2); 10 | -------------------------------------------------------------------------------- /test/examples/duplicate-import-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 1; -------------------------------------------------------------------------------- /test/examples/duplicate-import-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | /* error: type=SyntaxError message="expected one declaration for `a`, at importer.js:7:14 but found 2" */ 4 | import { a } from './exporter'; 5 | import { a } from './exporter'; 6 | 7 | assert.equal(a, 1); -------------------------------------------------------------------------------- /test/examples/duplicate-import-specifier-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 1; -------------------------------------------------------------------------------- /test/examples/duplicate-import-specifier-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | /* error: type=SyntaxError message="expected one declaration for `a`, at importer.js:5:14 but found 2" */ 4 | import { a, a } from './exporter'; 5 | assert.equal(a, 1); -------------------------------------------------------------------------------- /test/examples/export-and-import-reference-share-var/first.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 1; 4 | assert.equal(a, 1); -------------------------------------------------------------------------------- /test/examples/export-and-import-reference-share-var/second.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a } from './first'; 4 | 5 | // This variable declaration is going to be altered because `b` needs to be 6 | // re-written. We need to make sure that the `a` re-writing and the unaffected 7 | // `c` declarator are not being clobbered by that alteration. 8 | var a_ = a, b = 9, c = 'c'; 9 | 10 | assert.equal(a, 1); 11 | assert.equal(a_, 1); 12 | assert.equal(b, 9); 13 | assert.equal(c, 'c'); 14 | 15 | export { b }; 16 | -------------------------------------------------------------------------------- /test/examples/export-class-expression/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export default class {}; 4 | -------------------------------------------------------------------------------- /test/examples/export-class-expression/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import Foo from './exporter'; 4 | 5 | assert.strictEqual(new Foo().constructor, Foo); 6 | -------------------------------------------------------------------------------- /test/examples/export-class/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export class Foo {} 4 | -------------------------------------------------------------------------------- /test/examples/export-class/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { Foo } from './exporter'; 4 | 5 | assert.strictEqual(new Foo().constructor, Foo); 6 | -------------------------------------------------------------------------------- /test/examples/export-default-class/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export default class Point { 4 | constructor(x, y) { 5 | this.x = x; 6 | this.y = y; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/examples/export-default-class/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import Point from './exporter'; 4 | 5 | assert.strictEqual(new Point(1, 2).x, 1); 6 | assert.strictEqual(new Point(1, 2).y, 2); 7 | -------------------------------------------------------------------------------- /test/examples/export-default-function/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export default function () { 4 | return 1; 5 | } 6 | -------------------------------------------------------------------------------- /test/examples/export-default-function/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import fn1 from './exporter'; 4 | 5 | import { default as fn2 } from './exporter'; 6 | 7 | assert.equal(fn1(), 1); 8 | assert.equal(fn2(), 1); 9 | -------------------------------------------------------------------------------- /test/examples/export-default-named-function/exporter.js: -------------------------------------------------------------------------------- 1 | export default function foo() { 2 | return 1; 3 | } 4 | 5 | export function callsFoo() { 6 | return foo(); 7 | } -------------------------------------------------------------------------------- /test/examples/export-default-named-function/importer.js: -------------------------------------------------------------------------------- 1 | import foo, { callsFoo } from './exporter'; 2 | 3 | assert.strictEqual(foo(), 1); 4 | assert.strictEqual(callsFoo(), 1); -------------------------------------------------------------------------------- /test/examples/export-default/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | var a = 42; 4 | 5 | export function change() { 6 | a++; 7 | } 8 | 9 | assert.equal(a, 42); 10 | export default a; 11 | 12 | // Any replacement for the `export default` above needs to happen in the same 13 | // location. It cannot be done, say, at the end of the file. Otherwise the new 14 | // value of `a` will be used and will be incorrect. 15 | a = 0; 16 | assert.equal(a, 0); 17 | -------------------------------------------------------------------------------- /test/examples/export-default/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import value from './exporter'; 4 | import { change } from './exporter'; 5 | assert.equal(value, 42); 6 | 7 | change(); 8 | assert.equal( 9 | value, 10 | 42, 11 | 'default export should not be bound' 12 | ); 13 | -------------------------------------------------------------------------------- /test/examples/export-from-default/first.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export default 1; 4 | -------------------------------------------------------------------------------- /test/examples/export-from-default/second.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export { default } from './first'; 4 | -------------------------------------------------------------------------------- /test/examples/export-from-default/third.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import a from './second'; 4 | assert.equal(a, 1); 5 | -------------------------------------------------------------------------------- /test/examples/export-from/first.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 1; 4 | -------------------------------------------------------------------------------- /test/examples/export-from/second.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export { a } from './first'; 4 | 5 | // This `a` reference should not be re-written because this export is not 6 | // creating a local binding. 7 | assert.equal(typeof a, 'undefined'); 8 | -------------------------------------------------------------------------------- /test/examples/export-from/third.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a } from './second'; 4 | assert.equal(a, 1); 5 | -------------------------------------------------------------------------------- /test/examples/export-function/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export function foo() { 4 | return 121; 5 | } 6 | assert.equal(foo(), 121); 7 | -------------------------------------------------------------------------------- /test/examples/export-function/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { foo } from './exporter'; 4 | assert.equal(foo(), 121); 5 | -------------------------------------------------------------------------------- /test/examples/export-list/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | var a = 1; 4 | var b = 2; 5 | 6 | function incr() { 7 | var c = a++; // Capture `a++` to force us to use a temporary variable. 8 | b++; 9 | } 10 | 11 | export { a, b, incr }; 12 | -------------------------------------------------------------------------------- /test/examples/export-list/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a, b, incr } from './exporter'; 4 | 5 | assert.equal(a, 1); 6 | assert.equal(b, 2); 7 | incr(); 8 | assert.equal(a, 2); 9 | assert.equal(b, 3); 10 | -------------------------------------------------------------------------------- /test/examples/export-mixins/exporter.js: -------------------------------------------------------------------------------- 1 | export default 1; 2 | export var bar = 2; 3 | -------------------------------------------------------------------------------- /test/examples/export-mixins/importer.js: -------------------------------------------------------------------------------- 1 | import foo, { bar } from './exporter'; 2 | 3 | assert.equal(foo, 1); 4 | assert.equal(bar, 2); 5 | -------------------------------------------------------------------------------- /test/examples/export-named-class/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | class Foo {} 4 | export { Foo }; 5 | -------------------------------------------------------------------------------- /test/examples/export-named-class/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { Foo } from './exporter'; 4 | 5 | assert.strictEqual(new Foo().constructor, Foo); 6 | -------------------------------------------------------------------------------- /test/examples/export-not-at-top-level-fails/index.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | function foo() { 4 | /* error: type=Error message="Line 5: Unexpected reserved word" */ 5 | export { foo }; 6 | } 7 | -------------------------------------------------------------------------------- /test/examples/export-var/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 1; 4 | assert.equal(a, 1); 5 | -------------------------------------------------------------------------------- /test/examples/export-var/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a } from './exporter'; 4 | assert.equal(a, 1); 5 | -------------------------------------------------------------------------------- /test/examples/import-as/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 'a'; 4 | export var b = 'b'; 5 | export default 'DEF'; 6 | -------------------------------------------------------------------------------- /test/examples/import-as/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a as b, b as a, default as def } from './exporter'; 4 | 5 | assert.equal(b, 'a'); 6 | assert.equal(a, 'b'); 7 | assert.equal(def, 'DEF'); 8 | -------------------------------------------------------------------------------- /test/examples/import-chain/first.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var value = 42; 4 | -------------------------------------------------------------------------------- /test/examples/import-chain/second.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { value } from './first'; 4 | export { value }; 5 | -------------------------------------------------------------------------------- /test/examples/import-chain/third.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { value } from './second'; 4 | assert.equal(value, 42); 5 | -------------------------------------------------------------------------------- /test/examples/import-not-at-top-level-fails/index.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | function foo() { 4 | /* error: type=Error message="Line 5: Unexpected reserved word" */ 5 | import foo from './index'; 6 | } 7 | -------------------------------------------------------------------------------- /test/examples/module-level-declarations/mod.js: -------------------------------------------------------------------------------- 1 | var a = 1; 2 | 3 | assert.equal(a, 1); 4 | assert.equal(getA(), 1); 5 | 6 | function getA() { 7 | return a; 8 | } -------------------------------------------------------------------------------- /test/examples/named-function-expression/exporter.js: -------------------------------------------------------------------------------- 1 | export var a = 1; -------------------------------------------------------------------------------- /test/examples/named-function-expression/importer.js: -------------------------------------------------------------------------------- 1 | import { a } from './exporter'; 2 | 3 | var getA = function getA() { 4 | var a = 2; 5 | return a; 6 | }; 7 | 8 | assert.strictEqual(a, 1); 9 | assert.strictEqual(getA(), 2); -------------------------------------------------------------------------------- /test/examples/namespace-reassign-import-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var foo = 1; -------------------------------------------------------------------------------- /test/examples/namespace-reassign-import-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import * as exp from './exporter'; 4 | 5 | /* error: type=SyntaxError message="Cannot reassign imported binding `foo` at importer.js:6:1" */ 6 | exp.foo = 2; -------------------------------------------------------------------------------- /test/examples/namespace-update-import-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var foo = 1; -------------------------------------------------------------------------------- /test/examples/namespace-update-import-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import * as exp from './exporter'; 4 | 5 | /* error: type=SyntaxError message="Cannot reassign imported binding of namespace `exp` at importer.js:6:1" */ 6 | exp['foo']++; -------------------------------------------------------------------------------- /test/examples/namespaces/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 'a'; 4 | export var b = 'b'; 5 | export default 'DEF'; 6 | -------------------------------------------------------------------------------- /test/examples/namespaces/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import * as foo from './exporter'; 4 | 5 | assert.equal(foo['default'], 'DEF'); 6 | assert.equal(foo.b, 'b'); 7 | assert.equal(foo.a, 'a'); 8 | 9 | var keys = []; 10 | for (var key in foo) { 11 | keys.push(key); 12 | } 13 | assert.deepEqual(keys.sort(), ['a', 'b', 'default']); 14 | -------------------------------------------------------------------------------- /test/examples/re-export-default-import/first.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export default function hi() { 4 | return 'hi'; 5 | } 6 | -------------------------------------------------------------------------------- /test/examples/re-export-default-import/second.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | import hi from './first'; 3 | export { hi }; 4 | -------------------------------------------------------------------------------- /test/examples/re-export-default-import/third.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { hi } from './second'; 4 | assert.equal(hi(), 'hi'); 5 | -------------------------------------------------------------------------------- /test/examples/re-export-namespace-import/first.js: -------------------------------------------------------------------------------- 1 | export var a = 1; 2 | export var b = 2; 3 | -------------------------------------------------------------------------------- /test/examples/re-export-namespace-import/second.js: -------------------------------------------------------------------------------- 1 | import * as mod from './first'; 2 | export { mod }; 3 | -------------------------------------------------------------------------------- /test/examples/re-export-namespace-import/third.js: -------------------------------------------------------------------------------- 1 | import { mod } from './second'; 2 | 3 | assert.strictEqual(mod.a, 1); 4 | assert.strictEqual(mod.b, 2); 5 | -------------------------------------------------------------------------------- /test/examples/reassign-import-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var x = 1; -------------------------------------------------------------------------------- /test/examples/reassign-import-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { x } from './exporter'; 4 | 5 | (function() { 6 | for(var x = 0; x < 1; x++){} 7 | for(var x = 0; x < 1; x++){} 8 | }); 9 | 10 | /* error: type=SyntaxError message="Cannot reassign imported binding `x` at importer.js:11:1" */ 11 | x = 10; 12 | -------------------------------------------------------------------------------- /test/examples/reassign-import-not-at-top-level-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var x = 1; 4 | -------------------------------------------------------------------------------- /test/examples/reassign-import-not-at-top-level-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { x } from './exporter'; 4 | 5 | export function foo () { 6 | var x = 1; 7 | } 8 | export function bar () { 9 | /* error: type=SyntaxError message="Cannot reassign imported binding `x` at importer.js:10:3" */ 10 | x = 1; 11 | } 12 | -------------------------------------------------------------------------------- /test/examples/this-is-global/mod.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | assert.strictEqual( 4 | this, 5 | global, 6 | '`this` (keys=' + Object.keys(this) + ') does not equal ' + 7 | '`global` (keys=' + Object.keys(global) + ')' 8 | ); -------------------------------------------------------------------------------- /test/examples/update-expression-of-import-fails/exporter.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | export var a = 0; -------------------------------------------------------------------------------- /test/examples/update-expression-of-import-fails/importer.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { a } from './exporter'; 4 | 5 | /* error: type=SyntaxError message="Cannot reassign imported binding `a` at importer.js:6:1" */ 6 | a++; -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | Error.stackTraceLimit = 50; 4 | 5 | var fs = require('fs'); 6 | var Path = require('path'); 7 | var vm = require('vm'); 8 | var assert = require('assert'); 9 | var es6class = require('es6-class'); 10 | 11 | var modules = require('../lib'); 12 | var utils = require('../lib/utils'); 13 | var endsWith = utils.endsWith; 14 | var ExpectedError = require('./support/expected_error'); 15 | 16 | var examples = Path.join(__dirname, 'examples'); 17 | 18 | var paths = []; 19 | var formatters = require('../lib/formatters'); 20 | var formatterNames = Object.keys(formatters).filter(function(formatter) { 21 | return formatter !== 'DEFAULT'; 22 | }); 23 | var formatter = formatters.DEFAULT; 24 | 25 | var getopt = require('posix-getopt'); 26 | var parser = new getopt.BasicParser('h(help)f:(format)', process.argv); 27 | var option; 28 | 29 | while ((option = parser.getopt()) !== undefined) { 30 | if (option.error) { 31 | usage(); 32 | process.exit(1); 33 | } 34 | 35 | switch (option.option) { 36 | case 'f': 37 | formatter = option.optarg; 38 | if (formatterNames.indexOf(formatter) < 0) { 39 | usage(); 40 | process.exit(1); 41 | } 42 | break; 43 | 44 | case 'h': 45 | usage(); 46 | process.exit(0); 47 | break; 48 | } 49 | } 50 | 51 | paths.push.apply(paths, process.argv.slice(parser.optind())); 52 | 53 | if (paths.length === 0) { 54 | paths = fs.readdirSync(examples).map(function(example) { 55 | return Path.join(examples, example); 56 | }); 57 | } else { 58 | var cwd = process.cwd(); 59 | paths = paths.map(function(example) { 60 | return Path.resolve(cwd, example); 61 | }); 62 | } 63 | 64 | var results = Path.join(__dirname, 'results'); 65 | if (fs.existsSync(results)) { 66 | rmrf(results); 67 | } 68 | fs.mkdirSync(results); 69 | runTests(paths); 70 | 71 | function runTests(paths) { 72 | var passed = 0, failed = 0; 73 | paths.forEach(function(path) { 74 | if (runTestDir(path)) { 75 | passed++; 76 | } else { 77 | failed++; 78 | } 79 | }); 80 | 81 | console.log(); 82 | console.log('%d passed, %s failed.', passed, failed); 83 | process.exit( 84 | (passed + failed === 0) ? 1 : // no tests, fail 85 | failed === 0 ? 0 : // no failed, pass 86 | 1); // some failed, fail 87 | } 88 | 89 | function runTestDir(testDir) { 90 | var passed = false; 91 | var testName = Path.basename(testDir); 92 | 93 | var options = { 94 | resolvers: [new modules.FileResolver([testDir])], 95 | formatter: formatters[formatter] 96 | }; 97 | var container = new modules.Container(options); 98 | 99 | var expectedError; 100 | try { 101 | fs.readdirSync(testDir).forEach(function(child) { 102 | var mod = container.getModule(child); 103 | var contents = fs.readFileSync(mod.path).toString(); 104 | var newExpectedError = ExpectedError.getFromSource(contents); 105 | 106 | assert.ok( 107 | !newExpectedError || !expectedError, 108 | 'found more than one error comment!' 109 | ); 110 | 111 | expectedError = newExpectedError; 112 | }); 113 | 114 | var resultPath = Path.join(results, testName + '.js'); 115 | container.write(resultPath); 116 | 117 | var testAssert = wrappedAssert(); 118 | if (fs.statSync(resultPath).isDirectory()) { 119 | fs.readdirSync(resultPath).forEach(function(child) { 120 | if (Path.extname(child) === '.js') { 121 | requireTestFile('./' + child, resultPath, testAssert); 122 | } 123 | }); 124 | } else { 125 | requireTestFile(resultPath, process.cwd(), testAssert); 126 | } 127 | 128 | assert.ok( 129 | expectedError || testAssert.count > 0, 130 | 'expected at least one assertion' 131 | ); 132 | 133 | if (expectedError) { 134 | expectedError.assertMatch(null); 135 | } 136 | 137 | passed = true; 138 | printSuccess(testName); 139 | } catch (ex) { 140 | if (!(ex instanceof assert.AssertionError) && expectedError) { 141 | ex = expectedError.matchError(ex); 142 | } 143 | 144 | if (ex) { 145 | printFailure(testName, ex); 146 | console.log(); 147 | } else { 148 | printSuccess(testName); 149 | passed = true; 150 | } 151 | } 152 | 153 | return passed; 154 | } 155 | 156 | // TODO: Just use the real node require system with proxyquire? 157 | var testFileCache; 158 | var testFileGlobal; 159 | function requireTestFile(path, relativeTo, assert) { 160 | if (path[0] === '.') { 161 | path = Path.resolve(relativeTo, path); 162 | } 163 | 164 | if (!testFileCache) { testFileCache = {}; } 165 | 166 | if (path in testFileCache) { 167 | return testFileCache[path]; 168 | } else if (!fs.existsSync(path) && !endsWith(path, '.js')) { 169 | return requireTestFile(path + '.js'); 170 | } 171 | 172 | var code = fs.readFileSync(path); 173 | var mod = {exports: {}}; 174 | testFileCache[path] = mod.exports; 175 | 176 | if (!testFileGlobal) { testFileGlobal = {}; } 177 | 178 | testFileGlobal.assert = assert; 179 | testFileGlobal.global = testFileGlobal; 180 | testFileGlobal.module = mod; 181 | testFileGlobal.exports = mod.exports; 182 | testFileGlobal.require = function(requiredPath) { 183 | return requireTestFile(requiredPath, Path.dirname(path), assert); 184 | }; 185 | 186 | // Hack to work around an issue where vm does not set `this` to the context. 187 | code = '(function(){' + es6class.compile(code).code + '\n}).call(global);'; 188 | vm.runInNewContext(code, testFileGlobal, path); 189 | 190 | testFileCache[path] = mod.exports; 191 | return mod.exports; 192 | } 193 | 194 | function wrappedAssert() { 195 | var result = {count: 0}; 196 | 197 | Object.getOwnPropertyNames(assert).forEach(function(property) { 198 | result[property] = function() { 199 | result.count++; 200 | return assert[property].apply(assert, arguments); 201 | }; 202 | }); 203 | 204 | return result; 205 | } 206 | 207 | function rmrf(path) { 208 | var stat = fs.statSync(path); 209 | if (stat.isDirectory()) { 210 | fs.readdirSync(path).forEach(function(child) { 211 | rmrf(Path.join(path, child)); 212 | }); 213 | fs.rmdirSync(path); 214 | } else if (stat.isFile()) { 215 | fs.unlinkSync(path); 216 | } 217 | } 218 | 219 | /** 220 | * Prints a line to stdout for the given test indicating that it passed. 221 | * 222 | * @param {string} testName 223 | */ 224 | function printSuccess(testName) { 225 | console.log('\x1b[32m✓ \x1b[0m' + testName); 226 | } 227 | 228 | /** 229 | * Prints a line to stdout for the given test indicating that it failed. In 230 | * addition, prints any additional information indented one level. 231 | * 232 | * @param {string} testName 233 | * @param {Error} error 234 | */ 235 | function printFailure(testName, error) { 236 | console.log('\x1b[31m✘ ' + testName + '\x1b[0m'); 237 | console.log(); 238 | if (error.stack) { 239 | console.log(error.stack); 240 | } else { 241 | console.log(error.message); 242 | } 243 | } 244 | 245 | function usage() { 246 | console.log('node test/runner.js [OPTIONS] [EXAMPLE1 [EXAMPLE2 ...]]'); 247 | console.log(); 248 | console.log(' -f, --format Choose from: %s.', formatterNames.join(', ')); 249 | console.log(' -h, --help Show this help message.'); 250 | } 251 | -------------------------------------------------------------------------------- /test/support/expected_error.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | /** 6 | * @param {string|Error=} type 7 | * @param {string=} message 8 | * @constructor 9 | */ 10 | function ExpectedError(type, message) { 11 | this.type = type; 12 | this.message = message; 13 | } 14 | 15 | /** 16 | * Builds an ExpectedError from the given source code. 17 | * 18 | * @param {string} source 19 | * @return {?ExpectedError} 20 | */ 21 | ExpectedError.getFromSource = function(source) { 22 | var errorMatch = source.match(/\/\*\s*error:\s*(.+?)\*\//); 23 | if (!errorMatch) { 24 | return null; 25 | } 26 | 27 | var errorInfo = errorMatch[1]; 28 | var expectedTypeMatch = errorInfo.match(/type=([a-zA-Z]+)/); 29 | var expectedMessageMatch = errorInfo.match(/message="([^"]+)"/); 30 | 31 | assert.ok( 32 | expectedTypeMatch || expectedMessageMatch, 33 | 'expected error comment contains neither a type or a message: ' + 34 | errorInfo 35 | ); 36 | 37 | return new ExpectedError( 38 | expectedTypeMatch && expectedTypeMatch[1], 39 | expectedMessageMatch && expectedMessageMatch[1] 40 | ); 41 | }; 42 | 43 | /** 44 | * Determines whether the given error matches the expected error type. 45 | * 46 | * @param {!Error} error 47 | * @return {boolean} 48 | */ 49 | ExpectedError.prototype.matchesType = function(error) { 50 | return !this.type || 51 | (typeof this.type === 'function' && error instanceof this.type) || 52 | (this.type === error.constructor) || 53 | (this.type === error.constructor.name); 54 | }; 55 | 56 | /** 57 | * Determines whether the given error matches the expected error message. 58 | * 59 | * @param {!Error} error 60 | * @return {boolean} 61 | */ 62 | ExpectedError.prototype.matchesMessage = function(error) { 63 | return !this.message || 64 | (this.message === error.message) || 65 | (this.message.test && this.message.test(error.message)); 66 | }; 67 | 68 | /** 69 | * Asserts that the given error matches the expected error info. 70 | * 71 | * @param {?Error} error 72 | */ 73 | ExpectedError.prototype.assertMatch = function(error) { 74 | var matchError = this.matchError(error); 75 | if (matchError) { 76 | throw matchError; 77 | } 78 | }; 79 | 80 | /** 81 | * Gets the error to throw if the given error does not match. 82 | * 83 | * @param {?Error} error 84 | * @return {?AssertionError} 85 | */ 86 | ExpectedError.prototype.matchError = function(error) { 87 | var matchesType = error && this.matchesType(error); 88 | var matchesMessage = error && this.matchesMessage(error); 89 | 90 | if (matchesType && matchesMessage) { 91 | return null; 92 | } 93 | 94 | var assertMessage = 'expected error'; 95 | 96 | if (!matchesType && this.type) { 97 | assertMessage += ' type to equal ' + this.type; 98 | if (!matchesMessage && this.message) { 99 | assertMessage += ' and'; 100 | } 101 | } 102 | if (!matchesMessage && this.message) { 103 | assertMessage += ' message to match ' + 104 | (typeof this.message === 'string' ? '"' + this.message + '"' : this.message); 105 | } 106 | 107 | if (error) { 108 | assertMessage += ', but got ' + error; 109 | } else { 110 | assertMessage += ', but no error was thrown' 111 | } 112 | 113 | return new assert.AssertionError({ message: assertMessage }); 114 | }; 115 | 116 | module.exports = ExpectedError; 117 | -------------------------------------------------------------------------------- /test/support/test_formatter.js: -------------------------------------------------------------------------------- 1 | var Formatter = require('../../lib/formatters/formatter'); 2 | var extend = require('../../lib/utils').extend; 3 | 4 | /** 5 | * This basic formatter does not alter the AST at all, and only exists 6 | * help write unit tests for things that depend on formatters. 7 | * 8 | * @class 9 | * @extends Formatter 10 | */ 11 | function TestFormatter() { 12 | Formatter.call(this); 13 | this.processedExportDeclarationCount = 0; 14 | this.processedExportReassignmentCount = 0; 15 | this.processedImportDeclarationCount = 0; 16 | this.processedFunctionDeclarationCount = 0; 17 | this.processedVariableDeclarationCount = 0; 18 | } 19 | extend(TestFormatter, Formatter); 20 | 21 | /** 22 | * @override 23 | */ 24 | TestFormatter.prototype.build = function(modules) { 25 | return modules.map(function(mod) { 26 | var ast = mod.ast; 27 | ast.filename = mod.relativePath; 28 | return ast; 29 | }); 30 | }; 31 | 32 | /** 33 | * @override 34 | */ 35 | TestFormatter.prototype.processExportDeclaration = function() { 36 | this.processedExportDeclarationCount++; 37 | return null; 38 | }; 39 | 40 | /** 41 | * @override 42 | */ 43 | TestFormatter.prototype.processExportReassignment = function() { 44 | this.processedExportReassignmentCount++; 45 | return null; 46 | }; 47 | 48 | /** 49 | * @override 50 | */ 51 | TestFormatter.prototype.processImportDeclaration = function() { 52 | this.processedImportDeclarationCount++; 53 | return null; 54 | }; 55 | 56 | /** 57 | * @override 58 | */ 59 | TestFormatter.prototype.processFunctionDeclaration = function() { 60 | this.processedFunctionDeclarationCount++; 61 | return null; 62 | }; 63 | 64 | /** 65 | * @override 66 | */ 67 | TestFormatter.prototype.processVariableDeclaration = function() { 68 | this.processedVariableDeclarationCount++; 69 | return null; 70 | }; 71 | 72 | exports.TestFormatter = TestFormatter; 73 | -------------------------------------------------------------------------------- /test/support/test_resolver.js: -------------------------------------------------------------------------------- 1 | var Module = require('../../lib/module'); 2 | var Path = require('path'); 3 | 4 | /** 5 | * This basic resolver just returns a module whose #src is set to an 6 | * empty string to prevent an attempt to read from the file system. 7 | * 8 | * @class 9 | * @param {Object.=} sources 10 | */ 11 | function TestResolver(sources) { 12 | this.sources = sources || {}; 13 | } 14 | 15 | /** 16 | * @param {string} path 17 | * @param {Module} mod 18 | * @param {Container} container 19 | * @returns {Module} 20 | */ 21 | TestResolver.prototype.resolveModule = function(path, mod, container) { 22 | if (mod) { 23 | path = Path.normalize(Path.join(mod.relativePath, '..', path)); 24 | } 25 | 26 | var cachedModule = container.getCachedModule(path); 27 | if (cachedModule) { 28 | return cachedModule; 29 | } 30 | 31 | var resolved = new Module(path, path, container); 32 | resolved.src = this.sources[path] || ''; 33 | return resolved; 34 | }; 35 | 36 | exports.TestResolver = TestResolver; 37 | -------------------------------------------------------------------------------- /test/unit/container_test.js: -------------------------------------------------------------------------------- 1 | var Container = require('../../lib/container'); 2 | var Module = require('../../lib/module'); 3 | var Path = require('path'); 4 | var TestFormatter = require('../support/test_formatter').TestFormatter; 5 | var TestResolver = require('../support/test_resolver').TestResolver; 6 | var assert = require('assert'); 7 | var fs = require('fs'); 8 | var tmp = require('tmp'); 9 | 10 | describe('Container', function() { 11 | describe('#write', function() { 12 | it('allows multiple calls but only converts once', function(done) { 13 | var buildCallCount = 0; 14 | var source = 'var a = 1;'; 15 | var formatter = new TestFormatter(); 16 | 17 | /** 18 | * This formatter only exists to count the number of times #build is 19 | * called and create a result of the right data structure. 20 | * 21 | * @param {Module[]} modules 22 | * @returns {File[]} 23 | */ 24 | formatter.build = function(modules) { 25 | buildCallCount++; 26 | return TestFormatter.prototype.build.call(this, modules); 27 | }; 28 | 29 | var container = new Container({ 30 | formatter: formatter, 31 | resolvers: [new TestResolver({ 32 | 'a.js': source 33 | })] 34 | }); 35 | 36 | // Ensure we have a module to write at all. 37 | container.getModule('a.js'); 38 | 39 | tmp.dir(function(err, path) { 40 | if (err) { return done(err); } 41 | 42 | // Write the contents to a temporary directory. 43 | container.write(path); 44 | assert.strictEqual(buildCallCount, 1); 45 | 46 | // Ensure that the written file contains the original code. 47 | var a1 = fs.readFileSync(Path.join(path, 'a.js'), 'utf8'); 48 | assert.ok( 49 | a1.indexOf(source) === 0, 50 | 'expected written source to start with original source, but was: ' + a1 51 | ); 52 | 53 | tmp.dir(function(err, path2) { 54 | if (err) { return done(err); } 55 | assert.notStrictEqual(path, path2); 56 | 57 | // Write to yet another temporary directory with the same container. 58 | container.write(path2); 59 | assert.strictEqual(buildCallCount, 1); 60 | 61 | // Ensure that the written file contains the original code. 62 | var a2 = fs.readFileSync(Path.join(path2, 'a.js'), 'utf8'); 63 | assert.ok( 64 | a2.indexOf(source) === 0, 65 | 'expected written source to start with original source, but was: ' + a2 66 | ); 67 | 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | it('freezes the container, effectively preventing adding new modules', function(done) { 74 | var container = new Container({ 75 | formatter: new TestFormatter(), 76 | resolvers: [new TestResolver()] 77 | }); 78 | 79 | container.getModule('a.js'); 80 | 81 | tmp.dir(function(err, path) { 82 | if (err) { return done(err); } 83 | 84 | container.write(path); 85 | 86 | try { 87 | container.getModule('b.js'); 88 | assert.fail('expected an exception'); 89 | } catch (ex) { 90 | assert.strictEqual( 91 | 'container has already converted contained modules and cannot add new module: b.js', 92 | ex.message 93 | ); 94 | } 95 | 96 | done(); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('#transform', function() { 102 | var formatter = new TestFormatter(); 103 | var source = 'export var a = 1;'; 104 | var container = new Container({ 105 | formatter: formatter, 106 | resolvers: [new TestResolver({ 107 | 'a.js': source 108 | })] 109 | }); 110 | 111 | // Ensure we have a module to write at all. 112 | container.getModule('a.js'); 113 | 114 | var files = container.transform(); 115 | assert.strictEqual(files.length, 1); 116 | assert.strictEqual(files[0].filename, 'a.js'); 117 | assert.strictEqual(files[0].code, source); 118 | assert.strictEqual(typeof files[0].map, 'object'); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/unit/mkdirp_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var mkdirpSync = require('../../lib/utils').mkdirpSync; 3 | 4 | describe('mkdirpSync', function() { 5 | var calls; 6 | var fs = { 7 | existsSync: function(path) { 8 | calls.push(['existsSync', path]); 9 | return path === '/' || path === '/path' || path === 'path'; 10 | }, 11 | 12 | mkdirSync: function(path) { 13 | calls.push(['mkdirSync', path]); 14 | } 15 | }; 16 | 17 | beforeEach(function() { 18 | calls = []; 19 | }); 20 | 21 | context('given absolute paths', function() { 22 | it('checks each path component, making the ones that do not exist', function() { 23 | mkdirpSync('/path/to/some/dir', { fs: fs }); 24 | assert.deepEqual( 25 | calls, 26 | [ 27 | ['existsSync', '/path'], 28 | ['existsSync', '/path/to'], 29 | ['mkdirSync', '/path/to'], 30 | ['existsSync', '/path/to/some'], 31 | ['mkdirSync', '/path/to/some'], 32 | ['existsSync', '/path/to/some/dir'], 33 | ['mkdirSync', '/path/to/some/dir'] 34 | ] 35 | ); 36 | }); 37 | }); 38 | 39 | context('given relative paths', function() { 40 | it('checks each path component, making the ones that do not exist', function() { 41 | mkdirpSync('path/to/some/dir', { fs: fs }); 42 | assert.deepEqual( 43 | calls, 44 | [ 45 | ['existsSync', 'path'], 46 | ['existsSync', 'path/to'], 47 | ['mkdirSync', 'path/to'], 48 | ['existsSync', 'path/to/some'], 49 | ['mkdirSync', 'path/to/some'], 50 | ['existsSync', 'path/to/some/dir'], 51 | ['mkdirSync', 'path/to/some/dir'] 52 | ] 53 | ); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/unit/module_binding_specifier_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Container = require('../../lib/container'); 3 | var Module = require('../../lib/module'); 4 | var TestFormatter = require('../support/test_formatter').TestFormatter; 5 | var TestResolver = require('../support/test_resolver').TestResolver; 6 | 7 | describe('ModuleBindingSpecifier', function() { 8 | describe('#terminalExportSpecifier', function() { 9 | var sources; 10 | var container; 11 | 12 | beforeEach(function() { 13 | container = new Container({ 14 | formatter: new TestFormatter(), 15 | resolvers: [new TestResolver(sources)] 16 | }); 17 | }); 18 | 19 | function getExportSpecifier(modulePath, exportedName) { 20 | var mod = container.getModule(modulePath); 21 | var specifier = mod.exports.findSpecifierByName(exportedName); 22 | if (!specifier) { 23 | throw new Error('unable to find export `' + exportedName + '` in module: ' + modulePath); 24 | } 25 | return specifier; 26 | } 27 | 28 | context('when the export is a variable declaration', function() { 29 | before(function() { 30 | sources = { 'index.js': 'export var a = 1;' }; 31 | }); 32 | 33 | it('is the export itself', function() { 34 | var specifier = getExportSpecifier('index.js', 'a'); 35 | assert.strictEqual(specifier.terminalExportSpecifier, specifier); 36 | }); 37 | }); 38 | 39 | context('when the export is a function declaration', function() { 40 | before(function() { 41 | sources = { 'index.js': 'export function a() {}' }; 42 | }); 43 | 44 | it('is the export itself', function() { 45 | var specifier = getExportSpecifier('index.js', 'a'); 46 | assert.strictEqual(specifier.terminalExportSpecifier, specifier); 47 | }); 48 | }); 49 | 50 | context('when the export binds an import by name from another module', function() { 51 | before(function() { 52 | sources = { 53 | 'index.js': 'import { a } from "middle.js";\nexport { a };', 54 | 'middle.js': 'import { a } from "root.js";\nexport { a };', 55 | 'root.js': 'export var a = 1;' 56 | }; 57 | }); 58 | 59 | it('follows the trail of imports until it finds the original', function() { 60 | var rootA = getExportSpecifier('root.js', 'a'); 61 | var specifier = getExportSpecifier('index.js', 'a'); 62 | assert.strictEqual( 63 | specifier.terminalExportSpecifier, 64 | rootA, 65 | 'expected ' + specifier.terminalExportSpecifier + ' to equal ' + rootA 66 | ); 67 | }); 68 | }); 69 | 70 | context('when the export directly re-exports a binding by name from another module', function() { 71 | before(function() { 72 | sources = { 73 | 'index.js': 'import { a } from "middle.js";\nexport { a };', 74 | 'middle.js': 'export { a } from "root.js";', 75 | 'root.js': 'export var a = 1;' 76 | }; 77 | }); 78 | 79 | it('follows the trail of imports until it finds the original', function() { 80 | var rootA = getExportSpecifier('root.js', 'a'); 81 | var specifier = getExportSpecifier('index.js', 'a'); 82 | assert.strictEqual( 83 | specifier.terminalExportSpecifier, 84 | rootA, 85 | 'expected ' + specifier.terminalExportSpecifier + ' to equal ' + rootA 86 | ); 87 | }); 88 | }); 89 | 90 | xcontext('when the export binds an import through a batch export', function() { 91 | before(function() { 92 | sources = { 93 | 'index.js': 'import { a } from "middle.js";\nexport { a };', 94 | 'middle.js': 'export * from "root.js";', 95 | 'root.js': 'export var a = 1;' 96 | }; 97 | }); 98 | 99 | it('follows the trail of imports until it finds the original', function() { 100 | var rootA = getExportSpecifier('root.js', 'a'); 101 | var specifier = getExportSpecifier('index.js', 'a'); 102 | assert.strictEqual( 103 | getExportSpecifier('index.js', 'a').terminalExportSpecifier, 104 | rootA, 105 | 'expected ' + specifier.terminalExportSpecifier + ' to equal ' + rootA 106 | ); 107 | }); 108 | }); 109 | }); 110 | }); -------------------------------------------------------------------------------- /test/unit/rewriter_test.js: -------------------------------------------------------------------------------- 1 | var Container = require('../../lib/container'); 2 | var Module = require('../../lib/module'); 3 | var Rewriter = require('../../lib/rewriter'); 4 | var TestFormatter = require('../support/test_formatter').TestFormatter; 5 | var TestResolver = require('../support/test_resolver').TestResolver; 6 | var assert = require('assert'); 7 | 8 | describe('Rewriter', function() { 9 | describe('#rewrite', function() { 10 | context('when a module has no imports or exports', function() { 11 | it('does not traverse the module at all', function() { 12 | var formatter = new TestFormatter(); 13 | var container = new Container({ 14 | formatter: formatter, 15 | resolvers: [new TestResolver({ 16 | 'a.js': 'var foo = 1\nfunction bar() {}' 17 | })] 18 | }); 19 | 20 | var a = container.getModule('a.js'); 21 | var rewriter = new Rewriter(formatter); 22 | rewriter.rewrite(container.getModules()); 23 | 24 | assert.strictEqual(formatter.processedExportDeclarationCount, 0); 25 | assert.strictEqual(formatter.processedFunctionDeclarationCount, 0); 26 | assert.strictEqual(formatter.processedVariableDeclarationCount, 0); 27 | }); 28 | }); 29 | }); 30 | }); -------------------------------------------------------------------------------- /test/unit/sorting_test.js: -------------------------------------------------------------------------------- 1 | var sort = require('../../lib/sorting').sort; 2 | var assert = require('assert'); 3 | var util = require('util'); 4 | 5 | function assertArraysEqual(a, b) { 6 | var message = 'expected ' + util.inspect(a) + ' to equal ' + util.inspect(b); 7 | assert.ok(a.length === b.length, message); 8 | 9 | for (var i = 0, length = a.length; i < length; i++) { 10 | assert.ok(a[i] === b[i], message); 11 | } 12 | } 13 | 14 | describe('sort', function() { 15 | it('returns an empty list given an empty list', function() { 16 | assertArraysEqual(sort([]), []); 17 | }); 18 | 19 | it('returns a list of a single module given a single module', function() { 20 | var mod = { id: 'testmod', imports: { modules: [] } }; 21 | assertArraysEqual(sort([mod]), [mod]); 22 | }); 23 | 24 | it('properly orders a linear set of modules', function() { 25 | var a = { id: 'a', imports: { modules: [] } }; 26 | var b = { id: 'b', imports: { modules: [a] } }; 27 | var c = { id: 'c', imports: { modules: [b] } }; 28 | 29 | assertArraysEqual(sort([b, a, c]), [a, b, c]); 30 | }); 31 | 32 | it('properly orders a tree of modules', function() { 33 | var b = { id: 'b', imports: { modules: [] } }; 34 | var c = { id: 'c', imports: { modules: [] } }; 35 | var a = { id: 'a', imports: { modules: [b, c] } }; 36 | 37 | assertArraysEqual(sort([b, a, c]), [b, c, a]); 38 | assertArraysEqual(sort([c, a, b]), [c, b, a]); 39 | }); 40 | 41 | it('properly orders a DAG of modules', function() { 42 | var a = { id: 'a', imports: { modules: [] } }; 43 | var b = { id: 'b', imports: { modules: [] } }; 44 | var c = { id: 'c', imports: { modules: [] } }; 45 | 46 | a.imports.modules.push(b, c); 47 | b.imports.modules.push(c); 48 | 49 | assertArraysEqual(sort([a, b, c]), [c, b, a]); 50 | assertArraysEqual(sort([b, a, c]), [c, b, a]); 51 | }); 52 | 53 | it('orders a simple cyclic graph by last-required', function() { 54 | var a = { id: 'a', imports: { modules: [] } }; 55 | var b = { id: 'b', imports: { modules: [] } }; 56 | 57 | a.imports.modules.push(b); 58 | b.imports.modules.push(a); 59 | 60 | assertArraysEqual(sort([a, b]), [b, a]); 61 | assertArraysEqual(sort([b, a]), [a, b]); 62 | }); 63 | 64 | it('orders a complex cyclic graph by last-required', function() { 65 | var a = { id: 'a', imports: { modules: [] } }; 66 | var b = { id: 'b', imports: { modules: [a] } }; 67 | var c = { id: 'c', imports: { modules: [b] } }; 68 | var d = { id: 'd', imports: { modules: [c] } }; 69 | 70 | a.imports.modules.push(d); 71 | 72 | assertArraysEqual(sort([b, c, d, a]), [c, d, a, b]); 73 | assertArraysEqual(sort([b, c, a, d]), [c, d, a, b]); 74 | assertArraysEqual(sort([c, d, a, b]), [d, a, b, c]); 75 | assertArraysEqual(sort([c, b, a, d]), [d, a, b, c]); 76 | }); 77 | }); --------------------------------------------------------------------------------