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()