Skip to content
substrate-systemPublic

About

CLI for tests in Cloudflare runtime

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

cfout

tests types module install size semantic versioning license

Run tests in the Cloudflare Workers runtime. Pipe in a bundled test script, get TAP out, exit non-zero on failure.

The usual way to test inside the real runtime (workerd) is vitest with @cloudflare/vitest-pool-workers, which means adopting vitest, a vite config, a wrangler config and a pool plugin.

cfout reads one bundled JavaScript module from stdin, runs it in workerd locally via Miniflare, streams its console output to the terminal, and exits with a code that reflects the result. There is no config file. Flags mirror tapout, which does the same thing for browsers.

Contents

Install

npm i -D @substrate-system/cfout

Requires Node 22 or newer. The only runtime dependency is miniflare, which brings the workerd binary. wrangler is not needed.

Example

npx esbuild --bundle test/index.ts --format=esm | npx cfout | npx tap-spec
// test/index.ts
import { test } from '@substrate-system/tapzero'

test('example', async t => {
    await new Promise(resolve => setTimeout(resolve, 10))
    t.ok(true, 'timers work')
})

In package.json:

{
  "scripts": {
    "test": "esbuild --bundle test/index.ts --format=esm | cfout | tap-spec"
  }
}

Input

The input via stdin must be one fully bundled ES module.

  • No unresolved bare imports (import x from 'some-package').
  • Static imports of Node built-ins (node:*) are allowed, but only work with --compat-flag nodejs_compat. Mark them external when bundling:
    npx esbuild --bundle test/index.ts --format=esm --external:'node:*' | npx cfout --compat-flag nodejs_compat

CLI

Usage: cfout [options]

Reads a bundled ES module from stdin, runs it in the Cloudflare Workers
runtime, and prints its output as TAP.

Options:
  -t, --timeout <ms>         Timeout in milliseconds (default: 5000)
  --compat-date <date>       Workers compatibility date (default: newest
                             the installed runtime supports)
  --compat-flag <name>       Compatibility flag, repeatable
  --var <KEY=VALUE>          Variable exposed as globalThis.env.KEY,
                             repeatable
  --env <path>               .env file of variables, repeatable; --var
                             overrides it
  -h, --help                 Show this help message

-t, --timeout <ms>

How long to wait for the run to complete, in milliseconds. Must be a positive integer. Default 5000. The clock starts when the test request is dispatched, so workerd startup does not count. See completion.

--compat-date <date>

The Workers compatibility date, as YYYY-MM-DD. Defaults to the newest date the installed workerd supports, derived from miniflare's version (4.20260702.0 means 2026-07-02). "Today" is not used because workerd refuses to start with a date newer than it supports.

--compat-flag <name>

A compatibility flag. Repeat it for more than one flag. nodejs_compat is off by default, so Node-only usage is caught unless you opt in.

npx cfout --compat-flag nodejs_compat --compat-flag no_global_navigator

--var <KEY=VALUE>

A string variable, exposed to the test as globalThis.env.KEY. Repeat it for more than one variable. The value is everything after the first =, so --var A=b=c sets A to b=c. See variables.

--env <path>

Load variables from a .env file, such as wrangler's .dev.vars. A relative path resolves against the current directory. The file uses dotenv syntax: KEY=VALUE lines, # comments, and quoted values. Repeat the flag to load more than one file. A later file overrides an earlier one, and --var overrides any file. A missing file is an error.

npx esbuild --bundle test/index.ts --format=esm | npx cfout --env .dev.vars

-h, --help

Print the usage message.

A bad or unknown argument prints Error: <message> and the usage to stderr, and exits 1.

Output

console.log, console.info and console.debug go to stdout. console.warn and console.error go to stderr. Only test output is written to stdout, so it can be piped straight into a TAP reporter. workerd's own stderr is passed through to stderr.

Completion

A run ends at the first of these:

  1. testsFinished. The test code sets globalThis.testsFinished = true.
  2. A TAP plan. A top-level plan line, 1..N, optionally with a directive (1..0 # skip). A plan before them ends the run once N test points have been seen. Indented (subtest) plans are ignored. When the run ends, if the number of test points differs from the plan, cfout prints not ok - planned N tests but ran M.
  3. An uncaught error. A throw during module evaluation, an unhandledrejection event, or a throw from a timer callback queued at module top level (see shims). cfout prints one not ok - <message> line on stdout and the message and stack on stderr.
  4. Timeout. After --timeout milliseconds cfout stops reading, shuts down workerd, prints cfout: timed out after <ms>ms to stderr and exits 1. Output printed before the timeout is already on stdout.

tapzero prints a plan after its last test, so a tapzero suite ends by rule 2 with no extra code. Other test code can log a plan or set testsFinished:

test('all done', () => {
  globalThis.testsFinished = true
})

There is no idle-based "auto finish" mode.

Exit code

0 when the run completed by rule 1 or 2 and there was

  • no not ok line,
  • no plan mismatch,
  • no uncaught error, and
  • no console.error call.

1 otherwise. Any console.error call fails the run, even if every test passed. Use console.warn for diagnostics that should not fail.

Variables

Pass string variables with --var:

npx esbuild --bundle test/index.ts --format=esm | npx cfout --var API_URL=http://localhost:8787 --var MODE=test

Or load them from a .env file with --env .dev.vars.

They arrive as the worker's env and are assigned to globalThis.env before your module is imported, so they can be read at module top level or inside tests:

const { API_URL, MODE } = globalThis.env

The name matches what the same code would call it inside a real worker. Only string values are supported. KV, D1, R2, Durable Objects and other bindings are not.

What is shimmed

workerd evaluates a module's top level in "global scope", where timers, random values and I/O are not allowed. tapzero's first test() call schedules a setTimeout, so without help every tapzero bundle would throw at once. cfout shims two things while your module is evaluated:

  • Timers. setTimeout and setInterval calls made at module top level are queued, then replayed with the real timers once the module has finished evaluating. clearTimeout and clearInterval work with the ids returned by queued calls. A throw from a replayed callback is reported as an uncaught error.
  • Console. Output written during module evaluation is buffered and printed right after the module finishes evaluating.

These are deliberately not shimmed, because they are real Workers constraints that this tool exists to catch:

  • crypto.getRandomValues
  • crypto.randomUUID
  • fetch

Calling one of them at module top level fails the run with workerd's own error message, which explains the fix: call it inside a function that runs later, such as a test body.

Known limitations

  • Throws in late timers are not caught. If a timer that was scheduled after the initial replay (for example, from inside a test) throws, workerd reports no error event to cfout and cancels the request. The run ends without a completion rule and exits 1, without a not ok - <message> line. tapzero catches errors inside test bodies itself, so this only affects test code that schedules its own fire-and-forget timers.
  • Rejections during module evaluation are not reported. workerd does not dispatch unhandledrejection for a promise rejected while the module evaluates, for example a bare top-level Promise.reject(...). The run still exits 1, but without a message. Rejections that happen later, such as in an async test, are reported.
  • Top-level await is not supported. A module that uses top-level await gets its request cancelled with no output.

License

Big Time

About

CLI for tests in Cloudflare runtime

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages