Skip to content

Commit 81ccf32

Browse files
committed
Add the distinction between Script and CommonJS module in Input.
Scripts must be executed in the global scope, so that top-level declarations end up being available to other scripts, and also as members of the global object. Previously, `NodeJSEnv` would only load `Input.ScriptsToLoad` as true scripts if they were in-memory, by piping them to the standard input of Node.js. For actual files, it used `require`, which loads them as CommonJS modules, producing the wrong behavior for top-level declarations. We now introduce a separate `Input.CommonJSModulesToLoad`. For those, `NodeJSEnv` always uses `require`, even for in-memory ones. For `Input.ScriptsToLoad`, we use the `vm` module of Node.js and its method `runInThisContext` in order to actually run files as scripts, without losing source information.
1 parent 281743e commit 81ccf32

File tree

2 files changed

+118
-30
lines changed

2 files changed

+118
-30
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ abstract class Input private ()
2828
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
31+
32+
/** All files are to be loaded as CommonJS modules, in the given order. */
33+
final case class CommonJSModulesToLoad(modules: List[VirtualBinaryFile])
34+
extends Input
3135
}
3236

3337
class UnsupportedInputException(msg: String, cause: Throwable)

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

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,36 @@ final class NodeJSEnv(config: NodeJSEnv.Config) extends JSEnv {
3434

3535
def start(input: Input, runConfig: RunConfig): JSRun = {
3636
NodeJSEnv.validator.validate(runConfig)
37-
internalStart(initFiles ++ inputFiles(input), runConfig)
37+
validateInput(input)
38+
internalStart(initFiles, input, runConfig)
3839
}
3940

4041
def startWithCom(input: Input, runConfig: RunConfig,
4142
onMessage: String => Unit): JSComRun = {
4243
NodeJSEnv.validator.validate(runConfig)
44+
validateInput(input)
4345
ComRun.start(runConfig, onMessage) { comLoader =>
44-
val files = initFiles ::: (comLoader :: inputFiles(input))
45-
internalStart(files, runConfig)
46+
internalStart(initFiles :+ comLoader, input, runConfig)
4647
}
4748
}
4849

49-
private def internalStart(files: List[VirtualBinaryFile],
50+
private def validateInput(input: Input): Unit = {
51+
input match {
52+
case _:Input.ScriptsToLoad | _:Input.CommonJSModulesToLoad =>
53+
// ok
54+
case _ =>
55+
throw new UnsupportedInputException(input)
56+
}
57+
}
58+
59+
private def internalStart(initFiles: List[VirtualBinaryFile], input: Input,
5060
runConfig: RunConfig): JSRun = {
5161
val command = config.executable :: config.args
5262
val externalConfig = ExternalJSRun.Config()
5363
.withEnv(env)
5464
.withRunConfig(runConfig)
55-
ExternalJSRun.start(command, externalConfig)(NodeJSEnv.write(files))
65+
ExternalJSRun.start(command, externalConfig)(
66+
NodeJSEnv.write(initFiles, input))
5667
}
5768

5869
private def initFiles: List[VirtualBinaryFile] = {
@@ -103,39 +114,112 @@ object NodeJSEnv {
103114
)
104115
}
105116

106-
private def write(files: List[VirtualBinaryFile])(out: OutputStream): Unit = {
117+
private def write(initFiles: List[VirtualBinaryFile], input: Input)(
118+
out: OutputStream): Unit = {
107119
val p = new PrintStream(out, false, "UTF8")
108120
try {
109-
files.foreach {
110-
case file: FileVirtualBinaryFile =>
111-
val fname = file.file.getAbsolutePath
112-
p.println(s"""require("${escapeJS(fname)}");""")
113-
case f =>
114-
val in = f.inputStream
115-
try {
116-
val buf = new Array[Byte](4096)
117-
118-
@tailrec
119-
def loop(): Unit = {
120-
val read = in.read(buf)
121-
if (read != -1) {
122-
p.write(buf, 0, read)
123-
loop()
124-
}
125-
}
126-
127-
loop()
128-
} finally {
129-
in.close()
130-
}
131-
132-
p.println()
121+
def writeRunScript(file: VirtualBinaryFile): Unit = {
122+
file match {
123+
case file: FileVirtualBinaryFile =>
124+
val pathJS = "\"" + escapeJS(file.file.getAbsolutePath) + "\""
125+
p.println(s"""
126+
require('vm').runInThisContext(
127+
require('fs').readFileSync($pathJS, { encoding: "utf-8" }),
128+
{ filename: $pathJS, displayErrors: true }
129+
);
130+
""")
131+
132+
case _ =>
133+
val code = readInputStreamToString(file.inputStream)
134+
val codeJS = "\"" + escapeJS(code) + "\""
135+
val pathJS = "\"" + escapeJS(file.path) + "\""
136+
p.println(s"""
137+
require('vm').runInThisContext(
138+
$codeJS,
139+
{ filename: $pathJS, displayErrors: true }
140+
);
141+
""")
142+
}
143+
}
144+
145+
def writeRequire(file: VirtualBinaryFile): Unit = {
146+
file match {
147+
case file: FileVirtualBinaryFile =>
148+
p.println(s"""require("${escapeJS(file.file.getAbsolutePath)}")""")
149+
150+
case _ =>
151+
val f = tmpFile(file.path, file.inputStream)
152+
p.println(s"""require("${escapeJS(f.getAbsolutePath)}")""")
153+
}
154+
}
155+
156+
for (initFile <- initFiles)
157+
writeRunScript(initFile)
158+
159+
input match {
160+
case Input.ScriptsToLoad(scripts) =>
161+
for (script <- scripts)
162+
writeRunScript(script)
163+
164+
case Input.CommonJSModulesToLoad(modules) =>
165+
for (module <- modules)
166+
writeRequire(module)
133167
}
134168
} finally {
135169
p.close()
136170
}
137171
}
138172

173+
private def readInputStreamToString(inputStream: InputStream): String = {
174+
val baos = new java.io.ByteArrayOutputStream
175+
val in = inputStream
176+
try {
177+
val buf = new Array[Byte](4096)
178+
179+
@tailrec
180+
def loop(): Unit = {
181+
val read = in.read(buf)
182+
if (read != -1) {
183+
baos.write(buf, 0, read)
184+
loop()
185+
}
186+
}
187+
188+
loop()
189+
} finally {
190+
in.close()
191+
}
192+
new String(baos.toByteArray(), StandardCharsets.UTF_8)
193+
}
194+
195+
private def tmpFile(path: String, content: InputStream): File = {
196+
import java.nio.file.{Files, StandardCopyOption}
197+
198+
try {
199+
val f = createTmpFile(path)
200+
Files.copy(content, f.toPath(), StandardCopyOption.REPLACE_EXISTING)
201+
f
202+
} finally {
203+
content.close()
204+
}
205+
}
206+
207+
// tmpSuffixRE and createTmpFile copied from HTMLRunnerBuilder.scala
208+
209+
private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r
210+
211+
private def createTmpFile(path: String): File = {
212+
/* - createTempFile requires a prefix of at least 3 chars
213+
* - we use a safe part of the path as suffix so the extension stays (some
214+
* browsers need that) and there is a clue which file it came from.
215+
*/
216+
val suffix = tmpSuffixRE.findFirstIn(path).orNull
217+
218+
val f = File.createTempFile("tmp-", suffix)
219+
f.deleteOnExit()
220+
f
221+
}
222+
139223
/** Requirements for source map support. */
140224
sealed abstract class SourceMap
141225

0 commit comments

Comments
 (0)