-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
consider es6 module syntax to be plain JS [fixes #3162] #4160
Conversation
👍 |
please |
This is really hacky and will break scoping. We will either add true support for module syntax or nothing at all. |
@michaelficarra I'm not sure I understand what outcome you'd like to see here. Would you like to parse the entire import/export statement, then render it back 1:1? Assignment symbols like |
Yes.
That's not part of ES6 for exactly this reason. |
@michaelficarra I also do not understand why you think this is not a good idea. Using I fail to see what is so wrong in adopting ES6's syntax for |
@gamaralf As I said, proper support of how imports affect scope is needed for this to be acceptable. It's not a syntax issue. This is just a hack that will cause worse problems. |
@michaelficarra It would be helpful in this instance to have some examples of how imports would affect scope; I'm trying to think of some like if shadowing an existing variable or something. |
|
Yep. We compile code (especially around assignment) differently based on whether references are in scope or not. Same thing with the compilation of |
Can we re-open this PR since #3162 (comment) ? |
No, this is still a completely misguided implementation. We cannot just naively pass import/export declarations through to the output without understanding how they affect scope. |
I'm not clear how the scope would be affected any differently than backticked JS passthroughs, which this implementation is modelled after. Variables do not need to be declared beforehand, since import statements are implicit-declaration. The lexical scope would be maintained 1:1 with the output JS. Maybe a failing test case would help me understand your point? |
a = 1 The above compiles to: var a;
a = 1; Now take a look at this: import a from 'something'
a = 1 With your patch, that would compile to: import a from 'something';
var a;
a = 1; That is, as far as I understand, invalid JavaScript. That would need to be compiled to: import a from 'something';
a = 1; That means that we have to track scope. Also, your patch has several other problems. If you accidentally make an error (which at least I do a lot), your patch will import a from 'something Ooops! Forgot the closing quote. But your patch would compile it to: import a from 'something; With your patch, it does not seem to be possible to break an import over several import {
one,
two,
three,
} from 'utils' Your patch does not handle a = ->
import b from 'something' I suspect people would expect the following to work: export fn = (args...) ->
console.log args I think I’m in favor of supporting that (but not Finally, by parsing imports and exports we also allow tools like CoffeeLint to write linting rules for them. It also allows decaf, which constructs a real JavaScript AST instead of concatenating strings like CoffeeScript does, to output the I hope the above will enlighten people why it matters to write a proper implementation with actual parsing and compiling, rather than sticking a quick hack into the code base. Having worked on all parts of CoffeeScript myself, writing the code needed seems pretty easy. The hard parts are:
|
Thank you @lydell. You've hit all the points I wanted to and more. |
@lydell Thank you so much for that. These are all great examples of how this approach falls flat. It seems that v8 is getting close to a working implementation (right now it only parses), so hopefully we'll have a reliable way to test the actual functionality soon, rather than string comparisons. I'll take another stab at implementing this with those cases in mind. Thanks again! |
Another thing worth considering: Adding |
@lydell Not quite, it's whenever a file is treated as a Module, regardless of use of imports/exports. But you're right that it is a concern, as imports/exports can only exist in Modules, so the program must be strict mode. It's a tricky issue that we'll have to consider. |
@lydell and @michaelficarra, I’ve been looking through the code, and the code in this PR, with the thought of trying to implement modules that satisfies all the criteria that @lydell lists above. Do you mind sharing some guidance as to where in the code you would recommend implementing this? If not in As far as I can gather from the various issues threads that discuss adding module support, the planned implementation is to match or almost match the ES2015 syntax for Because this is the plan, it really does seem like almost a “pass-through,” like a JavaScript literal, but we need to do some processing to pull out the symbols so that we don’t redefine them with With regard to some of your caveats, I feel like a more careful regex could detect broken strings or multiline I think this PR’s tests are the best we can do until the Node runtime supports modules, unless we want to bring in Babel and get much more complicated. I think I would be okay with string comparison tests for the time being until Node catches up. |
On MDN it describes |
@lydell's example above is still an issue, though no more than any other passthrough code would be currently. Agreed that a smarter regex might be able to satisfy his last example:
but given the wide range of examples in the test file, I think he's correct in wanting to fully parse the statements. One of the many benefits of CS is that it virtually always creates linted JS (backticks excepted). Partially related: It appears that |
(rewriter.coffee might need adjustments as well, but I don't think so.)
It's not. It's just similar syntax.
We can't do that, because ES2015 can't do that by design.
Why is that a problem? |
@lydell, I’ve tried taking a crack at this: https://github.com/GeoffreyBooth/coffeescript/commits/import-export I don’t seem to be getting very far. I’ve written a test that fails, so there’s that at least. I’ve removed Do you mind taking a look at the commits on that branch and providing a bit of guidance? I feel like if I can figure out how to hook into CoffeeScript’s stream, I can figure out the rest of how to parse the multitude of |
@GeoffreyBooth Your branch seems to be working for me:
|
Hmm, I was using Thanks for |
@GeoffreyBooth It's all about time. Rebuilding the parser is the slowest and most uncommon operation, so that only happens if you run
Also be sure to read through https://github.com/jashkenas/coffeescript/wiki/%5BHowto%5D%20Hacking%20on%20the%20CoffeeScript%20Compiler. |
@lydell, thanks, yes I’ve been poring over that page. I wish it mentioned that This attempt at adding module support has also attracted @JimPanic from this thread. We’ve been poring over lexer.coffee, grammar.coffee and nodes.coffee. From what I can tell, lexer.coffee’s I’ve been torn between the approach in #4160, of creating a new function here e.g. |
lexer.coffee splits a string of CoffeeScript code into an array of tokens. A token is what a syntax highlighter would give a certain color to. Each token also has a type. lexer.coffee doesn't care much in which order tokens are. For example, it would split rewriter.coffee hacks the tokens: It adds a few fake once here and there and such. grammar.coffee is used to describe a Jison grammar to parse the tokens. Jison generates a parser (lib/coffee-script/parser.js) from that grammar. Array of tokens in, tree of objects (an AST) out. The objects – nodes – are described in nodes.coffee. nodes.coffee describes all possible nodes in the CoffeeScript "AST". They are also responsible for turning themselves into a string of JavaScript.
That's absolutely the wrong way to go.
Don't try to parse anything in lexer.coffee. That's not its job. Parsing is done in parser.js, which is generated from the grammar in grammar.coffee.
Make the |
@lydell: Thank you for your insights! I tried to go about as you said, but added a seperate function instead (that I don't call yet, d'oh). I also defined Is this the correct direction so far? Any further hints you can provide maybe? https://github.com/GeoffreyBooth/coffeescript/pull/2/files |
@GeoffreyBooth your import-export branch is starting to look really good. Keep up the good work! Also credits to @JimPanic. ✨ |
@lydell thanks for the encouragement 😃 Regarding your comments above about tracking scope, I did some testing and it seems like we might actually not have to track scope at all. I know this seems to good to be true, so I want to run this by you and maybe you can point out what I’m missing. So I put the following code into the Babel REPL (see here): import { foo } from 'lib';
foo = true; And it returns an error:
And this is when I had an “aha” moment. If imported members are always read-only, and it would make sense that the spec would define them as such, then CoffeeScript will never create a Currently in my import { foo } from 'lib'
foo.bar() compiles, without me doing anything to track identifiers, into this JavaScript: import { foo } from 'lib';
foo.bar(); which makes complete sense, since this Coffee: foo.bar() compiles into this: foo.bar(); because CoffeeScript doesn’t create a Going back to the original example, written as Coffee: import { foo } from 'lib';
foo = true; compiles to this JavaScript: var foo;
import { foo } from 'lib';
foo = true; which of course would trigger an error in Babel. But I’m thinking, maybe that’s okay. This is clearly a syntax error—does it matter whether the error comes from CoffeeScript, Babel, or eventually from a Node/JS runtime that supports So I guess the question is, assuming correct |
I had no idea that the following is illegal JS: import { foo } from 'lib';
foo = true; Thanks for letting me know! That definitely changes everything about my scope tracking comment. I'd say leave everything the way you have it now for I think that the output of CoffeeScript is always valid JavaScript (except invalid regex literals). If so, that is no longer true. So it might be worth looking into finding invalid assignments due to |
@lydell That's not a syntax error. It's only a syntax error if there's duplicate declarations (let/var, let/let, let/const, import/var, etc). The only allowed duplicates are old style bindings: var/var, var/FD, var/parameter, etc. |
@michaelficarra Is it a runtime error then, or not an error at all (except in babel)? |
@michaelficarra would also love your input on coffeescript6/discuss#21 if you get a chance. I asked over there if anyone could comment on how good (or not) the CoffeeScript Redux project would be as a starting point for making a new CoffeeScript compiler that output ES2015+. |
@lydell, I noticed while testing my branch that compiling a full file, for example: import { hello } from './hello.js'
hello.sayHi() produces: (function() {
import { hello } from './hello.js';
hello.sayHi();
}).call(this); which Babel chokes on:
however if the CoffeeScript compiler is given the flag How should we handle this? If a file contains |
@GeoffreyBooth In my opinion, |
@lydell Yeah, it would be a runtime error like |
I'm trying to work out what it means that the issue has been closed, not accepted, and the discussion is still continuing. Is there any chance some of this will ever get accepted in CoffeeScript? |
The original code submitted in this PR won’t be accepted, but we’re working on new code here. When it’s ready, I’ll submit it as a new pull request. Apologies for the confusion caused by us hijacking an old PR thread for discussion; the new PR was inspired by this old one. |
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](#4160) and continued [here](GeoffreyBooth#2). @lydell provided guidance, and @JimPanic and @rattrayalex contributed essential code.
per issue #3162
allows one to write es6 module import / export statements without backticks
tests included, comments welcome