UNPKG

6.1 kBPlain TextView Raw
1#!/usr/bin/env node
2/* eslint no-sync:0, no-inner-declarations:0 */
3
4// local
5import { Version, Versions } from './index.js'
6import { parseExitCode } from './util.js'
7
8// external
9import versionRange from 'version-range'
10import minimist from 'minimist'
11import textTable from 'text-table'
12import stringWidth from 'string-width'
13import Logger from 'logger-clearable'
14import Spinner from 'spinner-title'
15import { isReadable } from '@bevry/fs-readable'
16import wait from '@bevry/wait'
17import { readJSON } from '@bevry/json'
18import {
19 preloadNodeVersions,
20 filterSignificantNodeVersions,
21} from '@bevry/nodejs-versions'
22import filedirname from 'filedirname'
23const [file, dir] = filedirname()
24
25// builtin
26import process from 'node:process'
27import { join } from 'node:path'
28
29// prepare
30const spinner: any = null
31async function parse(): Promise<null | any> {
32 // parse the cli flags/arguments
33 const cli: any = minimist(process.argv.slice(2), {
34 '--': true,
35 alias: {
36 h: 'help',
37 j: 'json',
38 n: 'node',
39 s: 'serial',
40 },
41 string: ['node', 'spinner'],
42 // boolean: ['json', 'serial', 'verbose'], <-- this defaults to false instead of null, preventing accurate fallbacks
43 })
44 if (cli.help) {
45 // output help and exit
46 process.stdout.write(
47 [
48 'Usage:',
49 '',
50 ' -n/--node [version]: Add a Node.js version to test',
51 ' -s/--serial: Run tests serially, one after the other',
52 ' -j/--json: Output the test results as JSON',
53 ' --spinner [spinner] Which spinner to use in the title bar',
54 ' --verbose: Report details about all statuses, not just failures',
55 ' --version: Output the version of Testen',
56 ' --help: Output this help',
57 ' -- [command]: The test command you expect',
58 '',
59 ].join('\n'),
60 )
61 return null
62 }
63 if (cli.version) {
64 // output version and exit
65 const testenPackage: any = await readJSON(join(dir, 'package.json'))
66 process.stdout.write(testenPackage.version + '\n')
67 return null
68 }
69
70 // return configuration from cli
71 const cliTestenConfig: any = {}
72 if (cli.node)
73 cliTestenConfig.node =
74 Array.isArray(cli.node) && cli.node.length === 1 ? cli.node[0] : cli.node
75 if (cli['--'] && cli['--'].join(''))
76 cliTestenConfig.command = cli['--'].join(' ')
77 if (cli.spinner != null) cliTestenConfig.spinner = cli.spinner
78 if (cli.serial != null) cliTestenConfig.serial = cli.serial
79 if (cli.json != null) cliTestenConfig.json = cli.json
80 if (cli.verbose != null) cliTestenConfig.verbose = cli.verbose
81 return cliTestenConfig
82}
83async function testen(customTestenConfig = {}) {
84 // fetch the user package configuration
85 const cwd = process.cwd()
86 const userPackagePath = `${cwd}/package.json`
87 const userPackage: any = (await isReadable(userPackagePath))
88 ? await readJSON(userPackagePath)
89 : {}
90
91 // merge the default, package, and custom configuration
92 const testenConfig = Object.assign(
93 {
94 node: (userPackage.engines && userPackage.engines.node) || '',
95 command: 'npm test',
96 spinner: 'monkey',
97 serial: false,
98 json: false,
99 verbose: false,
100 },
101 userPackage.testen || {},
102 customTestenConfig,
103 )
104
105 // parse node versions
106 const nodeVersions = []
107 if (Array.isArray(testenConfig.node) && testenConfig.node.length) {
108 // specific versions
109 nodeVersions.push(...testenConfig.node)
110 } else if (testenConfig.node && /^[\d\w.-]+$/.test(testenConfig.node)) {
111 // specific version
112 nodeVersions.push(testenConfig.node)
113 } else if (testenConfig.node) {
114 // range
115 await preloadNodeVersions()
116 nodeVersions.push(
117 ...filterSignificantNodeVersions({
118 maintainedOrLTS: true,
119 released: true,
120 }).filter((version: string) => versionRange(version, testenConfig.node)),
121 )
122 } else {
123 nodeVersions.push('current', 'stable', 'system')
124 }
125 if (!nodeVersions || !nodeVersions.length) {
126 throw new Error('No node versions specified')
127 }
128
129 // create the testen instance
130 const listeners = []
131 let interval: any = null
132 let spinner: any = null
133 if (!testenConfig.json) {
134 // create and start spinner
135 spinner = new Spinner({
136 style: testenConfig.spinner,
137 interval: 1000,
138 } as any)
139 spinner.start()
140 // prepare the logging, note that logger-clearable uses setImmediate, so requires a wait
141 const logger = new Logger()
142 function table(result: any) {
143 return textTable(result, { stringLength: stringWidth })
144 }
145 function refresh(versions: Versions) {
146 const messages: any = []
147 versions.array.forEach(function (V: Version) {
148 if (V.success === false || testenConfig.verbose) {
149 messages.push(V.message)
150 }
151 })
152 if (messages.length) {
153 return (
154 '\n' + messages.join('\n\n') + '\n\n' + table(versions.table) + '\n\n'
155 )
156 } else {
157 return '\n' + table(versions.table) + '\n\n'
158 }
159 }
160 /* eslint-disable-next-line no-inner-declarations */
161 function refresher() {
162 /* eslint-disable-next-line no-use-before-define */
163 logger.queue(() => refresh(versions))
164 }
165 interval = setInterval(refresher, 1000)
166 listeners.push(refresher)
167 }
168 const versions = new Versions(nodeVersions, listeners as any)
169
170 // run
171 await versions.load()
172 await versions.install()
173 await versions.test(testenConfig.command, testenConfig.serial)
174
175 // output and cleanup
176 if (testenConfig.json) {
177 // output json
178 process.stdout.write(JSON.stringify(versions.json, null, ' '))
179 } else {
180 // output already occurred via listeners
181 // cleanup our listeners
182 if (interval) {
183 clearInterval(interval) // stop the refresh interval
184 interval = null
185 }
186 spinner.stop() // stop the spinner
187 await wait(0) // wait for the logger to finish
188 }
189
190 // return versions instance
191 return versions
192}
193
194// start the cli
195Promise.resolve()
196 .then(parse)
197 .then(async function (cliTestenConfig: any = null) {
198 if (cliTestenConfig) {
199 const versions = await testen(cliTestenConfig)
200 process.exitCode = versions.success ? 0 : 1
201 }
202 })
203 .catch(function (err: any) {
204 if (spinner) spinner.stop()
205 process.stderr.write((err.stack || err.message || err).toString())
206 process.exitCode = parseExitCode(err.code) || 2
207 })