diff --git a/src/nodeJs/build.fan b/src/nodeJs/build.fan
new file mode 100644
index 000000000..2be8d27d3
--- /dev/null
+++ b/src/nodeJs/build.fan
@@ -0,0 +1,45 @@
+#! /usr/bin/env fan
+//
+// Copyright (c) 2006, Brian Frank and Andy Frank
+// Licensed under the Academic Free License version 3.0
+//
+// History:
+// 17 Jul 2023 Matthew Giannini creation
+//
+
+using build
+
+**
+** Build: nodeJs
+**
+class Build : BuildPod
+{
+ new make()
+ {
+ podName = "nodeJs"
+ summary = "Utilities for running Fantom in Node JS"
+ meta = ["org.name": "Fantom",
+ "org.uri": "https://fantom.org/",
+ "proj.name": "Fantom Core",
+ "proj.uri": "https://fantom.org/",
+ "license.name": "Academic Free License 3.0",
+ "vcs.name": "Git",
+ "vcs.uri": "https://github.com/fantom-lang/fantom",
+ ]
+ depends = ["sys 1.0",
+ "compiler 1.0",
+ "compilerEs 1.0",
+ "fandoc 1.0",
+ "util 1.0",
+ ]
+ srcDirs = [
+ `fan/`,
+ `fan/cmd/`,
+ // `fan/ts/`,
+ ]
+ resDirs = [
+ `res/`,
+ ]
+ docApi = false
+ }
+}
\ No newline at end of file
diff --git a/src/nodeJs/fan/EmitUtil.fan b/src/nodeJs/fan/EmitUtil.fan
new file mode 100644
index 000000000..9089136f0
--- /dev/null
+++ b/src/nodeJs/fan/EmitUtil.fan
@@ -0,0 +1,202 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 27 Jul 2023 Matthew Giannini Creation
+//
+
+using compilerEs
+using util
+
+**
+** Utility for emitting various JS code
+**
+internal class EmitUtil
+{
+
+//////////////////////////////////////////////////////////////////////////
+// Constructor
+//////////////////////////////////////////////////////////////////////////
+
+ new make(NodeJsCmd cmd)
+ {
+ this.cmd = cmd
+ }
+
+ private NodeJsCmd cmd
+ private Pod[] depends := [Pod.find("sys")]
+ private File? scriptJs := null
+ private ModuleSystem ms() { cmd.ms }
+ private Bool isCjs() { ms.moduleType == "cjs" }
+
+//////////////////////////////////////////////////////////////////////////
+// Configure Dependencies
+//////////////////////////////////////////////////////////////////////////
+
+ ** Configure the pod dependencies before emitting any code
+ This withDepends(Pod[] pods)
+ {
+ this.depends = Pod.orderByDepends(Pod.flattenDepends(pods))
+ return this
+ }
+
+ ** Configure the script js for a Fantom script
+ This withScript(File scriptJs)
+ {
+ this.scriptJs = scriptJs
+ return this
+ }
+
+//////////////////////////////////////////////////////////////////////////
+// Util
+//////////////////////////////////////////////////////////////////////////
+
+ private File? podJsFile(Pod pod, Str name := pod.name)
+ {
+ ext := isCjs ? "js" : "mjs"
+ script := "${name}.${ext}"
+ return pod.file(`/js/$script`, false)
+ }
+
+//////////////////////////////////////////////////////////////////////////
+// Emit
+//////////////////////////////////////////////////////////////////////////
+
+ Void writePackageJson([Str:Obj?] json := [:])
+ {
+ if (json["name"] == null) json["name"] = "@fantom/fan"
+ if (json["version"] == null) json["version"] = Pod.find("sys").version.toStr
+ ms.writePackageJson(json)
+ }
+
+ ** Copy all pod js files into '
/node_modules/'.
+ // ** Also copies in mime.js, units.js, and indexed-props.js
+ Void writeNodeModules()
+ {
+ writeFanJs
+ writeNode
+ writeDepends
+ writeScriptJs
+ writeMimeJs
+ writeUnitsJs
+ // TODO: indexed-props?
+ }
+
+ ** Write 'es6.js' (grab it from sys.js)
+ Void writeFanJs()
+ {
+ out := ms.file("fan").out
+ podJsFile(Pod.find("sys"), "fan").in.pipe(out)
+ out.flush.close
+ }
+
+
+ ** Write 'es6.js' (grab it from sys.js)
+ Void writeEs6()
+ {
+ out := ms.file("es6").out
+ podJsFile(Pod.find("sys"), "es6").in.pipe(out)
+ out.flush.close
+ }
+
+ ** Write 'node.js'
+ Void writeNode()
+ {
+ modules := ["os", "path", "fs", "crypto", "url", "zlib"]
+ out := ms.file("node").out
+ ms.writeBeginModule(out)
+ modules.each |m, i| { ms.writeInclude(out, m) }
+ ms.writeExports(out, modules)
+ ms.writeEndModule(out).flush.close
+ }
+
+ ** Write js from configured pod dependencies
+ Void writeDepends()
+ {
+ copyOpts := ["overwrite": true]
+
+ this.depends.each |pod|
+ {
+ file := podJsFile(pod)
+ target := ms.file(pod.name)
+ if (file != null)
+ {
+ file.copyTo(target, copyOpts)
+ // if (pod.name == "sys")
+ // {
+ // out := target.out
+ // file.in.pipe(out)
+ // out.flush.close
+ // }
+ // else file.copyTo(target, copyOpts)
+ }
+ }
+ }
+
+ ** Write the fantom script if one was configured
+ Void writeScriptJs()
+ {
+ if (scriptJs == null) return
+ out := ms.file(scriptJs.basename).out
+ try
+ {
+ scriptJs.in.pipe(out)
+ }
+ finally out.flush.close
+ }
+
+ ** Write the code for configuring MIME types to 'fan_mime.js'
+ Void writeMimeJs()
+ {
+ out := ms.file("fan_mime").out
+ JsExtToMime(ms).write(out)
+ out.flush.close
+ }
+
+ ** Write the unit database to 'fan_units.js'
+ Void writeUnitsJs()
+ {
+ out := ms.file("fan_units").out
+ JsUnitDatabase(ms).write(out)
+ out.flush.close
+ }
+
+//////////////////////////////////////////////////////////////////////////
+// Str
+//////////////////////////////////////////////////////////////////////////
+
+ ** Get a Str with all the include statements for the configured
+ ** dependencies that is targetted for the current module system
+ ** This method assumes the script importing the modules is in
+ ** the parent directory.
+ Str includeStatements()
+ {
+ baseDir := "./${ms.moduleDir.name}/"
+ buf := Buf()
+ this.depends.each |pod|
+ {
+ if ("sys" == pod.name)
+ {
+ // need explicit js ext because node has built-in lib named sys
+ ms.writeInclude(buf.out, "sys.ext", baseDir)
+ ms.writeInclude(buf.out, "fan_mime.ext", baseDir)
+ }
+ else ms.writeInclude(buf.out, "${pod.name}.ext", baseDir)
+ }
+ if (scriptJs != null) throw Err("TODO: script js")
+ // if (scriptJs != null)
+ // buf.add("import * as ${scriptJs.basename} from './${ms.moduleType}/${scriptJs.name}';\n")
+ return buf.flip.readAllStr
+ }
+
+ ** Get the JS code to configure the Env home, work and temp directories.
+ Str envDirs()
+ {
+ buf := StrBuf()
+ buf.add(" sys.Env.cur().__homeDir = sys.File.os(${Env.cur.homeDir.pathStr.toCode});\n")
+ buf.add(" sys.Env.cur().__workDir = sys.File.os(${Env.cur.workDir.pathStr.toCode});\n")
+ buf.add(" sys.Env.cur().__tempDir = sys.File.os(${Env.cur.tempDir.pathStr.toCode});\n")
+ return buf.toStr
+ }
+}
\ No newline at end of file
diff --git a/src/nodeJs/fan/Main.fan b/src/nodeJs/fan/Main.fan
new file mode 100644
index 000000000..76a92032c
--- /dev/null
+++ b/src/nodeJs/fan/Main.fan
@@ -0,0 +1,31 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 27 Jul 2023 Matthew Giannini Creation
+//
+
+using util
+
+**
+** Command line main
+**
+class Main
+{
+ static Int main(Str[] args)
+ {
+ // lookup command
+ if (args.isEmpty) args = ["help"]
+ name := args.first
+ cmd := NodeJsCmd.find(name)
+ if (cmd == null)
+ {
+ echo("ERROR: unknown nodeJs command '$name'")
+ return 1
+ }
+
+ // strip commandname from args and process as util::AbstractMain
+ return cmd.main(args.dup[1..-1])
+ }
+}
diff --git a/src/nodeJs/fan/NodeJsCmd.fan b/src/nodeJs/fan/NodeJsCmd.fan
new file mode 100644
index 000000000..c68249865
--- /dev/null
+++ b/src/nodeJs/fan/NodeJsCmd.fan
@@ -0,0 +1,99 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 27 Jul 2023 Matthew Giannini Creation
+//
+
+using compilerEs
+using util
+
+**
+** NodeJs command target
+**
+abstract class NodeJsCmd : AbstractMain
+{
+ ** Find a specific target or return null
+ static NodeJsCmd? find(Str name)
+ {
+ list.find |t| { t.name == name || t.aliases.contains(name) }
+ }
+
+ ** List installed commands
+ static NodeJsCmd[] list()
+ {
+ NodeJsCmd[] acc := NodeJsCmd#.pod.types.mapNotNull |t->NodeJsCmd?|
+ {
+ if (t.isAbstract || !t.fits(NodeJsCmd#)) return null
+ return t.make
+ }
+ acc.sort |a, b| { a.name <=> b.name }
+ return acc
+ }
+
+ ** App name is "nodeJs {name}"
+ override final Str appName() { "nodeJs ${name}" }
+
+ ** Log name is "nodeJs"
+ override Log log() { Log.get("nodeJs") }
+
+ ** Command name
+ abstract Str name()
+
+ ** Command aliases/shortcuts
+ virtual Str[] aliases() { Str[,] }
+
+ ** Run the command. Return zero on success
+ abstract override Int run()
+
+ ** Single line summary of the command for help
+ abstract Str summary()
+
+ @Opt { help="Verbose debug output"; aliases=["v"] }
+ Bool verbose
+
+ @Opt { help = "Root directory for staging Node.js environment"; aliases = ["d"] }
+ virtual File dir := Env.cur.tempDir.plus(`nodeJs/`)
+
+ @Opt { help = "Emit CommonJs" }
+ Bool cjs := false
+
+//////////////////////////////////////////////////////////////////////////
+// NodeJs
+//////////////////////////////////////////////////////////////////////////
+
+ protected Bool checkForNode()
+ {
+ cmd := ["which", "-s", "node"]
+ if ("win32" == Env.cur.os) cmd = ["where", "node"]
+ if (Process(cmd) { it.out = null }.run.join != 0)
+ {
+ err("Node not found")
+ printLine("Please ensure Node.js is installed and available in your PATH")
+ return false
+ }
+ return true
+ }
+
+ ** Get the module system environment
+ once ModuleSystem ms()
+ {
+ return this.cjs ? CommonJs(this.dir) : Esm(this.dir)
+ }
+
+ ** Get the JS emit utility
+ internal once EmitUtil emit() { EmitUtil(this) }
+
+//////////////////////////////////////////////////////////////////////////
+// Console
+//////////////////////////////////////////////////////////////////////////
+
+ ** Print a line to stdout
+ Void printLine(Str line := "") { Env.cur.out.printLine(line) }
+
+ ** Print error message and return 1
+ Int err(Str msg) { printLine("ERROR: ${msg}"); return 1 }
+
+
+}
diff --git a/src/nodeJs/fan/cmd/HelpCmd.fan b/src/nodeJs/fan/cmd/HelpCmd.fan
new file mode 100644
index 000000000..4be6c99f0
--- /dev/null
+++ b/src/nodeJs/fan/cmd/HelpCmd.fan
@@ -0,0 +1,58 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 27 Jul 2023 Matthew Giannini Creation
+//
+
+using util
+
+internal class HelpCmd : NodeJsCmd
+{
+ override Str name() { "help" }
+
+ override Str[] aliases() { ["-h", "-?"] }
+
+ override Str summary() { "Print listing of available commands" }
+
+ @Arg Str[] commandName := [,]
+
+ override Int run()
+ {
+ // if we have a command name, print its usage
+ if (commandName.size > 0)
+ {
+ cmdName :=commandName[0]
+ cmd := find(cmdName)
+ if (cmd == null) return err("Unknown help command '${cmdName}'")
+ printLine
+ printLine(cmd.summary)
+ printLine
+ if (!cmd.aliases.isEmpty)
+ {
+ printLine("Aliases:")
+ printLine(" " + cmd.aliases.join(" "))
+ }
+ ret := cmd.usage
+ printLine
+ return ret
+ }
+
+ // show summary for all commands; find longest command name
+ cmds := list
+ maxName := 4
+ cmds.each |cmd| { maxName = maxName.max(cmd.name.size) }
+
+ // print help
+ printLine
+ printLine("nodeJs commands:")
+ printLine
+ list.each |cmd|
+ {
+ printLine(cmd.name.padr(maxName) + " " + cmd.summary)
+ }
+ printLine
+ return 0
+ }
+}
diff --git a/src/nodeJs/fan/cmd/InitCmd.fan b/src/nodeJs/fan/cmd/InitCmd.fan
new file mode 100644
index 000000000..c1b4997e2
--- /dev/null
+++ b/src/nodeJs/fan/cmd/InitCmd.fan
@@ -0,0 +1,62 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 27 Jul 2023 Matthew Giannini Creation
+//
+
+using compiler
+using util
+
+internal class InitCmd : NodeJsCmd
+{
+ override Str name() { "init" }
+
+ override Str summary() { "Initialize Node.js environment for running Fantom modules" }
+
+ @Opt { help = "Root directory for staging Node.js environment"; aliases = ["-d"] }
+ override File dir := Env.cur.homeDir.plus(`lib/es/`)
+
+ override Int run()
+ {
+ emit.writePackageJson
+ emit.writeNodeModules
+ writeFantomJs
+ writeTsDecl
+ log.info("Initialized Node.js in: ${this.dir}")
+ return 0
+ }
+
+ ** Write 'fantom.js' which ensures that all the supporting
+ ** sys libraries are run.
+ private Void writeFantomJs()
+ {
+ out := ms.file("fantom").out
+ ms.writeBeginModule(out)
+ ["sys", "fan_mime", "fan_units"].each |m| { ms.writeInclude(out, "${m}.ext") }
+ ms.writeExports(out, ["sys"])
+ ms.writeEndModule(out).flush.close
+ }
+
+ ** Write 'sys.t.ds'
+ private Void writeTsDecl()
+ {
+ sysDecl := ms.moduleDir.plus(`sys.d.ts`)
+ sysDir := Env.cur.homeDir.plus(`src/sys/`)
+ ci := CompilerInput()
+ ci.podName = "sys"
+ ci.summary = "synthetic sys build"
+ ci.version = Pod.find("sys").version
+ ci.depends = Depend[,]
+ ci.inputLoc = Loc.makeFile(sysDir.plus(`build.fan`))
+ ci.baseDir = sysDir
+ ci.srcFiles = [sysDir.plus(`fan/`).uri]
+ ci.mode = CompilerInputMode.file
+ ci.output = CompilerOutputMode.podFile
+ ci.includeDoc = true
+ c := Compiler(ci)
+ c.frontend
+ sysDecl.out.writeChars(c.tsDecl).flush.close
+ }
+}
diff --git a/src/nodeJs/fan/cmd/RunCmd.fan b/src/nodeJs/fan/cmd/RunCmd.fan
new file mode 100644
index 000000000..4df5ffe9e
--- /dev/null
+++ b/src/nodeJs/fan/cmd/RunCmd.fan
@@ -0,0 +1,85 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 28 Jul 2023 Matthew Giannini Creation
+//
+
+using compiler
+using util
+
+internal class RunCmd : NodeJsCmd
+{
+ override Str name() { "run" }
+
+ override Str summary() { "Run a Fantom script in Node.js" }
+
+ @Opt { help = "Don't delete Node.js environment when done" }
+ Bool keep
+
+ @Arg { help = "Fantom script" }
+ File? script
+
+ const Str tempPod := "temp${Duration.now.toMillis}"
+
+ override Int run()
+ {
+ if (!script.exists) return err("${script} not found")
+
+ // compile the script
+ emit.withScript(this.compile)
+
+ // write modules
+ emit.writePackageJson(["name":"scriptRunner", "main":"scriptRunner.js"])
+ emit.writeNodeModules
+
+ // generate scriptRunner.js
+ template := this.typeof.pod.file(`/res/scriptRunnerTemplate.js`).readAllStr
+ template = template.replace("//{{include}}", emit.includeStatements)
+ template = template.replace("{{tempPod}}", tempPod)
+ template = template.replace("//{{envDirs}}", emit.envDirs)
+
+ f := this.dir.plus(`scriptRunner.js`)
+ f.out.writeChars(template).flush.close
+
+ // invoke node to run the script
+ Process(["node", "${f.normalize.osPath}"]).run.join
+
+ if (!keep) this.dir.delete
+
+ return 0
+ }
+
+ private File compile()
+ {
+ input := CompilerInput()
+ input.podName = tempPod
+ input.summary = ""
+ input.version = Version("0")
+ input.log.level = LogLevel.silent
+ input.isScript = true
+ input.srcStr = this.script.in.readAllStr
+ input.srcStrLoc = Loc("")
+ input.mode = CompilerInputMode.str
+ input.output = CompilerOutputMode.transientPod
+
+ // compile the source
+ compiler := Compiler(input)
+ CompilerOutput? co := null
+ try co = compiler.compile; catch {}
+ if (co == null)
+ {
+ buf := StrBuf()
+ compiler.errs.each |err| { buf.add("$err.line:$err.col:$err.msg\n") }
+ throw Err.make(buf.toStr)
+ }
+
+ // configure the dependencies
+ emit.withDepends(compiler.depends.map { Pod.find(it.name) })
+
+ // return generated js
+ return compiler.cjs.toBuf.toFile(`${tempPod}.js`)
+ }
+}
+
diff --git a/src/nodeJs/fan/cmd/TestCmd.fan b/src/nodeJs/fan/cmd/TestCmd.fan
new file mode 100644
index 000000000..567e63253
--- /dev/null
+++ b/src/nodeJs/fan/cmd/TestCmd.fan
@@ -0,0 +1,110 @@
+//
+// Copyright (c) 2023, SkyFoundry LLC
+// All Rights Reserved
+//
+// History:
+// 27 Jul 2023 Matthew Giannini Creation
+//
+
+using util
+
+internal class TestCmd : NodeJsCmd
+{
+ override Str name() { "test" }
+
+ override Str summary() { "Run tests in Node.js" }
+
+ @Opt { help = "Don't delete Node.js environment when done" }
+ Bool keep
+
+ @Arg { help = "[::[.]]" }
+ Str? spec
+
+ override Int run()
+ {
+ if (!checkForNode) return 1
+
+ pod := this.spec
+ type := "*"
+ method := "*"
+
+ // check for type
+ if (pod.contains("::"))
+ {
+ i := pod.index("::")
+ type = pod[i+2..-1]
+ pod = pod[0..i-1]
+ }
+
+ // check for method
+ if (type.contains("."))
+ {
+ i := type.index(".")
+ method = type[i+1..-1]
+ type = type[0..i-1]
+ }
+
+ p := Pod.find(pod)
+ emit.withDepends([p])
+ emit.writePackageJson(["name":"testRunner", "main":"testRunner.js"])
+ emit.writeNodeModules
+ testRunner(p, type, method)
+
+ if (!keep) this.dir.delete
+
+ return 0
+ }
+
+ private Void testRunner(Pod pod, Str type, Str method)
+ {
+ template := this.typeof.pod.file(`/res/testRunnerTemplate.js`).readAllStr
+ template = template.replace("//{{include}}", emit.includeStatements)
+ template = template.replace("//{{tests}}", testList(pod, type, method))
+ template = template.replace("//{{envDirs}}", emit.envDirs)
+
+ // write test runner
+ f := this.dir.plus(`testRunner.js`)
+ f.out.writeChars(template).flush.close
+
+ // invoke node to run tests
+ t1 := Duration.now
+ Process(["node", "${f.normalize.osPath}"]).run.join
+ t2 := Duration.now
+
+ printLine
+ printLine("Time: ${(t2-t1).toLocale}")
+ printLine
+ }
+
+ private Str testList(Pod pod, Str type, Str method)
+ {
+ buf := StrBuf()
+ buf.add("const tests = [\n")
+
+ types := type == "*" ? pod.types : [pod.type(type)]
+ types.findAll { it.fits(Test#) && it.hasFacet(Js#) }.each |t|
+ {
+ buf.add(" {'type': ${pod.name}.${t.name},\n")
+ .add(" 'qname': '${t.qname}',\n")
+ .add(" 'methods': [")
+ methods(t, method).each { buf.add("'${it.name}',") } ; buf.add("]\n")
+ buf.add(" },\n")
+ }
+ return buf.add("];\n").toStr
+ }
+
+ private static Method[] methods(Type type, Str methodName)
+ {
+ return type.methods.findAll |Method m->Bool|
+ {
+ if (m.isAbstract) return false
+ if (m.name.startsWith("test"))
+ {
+ if (methodName == "*") return true
+ return methodName == m.name
+ }
+ return false
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/nodeJs/res/scriptRunnerTemplate.js b/src/nodeJs/res/scriptRunnerTemplate.js
new file mode 100644
index 000000000..0065503ee
--- /dev/null
+++ b/src/nodeJs/res/scriptRunnerTemplate.js
@@ -0,0 +1,19 @@
+const __require = (typeof require !== 'undefined') ? require : null;
+
+//{{include}}
+
+try {
+//{{envDirs}}
+ {{tempPod}}.Main.main();
+} catch (err) {
+ console.log('ERROR: ' + err + '\n');
+ console.log(err.stack);
+ if (err == undefined) print('Undefined error\n');
+ else if (err.trace) err.trace();
+ else
+ {
+ var file = err.fileName; if (file == null) file = 'Unknown';
+ var line = err.lineNumber; if (line == null) line = 'Unknown';
+ sys.Env.cur().out().printLine(err + ' (' + file + ':' + line + ')\n');
+ }
+}
\ No newline at end of file
diff --git a/src/nodeJs/res/testRunnerTemplate.js b/src/nodeJs/res/testRunnerTemplate.js
new file mode 100644
index 000000000..ed9a36a2d
--- /dev/null
+++ b/src/nodeJs/res/testRunnerTemplate.js
@@ -0,0 +1,80 @@
+const __require = (typeof require !== 'undefined') ? require : null;
+
+//{{include}}
+
+//{{tests}}
+
+let methodCount = 0;
+let totalVerifyCount = 0;
+let failures = 0;
+let failureNames = [];
+
+const testRunner = function(type, method)
+{
+ let test;
+ const doCatchErr = function(err)
+ {
+ if (err == undefined) print('Undefined error\n');
+ else if (err.trace) err.trace();
+ else
+ {
+ let file = err.fileName; if (file == null) file = 'Unknown';
+ let line = err.lineNumber; if (line == null) line = 'Unknown';
+ // sys.Env.cur().out().printLine(err + ' (' + file + ':' + line + ')\n');
+ console.log(err + ' (' + file + ':' + line + ')\n');
+ }
+ }
+
+ try
+ {
+//{{envDirs}}
+ test = type.make();
+ test.setup();
+ test[method]();
+ return test.verifyCount$();
+ }
+ catch (err)
+ {
+ doCatchErr(err);
+ return -1;
+ }
+ finally
+ {
+ try { test.teardown(); }
+ catch (err) { doCatchErr(err); }
+ }
+}
+
+tests.forEach(function (test) {
+ console.log('');
+ test.methods.forEach(function (method) {
+ var qname = test.qname + '.' + method;
+ var verifyCount = -1;
+ console.log('-- Run: ' + qname + '...');
+ verifyCount = testRunner(test.type, method);
+ if (verifyCount < 0) {
+ failures++;
+ failureNames.push(qname);
+ } else {
+ console.log(' Pass: ' + qname + ' [' + verifyCount + ']');
+ methodCount++;
+ totalVerifyCount += verifyCount;
+ }
+ });
+});
+
+if (failureNames.length > 0) {
+ console.log('');
+ console.log("Failed:");
+ failureNames.forEach(function (qname) {
+ console.log(' ' + qname);
+ });
+ console.log('');
+}
+
+console.log('');
+console.log('***');
+console.log('*** ' +
+ (failures == 0 ? 'All tests passed!' : '' + failures + ' FAILURES') +
+ ' [' + tests.length + ' tests , ' + methodCount + ' methods, ' + totalVerifyCount + ' verifies]');
+console.log('***');