Support import and export of ES2015 modules (#4300)
This pull request adds support for ES2015 modules, by recognizing `import` and `export` statements. The following syntaxes are supported, based on the MDN [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) and [export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) pages:
```js
import "module-name"
import defaultMember from "module-name"
import * as name from "module-name"
import { } from "module-name"
import { member } from "module-name"
import { member as alias } from "module-name"
import { member1, member2 as alias2, … } from "module-name"
import defaultMember, * as name from "module-name"
import defaultMember, { … } from "module-name"
export default expression
export class name
export { }
export { name }
export { name as exportedName }
export { name as default }
export { name1, name2 as exportedName2, name3 as default, … }
export * from "module-name"
export { … } from "module-name"
```
As a subsitute for ECMAScript’s `export var name = …` and `export function name {}`, CoffeeScript also supports:
```js
export name = …
```
CoffeeScript also supports optional commas within `{ … }`.
This PR converts the supported `import` and `export` statements into ES2015 `import` and `export` statements; it **does not resolve the modules**. So any CoffeeScript with `import` or `export` statements will be output as ES2015, and will need to be transpiled by another tool such as Babel before it can be used in a browser. We will need to add a warning to the documentation explaining this.
This should be fully backwards-compatible, as `import` and `export` were previously reserved keywords. No flags are used.
There are extensive tests included, though because no current JavaScript runtime supports `import` or `export`, the tests compare strings of what the compiled CoffeeScript output is against what the expected ES2015 should be. I also conducted two more elaborate tests:
* I forked the [ember-piqu](https://github.com/pauc/piqu-ember) project, which was an Ember CLI app that used ember-cli-coffeescript and [ember-cli-coffees6](https://github.com/alexspeller/ember-cli-coffees6) (which adds “support” for `import`/`export` by wrapping such statements in backticks before passing the result to the CoffeeScript compiler). I removed `ember-cli-coffees6` and replaced the CoffeeScript compiler used in the build chain with this code, and the app built without errors. [Demo here.](https://github.com/GeoffreyBooth/coffeescript-modules-test-piqu)
* I also forked the [CoffeeScript version of Meteor’s Todos example app](https://github.com/meteor/todos/tree/coffeescript), and replaced all of its `require` statements with the `import` and `export` statements from the original ES2015 version of the app on its `master` branch. I then updated the `coffeescript` Meteor package in the app to use this new code, and again the app builds without errors. [Demo here.](https://github.com/GeoffreyBooth/coffeescript-modules-test-meteor-todos)
The discussion history for this work started [here](https://github.com/jashkenas/coffeescript/pull/4160) and continued [here](https://github.com/GeoffreyBooth/coffeescript/pull/2). @lydell provided guidance, and @JimPanic and @rattrayalex contributed essential code.
2016-09-14 14:46:05 -04:00
|
|
|
|
# Modules, a.k.a. ES2015 import/export
|
|
|
|
|
# ------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# Remember, we’re not *resolving* modules, just outputting valid ES2015 syntax.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# This is the CoffeeScript import and export syntax, closely modeled after the ES2015 syntax
|
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
|
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
|
|
|
|
|
|
|
|
|
|
# import "module-name"
|
|
|
|
|
# import defaultMember from "module-name"
|
|
|
|
|
# import * as name from "module-name"
|
|
|
|
|
# import { } from "module-name"
|
|
|
|
|
# import { member } from "module-name"
|
|
|
|
|
# import { member as alias } from "module-name"
|
|
|
|
|
# import { member1, member2 as alias2, … } from "module-name"
|
|
|
|
|
# import defaultMember, * as name from "module-name"
|
|
|
|
|
# import defaultMember, { … } from "module-name"
|
|
|
|
|
|
|
|
|
|
# export default expression
|
|
|
|
|
# export class name
|
|
|
|
|
# export { }
|
|
|
|
|
# export { name }
|
|
|
|
|
# export { name as exportedName }
|
|
|
|
|
# export { name as default }
|
|
|
|
|
# export { name1, name2 as exportedName2, name3 as default, … }
|
|
|
|
|
#
|
|
|
|
|
# export * from "module-name"
|
|
|
|
|
# export { … } from "module-name"
|
|
|
|
|
#
|
|
|
|
|
# As a subsitute for `export var name = …` and `export function name {}`,
|
|
|
|
|
# CoffeeScript also supports:
|
|
|
|
|
# export name = …
|
|
|
|
|
|
|
|
|
|
# CoffeeScript also supports optional commas within `{ … }`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Helper function
|
|
|
|
|
toJS = (str) ->
|
|
|
|
|
CoffeeScript.compile str, bare: yes
|
|
|
|
|
.replace /^\s+|\s+$/g, '' # Trim leading/trailing whitespace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Import statements
|
|
|
|
|
|
|
|
|
|
test "backticked import statement", ->
|
|
|
|
|
input = """
|
|
|
|
|
if Meteor.isServer
|
|
|
|
|
`import { foo, bar as baz } from 'lib'`"""
|
|
|
|
|
output = """
|
|
|
|
|
if (Meteor.isServer) {
|
|
|
|
|
import { foo, bar as baz } from 'lib';
|
|
|
|
|
}"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import an entire module for side effects only, without importing any bindings", ->
|
|
|
|
|
input = "import 'lib'"
|
|
|
|
|
output = "import 'lib';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import default member from module, adding the member to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import foo from 'lib'
|
|
|
|
|
foo.fooMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import foo from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import an entire module's contents as an alias, adding the alias to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import * as foo from 'lib'
|
|
|
|
|
foo.fooMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import * as foo from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import empty object", ->
|
|
|
|
|
input = "import { } from 'lib'"
|
|
|
|
|
output = "import {} from 'lib';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import empty object", ->
|
|
|
|
|
input = "import {} from 'lib'"
|
|
|
|
|
output = "import {} from 'lib';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import a single member of a module, adding the member to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { foo } from 'lib'
|
|
|
|
|
foo.fooMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import a single member of a module as an alias, adding the alias to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { foo as bar } from 'lib'
|
|
|
|
|
bar.barMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo as bar
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
bar.barMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import multiple members of a module, adding the members to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { foo, bar } from 'lib'
|
|
|
|
|
foo.fooMethod()
|
|
|
|
|
bar.barMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();
|
|
|
|
|
|
|
|
|
|
bar.barMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import multiple members of a module where some are aliased, adding the members or aliases to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { foo, bar as baz } from 'lib'
|
|
|
|
|
foo.fooMethod()
|
|
|
|
|
baz.bazMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar as baz
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();
|
|
|
|
|
|
|
|
|
|
baz.bazMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import default member and other members of a module, adding the members to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import foo, { bar, baz as qux } from 'lib'
|
|
|
|
|
foo.fooMethod()
|
|
|
|
|
bar.barMethod()
|
|
|
|
|
qux.quxMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import foo, {
|
|
|
|
|
bar,
|
|
|
|
|
baz as qux
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();
|
|
|
|
|
|
|
|
|
|
bar.barMethod();
|
|
|
|
|
|
|
|
|
|
qux.quxMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import default member from a module as well as the entire module's contents as an alias, adding the member and alias to the current scope", ->
|
|
|
|
|
input = """
|
|
|
|
|
import foo, * as bar from 'lib'
|
|
|
|
|
foo.fooMethod()
|
|
|
|
|
bar.barMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import foo, * as bar from 'lib';
|
|
|
|
|
|
|
|
|
|
foo.fooMethod();
|
|
|
|
|
|
|
|
|
|
bar.barMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "multiline simple import", ->
|
|
|
|
|
input = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar as baz
|
|
|
|
|
} from 'lib'"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar as baz
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "multiline complex import", ->
|
|
|
|
|
input = """
|
|
|
|
|
import foo, {
|
|
|
|
|
bar,
|
|
|
|
|
baz as qux
|
|
|
|
|
} from 'lib'"""
|
|
|
|
|
output = """
|
|
|
|
|
import foo, {
|
|
|
|
|
bar,
|
|
|
|
|
baz as qux
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "import with optional commas", ->
|
|
|
|
|
input = "import { foo, bar, } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "multiline import without commas", ->
|
|
|
|
|
input = """
|
|
|
|
|
import {
|
|
|
|
|
foo
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib'"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "multiline import with optional commas", ->
|
|
|
|
|
input = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar,
|
|
|
|
|
} from 'lib'"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "a variable can be assigned after an import", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { foo } from 'lib'
|
|
|
|
|
bar = 5"""
|
|
|
|
|
output = """
|
|
|
|
|
var bar;
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
foo
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
bar = 5;"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "variables can be assigned before and after an import", ->
|
|
|
|
|
input = """
|
|
|
|
|
foo = 5
|
|
|
|
|
import { bar } from 'lib'
|
|
|
|
|
baz = 7"""
|
|
|
|
|
output = """
|
|
|
|
|
var baz, foo;
|
|
|
|
|
|
|
|
|
|
foo = 5;
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
baz = 7;"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
# Export statements
|
|
|
|
|
|
|
|
|
|
test "export empty object", ->
|
|
|
|
|
input = "export { }"
|
|
|
|
|
output = "export {};"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export empty object", ->
|
|
|
|
|
input = "export {}"
|
|
|
|
|
output = "export {};"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export named members within an object", ->
|
|
|
|
|
input = "export { foo, bar }"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export named members as aliases, within an object", ->
|
|
|
|
|
input = "export { foo as bar, baz as qux }"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo as bar,
|
|
|
|
|
baz as qux
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export named members within an object, with an optional comma", ->
|
|
|
|
|
input = "export { foo, bar, }"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "multiline export named members within an object", ->
|
|
|
|
|
input = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
}"""
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "multiline export named members within an object, with an optional comma", ->
|
|
|
|
|
input = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar,
|
|
|
|
|
}"""
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default string", ->
|
|
|
|
|
input = "export default 'foo'"
|
|
|
|
|
output = "export default 'foo';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default number", ->
|
|
|
|
|
input = "export default 5"
|
|
|
|
|
output = "export default 5;"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default object", ->
|
|
|
|
|
input = "export default { foo: 'bar', baz: 'qux' }"
|
|
|
|
|
output = """
|
|
|
|
|
export default {
|
|
|
|
|
foo: 'bar',
|
|
|
|
|
baz: 'qux'
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default assignment expression", ->
|
|
|
|
|
input = "export default foo = 'bar'"
|
|
|
|
|
output = """
|
|
|
|
|
var foo;
|
|
|
|
|
|
|
|
|
|
export default foo = 'bar';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export assignment expression", ->
|
|
|
|
|
input = "export foo = 'bar'"
|
|
|
|
|
output = "export var foo = 'bar';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export multiline assignment expression", ->
|
|
|
|
|
input = """
|
|
|
|
|
export foo =
|
|
|
|
|
'bar'"""
|
|
|
|
|
output = "export var foo = 'bar';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export multiline indented assignment expression", ->
|
|
|
|
|
input = """
|
|
|
|
|
export foo =
|
|
|
|
|
'bar'"""
|
|
|
|
|
output = "export var foo = 'bar';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default function", ->
|
|
|
|
|
input = "export default ->"
|
|
|
|
|
output = "export default function() {};"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default multiline function", ->
|
|
|
|
|
input = """
|
|
|
|
|
export default (foo) ->
|
|
|
|
|
console.log foo"""
|
|
|
|
|
output = """
|
|
|
|
|
export default function(foo) {
|
|
|
|
|
return console.log(foo);
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export assignment function", ->
|
|
|
|
|
input = """
|
|
|
|
|
export foo = (bar) ->
|
|
|
|
|
console.log bar"""
|
|
|
|
|
output = """
|
|
|
|
|
export var foo = function(bar) {
|
|
|
|
|
return console.log(bar);
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export assignment function which contains assignments in its body", ->
|
|
|
|
|
input = """
|
|
|
|
|
export foo = (bar) ->
|
|
|
|
|
baz = '!'
|
|
|
|
|
console.log bar + baz"""
|
|
|
|
|
output = """
|
|
|
|
|
export var foo = function(bar) {
|
|
|
|
|
var baz;
|
|
|
|
|
baz = '!';
|
|
|
|
|
return console.log(bar + baz);
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export default predefined function", ->
|
|
|
|
|
input = """
|
|
|
|
|
foo = (bar) ->
|
|
|
|
|
console.log bar
|
|
|
|
|
export default foo"""
|
|
|
|
|
output = """
|
|
|
|
|
var foo;
|
|
|
|
|
|
|
|
|
|
foo = function(bar) {
|
|
|
|
|
return console.log(bar);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default foo;"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
# Uncomment this test once ES2015+ `class` support is added
|
|
|
|
|
|
|
|
|
|
# test "export default class", ->
|
|
|
|
|
# input = """
|
|
|
|
|
# export default class foo extends bar
|
|
|
|
|
# baz: ->
|
|
|
|
|
# console.log 'hello, world!'"""
|
|
|
|
|
# output = """
|
|
|
|
|
# export default class foo extends bar {
|
|
|
|
|
# baz: function {
|
|
|
|
|
# return console.log('hello, world!');
|
|
|
|
|
# }
|
|
|
|
|
# }"""
|
|
|
|
|
# eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
# Very limited tests for now, testing that `export class foo` either compiles
|
|
|
|
|
# identically (ES2015+) or at least into some function, leaving the specifics
|
|
|
|
|
# vague in case the CoffeeScript `class` interpretation changes
|
|
|
|
|
test "export class", ->
|
|
|
|
|
input = """
|
|
|
|
|
export class foo
|
|
|
|
|
baz: ->
|
|
|
|
|
console.log 'hello, world!'"""
|
|
|
|
|
output = toJS input
|
|
|
|
|
ok /^export (class foo|var foo = \(function)/.test toJS input
|
|
|
|
|
|
|
|
|
|
test "export class that extends", ->
|
|
|
|
|
input = """
|
|
|
|
|
export class foo extends bar
|
|
|
|
|
baz: ->
|
|
|
|
|
console.log 'hello, world!'"""
|
|
|
|
|
output = toJS input
|
|
|
|
|
ok /export (class foo|var foo = \(function)/.test(output) and \
|
|
|
|
|
not /var foo(;|,)/.test output
|
|
|
|
|
|
|
|
|
|
test "export default class that extends", ->
|
|
|
|
|
input = """
|
|
|
|
|
export default class foo extends bar
|
|
|
|
|
baz: ->
|
|
|
|
|
console.log 'hello, world!'"""
|
|
|
|
|
ok /export default (class foo|foo = \(function)/.test toJS input
|
|
|
|
|
|
|
|
|
|
test "export default named member, within an object", ->
|
|
|
|
|
input = "export { foo as default, bar }"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo as default,
|
|
|
|
|
bar
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Import and export in the same statement
|
|
|
|
|
|
|
|
|
|
test "export an entire module's contents", ->
|
|
|
|
|
input = "export * from 'lib'"
|
|
|
|
|
output = "export * from 'lib';"
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export members imported from another module", ->
|
|
|
|
|
input = "export { foo, bar } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo,
|
|
|
|
|
bar
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export as aliases members imported from another module", ->
|
|
|
|
|
input = "export { foo as bar, baz as qux } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
foo as bar,
|
|
|
|
|
baz as qux
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
2016-10-26 08:37:19 -04:00
|
|
|
|
test "export list can contain CS only keywords", ->
|
|
|
|
|
input = "export { unless } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
unless
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "export list can contain CS only keywords when aliasing", ->
|
|
|
|
|
input = "export { when as bar, baz as unless } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
export {
|
|
|
|
|
when as bar,
|
|
|
|
|
baz as unless
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
Support import and export of ES2015 modules (#4300)
This pull request adds support for ES2015 modules, by recognizing `import` and `export` statements. The following syntaxes are supported, based on the MDN [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) and [export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) pages:
```js
import "module-name"
import defaultMember from "module-name"
import * as name from "module-name"
import { } from "module-name"
import { member } from "module-name"
import { member as alias } from "module-name"
import { member1, member2 as alias2, … } from "module-name"
import defaultMember, * as name from "module-name"
import defaultMember, { … } from "module-name"
export default expression
export class name
export { }
export { name }
export { name as exportedName }
export { name as default }
export { name1, name2 as exportedName2, name3 as default, … }
export * from "module-name"
export { … } from "module-name"
```
As a subsitute for ECMAScript’s `export var name = …` and `export function name {}`, CoffeeScript also supports:
```js
export name = …
```
CoffeeScript also supports optional commas within `{ … }`.
This PR converts the supported `import` and `export` statements into ES2015 `import` and `export` statements; it **does not resolve the modules**. So any CoffeeScript with `import` or `export` statements will be output as ES2015, and will need to be transpiled by another tool such as Babel before it can be used in a browser. We will need to add a warning to the documentation explaining this.
This should be fully backwards-compatible, as `import` and `export` were previously reserved keywords. No flags are used.
There are extensive tests included, though because no current JavaScript runtime supports `import` or `export`, the tests compare strings of what the compiled CoffeeScript output is against what the expected ES2015 should be. I also conducted two more elaborate tests:
* I forked the [ember-piqu](https://github.com/pauc/piqu-ember) project, which was an Ember CLI app that used ember-cli-coffeescript and [ember-cli-coffees6](https://github.com/alexspeller/ember-cli-coffees6) (which adds “support” for `import`/`export` by wrapping such statements in backticks before passing the result to the CoffeeScript compiler). I removed `ember-cli-coffees6` and replaced the CoffeeScript compiler used in the build chain with this code, and the app built without errors. [Demo here.](https://github.com/GeoffreyBooth/coffeescript-modules-test-piqu)
* I also forked the [CoffeeScript version of Meteor’s Todos example app](https://github.com/meteor/todos/tree/coffeescript), and replaced all of its `require` statements with the `import` and `export` statements from the original ES2015 version of the app on its `master` branch. I then updated the `coffeescript` Meteor package in the app to use this new code, and again the app builds without errors. [Demo here.](https://github.com/GeoffreyBooth/coffeescript-modules-test-meteor-todos)
The discussion history for this work started [here](https://github.com/jashkenas/coffeescript/pull/4160) and continued [here](https://github.com/GeoffreyBooth/coffeescript/pull/2). @lydell provided guidance, and @JimPanic and @rattrayalex contributed essential code.
2016-09-14 14:46:05 -04:00
|
|
|
|
|
|
|
|
|
# Edge cases
|
|
|
|
|
|
|
|
|
|
test "multiline import with comments", ->
|
|
|
|
|
input = """
|
|
|
|
|
import {
|
|
|
|
|
foo, # Not as good as bar
|
|
|
|
|
bar as baz # I prefer qux
|
|
|
|
|
} from 'lib'"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
bar as baz
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`from` not part of an import or export statement can still be assigned", ->
|
|
|
|
|
from = 5
|
|
|
|
|
eq 5, from
|
|
|
|
|
|
|
|
|
|
test "a variable named `from` can be assigned after an import", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { foo } from 'lib'
|
|
|
|
|
from = 5"""
|
|
|
|
|
output = """
|
|
|
|
|
var from;
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
foo
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
from = 5;"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`from` can be assigned after a multiline import", ->
|
|
|
|
|
input = """
|
|
|
|
|
import {
|
|
|
|
|
foo
|
|
|
|
|
} from 'lib'
|
|
|
|
|
from = 5"""
|
|
|
|
|
output = """
|
|
|
|
|
var from;
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
foo
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
from = 5;"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`from` can be imported as a member name", ->
|
|
|
|
|
input = "import { from } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
from
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`from` can be imported as a member name and aliased", ->
|
|
|
|
|
input = "import { from as foo } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
from as foo
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`from` can be used as an alias name", ->
|
|
|
|
|
input = "import { foo as from } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo as from
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`as` can be imported as a member name", ->
|
|
|
|
|
input = "import { as } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
as
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`as` can be imported as a member name and aliased", ->
|
|
|
|
|
input = "import { as as foo } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
as as foo
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "`as` can be used as an alias name", ->
|
|
|
|
|
input = "import { foo as as } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo as as
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
2016-10-26 08:37:19 -04:00
|
|
|
|
test "CS only keywords can be used as imported names in import lists", ->
|
|
|
|
|
input = """
|
|
|
|
|
import { unless as bar } from 'lib'
|
|
|
|
|
bar.barMethod()"""
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
unless as bar
|
|
|
|
|
} from 'lib';
|
|
|
|
|
|
|
|
|
|
bar.barMethod();"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
2016-09-15 02:30:58 -04:00
|
|
|
|
test "`*` can be used in an expression on the same line as an export keyword", ->
|
|
|
|
|
input = "export foo = (x) -> x * x"
|
|
|
|
|
output = """
|
|
|
|
|
export var foo = function(x) {
|
|
|
|
|
return x * x;
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
input = "export default foo = (x) -> x * x"
|
|
|
|
|
output = """
|
|
|
|
|
var foo;
|
|
|
|
|
|
|
|
|
|
export default foo = function(x) {
|
|
|
|
|
return x * x;
|
|
|
|
|
};"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
Support import and export of ES2015 modules (#4300)
This pull request adds support for ES2015 modules, by recognizing `import` and `export` statements. The following syntaxes are supported, based on the MDN [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) and [export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) pages:
```js
import "module-name"
import defaultMember from "module-name"
import * as name from "module-name"
import { } from "module-name"
import { member } from "module-name"
import { member as alias } from "module-name"
import { member1, member2 as alias2, … } from "module-name"
import defaultMember, * as name from "module-name"
import defaultMember, { … } from "module-name"
export default expression
export class name
export { }
export { name }
export { name as exportedName }
export { name as default }
export { name1, name2 as exportedName2, name3 as default, … }
export * from "module-name"
export { … } from "module-name"
```
As a subsitute for ECMAScript’s `export var name = …` and `export function name {}`, CoffeeScript also supports:
```js
export name = …
```
CoffeeScript also supports optional commas within `{ … }`.
This PR converts the supported `import` and `export` statements into ES2015 `import` and `export` statements; it **does not resolve the modules**. So any CoffeeScript with `import` or `export` statements will be output as ES2015, and will need to be transpiled by another tool such as Babel before it can be used in a browser. We will need to add a warning to the documentation explaining this.
This should be fully backwards-compatible, as `import` and `export` were previously reserved keywords. No flags are used.
There are extensive tests included, though because no current JavaScript runtime supports `import` or `export`, the tests compare strings of what the compiled CoffeeScript output is against what the expected ES2015 should be. I also conducted two more elaborate tests:
* I forked the [ember-piqu](https://github.com/pauc/piqu-ember) project, which was an Ember CLI app that used ember-cli-coffeescript and [ember-cli-coffees6](https://github.com/alexspeller/ember-cli-coffees6) (which adds “support” for `import`/`export` by wrapping such statements in backticks before passing the result to the CoffeeScript compiler). I removed `ember-cli-coffees6` and replaced the CoffeeScript compiler used in the build chain with this code, and the app built without errors. [Demo here.](https://github.com/GeoffreyBooth/coffeescript-modules-test-piqu)
* I also forked the [CoffeeScript version of Meteor’s Todos example app](https://github.com/meteor/todos/tree/coffeescript), and replaced all of its `require` statements with the `import` and `export` statements from the original ES2015 version of the app on its `master` branch. I then updated the `coffeescript` Meteor package in the app to use this new code, and again the app builds without errors. [Demo here.](https://github.com/GeoffreyBooth/coffeescript-modules-test-meteor-todos)
The discussion history for this work started [here](https://github.com/jashkenas/coffeescript/pull/4160) and continued [here](https://github.com/GeoffreyBooth/coffeescript/pull/2). @lydell provided guidance, and @JimPanic and @rattrayalex contributed essential code.
2016-09-14 14:46:05 -04:00
|
|
|
|
test "`*` and `from` can be used in an export default expression", ->
|
|
|
|
|
input = """
|
|
|
|
|
export default foo.extend
|
|
|
|
|
bar: ->
|
|
|
|
|
from = 5
|
|
|
|
|
from = from * 3"""
|
|
|
|
|
output = """
|
|
|
|
|
export default foo.extend({
|
|
|
|
|
bar: function() {
|
|
|
|
|
var from;
|
|
|
|
|
from = 5;
|
|
|
|
|
return from = from * 3;
|
|
|
|
|
}
|
|
|
|
|
});"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "wrapped members can be imported multiple times if aliased", ->
|
|
|
|
|
input = "import { foo, foo as bar } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import {
|
|
|
|
|
foo,
|
|
|
|
|
foo as bar
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|
|
|
|
|
|
|
|
|
|
test "default and wrapped members can be imported multiple times if aliased", ->
|
|
|
|
|
input = "import foo, { foo as bar } from 'lib'"
|
|
|
|
|
output = """
|
|
|
|
|
import foo, {
|
|
|
|
|
foo as bar
|
|
|
|
|
} from 'lib';"""
|
|
|
|
|
eq toJS(input), output
|