Commit cd18352

Anton Golub <antongolub@antongolub.com>
2023-12-19 11:35:50
test: migrate to native node:test (#676)
* test: migrate to native node:test * test: add nodejs 20 to test matrix * test: fix win32 runner
1 parent a9b573e
.github/workflows/test.yml
@@ -8,7 +8,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [16.x, 18.x]
+        node-version: [16.x, 18.x, 20.x]
 
     steps:
       - uses: actions/checkout@v3
@@ -33,7 +33,7 @@ jobs:
           node-version: 16.x
       - run: npm i
       - run: npm run build
-      - run: npx uvu test win32
+      - run: node ./test/win32.test.js
         timeout-minutes: 1
         env:
           FORCE_COLOR: 3
test/all.test.js
@@ -0,0 +1,24 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import './cli.test.js'
+import './core.test.js'
+import './deps.test.js'
+import './experimental.test.js'
+import './extra.test.js'
+import './global.test.js'
+import './goods.test.js'
+import './package.test.js'
+import './win32.test.js'
+import './util.test.js'
test/cli.test.js
@@ -12,195 +12,195 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe, before, beforeEach } from 'node:test'
 import '../build/globals.js'
 
-const test = suite('cli')
-
-$.verbose = false
-
-// Helps detect unresolved ProcessPromise.
-let promiseResolved = false
-process.on('exit', () => {
-  if (!promiseResolved) {
-    console.error('Error: ProcessPromise never resolved.')
-    process.exitCode = 1
-  }
-})
-
-test('promise resolved', async () => {
-  await $`echo`
-  promiseResolved = true
-})
-
-test('prints version', async () => {
-  assert.match((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
-})
-
-test('prints help', async () => {
-  let p = $`node build/cli.js -h`
-  p.stdin.end()
-  let help = await p
-  assert.match(help.stdout, 'zx')
-})
-
-test('zx prints usage', async () => {
-  let p = $`node build/cli.js`
-  p.stdin.end()
-  let out = await p
-  assert.match(out.stdout, 'A tool for writing better scripts')
-})
-
-test('starts repl with --repl', async () => {
-  let p = $`node build/cli.js --repl`
-  p.stdin.write('await $`echo f"o"o`\n')
-  p.stdin.write('"b"+"ar"\n')
-  p.stdin.end()
-  let out = await p
-  assert.match(out.stdout, 'foo')
-  assert.match(out.stdout, 'bar')
-})
-
-test('starts repl with verbosity off', async () => {
-  let p = $`node build/cli.js --repl`
-  p.stdin.write('"verbose" + " is " + $.verbose\n')
-  p.stdin.end()
-  let out = await p
-  assert.match(out.stdout, 'verbose is false')
-})
-
-test('supports `--experimental` flag', async () => {
-  let out = await $`echo 'echo("test")' | node build/cli.js --experimental`
-  assert.match(out.stdout, 'test')
-})
-
-test('supports `--quiet` flag', async () => {
-  let p = await $`node build/cli.js test/fixtures/markdown.md`
-  assert.ok(!p.stderr.includes('ignore'), 'ignore was printed')
-  assert.ok(p.stderr.includes('hello'), 'no hello')
-  assert.ok(p.stdout.includes('world'), 'no world')
-})
-
-test('supports `--shell` flag ', async () => {
-  let shell = $.shell
-  let p =
-    await $`node build/cli.js --shell=${shell} <<< '$\`echo \${$.shell}\`'`
-  assert.ok(p.stderr.includes(shell))
-})
-
-test('supports `--prefix` flag ', async () => {
-  let prefix = 'set -e;'
-  let p =
-    await $`node build/cli.js --prefix=${prefix} <<< '$\`echo \${$.prefix}\`'`
-  assert.ok(p.stderr.includes(prefix))
-})
-
-test('scripts from https', async () => {
-  $`cat ${path.resolve('test/fixtures/echo.http')} | nc -l 8080`
-  let out = await $`node build/cli.js http://127.0.0.1:8080/echo.mjs`
-  assert.match(out.stderr, 'test')
-})
-
-test('scripts from https not ok', async () => {
-  $`echo $'HTTP/1.1 500\n\n' | nc -l 8081`
-  let out = await $`node build/cli.js http://127.0.0.1:8081`.nothrow()
-  assert.match(out.stderr, "Error: Can't get")
-})
-
-test('scripts with no extension', async () => {
-  await $`node build/cli.js test/fixtures/no-extension`
-  assert.ok(
-    /Test file to verify no-extension didn't overwrite similarly name .mjs file./.test(
-      (await fs.readFile('test/fixtures/no-extension.mjs')).toString()
+describe('cli', () => {
+  // Helps detect unresolved ProcessPromise.
+  let promiseResolved = false
+
+  beforeEach(() => {
+    $.verbose = false
+    process.on('exit', () => {
+      if (!promiseResolved) {
+        console.error('Error: ProcessPromise never resolved.')
+        process.exitCode = 1
+      }
+    })
+  })
+
+  test('promise resolved', async () => {
+    await $`echo`
+    promiseResolved = true
+  })
+
+  test('prints version', async () => {
+    assert.match((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
+  })
+
+  test('prints help', async () => {
+    let p = $`node build/cli.js -h`
+    p.stdin.end()
+    let help = await p
+    assert.match(help.stdout, /zx/)
+  })
+
+  test('zx prints usage', async () => {
+    let p = $`node build/cli.js`
+    p.stdin.end()
+    let out = await p
+    assert.match(out.stdout, /A tool for writing better scripts/)
+  })
+
+  test('starts repl with --repl', async () => {
+    let p = $`node build/cli.js --repl`
+    p.stdin.write('await $`echo f"o"o`\n')
+    p.stdin.write('"b"+"ar"\n')
+    p.stdin.end()
+    let out = await p
+    assert.match(out.stdout, /foo/)
+    assert.match(out.stdout, /bar/)
+  })
+
+  test('starts repl with verbosity off', async () => {
+    let p = $`node build/cli.js --repl`
+    p.stdin.write('"verbose" + " is " + $.verbose\n')
+    p.stdin.end()
+    let out = await p
+    assert.match(out.stdout, /verbose is false/)
+  })
+
+  test('supports `--experimental` flag', async () => {
+    let out = await $`echo 'echo("test")' | node build/cli.js --experimental`
+    assert.match(out.stdout, /test/)
+  })
+
+  test('supports `--quiet` flag', async () => {
+    let p = await $`node build/cli.js test/fixtures/markdown.md`
+    assert.ok(!p.stderr.includes('ignore'), 'ignore was printed')
+    assert.ok(p.stderr.includes('hello'), 'no hello')
+    assert.ok(p.stdout.includes('world'), 'no world')
+  })
+
+  test('supports `--shell` flag ', async () => {
+    let shell = $.shell
+    let p =
+      await $`node build/cli.js --shell=${shell} <<< '$\`echo \${$.shell}\`'`
+    assert.ok(p.stderr.includes(shell))
+  })
+
+  test('supports `--prefix` flag ', async () => {
+    let prefix = 'set -e;'
+    let p =
+      await $`node build/cli.js --prefix=${prefix} <<< '$\`echo \${$.prefix}\`'`
+    assert.ok(p.stderr.includes(prefix))
+  })
+
+  test('scripts from https', async () => {
+    $`cat ${path.resolve('test/fixtures/echo.http')} | nc -l 8080`
+    let out = await $`node build/cli.js http://127.0.0.1:8080/echo.mjs`
+    assert.match(out.stderr, /test/)
+  })
+
+  test('scripts from https not ok', async () => {
+    $`echo $'HTTP/1.1 500\n\n' | nc -l 8081`
+    let out = await $`node build/cli.js http://127.0.0.1:8081`.nothrow()
+    assert.match(out.stderr, /Error: Can't get/)
+  })
+
+  test('scripts with no extension', async () => {
+    await $`node build/cli.js test/fixtures/no-extension`
+    assert.ok(
+      /Test file to verify no-extension didn't overwrite similarly name .mjs file./.test(
+        (await fs.readFile('test/fixtures/no-extension.mjs')).toString()
+      )
     )
-  )
-})
-
-test('require() is working from stdin', async () => {
-  let out =
-    await $`node build/cli.js <<< 'console.log(require("./package.json").name)'`
-  assert.match(out.stdout, 'zx')
-})
-
-test('require() is working in ESM', async () => {
-  await $`node build/cli.js test/fixtures/require.mjs`
-})
-
-test('__filename & __dirname are defined', async () => {
-  await $`node build/cli.js test/fixtures/filename-dirname.mjs`
-})
-
-test('markdown scripts are working', async () => {
-  await $`node build/cli.js test/fixtures/markdown.md`
-})
-
-test('markdown scripts are working', async () => {
-  await $`node build/cli.js test/fixtures/markdown.md`
-})
-
-test('exceptions are caught', async () => {
-  let out1 = await $`node build/cli.js <<<${'await $`wtf`'}`.nothrow()
-  assert.match(out1.stderr, 'Error:')
-  let out2 = await $`node build/cli.js <<<'throw 42'`.nothrow()
-  assert.match(out2.stderr, '42')
-})
-
-test('eval works', async () => {
-  assert.is((await $`node build/cli.js --eval 'echo(42)'`).stdout, '42\n')
-  assert.is((await $`node build/cli.js -e='echo(69)'`).stdout, '69\n')
-})
-
-test('eval works with stdin', async () => {
-  let p = $`(printf foo; sleep 0.1; printf bar) | node build/cli.js --eval 'echo(await stdin())'`
-  assert.is((await p).stdout, 'foobar\n')
-})
-
-test('executes a script from $PATH', async () => {
-  const isWindows = process.platform === 'win32'
-  const oldPath = process.env.PATH
-
-  const envPathSeparator = isWindows ? ';' : ':'
-  process.env.PATH += envPathSeparator + path.resolve('/tmp/')
-
-  const toPOSIXPath = (_path) => _path.split(path.sep).join(path.posix.sep)
-
-  const zxPath = path.resolve('./build/cli.js')
-  const zxLocation = isWindows ? toPOSIXPath(zxPath) : zxPath
-  const scriptCode = `#!/usr/bin/env ${zxLocation}\nconsole.log('The script from path runs.')`
-
-  try {
-    await $`chmod +x ${zxLocation}`
-    await $`echo ${scriptCode}`.pipe(
-      fs.createWriteStream('/tmp/script-from-path', { mode: 0o744 })
+  })
+
+  test('require() is working from stdin', async () => {
+    let out =
+      await $`node build/cli.js <<< 'console.log(require("./package.json").name)'`
+    assert.match(out.stdout, /zx/)
+  })
+
+  test('require() is working in ESM', async () => {
+    await $`node build/cli.js test/fixtures/require.mjs`
+  })
+
+  test('__filename & __dirname are defined', async () => {
+    await $`node build/cli.js test/fixtures/filename-dirname.mjs`
+  })
+
+  test('markdown scripts are working', async () => {
+    await $`node build/cli.js test/fixtures/markdown.md`
+  })
+
+  test('markdown scripts are working', async () => {
+    await $`node build/cli.js test/fixtures/markdown.md`
+  })
+
+  test('exceptions are caught', async () => {
+    let out1 = await $`node build/cli.js <<<${'await $`wtf`'}`.nothrow()
+    assert.match(out1.stderr, /Error:/)
+    let out2 = await $`node build/cli.js <<<'throw 42'`.nothrow()
+    assert.match(out2.stderr, /42/)
+  })
+
+  test('eval works', async () => {
+    assert.equal((await $`node build/cli.js --eval 'echo(42)'`).stdout, '42\n')
+    assert.equal((await $`node build/cli.js -e='echo(69)'`).stdout, '69\n')
+  })
+
+  test('eval works with stdin', async () => {
+    let p = $`(printf foo; sleep 0.1; printf bar) | node build/cli.js --eval 'echo(await stdin())'`
+    assert.equal((await p).stdout, 'foobar\n')
+  })
+
+  test('executes a script from $PATH', async () => {
+    const isWindows = process.platform === 'win32'
+    const oldPath = process.env.PATH
+
+    const envPathSeparator = isWindows ? ';' : ':'
+    process.env.PATH += envPathSeparator + path.resolve('/tmp/')
+
+    const toPOSIXPath = (_path) => _path.split(path.sep).join(path.posix.sep)
+
+    const zxPath = path.resolve('./build/cli.js')
+    const zxLocation = isWindows ? toPOSIXPath(zxPath) : zxPath
+    const scriptCode = `#!/usr/bin/env ${zxLocation}\nconsole.log('The script from path runs.')`
+
+    try {
+      await $`chmod +x ${zxLocation}`
+      await $`echo ${scriptCode}`.pipe(
+        fs.createWriteStream('/tmp/script-from-path', { mode: 0o744 })
+      )
+      await $`script-from-path`
+    } finally {
+      process.env.PATH = oldPath
+      fs.rmSync('/tmp/script-from-path')
+    }
+  })
+
+  test('argv works with zx and node', async () => {
+    assert.equal(
+      (await $`node build/cli.js test/fixtures/argv.mjs foo`).toString(),
+      `global {"_":["foo"]}\nimported {"_":["foo"]}\n`
     )
-    await $`script-from-path`
-  } finally {
-    process.env.PATH = oldPath
-    fs.rmSync('/tmp/script-from-path')
-  }
-})
-
-test('argv works with zx and node', async () => {
-  assert.is(
-    (await $`node build/cli.js test/fixtures/argv.mjs foo`).toString(),
-    `global {"_":["foo"]}\nimported {"_":["foo"]}\n`
-  )
-  assert.is(
-    (await $`node test/fixtures/argv.mjs bar`).toString(),
-    `global {"_":["bar"]}\nimported {"_":["bar"]}\n`
-  )
-  assert.is(
-    (
-      await $`node build/cli.js --eval 'console.log(argv._.join(''))' baz`
-    ).toString(),
-    `baz\n`
-  )
-})
+    assert.equal(
+      (await $`node test/fixtures/argv.mjs bar`).toString(),
+      `global {"_":["bar"]}\nimported {"_":["bar"]}\n`
+    )
+    assert.equal(
+      (
+        await $`node build/cli.js --eval 'console.log(argv._.join(''))' baz`
+      ).toString(),
+      `baz\n`
+    )
+  })
 
-test('exit code can be set', async () => {
-  let p = await $`node build/cli.js test/fixtures/exit-code.mjs`.nothrow()
-  assert.is(p.exitCode, 42)
+  test('exit code can be set', async () => {
+    let p = await $`node build/cli.js test/fixtures/exit-code.mjs`.nothrow()
+    assert.equal(p.exitCode, 42)
+  })
 })
-
-test.run()
test/core.test.js
@@ -12,457 +12,464 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import chalk from 'chalk'
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe, beforeEach } from 'node:test'
 import { inspect } from 'node:util'
 import { Writable } from 'node:stream'
 import { Socket } from 'node:net'
 import { ProcessPromise, ProcessOutput } from '../build/index.js'
 import '../build/globals.js'
 
-const test = suite('core')
-
-$.verbose = false
+describe('core', () => {
+  beforeEach(() => {
+    $.verbose = false
+  })
 
-test('only stdout is used during command substitution', async () => {
-  let hello = await $`echo Error >&2; echo Hello`
-  let len = +(await $`echo ${hello} | wc -c`)
-  assert.is(len, 6)
-})
+  test('only stdout is used during command substitution', async () => {
+    let hello = await $`echo Error >&2; echo Hello`
+    let len = +(await $`echo ${hello} | wc -c`)
+    assert.equal(len, 6)
+  })
 
-test('env vars works', async () => {
-  process.env.ZX_TEST_FOO = 'foo'
-  let foo = await $`echo $ZX_TEST_FOO`
-  assert.is(foo.stdout, 'foo\n')
-})
+  test('env vars works', async () => {
+    process.env.ZX_TEST_FOO = 'foo'
+    let foo = await $`echo $ZX_TEST_FOO`
+    assert.equal(foo.stdout, 'foo\n')
+  })
 
-test('env vars is safe to pass', async () => {
-  process.env.ZX_TEST_BAR = 'hi; exit 1'
-  await $`echo $ZX_TEST_BAR`
-})
+  test('env vars is safe to pass', async () => {
+    process.env.ZX_TEST_BAR = 'hi; exit 1'
+    await $`echo $ZX_TEST_BAR`
+  })
 
-test('arguments are quoted', async () => {
-  let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
-  assert.is((await $`echo ${bar}`).stdout.trim(), bar)
-})
+  test('arguments are quoted', async () => {
+    let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
+    assert.equal((await $`echo ${bar}`).stdout.trim(), bar)
+  })
 
-test('undefined and empty string correctly quoted', async () => {
-  assert.is((await $`echo -n ${undefined}`).toString(), 'undefined')
-  assert.is((await $`echo -n ${''}`).toString(), '')
-})
+  test('undefined and empty string correctly quoted', async () => {
+    assert.equal((await $`echo -n ${undefined}`).toString(), 'undefined')
+    assert.equal((await $`echo -n ${''}`).toString(), '')
+  })
 
-test('can create a dir with a space in the name', async () => {
-  let name = 'foo bar'
-  try {
-    await $`mkdir /tmp/${name}`
-  } catch {
-    assert.unreachable()
-  } finally {
-    await fs.rmdir('/tmp/' + name)
-  }
-})
+  test('can create a dir with a space in the name', async () => {
+    let name = 'foo bar'
+    try {
+      await $`mkdir /tmp/${name}`
+    } catch {
+      assert.unreachable()
+    } finally {
+      await fs.rmdir('/tmp/' + name)
+    }
+  })
 
-test('pipefail is on', async () => {
-  let p
-  try {
-    p = await $`cat /dev/not_found | sort`
-  } catch (e) {
-    p = e
-  }
-  assert.is.not(p.exitCode, 0)
-})
+  test('pipefail is on', async () => {
+    let p
+    try {
+      p = await $`cat /dev/not_found | sort`
+    } catch (e) {
+      p = e
+    }
+    assert.notEqual(p.exitCode, 0)
+  })
 
-test('toString() is called on arguments', async () => {
-  let foo = 0
-  let p = await $`echo ${foo}`
-  assert.is(p.stdout, '0\n')
-})
+  test('toString() is called on arguments', async () => {
+    let foo = 0
+    let p = await $`echo ${foo}`
+    assert.equal(p.stdout, '0\n')
+  })
 
-test('can use array as an argument', async () => {
-  let args = ['-n', 'foo']
-  assert.is((await $`echo ${args}`).toString(), 'foo')
-})
+  test('can use array as an argument', async () => {
+    let args = ['-n', 'foo']
+    assert.equal((await $`echo ${args}`).toString(), 'foo')
+  })
 
-test('quiet() mode is working', async () => {
-  let stdout = ''
-  let log = console.log
-  console.log = (...args) => {
-    stdout += args.join(' ')
-  }
-  await $`echo 'test'`.quiet()
-  console.log = log
-  assert.is(stdout, '')
-  {
-    // Deprecated.
+  test('quiet() mode is working', async () => {
     let stdout = ''
     let log = console.log
     console.log = (...args) => {
       stdout += args.join(' ')
     }
-    await quiet($`echo 'test'`)
+    await $`echo 'test'`.quiet()
     console.log = log
-    assert.is(stdout, '')
-  }
-})
+    assert.equal(stdout, '')
+    {
+      // Deprecated.
+      let stdout = ''
+      let log = console.log
+      console.log = (...args) => {
+        stdout += args.join(' ')
+      }
+      await quiet($`echo 'test'`)
+      console.log = log
+      assert.equal(stdout, '')
+    }
+  })
 
-test('pipes are working', async () => {
-  let { stdout } = await $`echo "hello"`
-    .pipe($`awk '{print $1" world"}'`)
-    .pipe($`tr '[a-z]' '[A-Z]'`)
-  assert.is(stdout, 'HELLO WORLD\n')
-
-  try {
-    await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
-    assert.is((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n')
-
-    let r = $`cat`
-    fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
-    assert.is((await r).stdout, 'foo\n')
-  } finally {
-    await fs.rm('/tmp/output.txt')
-  }
-})
+  test('pipes are working', async () => {
+    let { stdout } = await $`echo "hello"`
+      .pipe($`awk '{print $1" world"}'`)
+      .pipe($`tr '[a-z]' '[A-Z]'`)
+    assert.equal(stdout, 'HELLO WORLD\n')
 
-test('ProcessPromise', async () => {
-  let contents = ''
-  let stream = new Writable({
-    write: function (chunk, encoding, next) {
-      contents += chunk.toString()
-      next()
-    },
-  })
-  let p = $`echo 'test'`.pipe(stream)
-  await p
-  assert.ok(p._piped)
-  assert.is(contents, 'test\n')
-  assert.instance(p.stderr, Socket)
-
-  let err
-  try {
-    $`echo 'test'`.pipe('str')
-  } catch (p) {
-    err = p
-  }
-  assert.is(err.message, 'The pipe() method does not take strings. Forgot $?')
-})
+    try {
+      await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
+      assert.equal((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n')
+
+      let r = $`cat`
+      fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
+      assert.equal((await r).stdout, 'foo\n')
+    } finally {
+      await fs.rm('/tmp/output.txt')
+    }
+  })
 
-test('ProcessPromise: inherits native Promise', async () => {
-  const p1 = $`echo 1`
-  const p2 = p1.then((v) => v)
-  const p3 = p2.then((v) => v)
-  const p4 = p3.catch((v) => v)
-  const p5 = p1.finally((v) => v)
-
-  assert.instance(p1, Promise)
-  assert.instance(p1, ProcessPromise)
-  assert.instance(p2, ProcessPromise)
-  assert.instance(p3, ProcessPromise)
-  assert.instance(p4, ProcessPromise)
-  assert.instance(p5, ProcessPromise)
-  assert.ok(p1 !== p2)
-  assert.ok(p2 !== p3)
-  assert.ok(p3 !== p4)
-  assert.ok(p5 !== p1)
-})
+  test('ProcessPromise', async () => {
+    let contents = ''
+    let stream = new Writable({
+      write: function (chunk, encoding, next) {
+        contents += chunk.toString()
+        next()
+      },
+    })
+    let p = $`echo 'test'`.pipe(stream)
+    await p
+    assert.ok(p._piped)
+    assert.equal(contents, 'test\n')
+    assert.ok(p.stderr instanceof Socket)
 
-test('cd() works with relative paths', async () => {
-  let cwd = process.cwd()
-  try {
-    fs.mkdirpSync('/tmp/zx-cd-test/one/two')
-    cd('/tmp/zx-cd-test/one/two')
-    let p1 = $`pwd`
-    assert.is($.cwd, undefined)
-    assert.match(process.cwd(), '/two')
-
-    cd('..')
-    let p2 = $`pwd`
-    assert.is($.cwd, undefined)
-    assert.match(process.cwd(), '/one')
-
-    cd('..')
-    let p3 = $`pwd`
-    assert.is($.cwd, undefined)
-    assert.match(process.cwd(), '/tmp/zx-cd-test')
-
-    const results = (await Promise.all([p1, p2, p3])).map((p) =>
-      path.basename(p.stdout.trim())
+    let err
+    try {
+      $`echo 'test'`.pipe('str')
+    } catch (p) {
+      err = p
+    }
+    assert.equal(
+      err.message,
+      'The pipe() method does not take strings. Forgot $?'
     )
-    assert.equal(results, ['two', 'one', 'zx-cd-test'])
-  } catch (e) {
-    assert.ok(!e, e)
-  } finally {
-    fs.rmSync('/tmp/zx-cd-test', { recursive: true })
-    cd(cwd)
-  }
-})
+  })
 
-test('cd() does affect parallel contexts', async () => {
-  const cwd = process.cwd()
-  try {
-    fs.mkdirpSync('/tmp/zx-cd-parallel/one/two')
-    await Promise.all([
-      within(async () => {
-        assert.is(process.cwd(), cwd)
-        await sleep(1)
-        cd('/tmp/zx-cd-parallel/one')
-        assert.match(process.cwd(), '/tmp/zx-cd-parallel/one')
-      }),
-      within(async () => {
-        assert.is(process.cwd(), cwd)
-        await sleep(2)
-        assert.is(process.cwd(), cwd)
-      }),
-      within(async () => {
-        assert.is(process.cwd(), cwd)
-        await sleep(3)
-        $.cwd = '/tmp/zx-cd-parallel/one/two'
-        assert.is(process.cwd(), cwd)
-        assert.match((await $`pwd`).stdout, '/tmp/zx-cd-parallel/one/two')
-      }),
-    ])
-  } catch (e) {
-    assert.ok(!e, e)
-  } finally {
-    fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
-    cd(cwd)
-  }
-})
+  test('ProcessPromise: inherits native Promise', async () => {
+    const p1 = $`echo 1`
+    const p2 = p1.then((v) => v)
+    const p3 = p2.then((v) => v)
+    const p4 = p3.catch((v) => v)
+    const p5 = p1.finally((v) => v)
+
+    assert(p1 instanceof Promise)
+    assert(p1 instanceof ProcessPromise)
+    assert(p2 instanceof ProcessPromise)
+    assert(p3 instanceof ProcessPromise)
+    assert(p4 instanceof ProcessPromise)
+    assert(p5 instanceof ProcessPromise)
+    assert.ok(p1 !== p2)
+    assert.ok(p2 !== p3)
+    assert.ok(p3 !== p4)
+    assert.ok(p5 !== p1)
+  })
 
-test('cd() fails on entering not existing dir', async () => {
-  assert.throws(() => cd('/tmp/abra-kadabra'))
-})
+  test('cd() works with relative paths', async () => {
+    let cwd = process.cwd()
+    try {
+      fs.mkdirpSync('/tmp/zx-cd-test/one/two')
+      cd('/tmp/zx-cd-test/one/two')
+      let p1 = $`pwd`
+      assert.equal($.cwd, undefined)
+      assert.ok(process.cwd().endsWith('/two'))
+
+      cd('..')
+      let p2 = $`pwd`
+      assert.equal($.cwd, undefined)
+      assert.ok(process.cwd().endsWith('/one'))
+
+      cd('..')
+      let p3 = $`pwd`
+      assert.equal($.cwd, undefined)
+      assert.ok(process.cwd().endsWith('/tmp/zx-cd-test'))
+
+      const results = (await Promise.all([p1, p2, p3])).map((p) =>
+        path.basename(p.stdout.trim())
+      )
+      assert.deepEqual(results, ['two', 'one', 'zx-cd-test'])
+    } catch (e) {
+      assert.ok(!e, e)
+    } finally {
+      fs.rmSync('/tmp/zx-cd-test', { recursive: true })
+      cd(cwd)
+    }
+  })
+
+  test('cd() does affect parallel contexts', async () => {
+    const cwd = process.cwd()
+    try {
+      fs.mkdirpSync('/tmp/zx-cd-parallel/one/two')
+      await Promise.all([
+        within(async () => {
+          assert.equal(process.cwd(), cwd)
+          await sleep(1)
+          cd('/tmp/zx-cd-parallel/one')
+          assert.ok(process.cwd().endsWith('/tmp/zx-cd-parallel/one'))
+        }),
+        within(async () => {
+          assert.equal(process.cwd(), cwd)
+          await sleep(2)
+          assert.equal(process.cwd(), cwd)
+        }),
+        within(async () => {
+          assert.equal(process.cwd(), cwd)
+          await sleep(3)
+          $.cwd = '/tmp/zx-cd-parallel/one/two'
+          assert.equal(process.cwd(), cwd)
+          assert.ok(
+            (await $`pwd`).stdout
+              .toString()
+              .trim()
+              .endsWith('/tmp/zx-cd-parallel/one/two')
+          )
+        }),
+      ])
+    } catch (e) {
+      assert.ok(!e, e)
+    } finally {
+      fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
+      cd(cwd)
+    }
+  })
 
-test('cd() accepts ProcessOutput in addition to string', async () => {
-  within(async () => {
-    const tmpDir = await $`mktemp -d`
-    cd(tmpDir)
-    assert.match(process.cwd(), tmpDir.toString().trimEnd())
+  test('cd() fails on entering not existing dir', async () => {
+    assert.throws(() => cd('/tmp/abra-kadabra'))
   })
-})
 
-test('kill() method works', async () => {
-  let p = $`sleep 9999`.nothrow()
-  setTimeout(() => {
-    p.kill()
-  }, 100)
-  await p
-})
+  test('cd() accepts ProcessOutput in addition to string', async () => {
+    within(async () => {
+      const tmpDir = await $`mktemp -d`
+      cd(tmpDir)
+      assert.equal(process.cwd(), tmpDir.toString().trimEnd())
+    })
+  })
 
-test('a signal is passed with kill() method', async () => {
-  let p = $`while true; do :; done`
-  setTimeout(() => p.kill('SIGKILL'), 100)
-  let signal
-  try {
+  test('kill() method works', async () => {
+    let p = $`sleep 9999`.nothrow()
+    setTimeout(() => {
+      p.kill()
+    }, 100)
     await p
-  } catch (p) {
-    signal = p.signal
-  }
-  assert.equal(signal, 'SIGKILL')
-})
+  })
 
-test('within() works', async () => {
-  let resolve, reject
-  let promise = new Promise((...args) => ([resolve, reject] = args))
+  test('a signal is passed with kill() method', async () => {
+    let p = $`while true; do :; done`
+    setTimeout(() => p.kill('SIGKILL'), 100)
+    let signal
+    try {
+      await p
+    } catch (p) {
+      signal = p.signal
+    }
+    assert.equal(signal, 'SIGKILL')
+  })
 
-  function yes() {
-    assert.equal($.verbose, true)
-    resolve()
-  }
+  test('within() works', async () => {
+    let resolve, reject
+    let promise = new Promise((...args) => ([resolve, reject] = args))
 
-  $.verbose = false
-  assert.equal($.verbose, false)
+    function yes() {
+      assert.equal($.verbose, true)
+      resolve()
+    }
 
-  within(() => {
-    $.verbose = true
-  })
-  assert.equal($.verbose, false)
+    $.verbose = false
+    assert.equal($.verbose, false)
 
-  within(async () => {
-    $.verbose = true
-    setTimeout(yes, 10)
-  })
-  assert.equal($.verbose, false)
+    within(() => {
+      $.verbose = true
+    })
+    assert.equal($.verbose, false)
 
-  await promise
-})
+    within(async () => {
+      $.verbose = true
+      setTimeout(yes, 10)
+    })
+    assert.equal($.verbose, false)
 
-test('within() restores previous cwd', async () => {
-  let resolve, reject
-  let promise = new Promise((...args) => ([resolve, reject] = args))
+    await promise
+  })
 
-  let pwd = await $`pwd`
+  test('within() restores previous cwd', async () => {
+    let resolve, reject
+    let promise = new Promise((...args) => ([resolve, reject] = args))
 
-  within(async () => {
-    $.verbose = false
-    cd('/tmp')
-    setTimeout(async () => {
-      assert.match((await $`pwd`).stdout, '/tmp')
-      resolve()
-    }, 1000)
-  })
+    let pwd = await $`pwd`
 
-  assert.equal((await $`pwd`).stdout, pwd.stdout)
-  await promise
-})
+    within(async () => {
+      $.verbose = false
+      cd('/tmp')
+      setTimeout(async () => {
+        assert.ok((await $`pwd`).stdout.trim().endsWith('/tmp'))
+        resolve()
+      }, 1000)
+    })
 
-test(`within() isolates nested context and returns cb result`, async () => {
-  within(async () => {
-    const res = await within(async () => {
-      $.verbose = true
+    assert.equal((await $`pwd`).stdout, pwd.stdout)
+    await promise
+  })
 
-      return within(async () => {
-        assert.equal($.verbose, true)
-        $.verbose = false
+  test(`within() isolates nested context and returns cb result`, async () => {
+    within(async () => {
+      const res = await within(async () => {
+        $.verbose = true
 
         return within(async () => {
-          assert.equal($.verbose, false)
-          $.verbose = true
-          return 'foo'
+          assert.equal($.verbose, true)
+          $.verbose = false
+
+          return within(async () => {
+            assert.equal($.verbose, false)
+            $.verbose = true
+            return 'foo'
+          })
         })
       })
+      assert.equal($.verbose, false)
+      assert.equal(res, 'foo')
     })
-    assert.equal($.verbose, false)
-    assert.equal(res, 'foo')
   })
-})
 
-test('stdio() works', async () => {
-  let p = $`printf foo`
-  await p
-  assert.throws(() => p.stdin)
-  assert.is((await p).stdout, 'foo')
+  test('stdio() works', async () => {
+    let p = $`printf foo`
+    await p
+    assert.throws(() => p.stdin)
+    assert.equal((await p).stdout, 'foo')
 
-  let b = $`read; printf $REPLY`
-  b.stdin.write('bar\n')
-  assert.is((await b).stdout, 'bar')
-})
+    let b = $`read; printf $REPLY`
+    b.stdin.write('bar\n')
+    assert.equal((await b).stdout, 'bar')
+  })
 
-test('snapshots works', async () => {
-  await within(async () => {
-    $.prefix += 'echo success;'
-    let p = $`:`
-    $.prefix += 'echo fail;'
-    let out = await p
-    assert.is(out.stdout, 'success\n')
-    assert.not.match(out.stdout, 'fail')
+  test('snapshots works', async () => {
+    await within(async () => {
+      $.prefix += 'echo success;'
+      let p = $`:`
+      $.prefix += 'echo fail;'
+      let out = await p
+      assert.equal(out.stdout, 'success\n')
+      assert.doesNotMatch(out.stdout, /fail/)
+    })
   })
-})
 
-test('timeout() works', async () => {
-  let exitCode, signal
-  try {
-    await $`sleep 9999`.timeout(10, 'SIGKILL')
-  } catch (p) {
-    exitCode = p.exitCode
-    signal = p.signal
-  }
-  assert.is(exitCode, null)
-  assert.is(signal, 'SIGKILL')
-})
+  test('timeout() works', async () => {
+    let exitCode, signal
+    try {
+      await $`sleep 9999`.timeout(10, 'SIGKILL')
+    } catch (p) {
+      exitCode = p.exitCode
+      signal = p.signal
+    }
+    assert.equal(exitCode, null)
+    assert.equal(signal, 'SIGKILL')
+  })
 
-test('timeout() expiration works', async () => {
-  let exitCode, signal
-  try {
-    await $`sleep 1`.timeout(999)
-  } catch (p) {
-    exitCode = p.exitCode
-    signal = p.signal
-  }
-  assert.is(exitCode, undefined)
-  assert.is(signal, undefined)
-})
+  test('timeout() expiration works', async () => {
+    let exitCode, signal
+    try {
+      await $`sleep 1`.timeout(999)
+    } catch (p) {
+      exitCode = p.exitCode
+      signal = p.signal
+    }
+    assert.equal(exitCode, undefined)
+    assert.equal(signal, undefined)
+  })
 
-test('$ thrown as error', async () => {
-  let err
-  try {
-    await $`wtf`
-  } catch (p) {
-    err = p
-  }
-  assert.ok(err.exitCode > 0)
-  assert.ok(err.stderr.includes('wtf: command not found'))
-  assert.ok(err[inspect.custom]().includes('Command not found'))
-})
+  test('$ thrown as error', async () => {
+    let err
+    try {
+      await $`wtf`
+    } catch (p) {
+      err = p
+    }
+    assert.ok(err.exitCode > 0)
+    assert.ok(err.stderr.includes('wtf: command not found'))
+    assert.ok(err[inspect.custom]().includes('Command not found'))
+  })
 
-test('error event is handled', async () => {
-  await within(async () => {
-    $.cwd = 'wtf'
+  test('error event is handled', async () => {
+    await within(async () => {
+      $.cwd = 'wtf'
+      try {
+        await $`pwd`
+        assert.unreachable('should have thrown')
+      } catch (err) {
+        assert.ok(err instanceof ProcessOutput)
+        assert.match(err.message, /No such file or directory/)
+      }
+    })
+  })
+
+  test('pipe() throws if already resolved', async (t) => {
+    let ok = true
+    let p = $`echo "Hello"`
+    await p
     try {
-      await $`pwd`
-      assert.unreachable('should have thrown')
+      await p.pipe($`less`)
+      ok = false
     } catch (err) {
-      assert.instance(err, ProcessOutput)
-      assert.match(err.message, /No such file or directory/)
+      assert.equal(
+        err.message,
+        `The pipe() method shouldn't be called after promise is already resolved!`
+      )
     }
+    assert.ok(ok, 'Expected failure!')
   })
-})
-
-test('pipe() throws if already resolved', async (t) => {
-  let ok = true
-  let p = $`echo "Hello"`
-  await p
-  try {
-    await p.pipe($`less`)
-    ok = false
-  } catch (err) {
-    assert.is(
-      err.message,
-      `The pipe() method shouldn't be called after promise is already resolved!`
-    )
-  }
-  assert.ok(ok, 'Expected failure!')
-})
 
-test('await $`cmd`.exitCode does not throw', async () => {
-  assert.is.not(await $`grep qwerty README.md`.exitCode, 0)
-  assert.is(await $`[[ -f README.md ]]`.exitCode, 0)
-})
+  test('await $`cmd`.exitCode does not throw', async () => {
+    assert.notEqual(await $`grep qwerty README.md`.exitCode, 0)
+    assert.equal(await $`[[ -f README.md ]]`.exitCode, 0)
+  })
 
-test('nothrow() do not throw', async () => {
-  let { exitCode } = await $`exit 42`.nothrow()
-  assert.is(exitCode, 42)
-  {
-    // Deprecated.
-    let { exitCode } = await nothrow($`exit 42`)
-    assert.is(exitCode, 42)
-  }
-})
+  test('nothrow() do not throw', async () => {
+    let { exitCode } = await $`exit 42`.nothrow()
+    assert.equal(exitCode, 42)
+    {
+      // Deprecated.
+      let { exitCode } = await nothrow($`exit 42`)
+      assert.equal(exitCode, 42)
+    }
+  })
 
-test('malformed cmd error', async () => {
-  assert.throws(() => $`\033`, /malformed/i)
-})
+  test('malformed cmd error', async () => {
+    assert.throws(() => $`\033`, /malformed/i)
+  })
 
-test('$ is a regular function', async () => {
-  const _$ = $.bind(null)
-  let foo = await _$`echo foo`
-  assert.is(foo.stdout, 'foo\n')
-  assert.ok(typeof $.call === 'function')
-  assert.ok(typeof $.apply === 'function')
-})
+  test('$ is a regular function', async () => {
+    const _$ = $.bind(null)
+    let foo = await _$`echo foo`
+    assert.equal(foo.stdout, 'foo\n')
+    assert.ok(typeof $.call === 'function')
+    assert.ok(typeof $.apply === 'function')
+  })
 
-test('halt() works', async () => {
-  let filepath = `/tmp/${Math.random().toString()}`
-  let p = $`touch ${filepath}`.halt()
-  await sleep(1)
-  assert.not.ok(
-    fs.existsSync(filepath),
-    'The cmd called, but it should not have been called'
-  )
-  await p.run()
-  assert.ok(fs.existsSync(filepath), 'The cmd should have been called')
-})
+  test('halt() works', async () => {
+    let filepath = `/tmp/${Math.random().toString()}`
+    let p = $`touch ${filepath}`.halt()
+    await sleep(1)
+    assert.ok(
+      !fs.existsSync(filepath),
+      'The cmd called, but it should not have been called'
+    )
+    await p.run()
+    assert.ok(fs.existsSync(filepath), 'The cmd should have been called')
+  })
 
-test('await on halted throws', async () => {
-  let p = $`sleep 1`.halt()
-  let ok = true
-  try {
-    await p
-    ok = false
-  } catch (err) {
-    assert.is(err.message, 'The process is halted!')
-  }
-  assert.ok(ok, 'Expected failure!')
+  test('await on halted throws', async () => {
+    let p = $`sleep 1`.halt()
+    let ok = true
+    try {
+      await p
+      ok = false
+    } catch (err) {
+      assert.equal(err.message, 'The process is halted!')
+    }
+    assert.ok(ok, 'Expected failure!')
+  })
 })
-
-test.run()
test/deps.test.js
@@ -12,74 +12,77 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe, before, beforeEach } from 'node:test'
 import { $ } from '../build/index.js'
 import { installDeps, parseDeps } from '../build/deps.js'
 
-const test = suite('deps')
-
-$.verbose = false
+describe('deps', () => {
+  beforeEach(() => {
+    $.verbose = false
+  })
 
-test('installDeps() loader works via JS API', async () => {
-  await installDeps({
-    cpy: '9.0.1',
-    'lodash-es': '4.17.21',
+  test('installDeps() loader works via JS API', async () => {
+    await installDeps({
+      cpy: '9.0.1',
+      'lodash-es': '4.17.21',
+    })
+    assert((await import('cpy')).default instanceof Function)
+    assert((await import('lodash-es')).pick instanceof Function)
   })
-  assert.instance((await import('cpy')).default, Function)
-  assert.instance((await import('lodash-es')).pick, Function)
-})
 
-test('installDeps() loader works via CLI', async () => {
-  let out =
-    await $`node build/cli.js --install <<< 'import _ from "lodash" /* @4.17.15 */; console.log(_.VERSION)'`
-  assert.match(out.stdout, '4.17.15')
-})
+  test('installDeps() loader works via CLI', async () => {
+    let out =
+      await $`node build/cli.js --install <<< 'import _ from "lodash" /* @4.17.15 */; console.log(_.VERSION)'`
+    assert.match(out.stdout, /4.17.15/)
+  })
 
-test('parseDeps(): import or require', async () => {
-  ;[
-    [`import "foo"`, { foo: 'latest' }],
-    [`import "foo"`, { foo: 'latest' }],
-    [`import * as bar from "foo"`, { foo: 'latest' }],
-    [`import('foo')`, { foo: 'latest' }],
-    [`require('foo')`, { foo: 'latest' }],
-    [`require('foo/bar')`, { foo: 'latest' }],
-    [`require('foo/bar.js')`, { foo: 'latest' }],
-    [`require('foo-bar')`, { 'foo-bar': 'latest' }],
-    [`require('foo_bar')`, { foo_bar: 'latest' }],
-    [`require('@foo/bar')`, { '@foo/bar': 'latest' }],
-    [`require('@foo/bar/baz')`, { '@foo/bar': 'latest' }],
-    [`require('foo.js')`, { 'foo.js': 'latest' }],
+  test('parseDeps(): import or require', async () => {
+    ;[
+      [`import "foo"`, { foo: 'latest' }],
+      [`import "foo"`, { foo: 'latest' }],
+      [`import * as bar from "foo"`, { foo: 'latest' }],
+      [`import('foo')`, { foo: 'latest' }],
+      [`require('foo')`, { foo: 'latest' }],
+      [`require('foo/bar')`, { foo: 'latest' }],
+      [`require('foo/bar.js')`, { foo: 'latest' }],
+      [`require('foo-bar')`, { 'foo-bar': 'latest' }],
+      [`require('foo_bar')`, { foo_bar: 'latest' }],
+      [`require('@foo/bar')`, { '@foo/bar': 'latest' }],
+      [`require('@foo/bar/baz')`, { '@foo/bar': 'latest' }],
+      [`require('foo.js')`, { 'foo.js': 'latest' }],
 
-    // ignores local deps
-    [`import '.'`, {}],
-    [`require('.')`, {}],
-    [`require('..')`, {}],
-    [`require('../foo.js')`, {}],
-    [`require('./foo.js')`, {}],
+      // ignores local deps
+      [`import '.'`, {}],
+      [`require('.')`, {}],
+      [`require('..')`, {}],
+      [`require('../foo.js')`, {}],
+      [`require('./foo.js')`, {}],
 
-    // ignores invalid pkg names
-    [`require('_foo')`, {}],
-    [`require('@')`, {}],
-    [`require('@/_foo')`, {}],
-    [`require('@foo')`, {}],
-  ].forEach(([input, result]) => {
-    assert.equal(parseDeps(input), result)
+      // ignores invalid pkg names
+      [`require('_foo')`, {}],
+      [`require('@')`, {}],
+      [`require('@/_foo')`, {}],
+      [`require('@foo')`, {}],
+    ].forEach(([input, result]) => {
+      assert.deepEqual(parseDeps(input), result)
+    })
   })
-})
 
-test('parseDeps(): import with org and filename', async () => {
-  assert.equal(parseDeps(`import "@foo/bar/file"`), { '@foo/bar': 'latest' })
-})
+  test('parseDeps(): import with org and filename', async () => {
+    assert.deepEqual(parseDeps(`import "@foo/bar/file"`), {
+      '@foo/bar': 'latest',
+    })
+  })
 
-test('parseDeps(): import with version', async () => {
-  assert.equal(parseDeps(`import "foo" // @2.x`), { foo: '2.x' })
-  assert.equal(parseDeps(`import "foo" // @^7`), { foo: '^7' })
-  assert.equal(parseDeps(`import "foo" /* @1.2.x */`), { foo: '1.2.x' })
-})
+  test('parseDeps(): import with version', async () => {
+    assert.deepEqual(parseDeps(`import "foo" // @2.x`), { foo: '2.x' })
+    assert.deepEqual(parseDeps(`import "foo" // @^7`), { foo: '^7' })
+    assert.deepEqual(parseDeps(`import "foo" /* @1.2.x */`), { foo: '1.2.x' })
+  })
 
-test('parseDeps(): multiline', () => {
-  const contents = `
+  test('parseDeps(): multiline', () => {
+    const contents = `
   require('a') // @1.0.0
   const b =require('b') /* @2.0.0 */
   const c = {
@@ -105,25 +108,24 @@ test('parseDeps(): multiline', () => {
   const { pick } = require("lodash") //  @4.17.15
   `
 
-  assert.equal(parseDeps(contents), {
-    a: '1.0.0',
-    b: '2.0.0',
-    c: '3.0.0',
-    d: '4.0.0',
-    e: '5.0.0',
-    f: '6.0.0',
-    g: '7.0.0',
-    h: '8.0.0',
-    i: '9.0.0',
-    j: '10.0.0',
-    foo: 'latest',
-    bar: '1.0.0',
-    baz: '^2.0',
-    '@qux/pkg': '^3.0',
-    qux: '^4.0.0-beta.0',
-    cpy: 'latest',
-    lodash: '4.17.15',
+    assert.deepEqual(parseDeps(contents), {
+      a: '1.0.0',
+      b: '2.0.0',
+      c: '3.0.0',
+      d: '4.0.0',
+      e: '5.0.0',
+      f: '6.0.0',
+      g: '7.0.0',
+      h: '8.0.0',
+      i: '9.0.0',
+      j: '10.0.0',
+      foo: 'latest',
+      bar: '1.0.0',
+      baz: '^2.0',
+      '@qux/pkg': '^3.0',
+      qux: '^4.0.0-beta.0',
+      cpy: 'latest',
+      lodash: '4.17.15',
+    })
   })
 })
-
-test.run()
test/experimental.test.js
@@ -12,12 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import { describe, before } from 'node:test'
 import '../build/globals.js'
 
-const test = suite('experimental')
-
-$.verbose = false
-
-test.run()
+describe('experimental', () => {
+  before(() => {
+    $.verbose = false
+  })
+})
test/extra.test.js
@@ -12,35 +12,36 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import assert from 'node:assert'
 import fs from 'node:fs'
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import { test, describe } from 'node:test'
 import { globby } from 'globby'
 
-const test = suite('extra')
-
-test('every file should have a license', async () => {
-  const files = await globby(['**/*.{ts,js,mjs}'], { gitignore: true })
-  for (const file of files) {
-    const content = fs.readFileSync(file).toString()
-    assert.match(
-      content.replace(/\d{4}/g, 'YEAR'),
-      '// Copyright YEAR Google LLC\n' +
-        '//\n' +
-        '// Licensed under the Apache License, Version 2.0 (the "License");\n' +
-        '// you may not use this file except in compliance with the License.\n' +
-        '// You may obtain a copy of the License at\n' +
-        '//\n' +
-        '//     https://www.apache.org/licenses/LICENSE-2.0\n' +
-        '//\n' +
-        '// Unless required by applicable law or agreed to in writing, software\n' +
-        '// distributed under the License is distributed on an "AS IS" BASIS,\n' +
-        '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
-        '// See the License for the specific language governing permissions and\n' +
-        '// limitations under the License.',
-      `No license header in ${file}.`
-    )
-  }
+describe('extra', () => {
+  test('every file should have a license', async () => {
+    const files = await globby(['**/*.{ts,js,mjs}'], { gitignore: true })
+    for (const file of files) {
+      const content = fs.readFileSync(file).toString()
+      assert(
+        content
+          .replace(/\d{4}/g, 'YEAR')
+          .includes(
+            '// Copyright YEAR Google LLC\n' +
+              '//\n' +
+              '// Licensed under the Apache License, Version 2.0 (the "License");\n' +
+              '// you may not use this file except in compliance with the License.\n' +
+              '// You may obtain a copy of the License at\n' +
+              '//\n' +
+              '//     https://www.apache.org/licenses/LICENSE-2.0\n' +
+              '//\n' +
+              '// Unless required by applicable law or agreed to in writing, software\n' +
+              '// distributed under the License is distributed on an "AS IS" BASIS,\n' +
+              '// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
+              '// See the License for the specific language governing permissions and\n' +
+              '// limitations under the License.'
+          ),
+        `No license header in ${file}.`
+      )
+    }
+  })
 })
-
-test.run()
test/global.test.js
@@ -12,25 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe } from 'node:test'
 import '../build/globals.js'
 import * as index from '../build/index.js'
 
-const test = suite('global')
+describe('global', () => {
+  test('global cd()', async () => {
+    const cwd = (await $`pwd`).toString().trim()
+    cd('/')
+    assert.equal((await $`pwd`).toString().trim(), path.resolve('/'))
+    cd(cwd)
+    assert.equal((await $`pwd`).toString().trim(), cwd)
+  })
 
-test('global cd()', async () => {
-  const cwd = (await $`pwd`).toString().trim()
-  cd('/')
-  assert.is((await $`pwd`).toString().trim(), path.resolve('/'))
-  cd(cwd)
-  assert.is((await $`pwd`).toString().trim(), cwd)
+  test('injects zx index to global', () => {
+    for (let [key, value] of Object.entries(index)) {
+      assert.equal(global[key], value)
+    }
+  })
 })
-
-test('injects zx index to global', () => {
-  for (let [key, value] of Object.entries(index)) {
-    assert.is(global[key], value)
-  }
-})
-
-test.run()
test/goods.test.js
@@ -13,100 +13,101 @@
 // limitations under the License.
 
 import chalk from 'chalk'
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe, beforeEach } from 'node:test'
 import '../build/globals.js'
 
-const test = suite('goods')
+describe('goods', () => {
+  beforeEach(() => {
+    $.verbose = false
+  })
 
-$.verbose = false
-
-function zx(script) {
-  return $`node build/cli.js --eval ${script}`.nothrow().timeout('5s')
-}
+  function zx(script) {
+    return $`node build/cli.js --eval ${script}`.nothrow().timeout('5s')
+  }
 
-test('question() works', async () => {
-  let p = $`node build/cli.js --eval "
+  test('question() works', async () => {
+    let p = $`node build/cli.js --eval "
   let answer = await question('foo or bar? ', { choices: ['foo', 'bar'] })
   echo('Answer is', answer)
 "`
-  p.stdin.write('foo\n')
-  p.stdin.end()
-  assert.match((await p).stdout, 'Answer is foo')
-})
-
-test('globby available', async () => {
-  assert.is(globby, glob)
-  assert.is(typeof globby, 'function')
-  assert.is(typeof globby.globbySync, 'function')
-  assert.is(typeof globby.globbyStream, 'function')
-  assert.is(typeof globby.generateGlobTasks, 'function')
-  assert.is(typeof globby.isDynamicPattern, 'function')
-  assert.is(typeof globby.isGitIgnored, 'function')
-  assert.is(typeof globby.isGitIgnoredSync, 'function')
-  assert.equal(await globby('*.md'), ['README.md'])
-})
-
-test('fetch() works', async () => {
-  assert.match(
-    await fetch('https://medv.io').then((res) => res.text()),
-    /Anton Medvedev/
-  )
-})
-
-test('echo() works', async () => {
-  let stdout = ''
-  let log = console.log
-  console.log = (...args) => {
-    stdout += args.join(' ')
-  }
-  echo(chalk.cyan('foo'), chalk.green('bar'), chalk.bold('baz'))
-  echo`${chalk.cyan('foo')} ${chalk.green('bar')} ${chalk.bold('baz')}`
-  echo(
-    await $`echo ${chalk.cyan('foo')}`,
-    await $`echo ${chalk.green('bar')}`,
-    await $`echo ${chalk.bold('baz')}`
-  )
-  console.log = log
-  assert.match(stdout, 'foo')
-})
-
-test('YAML works', async () => {
-  assert.equal(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
-})
-
-test('which() available', async () => {
-  assert.is(which.sync('npm'), await which('npm'))
-})
-
-test('minimist available', async () => {
-  assert.is(typeof minimist, 'function')
-})
-
-test('minimist works', async () => {
-  assert.equal(
-    minimist(
-      ['--foo', 'bar', '-a', '5', '-a', '42', '--force', './some.file'],
-      { boolean: 'force' }
-    ),
-    {
-      a: [5, 42],
-      foo: 'bar',
-      force: true,
-      _: ['./some.file'],
+    p.stdin.write('foo\n')
+    p.stdin.end()
+    assert.match((await p).stdout, /Answer is foo/)
+  })
+
+  test('globby available', async () => {
+    assert.equal(globby, glob)
+    assert.equal(typeof globby, 'function')
+    assert.equal(typeof globby.globbySync, 'function')
+    assert.equal(typeof globby.globbyStream, 'function')
+    assert.equal(typeof globby.generateGlobTasks, 'function')
+    assert.equal(typeof globby.isDynamicPattern, 'function')
+    assert.equal(typeof globby.isGitIgnored, 'function')
+    assert.equal(typeof globby.isGitIgnoredSync, 'function')
+    assert.deepEqual(await globby('*.md'), ['README.md'])
+  })
+
+  test('fetch() works', async () => {
+    assert.match(
+      await fetch('https://medv.io').then((res) => res.text()),
+      /Anton Medvedev/
+    )
+  })
+
+  test('echo() works', async () => {
+    let stdout = ''
+    let log = console.log
+    console.log = (...args) => {
+      stdout += args.join(' ')
     }
-  )
-})
-
-test('sleep() works', async () => {
-  const now = Date.now()
-  await sleep(100)
-  assert.ok(Date.now() >= now + 99)
-})
-
-test('retry() works', async () => {
-  const now = Date.now()
-  let p = await zx(`
+    echo(chalk.cyan('foo'), chalk.green('bar'), chalk.bold('baz'))
+    echo`${chalk.cyan('foo')} ${chalk.green('bar')} ${chalk.bold('baz')}`
+    echo(
+      await $`echo ${chalk.cyan('foo')}`,
+      await $`echo ${chalk.green('bar')}`,
+      await $`echo ${chalk.bold('baz')}`
+    )
+    console.log = log
+    assert.match(stdout, /foo/)
+  })
+
+  test('YAML works', async () => {
+    assert.deepEqual(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
+  })
+
+  test('which() available', async () => {
+    assert.equal(which.sync('npm'), await which('npm'))
+  })
+
+  test('minimist available', async () => {
+    assert.equal(typeof minimist, 'function')
+  })
+
+  test('minimist works', async () => {
+    assert.deepEqual(
+      minimist(
+        ['--foo', 'bar', '-a', '5', '-a', '42', '--force', './some.file'],
+        { boolean: 'force' }
+      ),
+      {
+        a: [5, 42],
+        foo: 'bar',
+        force: true,
+        _: ['./some.file'],
+      }
+    )
+  })
+
+  test('sleep() works', async () => {
+    const now = Date.now()
+    await sleep(100)
+    assert.ok(Date.now() >= now + 99)
+  })
+
+  test('retry() works', async () => {
+    const now = Date.now()
+    let p = await zx(`
     try {
       await retry(5, '50ms', () => $\`exit 123\`)
     } catch (e) {
@@ -115,14 +116,14 @@ test('retry() works', async () => {
     await retry(5, () => $\`exit 0\`)
     echo('success')
 `)
-  assert.match(p.toString(), 'exitCode: 123')
-  assert.match(p.toString(), 'success')
-  assert.ok(Date.now() >= now + 50 * (5 - 1))
-})
-
-test('retry() with expBackoff() works', async () => {
-  const now = Date.now()
-  let p = await zx(`
+    assert.ok(p.toString().includes('exitCode: 123'))
+    assert.ok(p.toString().includes('success'))
+    assert.ok(Date.now() >= now + 50 * (5 - 1))
+  })
+
+  test('retry() with expBackoff() works', async () => {
+    const now = Date.now()
+    let p = await zx(`
     try {
       await retry(5, expBackoff('60s', 0), () => $\`exit 123\`)
     } catch (e) {
@@ -130,37 +131,36 @@ test('retry() with expBackoff() works', async () => {
     }
     echo('success')
 `)
-  assert.match(p.toString(), 'exitCode: 123')
-  assert.match(p.toString(), 'success')
-  assert.ok(Date.now() >= now + 2 + 4 + 8 + 16 + 32)
-})
+    assert.ok(p.toString().includes('exitCode: 123'))
+    assert.ok(p.toString().includes('success'))
+    assert.ok(Date.now() >= now + 2 + 4 + 8 + 16 + 32)
+  })
 
-test('spinner() works', async () => {
-  let out = await zx(`
+  test('spinner() works', async () => {
+    let out = await zx(`
     echo(await spinner(async () => {
       await sleep(100)
       await $\`echo hidden\`
       return $\`echo result\`
     }))
   `)
-  assert.match(out.stdout, 'result')
-  assert.not.match(out.stderr, 'result')
-  assert.not.match(out.stderr, 'hidden')
-})
+    assert(out.stdout.includes('result'))
+    assert(!out.stderr.includes('result'))
+    assert(!out.stderr.includes('hidden'))
+  })
 
-test('spinner() with title works', async () => {
-  let out = await zx(`
+  test('spinner() with title works', async () => {
+    let out = await zx(`
     await spinner('processing', () => sleep(100))
   `)
-  assert.match(out.stderr, 'processing')
-})
+    assert.match(out.stderr, /processing/)
+  })
 
-test('spinner() stops on throw', async () => {
-  let out = await zx(`
+  test('spinner() stops on throw', async () => {
+    let out = await zx(`
     await spinner('processing', () => $\`wtf-cmd\`)
   `)
-  assert.match(out.stderr, 'Error:')
-  assert.is.not(out.exitCode, 0)
+    assert.match(out.stderr, /Error:/)
+    assert(out.exitCode !== 0)
+  })
 })
-
-test.run()
test/package.test.js
@@ -12,46 +12,44 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe, beforeEach } from 'node:test'
 import '../build/globals.js'
 
-const test = suite('package')
-
-test.before.each(async () => {
-  $.verbose = false
-  const pack = await $`npm pack`
-  await $`tar xf ${pack}`
-  await $`rm ${pack}`.nothrow()
-})
+describe('package', () => {
+  beforeEach(async () => {
+    $.verbose = false
+    const pack = await $`npm pack`
+    await $`tar xf ${pack}`
+    await $`rm ${pack}`.nothrow()
+  })
 
-test('ts project', async () => {
-  const pack = path.resolve('package')
-  const out = await within(async () => {
-    cd('test/fixtures/ts-project')
-    await $`npm i`
-    await $`rm -rf node_modules/zx`
-    await $`mv ${pack} node_modules/zx`
-    try {
-      await $`npx tsc`
-    } catch (err) {
-      throw new Error(err.stdout)
-    }
-    return $`node build/script.js`
+  test('ts project', async () => {
+    const pack = path.resolve('package')
+    const out = await within(async () => {
+      cd('test/fixtures/ts-project')
+      await $`npm i`
+      await $`rm -rf node_modules/zx`
+      await $`mv ${pack} node_modules/zx`
+      try {
+        await $`npx tsc`
+      } catch (err) {
+        throw new Error(err.stdout)
+      }
+      return $`node build/script.js`
+    })
+    assert.match(out.stderr, /ts-script/)
   })
-  assert.match(out.stderr, 'ts-script')
-})
 
-test('js project with zx', async () => {
-  const pack = path.resolve('package')
-  const out = await within(async () => {
-    cd('test/fixtures/js-project')
-    await $`rm -rf node_modules`
-    await $`mkdir node_modules`
-    await $`mv ${pack} node_modules/zx`
-    return $`node node_modules/zx/build/cli.js script.js`
+  test('js project with zx', async () => {
+    const pack = path.resolve('package')
+    const out = await within(async () => {
+      cd('test/fixtures/js-project')
+      await $`rm -rf node_modules`
+      await $`mkdir node_modules`
+      await $`mv ${pack} node_modules/zx`
+      return $`node node_modules/zx/build/cli.js script.js`
+    })
+    assert.match(out.stderr, /js-script/)
   })
-  assert.match(out.stderr, 'js-script')
 })
-
-test.run()
test/util.test.js
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe } from 'node:test'
 import {
   exitCodeInfo,
   errnoMessage,
@@ -26,70 +26,68 @@ import {
   randomId,
 } from '../build/util.js'
 
-const test = suite('util')
+describe('util', () => {
+  test('exitCodeInfo()', () => {
+    assert.equal(exitCodeInfo(2), 'Misuse of shell builtins')
+  })
 
-test('exitCodeInfo()', () => {
-  assert.is(exitCodeInfo(2), 'Misuse of shell builtins')
-})
+  test('errnoMessage()', () => {
+    assert.equal(errnoMessage(-2), 'No such file or directory')
+    assert.equal(errnoMessage(1e9), 'Unknown error')
+    assert.equal(errnoMessage(undefined), 'Unknown error')
+  })
 
-test('errnoMessage()', () => {
-  assert.is(errnoMessage(-2), 'No such file or directory')
-  assert.is(errnoMessage(1e9), 'Unknown error')
-  assert.is(errnoMessage(undefined), 'Unknown error')
-})
+  test('randomId()', () => {
+    assert.ok(/^[a-z0-9]+$/.test(randomId()))
+    assert.ok(
+      new Set(Array.from({ length: 1000 }).map(() => randomId())).size === 1000
+    )
+  })
 
-test('randomId()', () => {
-  assert.ok(/^[a-z0-9]+$/.test(randomId()))
-  assert.ok(
-    new Set(Array.from({ length: 1000 }).map(() => randomId())).size === 1000
-  )
-})
-
-test('noop()', () => {
-  assert.ok(noop() === undefined)
-})
+  test('noop()', () => {
+    assert.ok(noop() === undefined)
+  })
 
-test('isString()', () => {
-  assert.ok(isString('string'))
-  assert.not.ok(isString(1))
-})
+  test('isString()', () => {
+    assert.ok(isString('string'))
+    assert.ok(!isString(1))
+  })
 
-test('quote()', () => {
-  assert.ok(quote('string') === 'string')
-  assert.ok(quote(`'\f\n\r\t\v\0`) === `$'\\'\\f\\n\\r\\t\\v\\0'`)
-})
+  test('quote()', () => {
+    assert.ok(quote('string') === 'string')
+    assert.ok(quote(`'\f\n\r\t\v\0`) === `$'\\'\\f\\n\\r\\t\\v\\0'`)
+  })
 
-test('quotePowerShgell()', () => {
-  assert.is(quotePowerShell('string'), 'string')
-  assert.is(quotePowerShell(`'`), `''''`)
-})
+  test('quotePowerShgell()', () => {
+    assert.equal(quotePowerShell('string'), 'string')
+    assert.equal(quotePowerShell(`'`), `''''`)
+  })
 
-test('duration parsing works', () => {
-  assert.is(parseDuration(1000), 1000)
-  assert.is(parseDuration('2s'), 2000)
-  assert.is(parseDuration('500ms'), 500)
-  assert.throws(() => parseDuration('100'))
-  assert.throws(() => parseDuration(NaN))
-  assert.throws(() => parseDuration(-1))
-})
+  test('duration parsing works', () => {
+    assert.equal(parseDuration(1000), 1000)
+    assert.equal(parseDuration('2s'), 2000)
+    assert.equal(parseDuration('500ms'), 500)
+    assert.throws(() => parseDuration('100'))
+    assert.throws(() => parseDuration(NaN))
+    assert.throws(() => parseDuration(-1))
+  })
 
-test('formatCwd works', () => {
-  assert.is(
-    formatCmd(`echo $'hi'`),
-    "$ \u001b[92mecho\u001b[39m \u001b[93m$\u001b[39m\u001b[93m'hi\u001b[39m\u001b[93m'\u001b[39m\n"
-  )
-  assert.is(
-    formatCmd(`while true; do "$" done`),
-    '$ \u001b[96mwhile\u001b[39m \u001b[92mtrue\u001b[39m\u001b[96m;\u001b[39m \u001b[96mdo\u001b[39m \u001b[93m"$\u001b[39m\u001b[93m"\u001b[39m \u001b[96mdone\u001b[39m\n'
-  )
-  assert.is(
-    formatCmd(`echo '\n str\n'`),
-    "$ \u001b[92mecho\u001b[39m \u001b[93m'\u001b[39m\n> \u001b[93m str\u001b[39m\n> \u001b[93m'\u001b[39m\n"
-  )
-  assert.is(
-    formatCmd(`$'\\''`),
-    "$ \u001b[93m$\u001b[39m\u001b[93m'\u001b[39m\u001b[93m\\\u001b[39m\u001b[93m'\u001b[39m\u001b[93m'\u001b[39m\n"
-  )
+  test('formatCwd works', () => {
+    assert.equal(
+      formatCmd(`echo $'hi'`),
+      "$ \u001b[92mecho\u001b[39m \u001b[93m$\u001b[39m\u001b[93m'hi\u001b[39m\u001b[93m'\u001b[39m\n"
+    )
+    assert.equal(
+      formatCmd(`while true; do "$" done`),
+      '$ \u001b[96mwhile\u001b[39m \u001b[92mtrue\u001b[39m\u001b[96m;\u001b[39m \u001b[96mdo\u001b[39m \u001b[93m"$\u001b[39m\u001b[93m"\u001b[39m \u001b[96mdone\u001b[39m\n'
+    )
+    assert.equal(
+      formatCmd(`echo '\n str\n'`),
+      "$ \u001b[92mecho\u001b[39m \u001b[93m'\u001b[39m\n> \u001b[93m str\u001b[39m\n> \u001b[93m'\u001b[39m\n"
+    )
+    assert.equal(
+      formatCmd(`$'\\''`),
+      "$ \u001b[93m$\u001b[39m\u001b[93m'\u001b[39m\u001b[93m\\\u001b[39m\u001b[93m'\u001b[39m\u001b[93m'\u001b[39m\n"
+    )
+  })
 })
-
-test.run()
test/win32.test.js
@@ -12,34 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { suite } from 'uvu'
-import * as assert from 'uvu/assert'
+import assert from 'node:assert'
+import { test, describe, before, beforeEach } from 'node:test'
 import '../build/globals.js'
 
-const test = suite('win32')
-
-$.verbose = false
-
-if (process.platform === 'win32') {
-  test('should work with windows-specific commands', async () => {
-    const p = await $`echo $0` // Bash is first by default.
-    assert.match(p.stdout, /bash/)
-    await within(async () => {
-      $.shell = which.sync('powershell.exe')
-      $.quote = quotePowerShell
-      const p = await $`get-host`
-      assert.match(p.stdout, /PowerShell/)
-    })
+describe('win32', () => {
+  beforeEach(() => {
+    $.verbose = false
   })
 
-  test('quotePowerShell works', async () => {
-    await within(async () => {
-      $.shell = which.sync('powershell.exe')
-      $.quote = quotePowerShell
-      const p = await $`echo ${`Windows 'rulez!'`}`
-      assert.match(p.stdout, /Windows 'rulez!'/)
+  if (process.platform === 'win32') {
+    test('should work with windows-specific commands', async () => {
+      const p = await $`echo $0` // Bash is first by default.
+      assert.match(p.stdout, /bash/)
+      await within(async () => {
+        $.shell = which.sync('powershell.exe')
+        $.quote = quotePowerShell
+        const p = await $`get-host`
+        assert.match(p.stdout, /PowerShell/)
+      })
     })
-  })
-}
 
-test.run()
+    test('quotePowerShell works', async () => {
+      await within(async () => {
+        $.shell = which.sync('powershell.exe')
+        $.quote = quotePowerShell
+        const p = await $`echo ${`Windows 'rulez!'`}`
+        assert.match(p.stdout, /Windows 'rulez!'/)
+      })
+    })
+  }
+})
package-lock.json
@@ -34,8 +34,7 @@
         "madge": "^6.0.0",
         "prettier": "^2.8.8",
         "tsd": "^0.28.1",
-        "typescript": "^5.0.4",
-        "uvu": "^0.5.6"
+        "typescript": "^5.0.4"
       },
       "engines": {
         "node": ">= 16.0.0"
@@ -106,9 +105,9 @@
       }
     },
     "node_modules/@babel/core/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
@@ -170,9 +169,9 @@
       }
     },
     "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
@@ -208,9 +207,9 @@
       }
     },
     "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
@@ -1800,15 +1799,6 @@
         "node": ">=4.2.0"
       }
     },
-    "node_modules/dequal": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
-      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/detective-amd": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-4.0.1.tgz",
@@ -1955,15 +1945,6 @@
         "node": ">=4.2.0"
       }
     },
-    "node_modules/diff": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.3.1"
-      }
-    },
     "node_modules/diff-match-patch": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
@@ -3237,15 +3218,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/kleur": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
-      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/levn": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -3629,9 +3601,9 @@
       }
     },
     "node_modules/make-dir/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
@@ -3880,15 +3852,6 @@
         "node": "*"
       }
     },
-    "node_modules/mri": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
-      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -4912,9 +4875,9 @@
       }
     },
     "node_modules/read-pkg/node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "dev": true,
       "bin": {
         "semver": "bin/semver"
@@ -5178,18 +5141,6 @@
         "tslib": "^2.1.0"
       }
     },
-    "node_modules/sade": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
-      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
-      "dev": true,
-      "dependencies": {
-        "mri": "^1.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -5224,9 +5175,9 @@
       "dev": true
     },
     "node_modules/semver": {
-      "version": "7.3.7",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
-      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
@@ -5955,24 +5906,6 @@
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
       "dev": true
     },
-    "node_modules/uvu": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
-      "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
-      "dev": true,
-      "dependencies": {
-        "dequal": "^2.0.0",
-        "diff": "^5.0.0",
-        "kleur": "^4.0.3",
-        "sade": "^1.7.3"
-      },
-      "bin": {
-        "uvu": "bin.js"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/v8-to-istanbul": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
@@ -6052,9 +5985,9 @@
       }
     },
     "node_modules/word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"
@@ -6237,9 +6170,9 @@
       },
       "dependencies": {
         "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
           "dev": true
         }
       }
@@ -6288,9 +6221,9 @@
           }
         },
         "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
           "dev": true
         },
         "yallist": {
@@ -6319,9 +6252,9 @@
       },
       "dependencies": {
         "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
           "dev": true
         }
       }
@@ -7486,12 +7419,6 @@
         }
       }
     },
-    "dequal": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
-      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
-      "dev": true
-    },
     "detective-amd": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-4.0.1.tgz",
@@ -7602,12 +7529,6 @@
         }
       }
     },
-    "diff": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-      "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
-      "dev": true
-    },
     "diff-match-patch": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
@@ -8520,12 +8441,6 @@
       "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
       "dev": true
     },
-    "kleur": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
-      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
-      "dev": true
-    },
     "levn": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -8793,9 +8708,9 @@
       },
       "dependencies": {
         "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "version": "6.3.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+          "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
           "dev": true
         }
       }
@@ -8977,12 +8892,6 @@
         }
       }
     },
-    "mri": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
-      "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
-      "dev": true
-    },
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -9639,9 +9548,9 @@
           }
         },
         "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "version": "5.7.2",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+          "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
           "dev": true
         },
         "type-fest": {
@@ -9879,15 +9788,6 @@
         "tslib": "^2.1.0"
       }
     },
-    "sade": {
-      "version": "1.8.1",
-      "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
-      "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
-      "dev": true,
-      "requires": {
-        "mri": "^1.1.0"
-      }
-    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -9918,9 +9818,9 @@
       }
     },
     "semver": {
-      "version": "7.3.7",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
-      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
       "dev": true,
       "requires": {
         "lru-cache": "^6.0.0"
@@ -10456,18 +10356,6 @@
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
       "dev": true
     },
-    "uvu": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
-      "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
-      "dev": true,
-      "requires": {
-        "dequal": "^2.0.0",
-        "diff": "^5.0.0",
-        "kleur": "^4.0.3",
-        "sade": "^1.7.3"
-      }
-    },
     "v8-to-istanbul": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
@@ -10529,9 +10417,9 @@
       }
     },
     "word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
       "dev": true
     },
     "wrap-ansi": {
package.json
@@ -43,9 +43,9 @@
     "fmt:check": "prettier --check .",
     "build": "tsc --project tsconfig.prod.json",
     "build:check": "tsc",
-    "test": "npm run build && uvu test -i fixtures",
+    "test": "npm run build && node ./test/all.test.js",
     "test:types": "tsd",
-    "coverage": "c8 --check-coverage npm test -- -i package",
+    "coverage": "c8 --check-coverage npm test",
     "mutation": "stryker run",
     "circular": "madge --circular src/*",
     "version": "cat package.json | fx .version"
@@ -73,8 +73,7 @@
     "madge": "^6.0.0",
     "prettier": "^2.8.8",
     "tsd": "^0.28.1",
-    "typescript": "^5.0.4",
-    "uvu": "^0.5.6"
+    "typescript": "^5.0.4"
   },
   "publishConfig": {
     "registry": "https://wombat-dressing-room.appspot.com"