Skip to content

Commit 2a81c37

Browse files
committed
Fix scala-js/scala-js#2175: Add support for ECMAScript 2015 modules.
This is a port of b744d12e0c6d8af74960e0ce5071c8e0011249a5. This commit adds a third `ModuleKind` for ES modules, namely `ModuleKind.ESModule`. When emitting an ES module, `@JSImport`s and `@JSExportTopLevel`s straightforwardly map to ES `import` and `export` clauses, respectively. At the moment, imports are always implemented using a namespace import, then selecting fields inside the namespace. This is suboptimal because it can prevent advanced DCE across ES modules. Improving on this is left for future work. A new `Input.ESModulesToLoad` instructs a JSEnv to load files as ES modules. The Node.js-based environment, however, will only *actually* interpret the files as ES modules if their name ends with `.mjs`. This happens because of how Node.js itself identifies ES modules as of version 10.x, although it is still experimental, so that could change in the future. `.js` files will be loaded as CommonJS modules instead. Although setting `scalaJSLinkerConfig.moduleKind` to `ModuleKind.ESModule` is enough for the Scala.js linker to emit a valid ES module, two additional settings are required to *run* or *test* using Node.js: artifactPath in (proj, Compile, fastOptJS) := (crossTarget in (proj, Compile)).value / "somename.mjs" jsEnv := { new org.scalajs.jsenv.NodeJSEnv( org.scalajs.jsenv.NODEJSEnv.Config() .withArguments(List("--experimental-modules")) ) } The first setting is necessary to give the `.mjs` extension to the file produced by Scala.js, which in turn is necessary for Node.js to accept it as an ES module. The second setting will be necessary until Node.js declares its support for ES module as non-experimental. The version of the Closure Compiler that we use does not support ES modules yet, so we deactivate GCC when emitting an ES module. At this point, the emission of ES modules can be considered stable, but the support in `NodeJSEnv` is experimental (since the support of ES modules in Node.js is itself experimental). Running the full test suite with ES modules requires Node.js 10.2.0 or later. It has been tested with v10.12.0.
1 parent 802518a commit 2a81c37

File tree

2 files changed

+51
-1
lines changed

2 files changed

+51
-1
lines changed

js-envs/src/main/scala/org/scalajs/jsenv/Input.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ object Input {
2929
/** All files are to be loaded as scripts into the global scope in the order given. */
3030
final case class ScriptsToLoad(scripts: List[VirtualBinaryFile]) extends Input
3131

32+
/** All files are to be loaded as ES modules, in the given order.
33+
*
34+
* Some environments may not be able to execute several ES modules in a
35+
* deterministic order. If that is the case, they must reject an
36+
* `ESModulesToLoad` input if the `modules` argument has more than one
37+
* element.
38+
*/
39+
final case class ESModulesToLoad(modules: List[VirtualBinaryFile])
40+
extends Input
41+
3242
/** All files are to be loaded as CommonJS modules, in the given order. */
3343
final case class CommonJSModulesToLoad(modules: List[VirtualBinaryFile])
3444
extends Input

nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv {
4949

5050
private def validateInput(input: Input): Unit = {
5151
input match {
52-
case _:Input.ScriptsToLoad | _:Input.CommonJSModulesToLoad =>
52+
case _:Input.ScriptsToLoad | _:Input.ESModulesToLoad |
53+
_:Input.CommonJSModulesToLoad =>
5354
// ok
5455
case _ =>
5556
throw new UnsupportedInputException(input)
@@ -164,6 +165,34 @@ object NodeJSEnv {
164165
case Input.CommonJSModulesToLoad(modules) =>
165166
for (module <- modules)
166167
writeRequire(module)
168+
169+
case Input.ESModulesToLoad(modules) =>
170+
if (modules.nonEmpty) {
171+
val uris = modules.map {
172+
case module: FileVirtualBinaryFile =>
173+
module.file.toURI
174+
case module =>
175+
tmpFile(module.path, module.inputStream).toURI
176+
}
177+
178+
val imports = uris.map { uri =>
179+
s"""import("${escapeJS(uri.toASCIIString)}")"""
180+
}
181+
val importChain = imports.reduceLeft { (prev, imprt) =>
182+
s"""$prev.then(_ => $imprt)"""
183+
}
184+
185+
val importerFileContent = {
186+
s"""
187+
|$importChain.catch(e => {
188+
| console.error(e);
189+
| process.exit(1);
190+
|});
191+
""".stripMargin
192+
}
193+
val f = tmpFile("importer.js", importerFileContent)
194+
p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""")
195+
}
167196
}
168197
} finally {
169198
p.close()
@@ -192,6 +221,17 @@ object NodeJSEnv {
192221
new String(baos.toByteArray(), StandardCharsets.UTF_8)
193222
}
194223

224+
private def tmpFile(path: String, content: String): File = {
225+
import java.nio.file.{Files, StandardOpenOption}
226+
227+
val f = createTmpFile(path)
228+
val contentList = new java.util.ArrayList[String]()
229+
contentList.add(content)
230+
Files.write(f.toPath(), contentList, StandardCharsets.UTF_8,
231+
StandardOpenOption.TRUNCATE_EXISTING)
232+
f
233+
}
234+
195235
private def tmpFile(path: String, content: InputStream): File = {
196236
import java.nio.file.{Files, StandardCopyOption}
197237

0 commit comments

Comments
 (0)