├── test ├── config │ └── .gitkeep └── bin │ ├── fixtures │ ├── module_FIXTURE.mjs │ ├── script.js │ ├── module.js │ └── module.mjs │ └── eshost.js ├── .npmignore ├── .gitignore ├── .gitattributes ├── .travis.yml ├── lib ├── reporters │ ├── table.js │ └── default.js ├── config.js ├── Reporter.js └── host-manager.js ├── scripts └── prepare-environment.sh ├── package.json ├── README.md └── bin └── eshost.js /test/config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | -------------------------------------------------------------------------------- /test/bin/fixtures/module_FIXTURE.mjs: -------------------------------------------------------------------------------- 1 | export var x = 1; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | local-config.js 3 | test/config/.eshost-config.json 4 | -------------------------------------------------------------------------------- /test/bin/fixtures/script.js: -------------------------------------------------------------------------------- 1 | var x = 1; 2 | ((typeof print === 'function' && print) || console.log)(x === 1); 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # force unix line endings on all files, needed for Windows npm publishing 2 | * text eol=lf 3 | *.png binary 4 | -------------------------------------------------------------------------------- /test/bin/fixtures/module.js: -------------------------------------------------------------------------------- 1 | import { x } from './module_FIXTURE.mjs'; 2 | ((typeof print === 'function' && print) || console.log)(x === 1); 3 | -------------------------------------------------------------------------------- /test/bin/fixtures/module.mjs: -------------------------------------------------------------------------------- 1 | import { x } from './module_FIXTURE.mjs'; 2 | ((typeof print === 'function' && print) || console.log)(x === 1); 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | os: 4 | - linux 5 | - osx 6 | node_js: 7 | - 12 8 | dist: trusty 9 | install: | 10 | npm run build:env; 11 | script: 12 | - npm run test; 13 | -------------------------------------------------------------------------------- /lib/reporters/table.js: -------------------------------------------------------------------------------- 1 | const Reporter = require('../Reporter.js'); 2 | const chalk = require('chalk'); 3 | const Table = require('cli-table'); 4 | 5 | module.exports = class TableReporter extends Reporter { 6 | end() { 7 | if (this.options.unanimous && this.isUnanimous) { 8 | process.exit(0); 9 | // don't print anything 10 | } else { 11 | this.results.sort((a,b) => a[0].localeCompare(b[0])); 12 | 13 | const table = new Table(); 14 | if (this.options.coalesce) { 15 | for (const [ resultString, hosts ] of Reporter.coalesce(this.results)) { 16 | const joinedHosts = hosts.join("\n").trim(); 17 | table.push([joinedHosts, resultString]); 18 | } 19 | } else { 20 | table.push(...this.results); 21 | } 22 | 23 | console.log(table.toString()); 24 | 25 | if (this.options.unanimous) { 26 | process.exit(1); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/prepare-environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z $(which esvu) ]; then 4 | npm install --ignore-scripts; 5 | npm install esvu -g; 6 | esvu --engines=all; 7 | fi; 8 | 9 | export ESHOST_ESVU_PATH="${HOME}/.esvu/bin"; 10 | export PATH="${ESHOST_ESVU_PATH}:${PATH}"; 11 | 12 | ESHOST_CHAKRA_PATH=$ESHOST_ESVU_PATH/chakra; 13 | ESHOST_ENGINE262_PATH=$ESHOST_ESVU_PATH/engine262; 14 | ESHOST_JSC_PATH=$ESHOST_ESVU_PATH/jsc; 15 | ESHOST_JS_PATH=$ESHOST_ESVU_PATH/sm; 16 | ESHOST_V8_PATH=$ESHOST_ESVU_PATH/v8; 17 | ESHOST_XS_PATH=$ESHOST_ESVU_PATH/xs; 18 | ESHOST_NODE_PATH=`which node`; 19 | 20 | echo ESHOST_ESVU_PATH=${ESHOST_ESVU_PATH}; 21 | echo ESHOST_CHAKRA_PATH=${ESHOST_CHAKRA_PATH}; 22 | echo ESHOST_ENGINE262_PATH=${ESHOST_ENGINE262_PATH}; 23 | echo ESHOST_JSC_PATH=${ESHOST_JSC_PATH}; 24 | echo ESHOST_JS_PATH=${ESHOST_JS_PATH}; 25 | echo ESHOST_NODE_PATH=${ESHOST_NODE_PATH}; 26 | echo ESHOST_V8_PATH=${ESHOST_V8_PATH}; 27 | echo ESHOST_XS_PATH=${ESHOST_XS_PATH}; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eshost-cli", 3 | "version": "9.0.0", 4 | "description": "Run scripts in any ECMAScript host", 5 | "bin": { 6 | "eshost": "bin/eshost.js" 7 | }, 8 | "dependencies": { 9 | "ansi-styles": "^4.2.1", 10 | "chalk": "^4.1.0", 11 | "cli-table": "^0.3.1", 12 | "configstore": "^1.4.0", 13 | "eshost": "^9.0.0", 14 | "nconf": "^0.8.2", 15 | "temp": "^0.8.3", 16 | "unique-temp-dir": "^1.0.0", 17 | "yargs": "^3.31.0" 18 | }, 19 | "devDependencies": { 20 | "mocha": "^3.5.0" 21 | }, 22 | "scripts": { 23 | "build:env": "./scripts/prepare-environment.sh", 24 | "test": "export $(./scripts/prepare-environment.sh); mocha test/bin/eshost.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/bterlson/eshost-cli" 29 | }, 30 | "keywords": [ 31 | "javascript", 32 | "ecmascript", 33 | "host" 34 | ], 35 | "author": "Brian Terlson", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/bterlson/eshost/issues" 39 | }, 40 | "homepage": "https://github.com/bterlson/eshost-cli" 41 | } 42 | -------------------------------------------------------------------------------- /lib/reporters/default.js: -------------------------------------------------------------------------------- 1 | const Reporter = require('../Reporter.js'); 2 | const chalk = require('chalk'); 3 | 4 | module.exports = class DefaultReporter extends Reporter { 5 | end() { 6 | if (this.options.unanimous && this.isUnanimous) { 7 | process.exit(0); 8 | // don't print anything 9 | } else { 10 | this.results.sort((a,b) => a[0].localeCompare(b[0])); 11 | if (this.options.coalesce) { 12 | for (const [ resultString, hosts ] of Reporter.coalesce(this.results)) { 13 | const joinedHosts = hosts.join(", ").trim(); 14 | this.printHostResult(joinedHosts, resultString); 15 | } 16 | 17 | if (this.options.unanimous) { 18 | process.exit(1); 19 | } 20 | } else { 21 | this.results.forEach(row => { 22 | this.printHostResult(row[0], row[1]); 23 | }); 24 | } 25 | } 26 | } 27 | printHostResult(name, result) { 28 | const header = `#### ${name}`; 29 | console.log( 30 | this.options.color 31 | ? chalk[this.options.color.header](header) 32 | : header 33 | ); 34 | console.log(`${result}\n`); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | 7 | class Config { 8 | constructor(configPath) { 9 | Object.defineProperty(this, 'configPath', { 10 | value: configPath 11 | }); 12 | this.load(); 13 | } 14 | 15 | load() { 16 | this.hosts = {} 17 | 18 | if (!fs.existsSync(this.configPath)) { 19 | return; 20 | } 21 | 22 | const contents = fs.readFileSync(this.configPath, 'utf8'); 23 | 24 | if (!contents) { 25 | return; 26 | } 27 | 28 | const config = JSON.parse(contents); 29 | 30 | Object.keys(config).forEach(k => this[k] = config[k]); 31 | 32 | this.hosts = this.hosts || {}; 33 | 34 | for (const hostName in this.hosts) { 35 | const host = this.hosts[hostName]; 36 | 37 | if (typeof host !== 'object' || host === null) { 38 | continue; 39 | } 40 | 41 | host.path = this.resolvePath(host.path); 42 | } 43 | } 44 | 45 | resolvePath(input) { 46 | if (!input) { 47 | return input; 48 | } 49 | 50 | if (input.startsWith('~/')) { 51 | return path.join(os.homedir(), input.slice(2)); 52 | } else if (input === '~') { 53 | return os.homedir(); 54 | } else if (!path.isAbsolute(input)) { 55 | return path.join(path.dirname(this.configPath), input); 56 | } else { 57 | return input; 58 | } 59 | } 60 | 61 | save() { 62 | fs.writeFileSync(this.configPath, JSON.stringify(this, null, 2), 'utf8'); 63 | } 64 | 65 | static defaultConfig() { 66 | return new Config(Config.defaultConfigPath); 67 | } 68 | } 69 | 70 | Config.defaultConfigPath = path.join(os.homedir(), '.eshost-config.json'); 71 | 72 | module.exports = Config; 73 | -------------------------------------------------------------------------------- /lib/Reporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | 5 | module.exports = class Reporter { 6 | constructor(options = {}) { 7 | this.options = options; 8 | this.source = undefined; 9 | this.results = []; 10 | } 11 | 12 | start(source) { 13 | this.source = source; 14 | 15 | if (this.options.showSource) { 16 | this.printSource(source); 17 | } 18 | } 19 | 20 | result({name}, {stdout, error}) { 21 | let resultCell = stdout.trim(); 22 | if (error) { 23 | resultCell += this.options.color 24 | ? `\n${chalk[this.options.color.error](`${error.name}: ${error.message}`)}` 25 | : `\n${error.name}: ${error.message}`; 26 | } 27 | resultCell = resultCell.replace(/\r/g, ''); 28 | 29 | this.results.push([ 30 | name, resultCell 31 | ]); 32 | } 33 | 34 | end() {} 35 | 36 | get isUnanimous() { 37 | if (this.results.length === 1) { 38 | return true; 39 | } 40 | // Get the result string of the first result entry 41 | let first = this.results[0][1]; 42 | 43 | // Compare the first result string to all result strings 44 | for (let [ host, result ] of this.results) { 45 | if (result !== first) { 46 | return false; 47 | } 48 | } 49 | return true; 50 | } 51 | 52 | printSource(source) { 53 | const header = '## Source'; 54 | console.log( 55 | this.options.color 56 | ? chalk[this.options.color.header](header) 57 | : header 58 | ); 59 | console.log(`${source}\n`); 60 | } 61 | 62 | static coalesce(records) { 63 | const resultsMap = new Map(); 64 | for (const [ name, resultString ] of records) { 65 | if (!resultsMap.has(resultString)) { 66 | resultsMap.set(resultString, []); 67 | } 68 | resultsMap.get(resultString).push(name); 69 | } 70 | return [...resultsMap]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/host-manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs') 4 | const os = require('os'); 5 | const path = require('path'); 6 | 7 | const chalk = require('chalk'); 8 | const Table = require('cli-table') 9 | 10 | const Config = require('../lib/config') 11 | const esh = require('eshost'); 12 | 13 | const head = [ 14 | 'name', 15 | 'type', 16 | 'path', 17 | 'args', 18 | 'tags', 19 | ]; 20 | 21 | const colWidths = head.map(h => h.length + 2); 22 | 23 | exports.list = ({configPath, hosts}) => { 24 | const table = new Table({ 25 | colWidths, head 26 | }); 27 | 28 | const names = Object.keys(hosts); 29 | let output; 30 | 31 | if (!names.length) { 32 | output = 'No configured hosts'; 33 | } else { 34 | output = names.reduce((accum, name) => { 35 | const { 36 | type, 37 | path = '', 38 | args = '', 39 | tags = '' 40 | } = hosts[name]; 41 | 42 | const row = [ name, type, path, args, tags ]; 43 | const {colWidths: widths} = accum.options; 44 | 45 | // Update the stored colWidths for each cell, saving the largest/longest 46 | accum.options.colWidths = Array.from(widths, (width, index) => Math.max(width, String(row[index]).length + 2)); 47 | 48 | accum.push(row); 49 | return accum; 50 | }, table).sort().toString(); 51 | } 52 | 53 | console.log(output); 54 | }; 55 | 56 | exports.add = (config, hostName, hostType, hostPath, hostArgs, hostTags) => { 57 | if (config.hosts[hostName]) { 58 | console.log(chalk.red(`Host "${hostName}" already exists`)); 59 | return; 60 | } 61 | 62 | if (!esh.supportedHosts.includes(hostType)) { 63 | console.log( 64 | chalk.red( 65 | `Host type "${hostType}" not supported. Supported host types are: "${esh.supportedHosts.join(", ")}"` 66 | ) 67 | ); 68 | return; 69 | } 70 | 71 | if (hostPath && !path.isAbsolute(hostPath)) { 72 | hostPath = path.join(process.cwd(), hostPath); 73 | } 74 | 75 | config.hosts[hostName] = { 76 | type: hostType, 77 | path: hostPath, 78 | args: hostArgs, 79 | tags: hostTags, 80 | }; 81 | config.save(); 82 | }; 83 | 84 | exports.edit = (config, hostName, hostArgs, hostTags) => { 85 | if (!config.hosts[hostName]) { 86 | console.log(chalk.red(`Host "${hostName}" does not exist`)); 87 | process.exit(1); 88 | return; 89 | } 90 | 91 | if (hostArgs) { 92 | config.hosts[hostName].args = hostArgs; 93 | } 94 | 95 | if (hostTags) { 96 | config.hosts[hostName].tags = hostTags; 97 | } 98 | 99 | config.save(); 100 | }; 101 | 102 | exports.delete = (config, hostName) => { 103 | if (!hostName) { 104 | Object.keys(config.hosts).forEach(hostName => exports.delete(config, hostName)); 105 | } else { 106 | if (config.hosts[hostName]) { 107 | delete config.hosts[hostName]; 108 | config.save(); 109 | console.log(`Host "${hostName}" removed`); 110 | } else { 111 | console.log(`Host "${hostName}" not found`); 112 | } 113 | } 114 | }; 115 | 116 | const VU_BIN_PATH = { 117 | esvu: 'bin', 118 | jsvu: '', 119 | }; 120 | 121 | const VU_HOST_DETAILS = { 122 | /* 123 | hostName 124 | binaryName 125 | hostType 126 | hostTags 127 | */ 128 | esvu: { 129 | ch: ['ChakraCore', 'chakra', 'ch', ['web']], 130 | engine262: ['engine262', 'engine262', 'engine262', ['']], 131 | graaljs: ['GraalJS', 'graaljs', 'graaljs', ['']], 132 | hermes: ['Hermes', 'hermes', 'hermes', ['']], 133 | jsc: ['JavaScriptCore', 'jsc', 'jsc', ['web']], 134 | jsshell: ['SpiderMonkey', 'sm', 'jsshell', ['web']], 135 | qjs: ['QuickJS', 'quickjs-run-test262', 'qjs', ['embedded']], 136 | v8: ['V8', 'v8', 'd8', ['web']], 137 | xs: ['Moddable XS', 'xs', 'xs', ['embedded']], 138 | }, 139 | jsvu: { 140 | chakra: ['ChakraCore', 'chakra', 'ch', ['web']], 141 | hermes: ['Hermes', 'hermes', 'hermes', ['']], 142 | javascriptcore: ['JavaScriptCore', 'jsc', 'jsc', ['web']], 143 | // This version of quickjs does not include the necessary 144 | // host defined environment extensions to execute code in 145 | // eshost. 146 | quickjs: ['QuickJS', 'quickjs', ['']], 147 | spidermonkey: ['SpiderMonkey', 'sm', 'jsshell', ['web']], 148 | v8: ['V8', 'v8', 'd8', ['web']], 149 | xs: ['Moddable XS', 'xs', 'xs', ['embedded']], 150 | } 151 | }; 152 | 153 | exports.configureFromVersionUpdater = (config, vu, vuRoot, hostPrefix = '') => { 154 | if (!fs.existsSync(vuRoot)) { 155 | throw Error(`Version Updater "${vu}" config root "${vuRoot}" not found`); 156 | } 157 | 158 | const status = require(path.join(vuRoot, 'status.json')); 159 | const engines = status[vu === 'esvu' ? 'selectedEngines' : 'engines']; 160 | 161 | let extension = ''; 162 | if (os.platform() === 'win32') { 163 | extension = '.cmd'; 164 | } 165 | 166 | for (let engine of engines) { 167 | if (!VU_HOST_DETAILS[vu] || 168 | (VU_HOST_DETAILS[vu] && !VU_HOST_DETAILS[vu][engine])) { 169 | continue; 170 | } 171 | 172 | let [ 173 | hostName, 174 | binaryName, 175 | hostType, 176 | hostTags 177 | ] = VU_HOST_DETAILS[vu][engine]; 178 | 179 | if (!hostName) { 180 | continue; 181 | } 182 | 183 | const hostPath = path.resolve( 184 | vuRoot, VU_BIN_PATH[vu], binaryName + extension 185 | ); 186 | 187 | if (!fs.existsSync(hostPath)) { 188 | console.log(chalk.red(`"${hostName}" could not be configured because ${hostPath} was not found.`)); 189 | continue; 190 | } 191 | const version = status[engine] || (status.installed[engine] && status.installed[engine].version); 192 | 193 | if (hostPrefix) { 194 | hostName = `${hostPrefix}-${hostName}`; 195 | } 196 | 197 | hostTags.unshift(version); 198 | 199 | console.log(`Configuring ${vu} host with name "${hostName}" (type "${hostType}") at "${hostPath}"`); 200 | exports.add(config, hostName, hostType, hostPath, undefined, hostTags.filter(Boolean)); 201 | } 202 | }; 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eshost-cli 2 | 3 | [![Travis Build Status](https://travis-ci.org/bterlson/eshost-cli.svg?branch=master)](https://travis-ci.org/bterlson/eshost-cli) 4 | [![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/github/bterlson/eshost-cli?branch=master&svg=true)](https://ci.appveyor.com/project/bterlson/eshost-cli) 5 | 6 | 7 | eshost-cli makes it easy to run and compare ECMAScript code uniformly across a number of runtimes. Support for runtimes is provided by the library [eshost](https://github.com/bterlson/eshost). Every host is initialized with the [eshost runtime API](https://github.com/bterlson/eshost#runtime-library) available which provides a uniform way to print, create realms, and eval code. 8 | 9 | See eshost's [supported hosts](https://github.com/bterlson/eshost#supported-hosts) for a list of hosts, download/build locations, and other information. 10 | 11 | ## Usage 12 | 13 | See `--help` output for the full details. Basic usage: 14 | 15 | * Add hosts using `eshost --add --args `. 16 | * Automatically configure [`esvu`](https://github.com/devsnek/esvu)-installed hosts using `eshost --configure-esvu`. 17 | * Automatically configure [`jsvu`](https://github.com/GoogleChromeLabs/jsvu)-installed hosts using `eshost --configure-jsvu`. 18 | * Evaluate a *single* expression using `-e`: `eshost -e "[1,2,3].length"`. 19 | * Execute a multi-statement program using `-x`: `eshost -x "foo = 42; print(foo);"` 20 | * Execute a script using `eshost path/to/script.js`. 21 | * Execute an expression or multi-statement program as module code using `-m`: 22 | - `eshost -me "foo = 42"` (this example should result in errors!) 23 | - `eshost -mx "foo = 42; print(foo);"` (this example should result in errors!) 24 | * Execute a source file as module code by saving the file with an `.mjs` extension: `eshost file.mjs`; or by using the `-m` option: `eshost -m file.js` 25 | 26 | ### Install and Configure Hosts 27 | 28 | #### Linux and macOS 29 | 30 | Manually, using `esvu`: 31 | 32 | ```sh 33 | npm install esvu -g; 34 | export PATH="${HOME}/.esvu/bin:${PATH}"; 35 | 36 | esvu --engines=all; 37 | 38 | export ESHOST_PATH_CHAKRA=`which chakra`; 39 | export ESHOST_PATH_ENGINE262=`which engine262`; 40 | export ESHOST_PATH_HERMES=`which hermes`; 41 | export ESHOST_PATH_JAVASCRIPTCORE=`which javascriptcore`; 42 | export ESHOST_PATH_QUICKJS=`which qjs-for-eshost`; 43 | export ESHOST_PATH_SPIDERMONKEY=`which spidermonkey`; 44 | export ESHOST_PATH_V8=`which v8`; 45 | export ESHOST_PATH_XS=`which xs`; 46 | 47 | npm install -g eshost-cli; 48 | 49 | eshost --add "chakra" ch $ESHOST_PATH_CHAKRA; 50 | eshost --add "engine262" engine262 $ESHOST_PATH_ENGINE262; 51 | eshost --add "hermes" hermes $ESHOST_PATH_HERMES; 52 | eshost --add "javascriptcore" jsc $ESHOST_PATH_JAVASCRIPTCORE; 53 | eshost --add "quickjs" qjs $ESHOST_PATH_QUICKJS; 54 | eshost --add "spidermonkey" jsshell $ESHOST_PATH_SPIDERMONKEY; 55 | eshost --add "v8" d8 $ESHOST_PATH_V8; 56 | eshost --add "xs" xs $ESHOST_PATH_XS; 57 | ``` 58 | 59 | Manually, using `jsvu`: 60 | 61 | ```sh 62 | # Engine262 63 | git clone https://github.com/devsnek/engine262.git; 64 | cd engine262 && npm install && npm run build && npm link; 65 | cd ~/ 66 | 67 | # Everything else... 68 | npm install -g jsvu; 69 | export PATH="${HOME}/.jsvu:${PATH}"; 70 | 71 | jsvu --engines=chakra,hermes,javascriptcore,spidermonkey,v8,xs 72 | 73 | export ESHOST_PATH_CHAKRA=`which chakra`; 74 | export ESHOST_PATH_ENGINE262=`which engine262`; 75 | export ESHOST_PATH_HERMES=`which hermes`; 76 | export ESHOST_PATH_JAVASCRIPTCORE=`which javascriptcore`; 77 | export ESHOST_PATH_QUICKJS=`which qjs-for-eshost`; 78 | export ESHOST_PATH_SPIDERMONKEY=`which spidermonkey`; 79 | export ESHOST_PATH_V8=`which v8`; 80 | export ESHOST_PATH_XS=`which xs`; 81 | 82 | npm install -g eshost-cli; 83 | 84 | eshost --add "chakra" ch $ESHOST_PATH_CHAKRA; 85 | eshost --add "engine262" engine262 $ESHOST_PATH_ENGINE262; 86 | eshost --add "hermes" hermes $ESHOST_PATH_HERMES; 87 | eshost --add "javascriptcore" jsc $ESHOST_PATH_JAVASCRIPTCORE; 88 | eshost --add "quickjs" qjs $ESHOST_PATH_QUICKJS; 89 | eshost --add "spidermonkey" jsshell $ESHOST_PATH_SPIDERMONKEY; 90 | eshost --add "v8" d8 $ESHOST_PATH_V8; 91 | eshost --add "xs" xs $ESHOST_PATH_XS; 92 | ``` 93 | 94 | ##### This will install QuickJS on macOS 95 | 96 | ```sh 97 | if [ "$(uname)" = Darwin ]; then wget https://bellard.org/quickjs/quickjs-2021-03-27.tar.xz; tar -xf quickjs-2021-03-27.tar.xz; 98 | cd quickjs-2021-03-27 && make; if [ -f "$PWD/run-test262" ]; then ln -s $PWD/run-test262 /usr/local/bin/qjs-for-eshost; fi; fi; 99 | ``` 100 | 101 | 102 | 103 | #### Windows 104 | 105 | 106 | 107 | Manually, using `jsvu`: 108 | 109 | ```batch 110 | git clone https://github.com/devsnek/engine262.git 111 | cd .\engine262 112 | npm install 113 | npm run build 114 | npm link 115 | set NPM_GLOBAL_MODULE_PATH=%APPDATA%\npm\ 116 | set PATH=%PATH;%NPM_GLOBAL_MODULE_PATH% 117 | where engine262 118 | 119 | npm install jsvu 120 | 121 | jsvu --os=win64 --engines="chakra,spidermonkey,v8,xs" 122 | 123 | set PATH=%PATH;%USERPROFILE%\.jsvu\ 124 | set ESHOST_CHAKRA=%USERPROFILE%\.jsvu\chakra.cmd 125 | set ESHOST_ENGINE262=%NPM_GLOBAL_MODULE_PATH%\engine262.cmd 126 | set ESHOST_SPIDERMONKEY=%USERPROFILE%\.jsvu\spidermonkey.cmd 127 | set ESHOST_V8=%USERPROFILE%\.jsvu\v8.cmd 128 | set ESHOST_XS=%USERPROFILE%\.jsvu\xs.cmd 129 | 130 | npm install -g eshost-cli; 131 | 132 | eshost --add "chakra" ch %ESHOST_CHAKRA% 133 | eshost --add "engine262" engine262 %ESHOST_ENGINE262% 134 | eshost --add "spidermonkey" jsshell %ESHOST_SPIDERMONKEY% 135 | eshost --add "v8" d8 %ESHOST_V8% 136 | eshost --add "xs" xs %ESHOST_XS% 137 | ``` 138 | 139 | ### Examples 140 | 141 | ```console 142 | $ npm install -g eshost-cli 143 | $ eshost --help 144 | $ eshost --add --args 145 | $ eshost -e "Map.length" 146 | #### chakra 147 | 0 148 | 149 | #### engine262 150 | 0 151 | 152 | #### javascriptcore 153 | 0 154 | 155 | #### spidermonkey 156 | 0 157 | 158 | #### v8 159 | 0 160 | 161 | #### xs 162 | 0 163 | ``` 164 | 165 | 166 | ```console 167 | $ eshost --configure-esvu --esvu-prefix esvu 168 | $ eshost --tags esvu-web -itsx "let a = 40+2; print(a)" 169 | 170 | ## Source 171 | let a = 40+2; print(a) 172 | 173 | ┌──────────┬────┐ 174 | │ esvu-ch │ 42 │ 175 | │ esvu-jsc │ │ 176 | │ esvu-sm │ │ 177 | │ esvu-v8 │ │ 178 | └──────────┴────┘ 179 | ``` 180 | 181 | ```console 182 | $ eshost --configure-jsvu --jsvu-prefix jsvu 183 | $ eshost --tags jsvu-web -itsx "let a = 40+2; print(a)" 184 | 185 | ## Source 186 | let a = 40+2; print(a) 187 | 188 | ┌──────────┬────┐ 189 | │ jsvu-ch │ 42 │ 190 | │ jsvu-jsc │ │ 191 | │ jsvu-sm │ │ 192 | │ jsvu-v8 │ │ 193 | └──────────┴────┘ 194 | ``` 195 | 196 | ### Rules For Module Code 197 | 198 | Files containing the imported modules must be located in the same directory that the "entry point" file is located. Please read and accept the following examples. 199 | 200 | 1. Executing a program with module dependencies, where the entry point is a ".mjs" file: 201 | 202 | ```sh 203 | mkdir entry-point-mjs; 204 | cd entry-point-mjs; 205 | echo "export var a = 1;" >> export.mjs 206 | echo "import {a} from './export.mjs'; print(a);" >> import.mjs 207 | 208 | eshost --host="engine262,javascriptcore,spidermonkey,v8,xs" import.mjs 209 | #### engine262 210 | 1 211 | 212 | #### javascriptcore 213 | 1 214 | 215 | #### spidermonkey 216 | 1 217 | 218 | #### v8 219 | 1 220 | 221 | #### xs 222 | 1 223 | ``` 224 | 225 | 2. Executing a program with module dependencies, where the entry point is a ".js" file (Notice the use of the `-m` flag, **this is required for ".js" files**): 226 | 227 | ```sh 228 | mkdir entry-point-js; 229 | cd entry-point-js; 230 | echo "export var a = 1;" >> export.mjs 231 | echo "import {a} from './export.mjs'; print(a);" >> import.js 232 | 233 | eshost --host="engine262,javascriptcore,spidermonkey,v8,xs" -m import.js 234 | #### engine262 235 | 1 236 | 237 | #### javascriptcore 238 | 1 239 | 240 | #### spidermonkey 241 | 1 242 | 243 | #### v8 244 | 1 245 | 246 | #### xs 247 | 1 248 | ``` 249 | 250 | **Executing a multi-line program with module dependencies is not yet supported. Support is in progress.** 251 | 252 | 253 | ## Managing Hosts 254 | 255 | You can `--list`, `--add`, `--edit`, and `--delete` hosts. Adding a host requires a name, type, and path to the runtime executable. You can optionally pass arguments using `--args`. The same host can be added multiple times with different `--args` which makes it easy to compare the output of runtimes given different options (e.g. by turning language features on and off). 256 | 257 | Console hosts are either provided by the browser vendors or, more likely, built from source. [The `jsvu` CLI](https://github.com/GoogleChromeLabs/jsvu) makes it easy to install and update the most common JavaScript engine binaries. 258 | 259 | Host types are [those provided by eshost](https://github.com/bterlson/eshost#eshostcreateagenttype-string-options---agent), namely: 260 | 261 | ### Shells 262 | 263 | | Host Type | All Acceptable `type` Values | 264 | | ---- | -------------------- | 265 | | ChakraCore | `chakra`, `ch` | 266 | | Engine262 | `engine262` | 267 | | JavaScriptCore | `javascriptcore`, `jsc` | 268 | | Nashorn | `nashorn` | 269 | | Node | `node` | 270 | | QuickJS | `qjs` 1 | 271 | | SpiderMonkey | `jsshell`, `spidermonkey`, `sm` | 272 | | V8 | `d8`, `v8` | 273 | | XS | `xs` | 274 | 275 | * 1: **DO NOT USE `~/.jsvu/quickjs` WITH ESHOST-CLI**. The main `quickjs` binary does not support the [eshost runtime API](https://github.com/bterlson/eshost#runtime-library). For more, see [Install and Configure Hosts](#install-and-configure-hosts). 276 | 277 | ### Browsers 278 | 279 | | Host Type | All Acceptable `type` Values | 280 | | ---- | -------------------- | 281 | | chrome | `chrome` | 282 | | edge | `edge` | 283 | | firefox | `firefox` | 284 | | safari | `safari` | 285 | -------------------------------------------------------------------------------- /bin/eshost.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const fs = require('fs'); 5 | const os = require('os'); 6 | const path = require('path'); 7 | const readline = require('readline'); 8 | 9 | const chalk = require('chalk'); 10 | const esh = require('eshost'); 11 | const styles = require('ansi-styles'); 12 | const Table = require('cli-table'); 13 | const uniqueTempDir = require('unique-temp-dir'); 14 | const yargs = require('yargs'); 15 | 16 | const Config = require('../lib/config') 17 | const DefaultReporter = require('../lib/reporters/default'); 18 | const hostManager = require('../lib/host-manager') ; 19 | const TableReporter = require('../lib/reporters/table'); 20 | 21 | const usage = ` 22 | Usage: eshost [options] [input-file] 23 | eshost [options] -e "input-script" 24 | eshost --list 25 | eshost --add [host name] [host type] --args 26 | eshost --delete [host name] 27 | `.trim(); 28 | 29 | const yargv = yargs 30 | .strict() 31 | .usage(usage) 32 | .describe('e', 'eval an expression and print the result') 33 | .alias('e', 'eval') 34 | .describe('x', 'execute a multi-statement program') 35 | .alias('x', 'execute') 36 | .describe('h', 'select hosts by name (glob syntax is supported as well)') 37 | .alias('h', 'host') 38 | .describe('g', 'select host groups by host type') 39 | .alias('g', 'hostGroup') 40 | .nargs('g', 1) 41 | .describe('tags', 'select hosts by tag') 42 | .nargs('tags', 1) 43 | .describe('c', 'select a config file') 44 | .alias('c', 'config') 45 | .describe('table', 'output in a table') 46 | .boolean('table') 47 | .alias('table', 't') 48 | .describe('coalesce', 'coalesce like output into a single entry') 49 | .boolean('coalesce') 50 | .alias('coalesce', 's') 51 | .describe('color', 'specify the color to use in output, eg. --color.error red --color.header blue') 52 | .describe('no-color', 'do not colorize output') 53 | .boolean('no-color') 54 | .describe('showSource', 'show input source') 55 | .boolean('showSource') 56 | .alias('showSource', 'i') 57 | .describe('unanimous', 'If all engines agree, exit(0) with no output, otherwise print and exit(1); implies --coalesce') 58 | .boolean('unanimous') 59 | .alias('unanimous', 'u') 60 | .nargs('h', 1) 61 | .describe('async', 'wait for realm destruction before reporting results') 62 | .boolean('async') 63 | .alias('a', 'async') 64 | .describe('module', 'evaluate expression as module code') 65 | .boolean('module') 66 | .alias('m', 'module') 67 | .describe('raw', 'evaluate raw code, with no runtime helpers') 68 | .boolean('raw') 69 | .alias('r', 'raw') 70 | .default('r', false, '"raw" defaults to "false"') 71 | .boolean('l', 'list hosts') 72 | .alias('l', 'list') 73 | .describe('add', 'add a host') 74 | .nargs('add', 2) 75 | .describe('edit', 'edit a host') 76 | .nargs('edit', 1) 77 | .describe('delete', 'delete a host') 78 | .nargs('delete', 1) 79 | .describe('delete-all', 'delete all hosts') 80 | .describe('args', 'set arguments for a host entry (use with --add)') 81 | .nargs('args', 1) 82 | .describe('configure-esvu', 'Configure esvu hosts in the config') 83 | .boolean('configure-esvu') 84 | .describe('esvu-prefix', '[OPTIONAL] Set the prefix of the configured hosts. If prefix is "esvu" then hosts will be configured as, e.g., "esvu-sm". By default, no prefix (e.g. "sm"). Use this flag with --configure-esvu.') 85 | .nargs('esvu-prefix', 1) 86 | .describe('esvu-root', '[OPTIONAL] Use this path containing the .esvu folder (use this option if .esvu is located somewhere other than the home directory). Use this flag with --configure-esvu.') 87 | .nargs('esvu-root', 1) 88 | .describe('configure-jsvu', 'Configure jsvu hosts in the config') 89 | .boolean('configure-jsvu') 90 | .describe('jsvu-prefix', '[OPTIONAL] Set the prefix of the configured hosts. If prefix is "jsvu" then hosts will be configured as, e.g., "jsvu-sm". By default, no prefix (e.g. "sm"). Use this flag with --configure-jsvu.') 91 | .nargs('jsvu-prefix', 1) 92 | .describe('jsvu-root', '[OPTIONAL] Use this path containing the .jsvu folder (use this option if .jsvu is located somewhere other than the home directory). Use this flag with --configure-jsvu.') 93 | .nargs('jsvu-root', 1) 94 | .help('help') 95 | .example('eshost --list') 96 | .example('eshost --add d8 d8 path/to/d8 --args "--harmony"') 97 | .example('eshost --add ch ch path/to/ch --tags latest') 98 | .example('eshost --add ch ch path/to/ch --tags latest,greatest') 99 | .example('eshost --configure-esvu') 100 | .example('eshost --configure-esvu --esvu-prefix esvu') 101 | .example('eshost --configure-jsvu') 102 | .example('eshost --configure-jsvu --jsvu-prefix jsvu') 103 | .example('eshost test.js') 104 | .example('eshost -e "1+1"') 105 | .example('eshost -its -x "for (let i=0; i<10; ++i) { print(i) }"') 106 | .example('eshost -h d8 -h chakra test.js') 107 | .example('eshost -h d8,sm test.js') 108 | .example('eshost -g node,ch test.js') 109 | .example('eshost -h d8 -g node test.js') 110 | .example('eshost -h ch-*,node test.js') 111 | .example('eshost -h ch-1.?.? test.js') 112 | .example('eshost --tags latest test.js') 113 | .example('eshost --unanimous test.js') 114 | .fail((msg, error) => { 115 | if (error) { 116 | console.error(error.stack); 117 | } else { 118 | console.log(msg); 119 | } 120 | process.exit(1); 121 | }); 122 | 123 | const argv = yargv.argv; 124 | 125 | // --unanimous implies --coalesce 126 | if (argv.unanimous) { 127 | argv.coalesce = true; 128 | } 129 | 130 | let config; 131 | if (argv.c) { 132 | config = new Config(argv.c); 133 | } else { 134 | config = Config.defaultConfig(); 135 | } 136 | 137 | let hosts = []; 138 | if (Array.isArray(argv.h)) { 139 | hosts = argv.h; 140 | } else if (typeof argv.h === 'string') { 141 | hosts = argv.h.split(','); 142 | } 143 | 144 | // Add host glob matches to hosts 145 | let newHosts = []; 146 | for (let host of hosts) { 147 | if (host.indexOf('*') >= 0 || host.indexOf('?') >= 0) { 148 | let re = '^' + host 149 | .replace(/\./g, '\\.') // escape . with /\./ 150 | .replace(/\*/g, '.*') // change * to /.*/ for matches 151 | .replace(/\?/g, '.') // change ? to /./ for matches 152 | + '$'; 153 | let matcher = new RegExp(re); 154 | for (let hostName of Object.keys(config.hosts)) { 155 | if (matcher.test(hostName)) { 156 | newHosts.push(hostName); 157 | } 158 | } 159 | } else { 160 | newHosts.push(host); 161 | } 162 | hosts = newHosts; 163 | } 164 | 165 | let hostGroups; 166 | if (Array.isArray(argv.g)) { 167 | hostGroups = argv.g; 168 | } else if (typeof argv.g === 'string') { 169 | hostGroups = argv.g.split(','); 170 | } 171 | 172 | if (hostGroups) { 173 | for (let group of hostGroups) { 174 | for (let hostName of Object.keys(config.hosts)) { 175 | let hostType = config.hosts[hostName].type; 176 | if (group === hostType) { 177 | hosts.push(hostName); 178 | } 179 | } 180 | } 181 | } 182 | 183 | let hostTags; 184 | if (Array.isArray(argv.tags)) { 185 | hostTags = argv.tags; 186 | } else if (typeof argv.tags === 'string') { 187 | hostTags = argv.tags.split(','); 188 | } 189 | 190 | if (hostTags) { 191 | function includesAnyOf(a, b) { 192 | for (let x of a) { 193 | if (b.includes(x)) { 194 | return true; 195 | } 196 | } 197 | return false; 198 | } 199 | 200 | for (let hostName of Object.keys(config.hosts)) { 201 | let tags = config.hosts[hostName].tags; 202 | if (tags && includesAnyOf(tags, hostTags)) { 203 | hosts.push(hostName); 204 | } 205 | } 206 | } 207 | 208 | // if hosts is still empty, get all hosts from config 209 | if (hosts.length === 0) { 210 | hosts = Object.keys(config.hosts); 211 | } 212 | 213 | hosts = [ ... new Set(hosts) ]; // take unique elements 214 | 215 | let reporterOptions = { 216 | coalesce: argv.coalesce, 217 | color: { error: 'magentaBright', header: 'grey', ...(argv.color || {}) }, 218 | showSource: argv.showSource, 219 | unanimous: argv.unanimous 220 | }; 221 | 222 | if (argv.noColors) { 223 | reporterOptions.color = false; 224 | } 225 | 226 | let invalidColor = Object.values(reporterOptions.color).find(color => !styles[color]); 227 | 228 | if (invalidColor) { 229 | console.log(`Invalid color or style: "${invalidColor}"\n`); 230 | console.log(`Choose from:\n${Object.keys(styles).map(style => `- ${style}\n`).join('')}`); 231 | process.exit(1); 232 | } 233 | 234 | let reporter; 235 | if (argv.table) { 236 | reporter = new TableReporter(reporterOptions); 237 | } else { 238 | reporter = new DefaultReporter(reporterOptions); 239 | } 240 | 241 | if (argv.list || argv.add || argv.edit || argv.delete || 242 | argv['delete-all'] || argv.configureEsvu || argv.configureJsvu) { 243 | const message = `Using config "${config.configPath}"`; 244 | console.log( 245 | argv.noColors ? message : chalk.grey(message) 246 | ); 247 | } 248 | // list available hosts 249 | if (argv.list) { 250 | hostManager.list(config); 251 | process.exit(0); 252 | } 253 | 254 | 255 | // add a host 256 | if (argv.add) { 257 | hostManager.add(config, argv.add[0], argv.add[1], argv._[0], argv.args, hostTags); 258 | console.log(`Host "${argv.add[0]}" added`); 259 | process.exit(0); 260 | } 261 | 262 | // edit a host 263 | if (argv.edit) { 264 | hostManager.edit(config, argv.edit, argv.args, hostTags); 265 | console.log(`Host "${argv.edit}" edited`); 266 | process.exit(0); 267 | } 268 | 269 | if (argv.args) { 270 | // at this point in execution, implies (argv.args && !(argv.add || argv.edit)) 271 | console.error('Use --args with --add or --edit'); 272 | process.exit(1); 273 | } 274 | 275 | // delete a host 276 | if (argv.delete) { 277 | hostManager.delete(config, argv.delete); 278 | console.log(`Host "${argv.delete}" deleted`); 279 | process.exit(0); 280 | } 281 | 282 | // delete all hosts 283 | if (argv['delete-all']) { 284 | hostManager.delete(config); 285 | process.exit(0); 286 | } 287 | 288 | if (argv.configureEsvu || argv.configureJsvu) { 289 | const vu = argv.configureEsvu 290 | ? 'esvu' 291 | : 'jsvu'; 292 | const vuRoot = path.join(argv[`${vu}-root`] || os.homedir(), `.${vu}`); 293 | const vuPrefix = argv[`${vu}-prefix`] || ''; 294 | hostManager.configureFromVersionUpdater(config, vu, vuRoot, vuPrefix); 295 | hostManager.list(config); 296 | process.exit(0); 297 | } 298 | 299 | let fileArg = argv._[0]; 300 | const raw = argv.raw; 301 | const attrs = { 302 | flags: { 303 | raw, 304 | } 305 | }; 306 | 307 | process.stdin.setEncoding('utf8'); 308 | 309 | if (fileArg) { 310 | const fileArgJoinPath = !path.isAbsolute(fileArg) 311 | ? process.cwd() 312 | : ''; 313 | 314 | const file = path.join(fileArgJoinPath, fileArg); 315 | const contents = fs.readFileSync(file, 'utf8'); 316 | const attrs = { 317 | flags: { 318 | raw, 319 | module: file.endsWith('.mjs') || argv.module 320 | } 321 | }; 322 | runInEachHost({file, contents, attrs}, hosts); 323 | } else { 324 | const file = path.join(uniqueTempDir(), generateTempFileName()); 325 | const dirname = path.dirname(file); 326 | 327 | try { 328 | fs.statSync(dirname); 329 | } catch (error) { 330 | if (error.code === 'ENOENT') { 331 | try { 332 | fs.mkdirSync(dirname); 333 | } catch ({}) { 334 | // suppressed? 335 | } 336 | } 337 | } 338 | 339 | if (argv.e != null) { 340 | let contents = `print(${argv.e})`; 341 | fs.writeFileSync(file, contents); 342 | runInEachHost({file, contents, attrs}, hosts); 343 | } else if (argv.x) { 344 | let contents = argv.x; 345 | fs.writeFileSync(file, contents); 346 | runInEachHost({file, contents, attrs}, hosts); 347 | } else { 348 | let contents = ''; 349 | 350 | console.log('(Press -D to execute)'); 351 | 352 | let rl = readline.createInterface({ 353 | input: process.stdin, 354 | output: process.stdout 355 | }); 356 | 357 | rl.prompt(); 358 | 359 | rl.on('line', line => { 360 | line = line.trim(); 361 | 362 | if (line === '' && contents === '') { 363 | yargv.showHelp(); 364 | process.exit(1); 365 | } else { 366 | contents += `${line}\n`; 367 | } 368 | }); 369 | 370 | rl.on('close', async () => { 371 | fs.writeFileSync(file, contents); 372 | await runInEachHost({file, contents, attrs}, hosts); 373 | }); 374 | } 375 | } 376 | 377 | async function runInHost(testable, host) { 378 | let runner; 379 | const { 380 | args: hostArguments, 381 | path: hostPath, 382 | type: hostType, 383 | } = host; 384 | const shortName = '$262'; 385 | await esh.createAgent(hostType, { 386 | hostArguments, 387 | hostPath, 388 | shortName, 389 | }).then(r => { 390 | runner = r; 391 | return runner.evalScript(testable, { 392 | async: argv.async || (testable.attrs && testable.attrs.flags && testable.attrs.flags.module), 393 | module: argv.module || (testable.attrs && testable.attrs.flags && testable.attrs.flags.module) 394 | }); 395 | }).then(result => { 396 | reporter.result(host, result); 397 | return runner.destroy(); 398 | }).catch(e => { 399 | const message = `Failure attempting to eval script in agent: ${e.stack}`; 400 | console.error( 401 | argv.noColors ? message : chalk.red(message) 402 | ); 403 | }); 404 | } 405 | 406 | async function runInEachHost(testable, hosts) { 407 | reporter.start(testable.contents); 408 | 409 | await Promise.all( 410 | hosts.map(name => { 411 | const host = config.hosts[name]; 412 | host.name = name; 413 | return runInHost(testable, host); 414 | }) 415 | ).then(() => reporter.end()); 416 | } 417 | 418 | 419 | function generateTempFileName() { 420 | const now = Date.now(); 421 | return `f-${now}-${process.pid}-${(Math.random() * 0x100000000 + 1).toString(36)}.js`; 422 | } 423 | -------------------------------------------------------------------------------- /test/bin/eshost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const cp = require('child_process'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const tokenize = require('yargs/lib/tokenize-arg-string'); 9 | 10 | const Config = require('../../lib/config'); 11 | 12 | // To avoid messing with a real configuration, use a test-local config 13 | Config.defaultConfigPath = path.join(__dirname, '../config/.eshost-config.json'); 14 | 15 | console.log("Engines installed"); 16 | console.log("--------------------------------------------------------"); 17 | console.log(process.env.ESHOST_ESVU_PATH); 18 | console.log(process.env.ESHOST_CHAKRA_PATH); 19 | console.log(process.env.ESHOST_ENGINE262_PATH); 20 | console.log(process.env.ESHOST_JSC_PATH); 21 | console.log(process.env.ESHOST_JS_PATH); 22 | console.log(process.env.ESHOST_NODE_PATH); 23 | console.log(process.env.ESHOST_V8_PATH); 24 | console.log(process.env.ESHOST_XS_PATH); 25 | console.log("--------------------------------------------------------"); 26 | 27 | const hosts = [ 28 | ['chakra', { hostPath: process.env.ESHOST_CHAKRA_PATH }], 29 | ['engine262', { hostPath: process.env.ESHOST_ENGINE262_PATH }], 30 | ['jsc', { hostPath: process.env.ESHOST_JSC_PATH }], 31 | ['sm', { hostPath: process.env.ESHOST_JS_PATH }], 32 | ['node', { hostPath: process.env.ESHOST_NODE_PATH }], 33 | ['v8', { hostPath: process.env.ESHOST_V8_PATH }], 34 | ['xs', { hostPath: process.env.ESHOST_XS_PATH }], 35 | ]; 36 | 37 | const hostsByType = hosts.reduce((accum, [type, config]) => { 38 | accum[type] = config; 39 | return accum; 40 | }, {}); 41 | 42 | function eshost(command = []) { 43 | let args = []; 44 | 45 | if (Array.isArray(command)) { 46 | args.push(...command); 47 | } else { 48 | if (typeof command === 'string') { 49 | args.push(...tokenize(command)); 50 | } 51 | } 52 | 53 | let spawnArgs = [ 54 | './bin/eshost.js', 55 | '--config', 56 | Config.defaultConfigPath, 57 | ].concat(args); 58 | 59 | return new Promise((resolve, reject) => { 60 | let cps = cp.spawn(process.execPath, spawnArgs); 61 | let stdout = ''; 62 | let stderr = ''; 63 | cps.stdout.on('data', buffer => { stdout += buffer; }); 64 | cps.stderr.on('data', buffer => { stderr += buffer; }); 65 | cps.on('close', () => { 66 | cps = null; 67 | resolve({ stderr, stdout }); 68 | }); 69 | 70 | cps.on('error', error => { 71 | reject(error); 72 | }); 73 | }); 74 | } 75 | 76 | function resetHostConfig() { 77 | fs.writeFileSync(Config.defaultConfigPath, '{"hosts":{}}'); 78 | } 79 | 80 | function toHostPath(type) { 81 | return path.normalize(hostsByType[type].hostPath); 82 | } 83 | 84 | beforeEach(() => resetHostConfig()); 85 | afterEach(() => resetHostConfig()); 86 | 87 | describe('eshost --help', () => { 88 | it('displays help contents', () => { 89 | return eshost('--help').then(result => { 90 | assert.equal(result.stderr, ''); 91 | 92 | assert(result.stdout.includes('Usage: eshost [options] [input-file]')); 93 | assert(result.stdout.includes('eshost [options] -e "input-script"')); 94 | assert(result.stdout.includes('eshost --list')); 95 | assert(result.stdout.includes('eshost --add [host name] [host type] --args ')); 96 | assert(result.stdout.includes('eshost --delete [host name]')); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('eshost --list', () => { 102 | it('displays "No configured hosts" when no hosts are configured', () => { 103 | return eshost('--list').then(result => { 104 | assert.equal(result.stderr, ''); 105 | assert(result.stdout.startsWith(`Using config "${Config.defaultConfigPath}"`)); 106 | assert(result.stdout.includes('No configured hosts')); 107 | }); 108 | }); 109 | 110 | it('displays host table when there is one host configured', () => { 111 | fs.writeFileSync(Config.defaultConfigPath, JSON.stringify({ 112 | hosts: { 113 | js: { 114 | type: 'sm', 115 | path: toHostPath('sm') 116 | } 117 | } 118 | })); 119 | 120 | return eshost('--list').then(result => { 121 | assert.equal(result.stderr, ''); 122 | /* 123 | │ js │ sm │ /path/to/js │ │ │ 124 | */ 125 | assert(result.stdout.startsWith(`Using config "${Config.defaultConfigPath}"`)); 126 | assert(/\bjs\b/m.test(result.stdout)); 127 | assert(/\bsm\b/m.test(result.stdout)); 128 | assert(result.stdout.includes(toHostPath('sm'))); 129 | }); 130 | }); 131 | 132 | it('displays host table when there is more than one host configured', () => { 133 | fs.writeFileSync(Config.defaultConfigPath, JSON.stringify({ 134 | hosts: { 135 | js: { 136 | type: 'sm', 137 | path: toHostPath('sm') 138 | }, 139 | node: { 140 | type: 'node', 141 | path: toHostPath('node') 142 | } 143 | } 144 | })); 145 | 146 | return eshost('--list').then(result => { 147 | assert.equal(result.stderr, ''); 148 | /* 149 | │ js │ sm │ /path/to/js │ │ │ 150 | ├──────┼─────────┼───────────────┼──────┼──────┤ 151 | │ node │ node │ /path/to/node │ │ │ 152 | */ 153 | assert(result.stdout.startsWith(`Using config "${Config.defaultConfigPath}"`)); 154 | assert(/\bjs\b/m.test(result.stdout)); 155 | assert(/\bsm\b/m.test(result.stdout)); 156 | assert(result.stdout.includes(toHostPath('sm'))); 157 | assert(/\bnode\b/m.test(result.stdout)); 158 | assert(result.stdout.includes(toHostPath('node'))); 159 | }); 160 | }); 161 | }); 162 | 163 | describe('eshost --add', () => { 164 | it('allows adding a valid host', () => { 165 | return eshost('--add node node /path/to/node').then(result => { 166 | assert.equal(result.stderr, ''); 167 | assert(result.stdout.startsWith(`Using config "${Config.defaultConfigPath}"`)); 168 | assert(result.stdout.includes('Host "node" added')); 169 | }); 170 | }); 171 | 172 | it('disallows adding an invalid host', () => { 173 | return eshost('--add invalid invalid /path/to/invalid').then(result => { 174 | assert.equal(result.stderr, ''); 175 | assert(result.stdout.startsWith(`Using config "${Config.defaultConfigPath}"`)); 176 | assert(result.stdout.includes('Host type "invalid" not supported')); 177 | }); 178 | }); 179 | 180 | it('allows adding a valid host with --args', () => { 181 | let add = '--add ch ch /path/to/ch --args "-Intl-"'; 182 | return eshost(add).then(result => { 183 | assert.equal(result.stderr, ''); 184 | 185 | if (result.stdout.includes('Host "ch" added')) { 186 | return eshost('--list').then(result => { 187 | assert.equal(result.stderr, ''); 188 | /* 189 | │ ch │ ch │ /path/to/ch │ -Intl- │ │ 190 | */ 191 | assert(/-Intl-/m.test(result.stdout)); 192 | }); 193 | } else { 194 | return Promise.reject(`'${add}' failed`); 195 | } 196 | }); 197 | }); 198 | 199 | it('allows adding a valid host with --tags (1)', () => { 200 | let add = '--add ch ch /path/to/ch --tags latest'; 201 | return eshost(add).then(result => { 202 | assert.equal(result.stderr, ''); 203 | 204 | if (result.stdout.includes('Host "ch" added')) { 205 | return eshost('--list').then(result => { 206 | assert.equal(result.stderr, ''); 207 | /* 208 | │ ch │ ch │ /usr/local/bin/ch │ │ latest │ 209 | */ 210 | assert(/\blatest\b/m.test(result.stdout)); 211 | }); 212 | } else { 213 | return Promise.reject(`'${add}' failed`); 214 | } 215 | }); 216 | }); 217 | 218 | it('allows adding a valid host with --tags (>1)', () => { 219 | let add = '--add ch ch /path/to/ch --tags latest,greatest'; 220 | return eshost(add).then(result => { 221 | assert.equal(result.stderr, ''); 222 | 223 | if (result.stdout.includes('Host "ch" added')) { 224 | return eshost('--list').then(result => { 225 | assert.equal(result.stderr, ''); 226 | /* 227 | │ ch │ ch │ /usr/local/bin/ch │ │ latest,greatest │ 228 | */ 229 | assert(result.stdout.includes('latest,greatest')); 230 | }); 231 | } else { 232 | return Promise.reject(`'${add}' failed`); 233 | } 234 | }); 235 | }); 236 | }); 237 | 238 | describe('eshost --delete', () => { 239 | it('allows deleting a host', () => { 240 | fs.writeFileSync(Config.defaultConfigPath, JSON.stringify({ 241 | hosts: { 242 | js: { 243 | type: 'sm', 244 | path: toHostPath('sm') 245 | }, 246 | node: { 247 | type: 'node', 248 | path: toHostPath('node') 249 | } 250 | } 251 | })); 252 | 253 | return eshost('--delete node').then(result => { 254 | assert.equal(result.stderr, ''); 255 | assert(result.stdout.startsWith(`Using config "${Config.defaultConfigPath}"`)); 256 | assert(result.stdout.includes('Host "node" deleted')); 257 | }); 258 | }); 259 | }); 260 | 261 | describe('eshost --eval', () => { 262 | beforeEach(() => { 263 | fs.writeFileSync(Config.defaultConfigPath, JSON.stringify({ 264 | hosts: { 265 | node: { 266 | type: 'node', 267 | path: toHostPath('node'), 268 | args: '--harmony' 269 | }, 270 | v8: { 271 | type: 'v8', 272 | path: toHostPath('v8'), 273 | tags: [ 274 | 'latest', 275 | 'greatest' 276 | ] 277 | } 278 | } 279 | })); 280 | }); 281 | 282 | describe('(default)', () => { 283 | it('evaluates code and displays the result for all hosts', () => { 284 | return eshost('--eval " 1 + 1 "').then(result => { 285 | assert.equal(result.stderr, ''); 286 | assert(/#### v8\n2/m.test(result.stdout)); 287 | assert(/#### node\n2/m.test(result.stdout)); 288 | }); 289 | }); 290 | 291 | it('evaluates code and displays the result for a specific host', () => { 292 | return eshost('--eval " 1 + 1 " --host v8').then(result => { 293 | assert.equal(result.stderr, ''); 294 | assert(/#### v8\n2/m.test(result.stdout)); 295 | assert(!/#### node\n2/m.test(result.stdout)); 296 | }); 297 | }); 298 | 299 | it('evaluates code and displays the result for a specific host group', () => { 300 | return eshost('--eval " 1 + 1 " --hostGroup v8,node').then(result => { 301 | assert.equal(result.stderr, ''); 302 | assert(/#### v8\n2/m.test(result.stdout)); 303 | assert(/#### node\n2/m.test(result.stdout)); 304 | }); 305 | }); 306 | 307 | it('evaluates code and displays the result for a specific tag', () => { 308 | return eshost('--eval " 1 + 1 " --tags latest').then(result => { 309 | assert.equal(result.stderr, ''); 310 | assert(/#### v8\n2/m.test(result.stdout)); 311 | assert(!/#### node\n2/m.test(result.stdout)); 312 | }); 313 | }); 314 | 315 | it('evaluates code and displays the result for specific tags', () => { 316 | return eshost('--eval " 1 + 1 " --tags latest,greatest').then(result => { 317 | assert.equal(result.stderr, ''); 318 | assert(/#### v8\n2/m.test(result.stdout)); 319 | assert(!/#### node\n2/m.test(result.stdout)); 320 | }); 321 | }); 322 | }); 323 | 324 | describe('(deduping)', () => { 325 | it('dedupes hosts, for a specific host list', () => { 326 | return eshost('--eval " 1 + 1 " --host v8,v8,node,node').then(result => { 327 | assert.equal(result.stderr, ''); 328 | assert.equal(result.stdout.match(/#### v8\n2/gm).length, 1); 329 | assert.equal(result.stdout.match(/#### node\n2/gm).length, 1); 330 | }); 331 | }); 332 | 333 | it('dedupes hosts, for a specific host group list', () => { 334 | return eshost('--eval " 1 + 1 " --hostGroup v8,v8,node,node').then(result => { 335 | assert.equal(result.stderr, ''); 336 | assert.equal(result.stdout.match(/#### v8\n2/gm).length, 1); 337 | assert.equal(result.stdout.match(/#### node\n2/gm).length, 1); 338 | }); 339 | }); 340 | }); 341 | 342 | describe('--table', () => { 343 | it('evaluates code and displays the result for all hosts', () => { 344 | return eshost('--table --eval " 1 + 1 "').then(result => { 345 | assert.equal(result.stderr, ''); 346 | assert(/\│.+v8.+2.+\│/.test(result.stdout)); 347 | assert(/\│.+node.+2.+\│/.test(result.stdout)); 348 | }); 349 | }); 350 | 351 | it('evaluates code and displays the result for a specific host', () => { 352 | return eshost('--table --eval " 1 + 1 " --host v8').then(result => { 353 | assert.equal(result.stderr, ''); 354 | assert(/\│.+v8.+2.+\│/.test(result.stdout)); 355 | assert(!/\│.+node.+2.+\│/.test(result.stdout)); 356 | }); 357 | }); 358 | 359 | it('evaluates code and displays the result for a specific host group', () => { 360 | return eshost('--table --eval " 1 + 1 " --hostGroup v8,node').then(result => { 361 | assert.equal(result.stderr, ''); 362 | assert(/\│.+v8.+2.+\│/.test(result.stdout)); 363 | assert(/\│.+node.+2.+\│/.test(result.stdout)); 364 | }); 365 | }); 366 | 367 | it('evaluates code and displays the result for a specific tag', () => { 368 | return eshost('--table --eval " 1 + 1 " --tags latest').then(result => { 369 | assert.equal(result.stderr, ''); 370 | assert(/\│.+v8.+2.+\│/.test(result.stdout)); 371 | assert(!/\│.+node.+2.+\│/.test(result.stdout)); 372 | }); 373 | }); 374 | 375 | it('evaluates code and displays the result for specific tags', () => { 376 | return eshost('--table --eval " 1 + 1 " --tags latest,greatest').then(result => { 377 | assert.equal(result.stderr, ''); 378 | assert(/v8/.test(result.stdout)); 379 | assert(!/\│.+node.+2.+\│/.test(result.stdout)); 380 | }); 381 | }); 382 | }); 383 | }); 384 | 385 | describe('eshost --unanimous --eval', () => { 386 | beforeEach(() => { 387 | fs.writeFileSync(Config.defaultConfigPath, JSON.stringify({ 388 | hosts: { 389 | ch: { 390 | type: 'chakra', 391 | path: toHostPath('chakra'), 392 | }, 393 | js: { 394 | type: 'sm', 395 | path: toHostPath('sm'), 396 | } 397 | } 398 | })); 399 | }); 400 | 401 | describe('(default)', () => { 402 | it('displays nothing when all results are unanimous', () => { 403 | return eshost('--unanimous --eval " 1 + 1 "').then(result => { 404 | assert.equal(result.stderr, ''); 405 | assert.equal(result.stdout.trim().length, 0); 406 | }); 407 | }); 408 | 409 | it('displays results when results are varied', () => { 410 | // Using "gc" because that's guaranteed to be present in sm by default 411 | // and guaranteed to be absent from chakra by default. 412 | return eshost('--unanimous --eval "typeof gc"').then(result => { 413 | assert.equal(result.stderr, ''); 414 | assert(/#### js\nfunction/.test(result.stdout)); 415 | assert(/#### ch\nundefined/.test(result.stdout)); 416 | }); 417 | }); 418 | }); 419 | 420 | describe('--table', () => { 421 | it('displays nothing when all results are unanimous', () => { 422 | return eshost('--unanimous --table --eval " 1 + 1 "').then(result => { 423 | assert.equal(result.stderr, ''); 424 | assert.equal(result.stdout.trim().length, 0); 425 | }); 426 | }); 427 | 428 | it('displays results when results are varied', () => { 429 | // Using "gc" because that's guaranteed to be present in sm by default 430 | // and guaranteed to be absent from chakra by default. 431 | return eshost('--unanimous --table --eval "typeof gc"').then(result => { 432 | assert.equal(result.stderr, ''); 433 | assert(/ch.+undefined/.test(result.stdout)); 434 | assert(/js.+function/.test(result.stdout)); 435 | }); 436 | }); 437 | }); 438 | }); 439 | 440 | describe('eshost [input-file]', () => { 441 | beforeEach(() => { 442 | fs.writeFileSync(Config.defaultConfigPath, JSON.stringify({ 443 | hosts: { 444 | node: { 445 | type: 'node', 446 | path: toHostPath('node'), 447 | }, 448 | engine262: { 449 | type: 'engine262', 450 | path: toHostPath('engine262'), 451 | tags: [ 452 | 'latest', 453 | 'greatest' 454 | ] 455 | } 456 | } 457 | })); 458 | }); 459 | 460 | describe('script.js', () => { 461 | it('evaluates code and displays the result for all hosts', () => { 462 | return eshost('test/bin/fixtures/script.js').then(result => { 463 | assert.equal(result.stderr, ''); 464 | assert(/#### node/m.test(result.stdout)); 465 | assert(/#### engine262/m.test(result.stdout)); 466 | }); 467 | }); 468 | 469 | it('evaluates code and displays the result for a specific host', () => { 470 | return eshost('--host engine262 test/bin/fixtures/script.js').then(result => { 471 | assert.equal(result.stderr, ''); 472 | assert(/#### engine262\ntrue/m.test(result.stdout)); 473 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 474 | }); 475 | }); 476 | 477 | it('evaluates code and displays the result for a specific host group', () => { 478 | return eshost('--hostGroup engine262 test/bin/fixtures/script.js').then(result => { 479 | assert.equal(result.stderr, ''); 480 | assert(/#### engine262\ntrue/m.test(result.stdout)); 481 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 482 | }); 483 | }); 484 | 485 | it('evaluates code and displays the result for a specific tag', () => { 486 | return eshost('--tags latest test/bin/fixtures/script.js').then(result => { 487 | assert.equal(result.stderr, ''); 488 | assert(/#### engine262\ntrue/m.test(result.stdout)); 489 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 490 | }); 491 | }); 492 | 493 | it('evaluates code and displays the result for specific tags', () => { 494 | return eshost('--tags latest,greatest test/bin/fixtures/script.js').then(result => { 495 | assert.equal(result.stderr, ''); 496 | assert(/#### engine262\ntrue/m.test(result.stdout)); 497 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 498 | }); 499 | }); 500 | }); 501 | 502 | describe('absolute path to script.js', () => { 503 | it('evaluates code and displays the result for all hosts', () => { 504 | return eshost(`${__dirname}/fixtures/script.js`).then(result => { 505 | assert.equal(result.stderr, ''); 506 | assert(/#### node/m.test(result.stdout)); 507 | assert(/#### engine262/m.test(result.stdout)); 508 | }); 509 | }); 510 | 511 | it('evaluates code and displays the result for a specific host', () => { 512 | return eshost(`--host engine262 ${__dirname}/fixtures/script.js`).then(result => { 513 | assert.equal(result.stderr, ''); 514 | assert(/#### engine262\ntrue/m.test(result.stdout)); 515 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 516 | }); 517 | }); 518 | 519 | it('evaluates code and displays the result for a specific host group', () => { 520 | return eshost(`--hostGroup engine262 ${__dirname}/fixtures/script.js`).then(result => { 521 | assert.equal(result.stderr, ''); 522 | assert(/#### engine262\ntrue/m.test(result.stdout)); 523 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 524 | }); 525 | }); 526 | 527 | it('evaluates code and displays the result for a specific tag', () => { 528 | return eshost(`--tags latest ${__dirname}/fixtures/script.js`).then(result => { 529 | assert.equal(result.stderr, ''); 530 | assert(/#### engine262\ntrue/m.test(result.stdout)); 531 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 532 | }); 533 | }); 534 | 535 | it('evaluates code and displays the result for specific tags', () => { 536 | return eshost(`--tags latest,greatest ${__dirname}/fixtures/script.js`).then(result => { 537 | assert.equal(result.stderr, ''); 538 | assert(/#### engine262\ntrue/m.test(result.stdout)); 539 | assert(!/#### node\ntrue\n\n/m.test(result.stdout)); 540 | }); 541 | }); 542 | }); 543 | 544 | describe('module.mjs', () => { 545 | it('evaluates code and displays the result for all hosts', () => { 546 | return eshost('test/bin/fixtures/module.mjs').then(result => { 547 | assert(/#### node/m.test(result.stdout)); 548 | assert(/#### engine262/m.test(result.stdout)); 549 | }); 550 | }); 551 | 552 | it('evaluates code and displays the result for a specific host', () => { 553 | return eshost('--host engine262 test/bin/fixtures/module.mjs').then(result => { 554 | assert.equal(result.stderr, ''); 555 | assert(/#### engine262\ntrue/m.test(result.stdout)); 556 | }); 557 | }); 558 | 559 | it('evaluates code and displays the result for a specific host group', () => { 560 | return eshost('--hostGroup engine262 test/bin/fixtures/module.mjs').then(result => { 561 | assert.equal(result.stderr, ''); 562 | assert(/#### engine262\ntrue/m.test(result.stdout)); 563 | }); 564 | }); 565 | 566 | it('evaluates code and displays the result for a specific tag', () => { 567 | return eshost('--tags latest test/bin/fixtures/module.mjs').then(result => { 568 | assert.equal(result.stderr, ''); 569 | assert(/#### engine262\ntrue/m.test(result.stdout)); 570 | }); 571 | }); 572 | 573 | it('evaluates code and displays the result for specific tags', () => { 574 | return eshost('--tags latest,greatest test/bin/fixtures/module.mjs').then(result => { 575 | assert.equal(result.stderr, ''); 576 | assert(/#### engine262\ntrue/m.test(result.stdout)); 577 | }); 578 | }); 579 | }); 580 | 581 | describe('module.js', () => { 582 | it('evaluates code and displays the result for all hosts', () => { 583 | return eshost('-m test/bin/fixtures/module.js').then(result => { 584 | assert.equal(result.stderr, ''); 585 | assert(/#### node/m.test(result.stdout)); 586 | assert(/#### engine262/m.test(result.stdout)); 587 | }); 588 | }); 589 | 590 | it('evaluates code and displays the result for a specific host', () => { 591 | return eshost('-m --host engine262 test/bin/fixtures/module.js').then(result => { 592 | assert.equal(result.stderr, ''); 593 | assert(/#### engine262\ntrue/m.test(result.stdout)); 594 | }); 595 | }); 596 | 597 | it('evaluates code and displays the result for a specific host group', () => { 598 | return eshost('-m --hostGroup engine262 test/bin/fixtures/module.js').then(result => { 599 | assert.equal(result.stderr, ''); 600 | assert(/#### engine262\ntrue/m.test(result.stdout)); 601 | }); 602 | }); 603 | 604 | it('evaluates code and displays the result for a specific tag', () => { 605 | return eshost('-m --tags latest test/bin/fixtures/module.js').then(result => { 606 | assert.equal(result.stderr, ''); 607 | assert(/#### engine262\ntrue/m.test(result.stdout)); 608 | }); 609 | }); 610 | 611 | it('evaluates code and displays the result for specific tags', () => { 612 | return eshost('-m --tags latest,greatest test/bin/fixtures/module.js').then(result => { 613 | assert.equal(result.stderr, ''); 614 | assert(/#### engine262\ntrue/m.test(result.stdout)); 615 | }); 616 | }); 617 | }); 618 | 619 | describe('--table', () => { 620 | it('evaluates code and displays the result for all hosts', () => { 621 | return eshost('--table test/bin/fixtures/module.mjs').then(result => { 622 | assert.equal(result.stderr, ''); 623 | assert(/\│.+engine262.+\│/.test(result.stdout)); 624 | assert(/\│.+node.+\│/.test(result.stdout)); 625 | }); 626 | }); 627 | 628 | it('evaluates code and displays the result for a specific host group', () => { 629 | return eshost('--table test/bin/fixtures/module.mjs --hostGroup engine262,node').then(result => { 630 | assert.equal(result.stderr, ''); 631 | assert(/\│.+engine262.+\│/.test(result.stdout)); 632 | assert(/\│.+node.+\│/.test(result.stdout)); 633 | }); 634 | }); 635 | 636 | it('evaluates code and displays the result for a specific tag', () => { 637 | return eshost('--table test/bin/fixtures/module.mjs --tags latest').then(result => { 638 | assert.equal(result.stderr, ''); 639 | assert(/\│.+engine262.+\│/.test(result.stdout)); 640 | assert(!/\│.+node.+\│/.test(result.stdout)); 641 | }); 642 | }); 643 | 644 | it('evaluates code and displays the result for specific tags', () => { 645 | return eshost('--table test/bin/fixtures/module.mjs --tags latest,greatest').then(result => { 646 | assert.equal(result.stderr, ''); 647 | assert(/engine262/.test(result.stdout)); 648 | assert(!/\│.+node.+\│/.test(result.stdout)); 649 | }); 650 | }); 651 | }); 652 | }); 653 | --------------------------------------------------------------------------------