mirror of
https://github.com/jashkenas/coffeescript.git
synced 2026-05-03 03:00:14 -04:00
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.
This commit is contained in:
committed by
Simon Lydell
parent
133fadd36a
commit
66ac8af678
@@ -63,6 +63,13 @@ exports.compile = compile = withPrettyErrors (code, options) ->
|
||||
token[1] for token in tokens when token[0] is 'IDENTIFIER'
|
||||
)
|
||||
|
||||
# Check for import or export; if found, force bare mode
|
||||
unless options.bare? and options.bare is yes
|
||||
for token in tokens
|
||||
if token[0] in ['IMPORT', 'EXPORT']
|
||||
options.bare = yes
|
||||
break
|
||||
|
||||
fragments = parser.parse(tokens).compileToFragments options
|
||||
|
||||
currentLine = 0
|
||||
|
||||
@@ -97,6 +97,8 @@ grammar =
|
||||
o 'Return'
|
||||
o 'Comment'
|
||||
o 'STATEMENT', -> new StatementLiteral $1
|
||||
o 'Import'
|
||||
o 'Export'
|
||||
]
|
||||
|
||||
# All the different types of expressions in our language. The basic unit of
|
||||
@@ -349,6 +351,66 @@ grammar =
|
||||
o 'CLASS SimpleAssignable EXTENDS Expression Block', -> new Class $2, $4, $5
|
||||
]
|
||||
|
||||
Import: [
|
||||
o 'IMPORT String', -> new ImportDeclaration null, $2
|
||||
o 'IMPORT ImportDefaultSpecifier FROM String', -> new ImportDeclaration new ImportClause($2, null), $4
|
||||
o 'IMPORT ImportNamespaceSpecifier FROM String', -> new ImportDeclaration new ImportClause(null, $2), $4
|
||||
o 'IMPORT { } FROM String', -> new ImportDeclaration new ImportClause(null, new ImportSpecifierList []), $5
|
||||
o 'IMPORT { ImportSpecifierList OptComma } FROM String', -> new ImportDeclaration new ImportClause(null, new ImportSpecifierList $3), $7
|
||||
o 'IMPORT ImportDefaultSpecifier , ImportNamespaceSpecifier FROM String', -> new ImportDeclaration new ImportClause($2, $4), $6
|
||||
o 'IMPORT ImportDefaultSpecifier , { ImportSpecifierList OptComma } FROM String', -> new ImportDeclaration new ImportClause($2, new ImportSpecifierList $5), $9
|
||||
]
|
||||
|
||||
ImportSpecifierList: [
|
||||
o 'ImportSpecifier', -> [$1]
|
||||
o 'ImportSpecifierList , ImportSpecifier', -> $1.concat $3
|
||||
o 'ImportSpecifierList OptComma TERMINATOR ImportSpecifier', -> $1.concat $4
|
||||
o 'INDENT ImportSpecifierList OptComma OUTDENT', -> $2
|
||||
o 'ImportSpecifierList OptComma INDENT ImportSpecifierList OptComma OUTDENT', -> $1.concat $4
|
||||
]
|
||||
|
||||
ImportSpecifier: [
|
||||
o 'Identifier', -> new ImportSpecifier $1
|
||||
o 'Identifier AS Identifier', -> new ImportSpecifier $1, $3
|
||||
]
|
||||
|
||||
ImportDefaultSpecifier: [
|
||||
o 'Identifier', -> new ImportDefaultSpecifier $1
|
||||
]
|
||||
|
||||
ImportNamespaceSpecifier: [
|
||||
o 'IMPORT_ALL AS Identifier', -> new ImportNamespaceSpecifier new Literal($1), $3
|
||||
]
|
||||
|
||||
Export: [
|
||||
o 'EXPORT { }', -> new ExportNamedDeclaration new ExportSpecifierList []
|
||||
o 'EXPORT { ExportSpecifierList OptComma }', -> new ExportNamedDeclaration new ExportSpecifierList $3
|
||||
o 'EXPORT Class', -> new ExportNamedDeclaration $2
|
||||
o 'EXPORT Identifier = Expression', -> new ExportNamedDeclaration new Assign $2, $4, null,
|
||||
moduleDeclaration: 'export'
|
||||
o 'EXPORT Identifier = TERMINATOR Expression', -> new ExportNamedDeclaration new Assign $2, $5, null,
|
||||
moduleDeclaration: 'export'
|
||||
o 'EXPORT Identifier = INDENT Expression OUTDENT', -> new ExportNamedDeclaration new Assign $2, $5, null,
|
||||
moduleDeclaration: 'export'
|
||||
o 'EXPORT DEFAULT Expression', -> new ExportDefaultDeclaration $3
|
||||
o 'EXPORT EXPORT_ALL FROM String', -> new ExportAllDeclaration new Literal($2), $4
|
||||
o 'EXPORT { ExportSpecifierList OptComma } FROM String', -> new ExportNamedDeclaration new ExportSpecifierList($3), $7
|
||||
]
|
||||
|
||||
ExportSpecifierList: [
|
||||
o 'ExportSpecifier', -> [$1]
|
||||
o 'ExportSpecifierList , ExportSpecifier', -> $1.concat $3
|
||||
o 'ExportSpecifierList OptComma TERMINATOR ExportSpecifier', -> $1.concat $4
|
||||
o 'INDENT ExportSpecifierList OptComma OUTDENT', -> $2
|
||||
o 'ExportSpecifierList OptComma INDENT ExportSpecifierList OptComma OUTDENT', -> $1.concat $4
|
||||
]
|
||||
|
||||
ExportSpecifier: [
|
||||
o 'Identifier', -> new ExportSpecifier $1
|
||||
o 'Identifier AS Identifier', -> new ExportSpecifier $1, $3
|
||||
o 'Identifier AS DEFAULT', -> new ExportSpecifier $1, new Literal $3
|
||||
]
|
||||
|
||||
# Ordinary function invocation, or a chained series of calls.
|
||||
Invocation: [
|
||||
o 'Value OptFuncExist Arguments', -> new Call $1, $3, $2
|
||||
@@ -644,7 +706,7 @@ operators = [
|
||||
['right', 'YIELD']
|
||||
['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS']
|
||||
['right', 'FORIN', 'FOROF', 'BY', 'WHEN']
|
||||
['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS']
|
||||
['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS', 'IMPORT', 'EXPORT']
|
||||
['left', 'POST_IF']
|
||||
]
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ exports.Lexer = class Lexer
|
||||
@ends = [] # The stack for pairing up tokens.
|
||||
@tokens = [] # Stream of parsed tokens in the form `['TYPE', value, location data]`.
|
||||
@seenFor = no # Used to recognize FORIN and FOROF tokens.
|
||||
@seenImport = no # Used to recognize IMPORT FROM? AS? tokens.
|
||||
@seenExport = no # Used to recognize EXPORT FROM? AS? tokens.
|
||||
|
||||
@chunkLine =
|
||||
opts.line or 0 # The start line for the current @chunk.
|
||||
@@ -113,7 +115,15 @@ exports.Lexer = class Lexer
|
||||
if id is 'from' and @tag() is 'YIELD'
|
||||
@token 'FROM', id
|
||||
return id.length
|
||||
if id is 'as' and (@seenImport or @seenExport) and @tag() in ['IDENTIFIER', 'IMPORT_ALL', 'EXPORT_ALL']
|
||||
@token 'AS', id
|
||||
return id.length
|
||||
if id is 'default' and @seenExport
|
||||
@token 'DEFAULT', id
|
||||
return id.length
|
||||
|
||||
[..., prev] = @tokens
|
||||
|
||||
tag =
|
||||
if colon or prev? and
|
||||
(prev[0] in ['.', '?.', '::', '?::'] or
|
||||
@@ -130,6 +140,10 @@ exports.Lexer = class Lexer
|
||||
@seenFor = yes
|
||||
else if tag is 'UNLESS'
|
||||
tag = 'IF'
|
||||
else if tag is 'IMPORT'
|
||||
@seenImport = yes
|
||||
else if tag is 'EXPORT'
|
||||
@seenExport = yes
|
||||
else if tag in UNARY
|
||||
tag = 'UNARY'
|
||||
else if tag in RELATION
|
||||
@@ -201,6 +215,12 @@ exports.Lexer = class Lexer
|
||||
stringToken: ->
|
||||
[quote] = STRING_START.exec(@chunk) || []
|
||||
return 0 unless quote
|
||||
|
||||
# If the preceding token is `from` and this is an import or export statement,
|
||||
# properly tag the `from`.
|
||||
if @tokens.length and @value() is 'from' and (@seenImport or @seenExport)
|
||||
@tokens[@tokens.length - 1][0] = 'FROM'
|
||||
|
||||
regex = switch quote
|
||||
when "'" then STRING_SINGLE
|
||||
when '"' then STRING_DOUBLE
|
||||
@@ -317,9 +337,12 @@ exports.Lexer = class Lexer
|
||||
lineToken: ->
|
||||
return 0 unless match = MULTI_DENT.exec @chunk
|
||||
indent = match[0]
|
||||
|
||||
@seenFor = no
|
||||
|
||||
size = indent.length - 1 - indent.lastIndexOf '\n'
|
||||
noNewlines = @unfinished()
|
||||
|
||||
if size - @indebt is @indent
|
||||
if noNewlines then @suppressNewlines() else @newlineToken 0
|
||||
return indent.length
|
||||
@@ -425,8 +448,10 @@ exports.Lexer = class Lexer
|
||||
return value.length if skipToken
|
||||
|
||||
if value is ';'
|
||||
@seenFor = no
|
||||
@seenFor = @seenImport = @seenExport = no
|
||||
tag = 'TERMINATOR'
|
||||
else if value is '*' and @indent is 0 and (@seenImport or @seenExport)
|
||||
tag = if @seenImport then 'IMPORT_ALL' else 'EXPORT_ALL'
|
||||
else if value in MATH then tag = 'MATH'
|
||||
else if value in COMPARE then tag = 'COMPARE'
|
||||
else if value in COMPOUND_ASSIGN then tag = 'COMPOUND_ASSIGN'
|
||||
@@ -774,6 +799,7 @@ JS_KEYWORDS = [
|
||||
'return', 'throw', 'break', 'continue', 'debugger', 'yield'
|
||||
'if', 'else', 'switch', 'for', 'while', 'do', 'try', 'catch', 'finally'
|
||||
'class', 'extends', 'super'
|
||||
'import', 'export', 'default'
|
||||
]
|
||||
|
||||
# CoffeeScript-only keywords.
|
||||
@@ -800,8 +826,8 @@ COFFEE_KEYWORDS = COFFEE_KEYWORDS.concat COFFEE_ALIASES
|
||||
# used by CoffeeScript internally. We throw an error when these are encountered,
|
||||
# to avoid having a JavaScript error at runtime.
|
||||
RESERVED = [
|
||||
'case', 'default', 'function', 'var', 'void', 'with', 'const', 'let', 'enum'
|
||||
'export', 'import', 'native', 'implements', 'interface', 'package', 'private'
|
||||
'case', 'function', 'var', 'void', 'with', 'const', 'let', 'enum'
|
||||
'native', 'implements', 'interface', 'package', 'private'
|
||||
'protected', 'public', 'static'
|
||||
]
|
||||
|
||||
|
||||
159
src/nodes.coffee
159
src/nodes.coffee
@@ -1220,21 +1220,167 @@ exports.Class = class Class extends Base
|
||||
@body.expressions.unshift @directives...
|
||||
|
||||
klass = new Parens new Call func, args
|
||||
klass = new Assign @variable, klass if @variable
|
||||
klass = new Assign @variable, klass, null, { @moduleDeclaration } if @variable
|
||||
klass.compileToFragments o
|
||||
|
||||
#### Import and Export
|
||||
|
||||
exports.ModuleDeclaration = class ModuleDeclaration extends Base
|
||||
constructor: (@clause, @source) ->
|
||||
@checkSource()
|
||||
|
||||
children: ['clause', 'source']
|
||||
|
||||
isStatement: YES
|
||||
jumps: THIS
|
||||
makeReturn: THIS
|
||||
|
||||
checkSource: ->
|
||||
if @source? and @source instanceof StringWithInterpolations
|
||||
@source.error 'the name of the module to be imported from must be an uninterpolated string'
|
||||
|
||||
checkScope: (o, moduleDeclarationType) ->
|
||||
if o.indent.length isnt 0
|
||||
@error "#{moduleDeclarationType} statements must be at top-level scope"
|
||||
|
||||
exports.ImportDeclaration = class ImportDeclaration extends ModuleDeclaration
|
||||
compileNode: (o) ->
|
||||
@checkScope o, 'import'
|
||||
o.importedSymbols = []
|
||||
|
||||
code = []
|
||||
code.push @makeCode "#{@tab}import "
|
||||
code.push @clause.compileNode(o)... if @clause?
|
||||
|
||||
if @source?.value?
|
||||
code.push @makeCode ' from ' unless @clause is null
|
||||
code.push @makeCode @source.value
|
||||
|
||||
code.push @makeCode ';'
|
||||
code
|
||||
|
||||
exports.ImportClause = class ImportClause extends Base
|
||||
constructor: (@defaultBinding, @namedImports) ->
|
||||
|
||||
children: ['defaultBinding', 'namedImports']
|
||||
|
||||
compileNode: (o) ->
|
||||
code = []
|
||||
|
||||
if @defaultBinding?
|
||||
code.push @defaultBinding.compileNode(o)...
|
||||
code.push @makeCode ', ' if @namedImports?
|
||||
|
||||
if @namedImports?
|
||||
code.push @namedImports.compileNode(o)...
|
||||
|
||||
code
|
||||
|
||||
exports.ExportDeclaration = class ExportDeclaration extends ModuleDeclaration
|
||||
compileNode: (o) ->
|
||||
@checkScope o, 'export'
|
||||
|
||||
code = []
|
||||
code.push @makeCode "#{@tab}export "
|
||||
code.push @makeCode 'default ' if @ instanceof ExportDefaultDeclaration
|
||||
|
||||
if @ not instanceof ExportDefaultDeclaration and
|
||||
(@clause instanceof Assign or @clause instanceof Class)
|
||||
# When the ES2015 `class` keyword is supported, don’t add a `var` here
|
||||
code.push @makeCode 'var '
|
||||
@clause.moduleDeclaration = 'export'
|
||||
|
||||
if @clause.body? and @clause.body instanceof Block
|
||||
code = code.concat @clause.compileToFragments o, LEVEL_TOP
|
||||
else
|
||||
code = code.concat @clause.compileNode o
|
||||
|
||||
code.push @makeCode " from #{@source.value}" if @source?.value?
|
||||
code.push @makeCode ';'
|
||||
code
|
||||
|
||||
exports.ExportNamedDeclaration = class ExportNamedDeclaration extends ExportDeclaration
|
||||
|
||||
exports.ExportDefaultDeclaration = class ExportDefaultDeclaration extends ExportDeclaration
|
||||
|
||||
exports.ExportAllDeclaration = class ExportAllDeclaration extends ExportDeclaration
|
||||
|
||||
exports.ModuleSpecifierList = class ModuleSpecifierList extends Base
|
||||
constructor: (@specifiers) ->
|
||||
|
||||
children: ['specifiers']
|
||||
|
||||
compileNode: (o) ->
|
||||
code = []
|
||||
o.indent += TAB
|
||||
compiledList = (specifier.compileToFragments o, LEVEL_LIST for specifier in @specifiers)
|
||||
|
||||
if @specifiers.length isnt 0
|
||||
code.push @makeCode "{\n#{o.indent}"
|
||||
for fragments, index in compiledList
|
||||
code.push @makeCode(",\n#{o.indent}") if index
|
||||
code.push fragments...
|
||||
code.push @makeCode "\n}"
|
||||
else
|
||||
code.push @makeCode '{}'
|
||||
code
|
||||
|
||||
exports.ImportSpecifierList = class ImportSpecifierList extends ModuleSpecifierList
|
||||
|
||||
exports.ExportSpecifierList = class ExportSpecifierList extends ModuleSpecifierList
|
||||
|
||||
exports.ModuleSpecifier = class ModuleSpecifier extends Base
|
||||
constructor: (@original, @alias, @moduleDeclarationType) ->
|
||||
# The name of the variable entering the local scope
|
||||
@identifier = if @alias? then @alias.value else @original.value
|
||||
|
||||
children: ['original', 'alias']
|
||||
|
||||
compileNode: (o) ->
|
||||
o.scope.add @identifier, @moduleDeclarationType
|
||||
code = []
|
||||
code.push @makeCode @original.value
|
||||
code.push @makeCode " as #{@alias.value}" if @alias?
|
||||
code
|
||||
|
||||
exports.ImportSpecifier = class ImportSpecifier extends ModuleSpecifier
|
||||
constructor: (imported, local) ->
|
||||
super imported, local, 'import'
|
||||
|
||||
compileNode: (o) ->
|
||||
# Per the spec, symbols can’t be imported multiple times
|
||||
# (e.g. `import { foo, foo } from 'lib'` is invalid)
|
||||
if @identifier in o.importedSymbols or o.scope.check(@identifier)
|
||||
@error "'#{@identifier}' has already been declared"
|
||||
else
|
||||
o.importedSymbols.push @identifier
|
||||
super o
|
||||
|
||||
exports.ImportDefaultSpecifier = class ImportDefaultSpecifier extends ImportSpecifier
|
||||
|
||||
exports.ImportNamespaceSpecifier = class ImportNamespaceSpecifier extends ImportSpecifier
|
||||
|
||||
exports.ExportSpecifier = class ExportSpecifier extends ModuleSpecifier
|
||||
constructor: (local, exported) ->
|
||||
super local, exported, 'export'
|
||||
|
||||
#### Assign
|
||||
|
||||
# The **Assign** is used to assign a local variable to value, or to set the
|
||||
# property of an object -- including within object literals.
|
||||
exports.Assign = class Assign extends Base
|
||||
constructor: (@variable, @value, @context, options = {}) ->
|
||||
{@param, @subpattern, @operatorToken} = options
|
||||
{@param, @subpattern, @operatorToken, @moduleDeclaration} = options
|
||||
|
||||
children: ['variable', 'value']
|
||||
|
||||
isStatement: (o) ->
|
||||
o?.level is LEVEL_TOP and @context? and "?" in @context
|
||||
o?.level is LEVEL_TOP and @context? and (@moduleDeclaration or "?" in @context)
|
||||
|
||||
checkAssignability: (o, varBase) ->
|
||||
if Object::hasOwnProperty.call(o.scope.positions, varBase.value) and
|
||||
o.scope.variables[o.scope.positions[varBase.value]].type is 'import'
|
||||
varBase.error "'#{varBase.value}' is read-only"
|
||||
|
||||
assigns: (name) ->
|
||||
@[if @context is 'object' then 'value' else 'variable'].assigns name
|
||||
@@ -1268,10 +1414,15 @@ exports.Assign = class Assign extends Base
|
||||
unless varBase.isAssignable()
|
||||
@variable.error "'#{@variable.compile o}' can't be assigned"
|
||||
unless varBase.hasProperties?()
|
||||
if @param
|
||||
if @moduleDeclaration # `moduleDeclaration` can be `'import'` or `'export'`
|
||||
@checkAssignability o, varBase
|
||||
o.scope.add varBase.value, @moduleDeclaration
|
||||
else if @param
|
||||
o.scope.add varBase.value, 'var'
|
||||
else
|
||||
@checkAssignability o, varBase
|
||||
o.scope.find varBase.value
|
||||
|
||||
val = @value.compileToFragments o, LEVEL_LIST
|
||||
@variable.front = true if isValue and @variable.base instanceof Obj
|
||||
compiledName = @variable.compileToFragments o, LEVEL_LIST
|
||||
|
||||
Reference in New Issue
Block a user