Export default declaration as expression#630
Export default declaration as expression#630jridgewell wants to merge 4 commits intobabel:masterfrom
Conversation
|
These are not expressions, they are declarations. Parsing them as expressions will have incorrect behavior. The specification specifically lists functions (HoistableDeclaration) and classes as being parsed as declarations when they are on the right side of a https://tc39.github.io/ecma262/#sec-exports I believe it was done like that as a workaround for not being able to directly name a function or class as |
|
I know, I'm intentionally breaking from the spec here. Is there an observable difference in live bindings? |
|
This will also break babel module transforms and make it harder to write them correctly, particularly in the hoisted declaration case. I would prefer very much to not deviate from the spec unless it's not possible to represent correctly. The point of a function expression is not that its name is optional, it is that it is an expression, and it is not hoisted. |
|
As for the live bindings, it is probably not observable in "normal code", but in the runtime behaviour it is important when there are import cycles. // a.js (runs first)
import b from './b'
b() // declaration: fine; expression: TypeError// b.js
import './a'
export default function () {} |
|
Prefect, that's the example I needed to see. I still think this is ridiculous. |
|
Why are these cases different? Case 1(Included because I understand the argument for hoisting) test();
const a = 1;
function test() {
console.log(a); // TDZ Error
}Case 2// a.js (runs first)
import b from './b'
b() // Fine// b.js
import './a'
const a = 1;
export default function () {
console.log(a); // TDZ
}Case 3// a.js (runs first)
import b from './b'
b() // Not a function// b.js
import './a'
const a = 1;
function test() {
console.log(a); // Fine
}
export default test; |
|
Case 1 and Case 2 are essentially identical. However, an Thinking about it, this means Case 3 actually should be a TDZ error on the |
|
Yah, that's why I included case 1. Maybe this would be better explained as Classes? Case 1new Test(); // TDZ
class Test {
}Case 2// a.js (runs first)
import B from './b'
new B() // Fine// b.js
import './a'
export default class Test {
}Case 3// a.js (runs first)
import B from './b'
new B() // TDZ? Not a class, at least// b.js
import './a'
class Test {
}
export default Test;Aside: Is referencing a live-binding before it's declared a TDZ error? Makes sense if it was. |
|
It's harder to see the difference with With export default class Foo {}
Foo = class Bar {}
// with 'export default ClassDeclaration': default value is updated
// with 'export default ClassExpression': ReferenceError |
What?! Why is it a declaration?! 🙄 With that example specifically, // `class Foo {}` is really just `let Foo = class Foo {}`
// `export default let Foo = class Foo {}` is an error
// Instead, it's really transpiled as
let Foo = class Foo {};
export default Foo;
Foo = class Bar {}I don't mean to be picking at your arguments. I think I'm cranky from my hate of circular dependencies. 😬 |
|
nope, The runtime semantics even take into account the fact that the ClassDeclaration might be named: https://tc39.github.io/ecma262/#sec-exports-runtime-semantics-evaluation If you want to get into how it's "transpiled", it would be equivalent to |
|
Even ignoring circular dependencies and what imports what, even just accessing the class by name would be affected. e.g. if it's a declaration, the |
|
So I'm not arguing against |
|
Gotcha, then yeah I don't think there is a way to observe the different between an anonymous default-export class declaration vs expression. I think it's good to have anonymous class declaration be consistent with function declaration though. |
|
A difference not yet mentioned is that export default class {} export {};whereas the following is not: export default (class {}) export {};It really is a declaration, albeit an unnamed one, and trying to pretend otherwise is going to lead to confusion at the very least. I also don't understand why you want to make this change. For classes the difference in semantics is not observable (I think), but it trivially is for functions, as explained above. You can also test this yourself in a recent browser or with a command line JS engine: e.g., with a recent build of v8, run // a.js
import a from './a.js';
a(); // succeeds
export default function(){}vs // a.js
import a from './a.js';
a(); // fails
export default (function(){});This may also help you when considering examples. For example, in your cases 1/2/3 for functions above, case 2 and 3 actually both work fine: |
|
Oh, right, I got the order of execution for circular imports wrong 😆
|
|
Related: #502 |
|
Interestingly, the spec dealt differently with The names of each declaration type. Classes get an implicit name |
How many transforms will assume declarations have an id? The parsing and runtime rules can be followed while still preserving what Babel considers to be a
Why isn't a recursive parenthesis rule used like |
At the end of the day, these things are not expressions, they are syntactically declarations. We should not diverge the AST from the spec because some plugins may make assumptions that are incorrect. There are an infinite number of things people could do wrong, we can't possibly hope to defend agains that. |
No, they can't. As explained above, a function declaration and a function expressions in that position have observably different semantics. They cannot have the same AST.
Why would it be? Parentheses are already commonly used to distinguish function expressions from declarations. Not respecting them here would be weird. |
Our node representation has nothing to do with the code we transform into. All we needed to do is test if an (unparenthesized) It seems this isn't going to happen. Instead, how about catching these unnamed declarations before any transformers have the chance to run and giving them an id? babel/babel@7.0...jridgewell:export-default-expression |
But there are syntactic differences in how the code is serialized. Once it is in the AST, we don't know if there were parens or not. And it would mean there's no way to programmatically create one or the other of the cases. |
What's a
FunctionDeclaration? It has anid. It's callable in scope. Same forClassDeclarations.These aren't declarations, they're expressions. Sometimes the spec is stupid.