Skip to content
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

Emit ES modules from JavaScript backend #511

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions effekt/jvm/src/main/scala/effekt/Runner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ trait Runner[Executable] {
object JSNodeRunner extends Runner[String] {
import scala.sys.process.Process

val extension = "js"
val extension = "mjs"

def standardLibraryPath(root: File): File = root / "libraries" / "common"

Expand All @@ -158,27 +158,32 @@ object JSNodeRunner extends Runner[String] {
else Left("Cannot find nodejs. This is required to use the JavaScript backend.")

/**
* Creates an executable `.js` file besides the given `.js` file ([[path]])
* Creates an executable `.mjs` file besides the given `.mjs` file ([[path]])
* and then returns the absolute path of the created executable.
*/
def build(path: String)(using C: Context): String =
val out = C.config.outputPath().getAbsolutePath
val jsFilePath = (out / path).canonicalPath.escape
val jsFileName = path.unixPath.split("/").last
val mjsFilePath = (out / path).canonicalPath.escape

// create "executable" using shebang besides the .js file
val jsScript = s"require('./${jsFileName}').main()"
val mjsFileName = path.unixPath.split("/").last

// NOTE: This is a hack since this file cannot use ES imports & exports
// because it doesn't have the .mjs ending. Sigh.
// Also, we add the 'file://' prefix to satisfy Windows.
val jsScript = s"import('file://${mjsFileName}').then(({main}) => { main(); })"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need to revisit this: the path should be relative (see #566).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just try using file:filename instead 🤔

Suggested change
// NOTE: This is a hack since this file cannot use ES imports & exports
// because it doesn't have the .mjs ending. Sigh.
// Also, we add the 'file://' prefix to satisfy Windows.
val jsScript = s"import('file://${mjsFileName}').then(({main}) => { main(); })"
// NOTE: This is a hack since this file cannot use ES imports & exports
// because it doesn't have the .mjs ending. Sigh.
// Also, we add the 'file:' prefix to satisfy Windows.
val jsScript = s"import('file:${mjsFileName}').then(({main}) => { main(); })"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhoh.

(node:8164) UnhandledPromiseRejectionWarning: TypeError [ERR_INVALID_FILE_URL_PATH]: File URL path must be absolute

I'll need to think about this, worstcase we need to dynamically get the current file in JS...


os match {
case OS.POSIX =>
val shebang = "#!/usr/bin/env node"
val jsScriptFilePath = jsFilePath.stripSuffix(s".$extension")
val jsScriptFilePath = mjsFilePath.stripSuffix(s".$extension")
IO.createFile(jsScriptFilePath, s"$shebang\n$jsScript", true)
jsScriptFilePath

case OS.Windows =>
val jsMainFilePath = jsFilePath.stripSuffix(s".$extension") + "__main.js"
val jsMainFileName = jsFileName.stripSuffix(s".$extension") + "__main.js"
val exePath = jsFilePath.stripSuffix(s".$extension")
val jsMainFilePath = mjsFilePath.stripSuffix(s".$extension") + "__main.mjs"
val jsMainFileName = mjsFileName.stripSuffix(s".$extension") + "__main.mjs"
val exePath = mjsFilePath.stripSuffix(s".$extension")
IO.createFile(jsMainFilePath, jsScript)
createScript(exePath, "node", "$SCRIPT_DIR/" + jsMainFileName)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class JavaScript(additionalFeatureFlags: List[String] = Nil) extends Compiler[St

// Implementation of the Compiler Interface:
// -----------------------------------------
def extension = ".js"
def extension = ".mjs"

override def supportedFeatureFlags: List[String] = additionalFeatureFlags ++ TransformerMonadicWhole.jsFeatureFlags

Expand Down Expand Up @@ -48,7 +48,7 @@ class JavaScript(additionalFeatureFlags: List[String] = Nil) extends Compiler[St
case input @ CoreTransformed(source, tree, mod, core) =>
val mainSymbol = Context.checkMain(mod)
val mainFile = path(mod)
val doc = pretty(TransformerMonadicWhole.compile(input, mainSymbol).commonjs)
val doc = pretty(TransformerMonadicWhole.compile(input, mainSymbol).esModule)
(Map(mainFile -> doc.layout), mainFile)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ object PrettyPrinter extends ParenPrettyPrinter {
case Switch(sc, branches, default) => "switch" <+> parens(toDoc(sc)) <+> jsBlock(branches.map {
case (tag, stmts) => "case" <+> toDoc(tag) <> ":" <+> nested(stmts map toDoc)
} ++ default.toList.map { stmts => "default:" <+> nested(stmts map toDoc) })
case RawExport(name, expr) => "export const" <+> toDoc(name) <+> "=" <+> parens(toDoc(expr))
}

def jsMethod(c: js.Function): Doc = c match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ trait Transformer {

def jsModuleName(path: String): String = "$" + path.replace('/', '_').replace('-', '_')

def jsModuleFile(path: String): String = path.replace('/', '_').replace('-', '_') + ".js"
def jsModuleFile(path: String): String = path.replace('/', '_').replace('-', '_') + ".mjs"

val `fresh` = JSName("fresh")
val `ref` = JSName("ref")
Expand Down
23 changes: 13 additions & 10 deletions effekt/shared/src/main/scala/effekt/generator/js/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ case class Export(name: JSName, expr: Expr)
case class Module(name: JSName, imports: List[Import], exports: List[Export], stmts: List[Stmt]) {

/**
* Generates the Javascript module skeleton for whole program compilation
* Generates the Javascript module skeleton for ES modules
*/
def commonjs: List[Stmt] = {
def esModule: List[Stmt] = {
val effekt = js.Const(JSName("$effekt"), js.Object())

val importStmts = imports.map {
// const MOD = require(PATH)
// import * as MOD from PATH
case Import.All(name, file) =>
js.Const(name, js.Call(Variable(JSName("require")), List(JsString(s"./${file}"))))
js.RawStmt(s"import * as ${name.name} from '${file}';")

// const {NAMES, ...} = require(PATH)
// import {NAMES, ...} from PATH
case Import.Selective(names, file) =>
js.Destruct(names, js.Call(Variable(JSName("require")), List(JsString(s"./${ file }"))))
js.RawStmt(s"import { ${names.map(_.name).mkString(", ")} } from '${file}';")
}

val exportStatement = js.Assign(RawExpr(s"(typeof module != \"undefined\" && module !== null ? module : {}).exports = ${name.name}"),
js.Object(exports.map { e => e.name -> e.expr })
)
val exportStatements = exports.map { e =>
RawExport(e.name, e.expr)
}

List(effekt) ++ importStmts ++ stmts ++ List(exportStatement)
List(effekt) ++ importStmts ++ stmts ++ exportStatements
}

/**
Expand Down Expand Up @@ -189,6 +189,9 @@ enum Stmt {

// e.g. <EXPR>;
case ExprStmt(expr: Expr)

// e.g. export const <NAME> = <EXPR>;
case RawExport(name: JSName, expr: Expr)
}
export Stmt.*

Expand Down
2 changes: 1 addition & 1 deletion effekt/shared/src/main/scala/effekt/util/PathUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ trait PathUtils {
* filename of the module (like `data_option.js`).
*/
def moduleFileName(modulePath: String) =
modulePath.replace('/', '_') + ".js"
modulePath.replace('/', '_') + ".mjs"

def lastModified(src: Source): Long = src match {
case FileSource(name, encoding) => file(name).lastModified
Expand Down
2 changes: 1 addition & 1 deletion libraries/common/io/console.effekt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io
import io/time // just for testing

extern js """
const readline = require('node:readline');
import readline from 'node:readline';
"""

interface Console {
Expand Down
2 changes: 1 addition & 1 deletion libraries/common/io/files.effekt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import io
import io/error

extern js """
const fs = require("fs");
import fs from 'node:fs';
"""

extern type FileDescriptor
Expand Down
2 changes: 1 addition & 1 deletion libraries/common/io/network.effekt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import io

namespace js {
extern js """
const net = require('node:net');
import net from 'node:net';

function listen(server, port, host, listener) {
server.listen(port, host);
Expand Down
Loading