Skip to content

Commit 23112c7

Browse files
committed
Fix scala-js/scala-js#3616: Do not force all JSEnv inputs to be of the same type
1 parent 6fb7b91 commit 23112c7

File tree

5 files changed

+103
-110
lines changed

5 files changed

+103
-110
lines changed

js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/RunTests.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import org.scalajs.jsenv.test.kit.{TestKit, Run}
2727
private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) {
2828
private val kit = new TestKit(config.jsEnv, config.awaitTimeout)
2929

30-
private def withRun(input: Input)(body: Run => Unit) = {
30+
private def withRun(input: Seq[Input])(body: Run => Unit) = {
3131
if (withCom) kit.withComRun(input)(body)
3232
else kit.withRun(input)(body)
3333
}
@@ -141,7 +141,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) {
141141
val badFile = Jimfs.newFileSystem().getPath("nonexistent")
142142

143143
// `start` may not throw but must fail asynchronously
144-
withRun(Input.ScriptsToLoad(badFile :: Nil)) {
144+
withRun(Input.Script(badFile) :: Nil) {
145145
_.fails()
146146
}
147147
}
@@ -155,7 +155,7 @@ private[test] class RunTests(config: JSEnvSuiteConfig, withCom: Boolean) {
155155
val tmpPath = tmpFile.toPath
156156
Files.write(tmpPath, "console.log(\"test\");".getBytes(StandardCharsets.UTF_8))
157157

158-
withRun(Input.ScriptsToLoad(tmpPath :: Nil)) {
158+
withRun(Input.Script(tmpPath) :: Nil) {
159159
_.expectOut("test\n")
160160
.closeRun()
161161
}

js-envs-test-kit/src/main/scala/org/scalajs/jsenv/test/kit/TestKit.scala

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) {
5656
start(codeToInput(code))
5757

5858
/** Starts a [[Run]] for testing. */
59-
def start(input: Input): Run =
59+
def start(input: Seq[Input]): Run =
6060
start(input, RunConfig())
6161

6262
/** Starts a [[Run]] for testing. */
6363
def start(code: String, config: RunConfig): Run =
6464
start(codeToInput(code), config)
6565

6666
/** Starts a [[Run]] for testing. */
67-
def start(input: Input, config: RunConfig): Run = {
67+
def start(input: Seq[Input], config: RunConfig): Run = {
6868
val (run, out, err) = io(config)(jsEnv.start(input, _))
6969
new Run(run, out, err, timeout)
7070
}
@@ -74,15 +74,15 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) {
7474
startWithCom(codeToInput(code))
7575

7676
/** Starts a [[ComRun]] for testing. */
77-
def startWithCom(input: Input): ComRun =
77+
def startWithCom(input: Seq[Input]): ComRun =
7878
startWithCom(input, RunConfig())
7979

8080
/** Starts a [[ComRun]] for testing. */
8181
def startWithCom(code: String, config: RunConfig): ComRun =
8282
startWithCom(codeToInput(code), config)
8383

8484
/** Starts a [[ComRun]] for testing. */
85-
def startWithCom(input: Input, config: RunConfig): ComRun = {
85+
def startWithCom(input: Seq[Input], config: RunConfig): ComRun = {
8686
val msg = new MsgHandler
8787
val (run, out, err) = io(config)(jsEnv.startWithCom(input, _, msg.onMessage _))
8888
run.future.onComplete(msg.onRunComplete _)(TestKit.completer)
@@ -95,15 +95,15 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) {
9595
withRun(codeToInput(code))(body)
9696

9797
/** Convenience method to start a [[Run]] and close it after usage. */
98-
def withRun[T](input: Input)(body: Run => T): T =
98+
def withRun[T](input: Seq[Input])(body: Run => T): T =
9999
withRun(input, RunConfig())(body)
100100

101101
/** Convenience method to start a [[Run]] and close it after usage. */
102102
def withRun[T](code: String, config: RunConfig)(body: Run => T): T =
103103
withRun(codeToInput(code), config)(body)
104104

105105
/** Convenience method to start a [[Run]] and close it after usage. */
106-
def withRun[T](input: Input, config: RunConfig)(body: Run => T): T = {
106+
def withRun[T](input: Seq[Input], config: RunConfig)(body: Run => T): T = {
107107
val run = start(input, config)
108108
try body(run)
109109
finally run.close()
@@ -113,14 +113,14 @@ final class TestKit(jsEnv: JSEnv, timeout: FiniteDuration) {
113113
def withComRun[T](code: String)(body: ComRun => T): T = withComRun(codeToInput(code))(body)
114114

115115
/** Convenience method to start a [[ComRun]] and close it after usage. */
116-
def withComRun[T](input: Input)(body: ComRun => T): T = withComRun(input, RunConfig())(body)
116+
def withComRun[T](input: Seq[Input])(body: ComRun => T): T = withComRun(input, RunConfig())(body)
117117

118118
/** Convenience method to start a [[ComRun]] and close it after usage. */
119119
def withComRun[T](code: String, config: RunConfig)(body: ComRun => T): T =
120120
withComRun(codeToInput(code), config)(body)
121121

122122
/** Convenience method to start a [[ComRun]] and close it after usage. */
123-
def withComRun[T](input: Input, config: RunConfig)(body: ComRun => T): T = {
123+
def withComRun[T](input: Seq[Input], config: RunConfig)(body: ComRun => T): T = {
124124
val run = startWithCom(input, config)
125125
try body(run)
126126
finally run.close()
@@ -154,10 +154,10 @@ private object TestKit {
154154
private val completer =
155155
ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())
156156

157-
private def codeToInput(code: String): Input = {
157+
private def codeToInput(code: String): Seq[Input] = {
158158
val p = Files.write(
159159
Jimfs.newFileSystem().getPath("testScript.js"),
160160
code.getBytes(StandardCharsets.UTF_8))
161-
Input.ScriptsToLoad(List(p))
161+
List(Input.Script(p))
162162
}
163163
}

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,24 @@ import java.nio.file.Path
2626
abstract class Input private ()
2727

2828
object Input {
29-
/** All files are to be loaded as scripts into the global scope in the order given. */
30-
final case class ScriptsToLoad(scripts: List[Path]) extends Input
29+
/** The file is to be loaded as a script into the global scope. */
30+
final case class Script(script: Path) extends Input
3131

32-
/** All files are to be loaded as ES modules, in the given order.
32+
/** The file is to be loaded as an ES module.
3333
*
34-
* Some environments may not be able to execute several ES modules in a
34+
* Some environments may not be able to load several ES modules in a
3535
* deterministic order. If that is the case, they must reject an
36-
* `ESModulesToLoad` input if the `modules` argument has more than one
37-
* element.
36+
* `ESModule` input if it appears with other Inputs such that loading
37+
* in a deterministic order is not possible.
3838
*/
39-
final case class ESModulesToLoad(modules: List[Path])
40-
extends Input
39+
final case class ESModule(module: Path) extends Input
4140

42-
/** All files are to be loaded as CommonJS modules, in the given order. */
43-
final case class CommonJSModulesToLoad(modules: List[Path])
44-
extends Input
41+
/** The file is to be loaded as a CommonJS module. */
42+
final case class CommonJSModule(module: Path) extends Input
4543
}
4644

4745
class UnsupportedInputException(msg: String, cause: Throwable)
4846
extends IllegalArgumentException(msg, cause) {
4947
def this(msg: String) = this(msg, null)
50-
def this(input: Input) = this(s"Unsupported input: $input")
48+
def this(input: Seq[Input]) = this(s"Unsupported input: $input")
5149
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ trait JSEnv {
3333
* with the input's content (e.g. file does not exist, syntax error, etc.).
3434
* In this case, [[JSRun#future]] should be failed instead.
3535
*
36-
* @throws java.lang.IllegalArgumentException if the value of `input` or
37-
* `config` cannot be supported.
36+
* @throws UnsupportedInputException if the value of `input` cannot be
37+
* supported.
38+
* @throws java.lang.IllegalArgumentException if the value of `config` cannot
39+
* be supported.
3840
*/
39-
def start(input: Input, config: RunConfig): JSRun
41+
def start(input: Seq[Input], config: RunConfig): JSRun
4042

4143
/** Like [[start]], but initializes a communication channel.
4244
*
@@ -83,6 +85,6 @@ trait JSEnv {
8385
* [[JSRun#future]] of the returned [[JSComRun]] is completed. Further,
8486
* [[JSRun#future]] may only complete with no callback in-flight.
8587
*/
86-
def startWithCom(input: Input, config: RunConfig,
88+
def startWithCom(input: Seq[Input], config: RunConfig,
8789
onMessage: String => Unit): JSComRun
8890
}

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

Lines changed: 74 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -33,49 +33,40 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv {
3333

3434
val name: String = "Node.js"
3535

36-
def start(input: Input, runConfig: RunConfig): JSRun = {
36+
def start(input: Seq[Input], runConfig: RunConfig): JSRun = {
3737
NodeJSEnv.validator.validate(runConfig)
3838
validateInput(input)
39-
internalStart(initFiles, input, runConfig)
39+
internalStart(initFiles ++ input, runConfig)
4040
}
4141

42-
def startWithCom(input: Input, runConfig: RunConfig,
42+
def startWithCom(input: Seq[Input], runConfig: RunConfig,
4343
onMessage: String => Unit): JSComRun = {
4444
NodeJSEnv.validator.validate(runConfig)
4545
validateInput(input)
4646
ComRun.start(runConfig, onMessage) { comLoader =>
47-
internalStart(initFiles :+ comLoader, input, runConfig)
47+
internalStart(initFiles ++ (Input.Script(comLoader) +: input), runConfig)
4848
}
4949
}
5050

51-
private def validateInput(input: Input): Unit = {
52-
input match {
53-
case _:Input.ScriptsToLoad | _:Input.ESModulesToLoad |
54-
_:Input.CommonJSModulesToLoad =>
55-
// ok
56-
case _ =>
57-
throw new UnsupportedInputException(input)
58-
}
51+
private def validateInput(input: Seq[Input]): Unit = input.foreach {
52+
case _:Input.Script | _:Input.ESModule | _:Input.CommonJSModule =>
53+
// ok
54+
case _ =>
55+
throw new UnsupportedInputException(input)
5956
}
6057

61-
private def internalStart(initFiles: List[Path], input: Input, runConfig: RunConfig): JSRun = {
58+
private def internalStart(input: Seq[Input], runConfig: RunConfig): JSRun = {
6259
val command = config.executable :: config.args
6360
val externalConfig = ExternalJSRun.Config()
6461
.withEnv(env)
6562
.withRunConfig(runConfig)
66-
ExternalJSRun.start(command, externalConfig)(
67-
NodeJSEnv.write(initFiles, input))
63+
ExternalJSRun.start(command, externalConfig)(NodeJSEnv.write(input))
6864
}
6965

70-
private def initFiles: List[Path] = config.sourceMap match {
66+
private def initFiles: Seq[Input] = config.sourceMap match {
7167
case SourceMap.Disable => Nil
72-
case SourceMap.EnableIfAvailable => installSourceMapIfAvailable :: Nil
73-
case SourceMap.Enable => installSourceMap :: Nil
74-
}
75-
76-
private def inputFiles(input: Input) = input match {
77-
case Input.ScriptsToLoad(scripts) => scripts
78-
case _ => throw new UnsupportedInputException(input)
68+
case SourceMap.EnableIfAvailable => Input.Script(installSourceMapIfAvailable) :: Nil
69+
case SourceMap.Enable => Input.Script(installSourceMap) :: Nil
7970
}
8071

8172
private def env: Map[String, String] =
@@ -104,68 +95,70 @@ object NodeJSEnv {
10495
"require('source-map-support').install();".getBytes(StandardCharsets.UTF_8))
10596
}
10697

107-
private def write(initFiles: List[Path], input: Input)(out: OutputStream): Unit = {
108-
val p = new PrintStream(out, false, "UTF8")
109-
try {
110-
def writeRunScript(path: Path): Unit = {
111-
try {
112-
val f = path.toFile
113-
val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\""
114-
p.println(s"""
98+
private def write(input: Seq[Input])(out: OutputStream): Unit = {
99+
def runScript(path: Path): String = {
100+
try {
101+
val f = path.toFile
102+
val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\""
103+
s"""
104+
require('vm').runInThisContext(
105+
require('fs').readFileSync($pathJS, { encoding: "utf-8" }),
106+
{ filename: $pathJS, displayErrors: true }
107+
)
108+
"""
109+
} catch {
110+
case _: UnsupportedOperationException =>
111+
val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8)
112+
val codeJS = "\"" + escapeJS(code) + "\""
113+
val pathJS = "\"" + escapeJS(path.toString) + "\""
114+
s"""
115115
require('vm').runInThisContext(
116-
require('fs').readFileSync($pathJS, { encoding: "utf-8" }),
116+
$codeJS,
117117
{ filename: $pathJS, displayErrors: true }
118-
);
119-
""")
120-
} catch {
121-
case _: UnsupportedOperationException =>
122-
val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8)
123-
val codeJS = "\"" + escapeJS(code) + "\""
124-
val pathJS = "\"" + escapeJS(path.toString) + "\""
125-
p.println(s"""
126-
require('vm').runInThisContext(
127-
$codeJS,
128-
{ filename: $pathJS, displayErrors: true }
129-
);
130-
""")
131-
}
118+
)
119+
"""
132120
}
121+
}
133122

134-
for (initFile <- initFiles)
135-
writeRunScript(initFile)
136-
137-
input match {
138-
case Input.ScriptsToLoad(scripts) =>
139-
for (script <- scripts)
140-
writeRunScript(script)
141-
142-
case Input.CommonJSModulesToLoad(modules) =>
143-
for (module <- modules)
144-
p.println(s"""require("${escapeJS(toFile(module).getAbsolutePath)}")""")
145-
146-
case Input.ESModulesToLoad(modules) =>
147-
if (modules.nonEmpty) {
148-
val uris = modules.map(m => toFile(m).toURI)
149-
150-
val imports = uris.map { uri =>
151-
s"""import("${escapeJS(uri.toASCIIString)}")"""
152-
}
153-
val importChain = imports.reduceLeft { (prev, imprt) =>
154-
s"""$prev.then(_ => $imprt)"""
155-
}
156-
157-
val importerFileContent = {
158-
s"""
159-
|$importChain.catch(e => {
160-
| console.error(e);
161-
| process.exit(1);
162-
|});
163-
""".stripMargin
164-
}
165-
val f = createTmpFile("importer.js")
166-
Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8))
167-
p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""")
168-
}
123+
def requireCommonJSModule(module: Path): String =
124+
s"""require("${escapeJS(toFile(module).getAbsolutePath)}")"""
125+
126+
def importESModule(module: Path): String =
127+
s"""import("${escapeJS(toFile(module).toURI.toASCIIString)}")"""
128+
129+
def execInputExpr(input: Input): String = input match {
130+
case Input.Script(script) => runScript(script)
131+
case Input.CommonJSModule(module) => requireCommonJSModule(module)
132+
case Input.ESModule(module) => importESModule(module)
133+
}
134+
135+
val p = new PrintStream(out, false, "UTF8")
136+
try {
137+
if (!input.exists(_.isInstanceOf[Input.ESModule])) {
138+
/* If there is no ES module in the input, we can do everything
139+
* synchronously, and directly on the standard input.
140+
*/
141+
for (item <- input)
142+
p.println(execInputExpr(item) + ";")
143+
} else {
144+
/* If there is at least one ES module, we must asynchronous chain things,
145+
* and we must use an actual file to feed code to Node.js (because
146+
* `import()` cannot be used from the standard input).
147+
*/
148+
val importChain = input.foldLeft("Promise.resolve()") { (prev, item) =>
149+
s"$prev.\n then(${execInputExpr(item)})"
150+
}
151+
val importerFileContent = {
152+
s"""
153+
|$importChain.catch(e => {
154+
| console.error(e);
155+
| process.exit(1);
156+
|});
157+
""".stripMargin
158+
}
159+
val f = createTmpFile("importer.js")
160+
Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8))
161+
p.println(s"""require("${escapeJS(f.getAbsolutePath)}");""")
169162
}
170163
} finally {
171164
p.close()

0 commit comments

Comments
 (0)