#!/usr/bin/env node
// default to no color if requested via standard environment var
if (process.env.NO_COLOR === '1') {
process.env.TAP_COLORS = '0'
process.env.FORCE_COLOR = '0'
}
const signalExit = require('signal-exit')
const opener = require('opener')
const node = process.execPath
const fs = require('fs')
const fg = require('foreground-child')
const {spawn, spawnSync} = require('child_process')
const nycBin = require.resolve(
'nyc/' + require('nyc/package.json').bin.nyc
)
const glob = require('glob')
const isexe = require('isexe')
const yaml = require('tap-yaml')
const path = require('path')
const exists = require('fs-exists-cached').sync
const maybeResolve = id => {
try {
return require.resolve(id)
} catch (er) {}
}
const tsNode = maybeResolve('ts-node/register')
const flowNode = maybeResolve('flow-remove-types/register')
const jsx = require.resolve('./jsx.js')
const coverallsBin = maybeResolve('coveralls/bin/coveralls.js')
const which = require('which')
const {ProcessDB} = require('istanbul-lib-processinfo')
const rimraf = require('rimraf').sync
const {Repl} = require('../lib/repl.js')
/* istanbul ignore next */
const debug = process.env.TAP_DEBUG === '1'
|| /\btap\b/.test(process.env.NODE_DEBUG) ? (...args) => {
const {format} = require('util')
const prefix = `TAP ${process.pid} RUN: `
const msg = format(...args).trim()
console.error(prefix + msg.split('\n').join(`\n${prefix}`))
} : () => {}
const filesFromTest = exports.filesFromTest = (index, testFile) => {
const set = index.externalIds[testFile]
return !set ? null
: Object.keys(index.files).filter(file =>
index.files[file].includes(set.root) ||
set.children.some(c => index.files[file].includes(c)))
}
// returns a function that tells whether a given file should be run,
// because one or more of its deps have changed.
const getChangedFilter = exports.getChangedFilter = options => {
if (!options.changed)
return () => true
if (!options.coverage)
throw new Error('--changed requires coverage to be enabled')
const indexFile = '.nyc_output/processinfo/index.json'
if (!fs.existsSync(indexFile))
return () => true
const indexDate = fs.statSync(indexFile).mtime
const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'))
return testFile => {
if (fs.statSync(testFile).mtime > indexDate)
return true
const files = filesFromTest(index, testFile)
// if not found, probably a test file not run last time, so run it
if (!files)
return true
// if the file is gone, that's a pretty big change.
return files.some(f => !fs.existsSync(f) || fs.statSync(f).mtime > indexDate)
}
}
const defaultFiles = options => new Promise((res, rej) => {
debug('try to get default files')
const findit = require('findit')
const good = strToRegExp(options['test-regex'])
const bad = strToRegExp(options['test-ignore'])
const fileFilter = f => {
f = f.replace(/\\/g, '/')
debug('fileFilter', f)
const parts = f.split('/')
const include = good.test(f) &&
!bad.test(f) &&
!parts.includes('node_modules') &&
!parts.includes('tap-snapshots') &&
!parts.includes('fixtures') &&
!parts.includes('.git') &&
!parts.includes('.hg') &&
!parts.some(p => /^tap-testdir-/.test(p))
debug('include?', f, include)
return include
}
const addFile = files => f => fileFilter(f) && files.push(f)
// search in any folder that isn't node_modules, .git, or tap generated
// these can get pretty huge, so just walking them at all is costly
const entryFilter =
/^((node_modules|tap-snapshots|.git|.hg|fixtures)$|tap-testdir-)/
fs.readdir(process.cwd(), (er, entries) => {
debug('readdir cwd', er, entries)
Promise.all(entries.filter(entry => !entryFilter.test(entry))
.map(entry => new Promise((res, rej) => {
fs.lstat(entry, (er, stat) => {
debug('lstat', entry, er, stat)
// It's pretty unusual to have a file in cwd you can't even stat
/* istanbul ignore next */
if (er)
return rej(er)
if (stat.isFile())
return res(fileFilter(entry) ? [entry] : [])
if (!stat.isDirectory())
return res([])
const finder = findit(entry)
const files = []
finder.on('file', addFile(files))
finder.on('end', () => res(files))
finder.on('error', /* istanbul ignore next */ er => rej(er))
})
}))).then(a => res(a.reduce((a, i) => a.concat(i), []))).catch(rej)
})
})
const main = options =>
mainAsync(options).catch(er => onError(er))
const mainAsync = async options => {
debug('main', options)
if (require.main !== module)
return debug('not main module, do not run tests')
const rc = parseRcFile(options.rcfile)
debug('rc options', rc)
options._.update(rc)
const pj = parsePackageJson()
debug('package.json options', pj)
options._.update(pj)
if (options.files.length && !options._.length)
options._.push(...options.files)
// tell chalk if we want color or not.
if (!options.color) {
process.env.NO_COLOR = '1'
delete process.env.FORCE_COLOR
} else {
delete process.env.NO_COLOR
process.env.FORCE_COLOR = '3'
}
if (options.reporter === null)
options.reporter = options.color ? 'base' : 'tap'
if (options['dump-config']) {
console.log(yaml.stringify(Object.keys(options).filter(k =>
k !== 'dump-config' && k !== '_' && !/^[A-Z_]+$/.test(k)
).sort().reduce((set, k) =>
(set[k] = options[k], set), {})))
return
}
if (options.versions) {
const {libtap, tapParser, tapYaml, tcompare} = require('libtap/versions')
return console.log(yaml.stringify({
tap: require('../package.json').version,
libtap,
'tap-parser': tapParser,
nyc: require('nyc/package.json').version,
'tap-yaml': tapYaml,
tcompare
}))
}
if (options.version)
return console.log(require('../package.json').version)
if (options['parser-version'])
return console.log(require('libtap/versions').tapParser)
if (options['nyc-version'])
return console.log(require('nyc/package.json').version)
if (options['nyc-help'])
return nycHelp()
process.stdout.on('error', er => {
/* istanbul ignore else */
if (er.code === 'EPIPE')
process.exit()
else
throw er
})
if (options['libtap-settings'])
process.env.TAP_LIBTAP_SETTINGS = path.resolve(options['libtap-settings'])
require('../settings.js')
// we test this directly, not from here.
/* istanbul ignore next */
if (options.watch)
return new Repl(options, process.stdin, process.stdout)
options.grep = options.grep.map(strToRegExp)
// this is only testable by escaping from the covered environment
/* istanbul ignore next */
if (fs.existsSync('.nyc_output') &&
options._.length === 0 &&
(options['coverage-report'] &&
options._.explicit.has('coverage-report') ||
options['check-coverage'] &&
options._.explicit.has('check-coverage')))
return runCoverageReportOnly(options)
try {
debug('try to get default files?', options._.length === 0)
if (options._.length === 0)
options._.push.apply(options._, await defaultFiles(options))
debug('added default files', options._)
} /* istanbul ignore next */ catch (er) /* istanbul ignore next */ {
// This gets tested on Mac, but not Linux/travis, and is
// somewhat challenging to do in a cross-platform way.
/* istanbul ignore next */
return require('../lib/tap.js').threw(er)
}
options.files = globFiles(options._)
debug('after globbing', options.files)
if (options['output-dir'] !== null)
require('../settings.js').mkdirRecursiveSync(options['output-dir'])
if (options.files.length === 1 && options.files[0] === '-') {
debug('do stdin only')
setupTapEnv(options)
stdinOnly(options)
return
}
options.saved = readSaveFile(options)
options.changedFilter = getChangedFilter(options)
/* istanbul ignore next */
if (options.coverage && !process.env.NYC_CONFIG)
respawnWithCoverage(options)
else {
setupTapEnv(options)
runTests(options)
}
}
/* istanbul ignore next */
const nycReporter = options =>
options['coverage-report'] === false ? ['--silent']
: options['coverage-report'].map(cr =>
cr === 'html' ? '--reporter=lcov' : `--reporter=${cr}`)
const defaultNycExcludes = [
'coverage/**',
'packages/*/test/**',
'test/**',
'test{,-*}.js',
'**/*{.,-}test.js',
'**/__tests__/**',
'**/{ava,babel,jest,nyc,rollup,webpack}.config.js',
]
/* istanbul ignore next */
const runNyc = (cmd, programArgs, options, spawnOpts) => {
const reporter = nycReporter(options)
// note: these are forced to be numeric in the option parsing phase
const branches = Math.min(options.branches, 100)
const lines = Math.min(options.lines, 100)
const functions = Math.min(options.functions, 100)
const statements = Math.min(options.statements, 100)
const excludes = defaultNycExcludes.concat(options.files).map(f =>
'--exclude=' + f)
if (options.before)
excludes.push('--exclude=' + options.before)
if (options.after)
excludes.push('--exclude=' + options.after)
const args = [
nycBin,
...cmd,
...(options['show-process-tree'] ? ['--show-process-tree'] : []),
...excludes,
'--produce-source-map',
'--cache=true',
'--branches=' + branches,
'--watermarks.branches=' + branches,
'--watermarks.branches=' + (branches + (100 - branches)/2),
'--functions=' + functions,
'--watermarks.functions=' + functions,
'--watermarks.functions=' + (functions + (100 - functions)/2),
'--lines=' + lines,
'--watermarks.lines=' + lines,
'--watermarks.lines=' + (lines + (100 - lines)/2),
'--statements=' + statements,
'--watermarks.statements=' + statements,
'--watermarks.statements=' + (statements + (100 - statements)/2),
...reporter,
'--extension=.js',
'--extension=.jsx',
'--extension=.mjs',
'--extension=.cjs',
'--extension=.ts',
'--extension=.tsx',
...(options['nyc-arg'] || []),
]
if (options['check-coverage'])
args.push('--check-coverage')
args.push.apply(args, programArgs)
if (spawnOpts)
return fg(node, args, spawnOpts)
// fake it
process.argv = [
node,
...args,
]
require(nycBin)
if (reporter.includes('--reporter=lcov') && options.browser)
process.on('exit', () => openHtmlCoverageReport(options))
}
/* istanbul ignore next */
const runCoverageReportOnly = options => {
runNyc(['report'], [], options)
if (process.env.COVERALLS_REPO_TOKEN ||
process.env.__TAP_COVERALLS_TEST__) {
return pipeToCoveralls()
}
}
/* istanbul ignore next */
const pipeToCoveralls = async () => {
if (!coverallsBin) {
return
}
const reporter = spawn(node, [nycBin, 'report', '--reporter=text-lcov'], {
stdio: [ 0, 'pipe', 2 ]
})
const bin = process.env.__TAP_COVERALLS_TEST__
|| require.resolve('coveralls/bin/coveralls.js')
const ca = spawn(node, [bin], { stdio: ['pipe', 1, 2] })
reporter.stdout.pipe(ca.stdin)
await new Promise(resolve => ca.on('close', resolve))
}
/* istanbul ignore next */
const respawnWithCoverage = options => {
debug('respawn with coverage')
// If we have a coverage map, then include nothing by default here.
runNyc(options['coverage-map'] ? [
'--include=',
'--no-exclude-after-remap',
...(options.saved || options.changed ? ['--no-clean'] : []),
] : [], [
'--',
node,
...process.execArgv,
...process.argv.slice(1)
], options)
}
/* istanbul ignore next */
const openHtmlCoverageReport = (options, code, signal) => {
opener('coverage/lcov-report/index.html')
if (signal) {
setTimeout(() => {}, 200)
process.kill(process.pid, signal)
} else if (code) {
process.exitCode = code
}
}
const nycHelp = () => fg(node, [nycBin, '--help'])
// export for easier testing
const setupTapEnv = exports.setupTapEnv = options => {
process.env.TAP_TIMEOUT = options.timeout
if (options.color)
process.env.TAP_COLORS = '1'
else
process.env.TAP_COLORS = '0'
if (options.snapshot)
process.env.TAP_SNAPSHOT = '1'
if (options.bail)
process.env.TAP_BAIL = '1'
if (options['save-fixture'])
process.env.TAP_SAVE_FIXTURE = '1'
if (options.invert)
process.env.TAP_GREP_INVERT = '1'
if (options.grep.length)
process.env.TAP_GREP = options.grep.map(p => p.toString())
.join('\n')
if (options.only)
process.env.TAP_ONLY = '1'
}
const globFiles = files => Array.from(files.reduce((acc, f) =>
acc.concat(f === '-' ? f : glob.sync(f, { nonull: true })), [])
.reduce((set, f) => {
set.add(f)
return set
}, new Set()))
// caller-callsite requires that the call stack includes at least one
// spot where "this" is defined, so we bind this function to an object
// in this completely weird way, or else import-jsx doesn't work.
const makeReporter = exports.makeReporter = function (tap, options) {
//const treportTypes = require('treport/types')
const tapMochaReporter = require('tap-mocha-reporter')
// if it's a treport type, use that
const reporter = options.reporter
if (reporter === 'tap')
tap.pipe(process.stdout)
//else if (treportTypes.includes(reporter))
//require('treport')(tap, reporter)
else if (tapMochaReporter.types.includes(reporter))
tap.pipe(new tapMochaReporter(options.reporter))
else {
// might be a child process or a module
try {
which.sync(reporter)
// it's a cli reporter!
const c = spawn(reporter, options['reporter-arg'], {
stdio: ['pipe', 1, 2]
})
tap.pipe(c.stdin)
} catch (_) {
// resolve to cwd if it's a relative path
const rmod = /^\.\.?[\\\/]/.test(reporter) ? path.resolve(reporter) : reporter
// it'll often be jsx, and this is harmless if it isn't.
const R = require('@isaacs/import-jsx')(rmod)
if (typeof R !== 'function' || !R.prototype)
throw new Error(
`Invalid reporter: non-class exported by ${reporter}`)
else if (R.prototype.isReactComponent)
require('treport')(tap, R)
else if (R.prototype.write && R.prototype.end)
tap.pipe(new R(...(options['reporter-arg'])))
else
throw new Error(
`Invalid reporter: not a stream or react component ${reporter}`)
}
}
}.bind({})
const stdinOnly = options => {
// if we didn't specify any files, then just passthrough
// to the reporter, so we don't get '/dev/stdin' in the suite list.
// We have to pause() before piping to switch streams2 into old-mode
const tap = require('../lib/tap.js')
tap.writeSnapshot = false
tap.stdinOnly()
makeReporter(tap, options)
if (options['output-file'] !== null)
process.stdin.pipe(fs.createWriteStream(options['output-file']))
if (options['output-dir'] !== null)
process.stdin.pipe(fs.createWriteStream(options['output-dir'] +
'/stdin.tap'))
process.stdin.resume()
}
const readSaveFile = options => {
if (options.save)
try {
const s = fs.readFileSync(options.save, 'utf8').trim()
if (s)
return s.split('\n')
} catch (er) {}
return null
}
const saveFails = (options, tap) => {
if (!options.save)
return
let fails = []
const successes = []
tap.on('result', res => {
// we will continue to re-run todo tests, even though they're
// not technically "failures".
if (!res.ok && !res.extra.skip)
fails.push(res.extra.file)
else
successes.push(res.extra.file)
})
const save = () => {
fails = fails.reduce((set, f) => {
f = f.replace(/\\/g, '/')
/* istanbul ignore else */
if (set.indexOf(f) === -1)
set.push(f)
return set
}, [])
if (!fails.length)
rimraf(options.save)
else
try {
fs.writeFileSync(options.save, fails.join('\n') + '\n')
} catch (er) {}
}
tap.on('bailout', () => {
// add any pending test files to the fails list.
fails.push.apply(fails, options.files.filter(file =>
successes.indexOf(file) === -1))
save()
})
tap.on('end', save)
}
const filesMatch = (a, b) =>
a && b && path.resolve(a) === path.resolve(b)
const filterFiles = exports.filterFiles = (files, options, parallelOk) =>
files.filter(file =>
path.basename(file) === 'tap-parallel-ok' ?
((parallelOk[path.resolve(path.dirname(file))] = true), false)
: path.basename(file) === 'tap-parallel-not-ok' ?
parallelOk[path.resolve(path.dirname(file))] = false
// don't include the --before and --after scripts as files,
// so they're not run as tests if they would be found normally.
// This allows test/setup.js and test/teardown.js for example.
: filesMatch(file, options.before) ? false
: filesMatch(file, options.after) ? false
: options.saved && options.saved.length ? onSavedList(options.saved, file)
: options.changed ? options.changedFilter(file)
: true
)
// check if the file is on the list, or if it's a parent dir of
// any items that are on the list.
const onSavedList = (saved, file) =>
saved.indexOf(file) !== -1 ? true
: saved.some(f => f.indexOf(file + '/') === 0)
const isParallelOk = (parallelOk, file) => {
const dir = path.resolve(path.dirname(file))
return (dir in parallelOk) ? parallelOk[dir]
: exists(dir + '/tap-parallel-ok')
? parallelOk[dir] = true
: exists(dir + '/tap-parallel-not-ok')
? parallelOk[dir] = false
: dir.length >= process.cwd().length
? isParallelOk(parallelOk, dir)
: true
}
const getEnv = options => options['test-env'].reduce((env, kv) => {
const split = kv.split('=')
const key = split.shift()
if (!split.length)
delete env[key]
else
env[key] = split.join('=')
return env
}, {...process.env})
// the test that checks this escapes from NYC, so it'll never show up
/* istanbul ignore next */
const coverageMapOverride = (env, file, coverageMap) => {
if (coverageMap) {
/* istanbul ignore next */
env.NYC_CONFIG_OVERRIDE = JSON.stringify({
include: coverageMap(file) || ''
})
}
}
const runAllFiles = (options, env, tap, processDB) => {
debug('run all files')
let doStdin = false
let parallelOk = Object.create(null)
if (options['output-dir'] !== null) {
tap.on('spawn', t => {
const dir = options['output-dir'] + '/' + path.dirname(t.name)
require('../settings.js').mkdirRecursiveSync(dir)
const file = dir + '/' + path.basename(t.name) + '.tap'
t.proc.stdout.pipe(fs.createWriteStream(file))
})
tap.on('stdin', t => {
const file = options['output-dir'] + '/stdin.tap'
t.stream.pipe(fs.createWriteStream(file))
})
}
options.files = filterFiles(options.files, options, parallelOk)
if (options.files.length === 0 && !doStdin && !options.changed) {
tap.fail('no tests specified')
}
/* istanbul ignore next */
const coverageMap = options['coverage-map']
? require(path.resolve(options['coverage-map']))
: null
const seen = new Set()
for (let i = 0; i < options.files.length; i++) {
const file = options.files[i]
if (seen.has(file))
continue
debug('run file', file)
seen.add(file)
// Pick up stdin after all the other files are handled.
if (file === '-') {
doStdin = true
continue
}
let st
try {
st = fs.statSync(file)
} catch (er) {
tap.test(file, t => t.threw(er))
continue
}
if (st.isDirectory()) {
debug('is a directory', file)
const dir = filterFiles(fs.readdirSync(file).map(f =>
file.replace(/[\/\\]+$/, '') + '/' + f), options, parallelOk)
options.files.splice(i, 1, ...dir)
i--
} else {
const opt = { env, file, processDB }
coverageMapOverride(env, file, coverageMap)
if (options.timeout)
opt.timeout = options.timeout * 1000
if (options.jobs > 1)
opt.buffered = isParallelOk(parallelOk, file) !== false
if (options.flow && flowNode)
options['node-arg'].push('-r', flowNode)
if (tsNode && /\.([mc]?ts|tsx?)$/.test(file)) {
debug('typescript file', file)
const compilerOpts = JSON.parse(env.TS_NODE_COMPILER_OPTIONS || '{}')
if (options.jsx)
compilerOpts.jsx = 'react'
opt.env = {
...env,
TS_NODE_COMPILER_OPTIONS: JSON.stringify(compilerOpts),
}
const args = [
'-r', tsNode,
...options['node-arg'],
file,
...options['test-arg']
]
if (args.includes('ts-node/esm')) {
args.unshift('--experimental-modules')
args.splice(args.indexOf('--loader'), 1);
args.splice(args.indexOf('ts-node/esm'), 1);
}
tap.spawn(node, args, opt, file)
} else if (options.jsx && /\.jsx$/.test(file)) {
debug('jsx file', file)
const args = [
...(options['node-arg']),
jsx,
file,
...(options['test-arg']),
]
tap.spawn(node, args, opt, file)
} else if (/\.jsx$|\.tsx?$|\.[mc]?js$/.test(file)) {
debug('js file', file)
/* istanbul ignore next - version specific behavior */
const experimental = /^v10\./.test(process.version) && /\.mjs$/.test(file)
? ['--experimental-modules'] : []
const args = [
...options['node-arg'],
...experimental,
file,
...options['test-arg']
]
tap.spawn(node, args, opt, file)
} else if (/\.tap$/.test(file)) {
debug('tap file', file)
tap.spawn('cat', [file], opt, file)
} else if (isexe.sync(options.files[i])) {
debug('executable', file)
tap.spawn(options.files[i], options['test-arg'], opt, file)
} else {
debug('not a test file', file)
}
}
}
if (doStdin)
tap.stdin()
debug('scheduled all files for execution')
}
const runTests = options => {
debug('run tests')
// At this point, we know we need to use the tap root,
// because there are 1 or more files to spawn.
const tap = require('../lib/tap.js')
tap.writeSnapshot = false
if (options.comments) {
const onComment = c => {
if (!c.match(/^# (timeout=[0-9]+|time=[0-9\.]+m?s|Subtest(: .+)?)\n$/))
console.error(c.substr(2).trim())
}
const onChild = p => {
p.on('comment', onComment)
p.on('child', onChild)
}
tap.parser.on('comment', onComment)
tap.parser.on('child', onChild)
}
tap.runOnly = false
// greps are passed to children, but not the runner itself
tap.grep = []
tap.jobs = options.jobs
const env = getEnv(options)
/* istanbul ignore next */
const processDB = options.coverage && process.env.NYC_CONFIG
? new ProcessDB() : null
// run --before before everything, and --after as the very last thing
runBeforeAfter(options, env, tap, processDB)
tap.patchProcess()
// if not -Rtap, then output what the user wants.
// otherwise just dump to stdout
/* istanbul ignore next */
makeReporter(tap, options)
// need to replay the first version line, because the previous
// line will have flushed it out to stdout or the reporter already.
if (options['output-file'] !== null)
tap.pipe(fs.createWriteStream(options['output-file'])).write('TAP version 13\n')
saveFails(options, tap)
runAllFiles(options, env, tap, processDB)
/* istanbul ignore next */
if (process.env.COVERALLS_REPO_TOKEN ||
process.env.__TAP_COVERALLS_TEST__) {
tap.teardown(() => pipeToCoveralls())
}
tap.end()
debug('called tap.end()')
}
const beforeAfter = (env, args) => {
const {status, signal} = spawnSync(process.execPath, args, {
env,
stdio: 'inherit',
})
if (status || signal) {
const script = args.length === 1 ? args[0] : args[2]
const msg = `\n# failed ${script}\n# code=${status} signal=${signal}\n`
console.error(msg)
process.exitCode = status || 1
process.exit(status || 1)
}
}
const runBeforeAfter = (options, env, tap, processDB) => {
// Have to write the index before running a script so that this
// process is included in the DB, or else it'll crash when it
// tries to get the parent info.
/* istanbul ignore next */
if (processDB && (options.before || options.after))
processDB.writeIndex()
if (options.before) {
if (options.ts && tsNode && /\.tsx?$/.test(options.before)) {
beforeAfter(env, ['-r', tsNode, options.before])
} else {
beforeAfter(env, [options.before])
}
}
if (options.after) {
/* istanbul ignore next - run after istanbul's report */
signalExit(() => {
if (options.ts && tsNode && /\.tsx?$/.test(options.after)) {
beforeAfter(env, ['-r', tsNode, options.after])
} else {
beforeAfter(env, [options.after])
}
}, { alwaysLast: true })
}
}
const parsePackageJson = () => {
try {
return JSON.parse(fs.readFileSync('package.json', 'utf8')).tap || {}
} catch (er) {
return {}
}
}
const parseRcFile = path => {
let contents
try {
contents = fs.readFileSync(path, 'utf8')
} catch (_) {
try {
contents = fs.readFileSync(path + '.yaml', 'utf8')
} catch (_) {
try {
contents = fs.readFileSync(path + '.yml', 'utf8')
} catch (_) {}
}
}
if (!contents) {
// if no dotfile exists, just return an empty object
return {}
}
return yaml.parse(contents)
}
const strToRegExp = g => {
const p = g.match(/^\/(.*)\/([a-z]*)$/)
g = p ? p[1] : g
const flags = p ? p[2] : ''
return new RegExp(g, flags)
}
const onError = er => {
/* istanbul ignore else - parse errors are the only ones we ever expect */
if (er.name.match(/^AssertionError/) && !er.generatedMessage) {
console.error('Error: ' + er.message)
console.error('Run `tap --help` for usage information')
process.exit(1)
} else {
console.error(er)
process.exit(1)
}
}
try {
require('./jack.js')(main)
} catch (er) {
onError(er)
}
|