v7
1// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import { suite } from 'uvu'
16import * as assert from 'uvu/assert'
17import '../build/globals.js'
18import net from 'node:net'
19import getPort from 'get-port'
20
21const test = suite('cli')
22const getServer = (resp = [], log = console.log) => {
23 const server = net.createServer()
24 server.on('connection', (conn) => {
25 conn.on('data', (d) => {
26 conn.write(resp.shift() || 'pong')
27 })
28 })
29 server.stop = () => new Promise((resolve) => server.close(() => resolve()))
30 server.start = (port) =>
31 new Promise((resolve) => server.listen(port, () => resolve(server)))
32 return server
33}
34
35$.verbose = false
36
37// Helps detect unresolved ProcessPromise.
38let promiseResolved = false
39process.on('exit', () => {
40 if (!promiseResolved) {
41 console.error('Error: ProcessPromise never resolved.')
42 process.exitCode = 1
43 }
44})
45
46test('promise resolved', async () => {
47 await $`echo`
48 promiseResolved = true
49})
50
51test('prints version', async () => {
52 assert.match((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
53})
54
55test('prints help', async () => {
56 let p = $`node build/cli.js -h`
57 p.stdin.end()
58 let help = await p
59 assert.match(help.stdout, 'zx')
60})
61
62test('zx prints usage', async () => {
63 let p = $`node build/cli.js`
64 p.stdin.end()
65 let out = await p
66 assert.match(out.stdout, 'A tool for writing better scripts')
67})
68
69test('starts repl with --repl', async () => {
70 let p = $`node build/cli.js --repl`
71 p.stdin.write('await $`echo f"o"o`\n')
72 p.stdin.write('"b"+"ar"\n')
73 p.stdin.end()
74 let out = await p
75 assert.match(out.stdout, 'foo')
76 assert.match(out.stdout, 'bar')
77})
78
79test('starts repl with verbosity off', async () => {
80 let p = $`node build/cli.js --repl`
81 p.stdin.write('"verbose" + " is " + $.verbose\n')
82 p.stdin.end()
83 let out = await p
84 assert.match(out.stdout, 'verbose is false')
85})
86
87test('supports `--experimental` flag', async () => {
88 let out = await $`echo 'echo("test")' | node build/cli.js --experimental`
89 assert.match(out.stdout, 'test')
90})
91
92test('supports `--quiet` flag', async () => {
93 let p = await $`node build/cli.js test/fixtures/markdown.md`
94 assert.ok(!p.stderr.includes('ignore'), 'ignore was printed')
95 assert.ok(p.stderr.includes('hello'), 'no hello')
96 assert.ok(p.stdout.includes('world'), 'no world')
97})
98
99test('supports `--shell` flag ', async () => {
100 let shell = $.shell
101 let p =
102 await $`node build/cli.js --shell=${shell} <<< '$\`echo \${$.shell}\`'`
103 assert.ok(p.stderr.includes(shell))
104})
105
106test('supports `--prefix` flag ', async () => {
107 let prefix = 'set -e;'
108 let p =
109 await $`node build/cli.js --prefix=${prefix} <<< '$\`echo \${$.prefix}\`'`
110 assert.ok(p.stderr.includes(prefix))
111})
112
113test('scripts from https', async () => {
114 const resp = await fs.readFile(path.resolve('test/fixtures/echo.http'))
115 const port = await getPort()
116 const server = await getServer([resp]).start(port)
117 const out = await $`node build/cli.js http://127.0.0.1:${port}/script.mjs`
118
119 assert.match(out.toString(), /test/)
120 await server.stop()
121})
122
123test('scripts from https not ok', async () => {
124 const port = await getPort()
125 const resp = await fs.readFile(path.resolve('test/fixtures/500.http'))
126 const server = await getServer([resp]).start(port)
127 const out = await $`node build/cli.js http://127.0.0.1:${port}`.nothrow()
128
129 assert.match(out.stderr, /Error: Can't get/)
130 await server.stop()
131})
132
133test('scripts with no extension', async () => {
134 await $`node build/cli.js test/fixtures/no-extension`
135 assert.ok(
136 /Test file to verify no-extension didn't overwrite similarly name .mjs file./.test(
137 (await fs.readFile('test/fixtures/no-extension.mjs')).toString()
138 )
139 )
140})
141
142test('require() is working from stdin', async () => {
143 let out =
144 await $`node build/cli.js <<< 'console.log(require("./package.json").name)'`
145 assert.match(out.stdout, 'zx')
146})
147
148test('require() is working in ESM', async () => {
149 await $`node build/cli.js test/fixtures/require.mjs`
150})
151
152test('__filename & __dirname are defined', async () => {
153 await $`node build/cli.js test/fixtures/filename-dirname.mjs`
154})
155
156test('markdown scripts are working', async () => {
157 await $`node build/cli.js docs/markdown.md`
158})
159
160test('markdown scripts are working', async () => {
161 await $`node build/cli.js docs/markdown.md`
162})
163
164test('exceptions are caught', async () => {
165 let out1 = await $`node build/cli.js <<<${'await $`wtf`'}`.nothrow()
166 assert.match(out1.stderr, 'Error:')
167 let out2 = await $`node build/cli.js <<<'throw 42'`.nothrow()
168 assert.match(out2.stderr, '42')
169})
170
171test('eval works', async () => {
172 assert.is((await $`node build/cli.js --eval 'echo(42)'`).stdout, '42\n')
173 assert.is((await $`node build/cli.js -e='echo(69)'`).stdout, '69\n')
174})
175
176test('eval works with stdin', async () => {
177 let p = $`(printf foo; sleep 0.1; printf bar) | node build/cli.js --eval 'echo(await stdin())'`
178 assert.is((await p).stdout, 'foobar\n')
179})
180
181test('executes a script from $PATH', async () => {
182 const isWindows = process.platform === 'win32'
183 const oldPath = process.env.PATH
184
185 const envPathSeparator = isWindows ? ';' : ':'
186 process.env.PATH += envPathSeparator + path.resolve('/tmp/')
187
188 const toPOSIXPath = (_path) => _path.split(path.sep).join(path.posix.sep)
189
190 const zxPath = path.resolve('./build/cli.js')
191 const zxLocation = isWindows ? toPOSIXPath(zxPath) : zxPath
192 const scriptCode = `#!/usr/bin/env ${zxLocation}\nconsole.log('The script from path runs.')`
193
194 try {
195 await $`chmod +x ${zxLocation}`
196 await $`echo ${scriptCode}`.pipe(
197 fs.createWriteStream('/tmp/script-from-path', { mode: 0o744 })
198 )
199 await $`script-from-path`
200 } finally {
201 process.env.PATH = oldPath
202 fs.rmSync('/tmp/script-from-path')
203 }
204})
205
206test('argv works with zx and node', async () => {
207 assert.is(
208 (await $`node build/cli.js test/fixtures/argv.mjs foo`).toString(),
209 `global {"_":["foo"]}\nimported {"_":["foo"]}\n`
210 )
211 assert.is(
212 (await $`node test/fixtures/argv.mjs bar`).toString(),
213 `global {"_":["bar"]}\nimported {"_":["bar"]}\n`
214 )
215 assert.match(
216 (
217 await $`node build/cli.js --eval 'console.log(argv._)' foobarbaz`
218 ).toString(),
219 /foobarbaz/
220 )
221})
222
223test('exit code can be set', async () => {
224 let p = await $`node build/cli.js test/fixtures/exit-code.mjs`.nothrow()
225 assert.is(p.exitCode, 42)
226})
227
228test.run()