ClojureScript

Add support for ES6 default imports/exports

Details

  • Type: Enhancement Enhancement
  • Status: Open Open
  • Priority: Minor Minor
  • Resolution: Unresolved
  • Affects Version/s: None
  • Fix Version/s: None
  • Component/s: None
  • Labels:

Description

ES6 has special syntax for using "default" imports and there is currently no equivalent for CLJS when using imported ES6 code.

import * as name from "module-name";
(:require ["module-name" :as name])

import { export } from "module-name";
(:require ["module-name" :refer (export)])

import { export as alias } from "module-name";
(:require ["module-name" :refer (export) :rename {export alias}])

import defaultExport from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
;; no easy access to defaultExport

I'm proposing to add a :default to the ns :require.

(:require ["module-name" :as mod :default foo])

This makes it much more convenient to use rewritten ES6 code from CLJS. If "module-name" has a default export you currently have to write mod/default everywhere since they is no easy way to alias the default.

(:require ["material-ui/RaisedButton" :as RaisedButton])
;; :as is incorrect and the user must now use
RaisedButton/default

(:require ["material-ui/RaisedButton" :default RaisedButton])
;; would allow direct use of
RaisedButton

Internally the Closure compiler (and ES6 code rewritten by babel) will rewrite default exports to a .default property, so :default really is just a convenient way to access it.

The long version that already works is

(:require ["material-ui/RaisedButton" :refer (default) :rename {default RaisedButton}])

When ES6 becomes more widespread we should have convenient way to correctly refer to "default" exports.

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

Activity

Hide
Juho Teperi added a comment -

First take on the implementation. This implementation rewrites :default name to {{:refer [default] :rename {default name}}} at the lowest level (parse-require-spec), so I think other functions don't need to be changed.

I didn't update the require spec validation errors yet.

Show
Juho Teperi added a comment - First take on the implementation. This implementation rewrites :default name to {{:refer [default] :rename {default name}}} at the lowest level (parse-require-spec), so I think other functions don't need to be changed. I didn't update the require spec validation errors yet.
Hide
David Nolen added a comment - - edited

It seems to me the most desirable syntax for ES6 default imports is the following:

(:require ["material-ui/RaisedButton" :refer [RaisedButton]])

I don't see why we couldn't just make this work?

Show
David Nolen added a comment - - edited It seems to me the most desirable syntax for ES6 default imports is the following:
(:require ["material-ui/RaisedButton" :refer [RaisedButton]])
I don't see why we couldn't just make this work?
Hide
Thomas Heller added a comment -

I don't understand that suggestion. The default export has no name, the .default property is only used in interop by transpilers. In actual JS a live reference is used. How would you map RaisedButton back to default?

["material-ui/RaisedButton" :default foo] would be valid. The JS module may also actually have an `export { RaisedButton }` in addition to `export default ...`, which would lead to a conflict if you "guess" the wrong name. Default exports are not aliases for the module itself, that is separate.

I made a reference translation table [1] and adding the :default alias is the only way to reliably map all ES6 import/export features. I don't see a way to make :refer work that would not be totally non-intuitive and unreliable.

import defaultExport, * as name from "module-name";

[1] https://shadow-cljs.github.io/docs/UsersGuide.html#_using_npm_packages

Show
Thomas Heller added a comment - I don't understand that suggestion. The default export has no name, the .default property is only used in interop by transpilers. In actual JS a live reference is used. How would you map RaisedButton back to default? ["material-ui/RaisedButton" :default foo] would be valid. The JS module may also actually have an `export { RaisedButton }` in addition to `export default ...`, which would lead to a conflict if you "guess" the wrong name. Default exports are not aliases for the module itself, that is separate. I made a reference translation table [1] and adding the :default alias is the only way to reliably map all ES6 import/export features. I don't see a way to make :refer work that would not be totally non-intuitive and unreliable.
import defaultExport, * as name from "module-name";
[1] https://shadow-cljs.github.io/docs/UsersGuide.html#_using_npm_packages
Hide
David Nolen added a comment -

Let's leave rhetoric out of this discussion please and focus on the technical bits. I just don't want to include another directive in the ns form and I would prefer an approach that avoids that. From my point of view there has not been enough exploration of that alternative.

This ticket is mostly about convenience, and I think we need to stew on this one for a bit longer before deciding on anything.

Show
David Nolen added a comment - Let's leave rhetoric out of this discussion please and focus on the technical bits. I just don't want to include another directive in the ns form and I would prefer an approach that avoids that. From my point of view there has not been enough exploration of that alternative. This ticket is mostly about convenience, and I think we need to stew on this one for a bit longer before deciding on anything.
Hide
jcr added a comment -

Is there a reason we can't use metadata to deal with it? I.e.

(:require ["material-ui/RaisedButton" :refer [^:default RaisedButton]])
(:require ["module-name" :as mod :refer [^:default foo])
(:require ["module-name" :refer [^:default foo, bar, baz])

This doesn't require additional ns directives and doesn't cause any ambiguities either.

Since this wasn't used for macros (i.e. we have :require-macros instead of plain :require + ^:macros or :refer [^:macro foo]), I assume there may be some nuances I am not aware of that prevent implementing it. Are there any?

Show
jcr added a comment - Is there a reason we can't use metadata to deal with it? I.e.
(:require ["material-ui/RaisedButton" :refer [^:default RaisedButton]])
(:require ["module-name" :as mod :refer [^:default foo])
(:require ["module-name" :refer [^:default foo, bar, baz])
This doesn't require additional ns directives and doesn't cause any ambiguities either. Since this wasn't used for macros (i.e. we have :require-macros instead of plain :require + ^:macros or :refer [^:macro foo]), I assume there may be some nuances I am not aware of that prevent implementing it. Are there any?
Hide
Thomas Heller added a comment -

The ^:default hint seems far more complicated to me from an implementation standpoint.

There is no equivalent for default import/export in Java/JVM and Clojure. There is also no equivalent in CommonJS even. It is strictly about ES6. Therefore bolting it onto "other" methods seems incorrect to me.

The reason it should be separate IMHO is that using the .default property is not strictly correct and the fact that it exists at all is purely for compatibility reasons with CommonJS. In strict ES6 they don't exist as part of the "Object" created by :as.

;; a.js
export let a = 1;
export let b = 2;
export default 3;

;; b.js
import x, * as y from "./a.js"

// these exist
y.a
y.b
// this is NOT valid in ES6 and does not exist
y.default
// instead x must be used
x

We certainly rely on the .default property existing for the time being given that CLJS does not emit ES6. webpack already changed how the default exports are handled however and I do expect the Closure Compiler to do something similar in the future.

Show
Thomas Heller added a comment - The ^:default hint seems far more complicated to me from an implementation standpoint. There is no equivalent for default import/export in Java/JVM and Clojure. There is also no equivalent in CommonJS even. It is strictly about ES6. Therefore bolting it onto "other" methods seems incorrect to me. The reason it should be separate IMHO is that using the .default property is not strictly correct and the fact that it exists at all is purely for compatibility reasons with CommonJS. In strict ES6 they don't exist as part of the "Object" created by :as.
;; a.js
export let a = 1;
export let b = 2;
export default 3;

;; b.js
import x, * as y from "./a.js"

// these exist
y.a
y.b
// this is NOT valid in ES6 and does not exist
y.default
// instead x must be used
x
We certainly rely on the .default property existing for the time being given that CLJS does not emit ES6. webpack already changed how the default exports are handled however and I do expect the Closure Compiler to do something similar in the future.
Hide
Thomas Heller added a comment -

Turns out I'm actually wrong. Re-read the spec and .default is actually defined as a getter, so it is accessible. Nevermind my previous post then.

Doesn't change my opinion about the usefulness of :default as a simpler aliasing method though.

Show
Thomas Heller added a comment - Turns out I'm actually wrong. Re-read the spec and .default is actually defined as a getter, so it is accessible. Nevermind my previous post then. Doesn't change my opinion about the usefulness of :default as a simpler aliasing method though.
Hide
Paulus Esterhazy added a comment -

I ran into ES6 default exports, which are common these days. Folks stumble over `default` in the code, e.g.: https://github.com/pesterhazy/cljs-spa-example/issues/13. I wanted to report my findings.

As explained in the description of this issue, you can use a combination of `:refer` and `:rename` to get rid of the references to `:default` in the code, moving it into the NS declaration. It's not the most elegant solution but gets the job done and works with global exports: https://github.com/pesterhazy/cljs-spa-example/pull/14/files

Note that AIUI because `default` is a strict keyword in ES3, you need to set the following compiler option for this to work:

```
:language-out :ecmascript5
```

Otherwise `default` gets munged to `default$`

Show
Paulus Esterhazy added a comment - I ran into ES6 default exports, which are common these days. Folks stumble over `default` in the code, e.g.: https://github.com/pesterhazy/cljs-spa-example/issues/13. I wanted to report my findings. As explained in the description of this issue, you can use a combination of `:refer` and `:rename` to get rid of the references to `:default` in the code, moving it into the NS declaration. It's not the most elegant solution but gets the job done and works with global exports: https://github.com/pesterhazy/cljs-spa-example/pull/14/files Note that AIUI because `default` is a strict keyword in ES3, you need to set the following compiler option for this to work: ``` :language-out :ecmascript5 ``` Otherwise `default` gets munged to `default$`

People

Vote (14)
Watch (5)

Dates

  • Created:
    Updated: