The dusk command
The dusk command in the Dusk guide.
dusk is the command line of @shadow-js/dusk. The scripts that create-shadow-app writes run it, and npx dusk
runs it in any project that depends on @shadow-js/dusk. Its output is Dusk's: the banner, the request lines and the
build summary below, with Vite's own messages worded for Dusk.
| Command | What it does |
|---|---|
dusk dev |
Starts the development server on port 3000, with the app's config. Edits apply through hot updates; server errors show in the overlay. |
dusk build |
Builds the app for production and prints the summary: the phases, the route table (kind, size and first-load JavaScript of each page), and the budget warnings. |
dusk start (alias dusk preview) |
Runs the production build with the Node adapter, and prints the address it listens on. |
dusk doctor |
Checks the project and prints what to fix (see "Checking a project" below). |
dusk completion <bash|zsh|fish> |
Prints a shell completion script for the commands and flags. Load it with source <(dusk completion bash) or dusk completion fish | source. |
dusk --help, dusk --version |
Prints the usage, or the version. |
Flags:
| Flag | Command | Meaning |
|---|---|---|
-p, --port <n> |
dev, start |
The port to listen on. |
--host [<host>] |
dev, start |
The interface to listen on. A bare --host listens on all interfaces. |
--strictPort |
dev |
Fails when the port is in use, instead of trying the next one. |
--open |
dev |
Opens the browser at the dev server's address. |
--verbose |
dev |
Also prints the quiet events: hot updates, page reloads, and a line for every asset request. |
--log |
start |
Prints one line per request the app answers. Static files are not logged. |
--workers <n|auto> |
start |
Runs n server processes that share the port (auto: one per core, at most 16); DUSK_WORKERS is the default. With --json the ready line adds data.workers. See Using every core. |
--json |
every command | Prints JSON on stdout instead of words, and nothing else there: one document for build, doctor, completion, --help and --version, JSON Lines for dev and start (see "Output for scripts"). |
The port and the host are taken from the first of these that is set: the flag, the PORT and HOST environment
variables, the server section of the app's config file, and the defaults. The defaults of dusk dev are port 3000 and
localhost, which is not reachable from other machines; --host opens it to the network. dusk start uses the defaults
of the Node adapter's server: port 3000 on all interfaces.
One look for every command#
dusk dev, dusk build, dusk start and dusk preview speak the same way: the same header (◆ Dusk 1.0.0 (mode)),
the same markers (✓ for a step that finished, ⚠ warn for a warning, ✗ before an error), the same durations (34ms
below a second, 1.2s above) and the same two-space indent for the body. A command that makes you wait shows a spinner
on one line (dusk build, and the start of dusk dev); it is erased when the work ends, and anything printed meanwhile
(a warning from a plugin) lands on a clean line.
The output adapts to where it goes:
- Color is off when
NO_COLORis set, when the output is not a terminal, or on CI;FORCE_COLORturns it on. - Glyphs are Unicode (
✓ ○ ƒ ┌ ├ └) on every terminal that can draw them, and ASCII (+ o f , | \) on the Linux console and on a Windows console that is not Windows Terminal, an editor terminal or aTERM-aware shell. Nobody sees a box of missing characters. - Width: the route table gives up its optional columns before it wraps (see below). A pipe or a log file has no width limit, no spinner, and no keys.
A failure prints ✗ dusk <command>: <message> on stderr and exits 1; a refused command line prints ✗ dusk: <message>,
then the help, and exits 2. A mistyped command is answered with the nearest one (unknown command "biuld". Did you mean "build"?).
Development output#
dusk dev prints a banner with the address, the env files whose variables the server reads, the adapter, and how long
the start took. Then one line per page or API request, with the status and the time:
◆ Dusk 1.0.0 (development)
- Local: http://localhost:3000/
- Network: not exposed (pass --host to listen on the network)
- Environments: .env.local
- Adapter: none
✓ Ready in 1.7s
GET / 200 in 3.1s
GET /f/abc/preview 200 in 249ms
A request for a module, a stylesheet or a file of public/ is an asset and is not printed; --verbose prints those and
the hot updates too. In a terminal the status is green for 2xx, cyan for 3xx, yellow for 4xx and red for 5xx; the time is
dim, and yellow from one second up so a slow page stands out. Hot updates and page reloads are quiet by default.
When a person is at the terminal, single keys work without Enter: r restarts the server, o opens the app in the
browser, c clears the console, q stops the server, and h lists the keys. A pipe, a CI run or a non-terminal input
has no keys.
Build output#
dusk build writes dist/client, dist/server/entry.js, dist/dusk-manifest.json, the prerendered pages, and the
adapter's output. Its summary is the sample below (the route table of a real app has one row per page and API route):
◆ Dusk 1.0.0 (production)
✓ Compiled client dist/client 1.2s
✓ Compiled server dist/server 900ms
✓ Prerendered 2 pages 300ms
✓ Secret scan no secrets in 14 text files 40ms
✓ node adapter dist/node 5ms
wrote dist/node/server.mjs (start with: node dist/node/server.mjs)
Route Kind Size First Load JS Middleware
┌ ○ / static (file) 33.6 kB 72.4 kB -
├ ƒ /dashboard dynamic 53.2 kB 118.0 kB ⚠ 1
└ λ /api/env API - - -
+ First Load JS shared by all 64.8 kB (2 files)
○ static ƒ dynamic λ API
⚠ warn /dashboard loads 118.0 kB of JavaScript first, over its budget of 100.0 kB (build.budgets in dusk.config.ts).
✓ Built in 2.5s, output in dist/
Serve it with `dusk start`, or run `node dist/node/server.mjs`.
The route table:
- Symbol and branch: each route has a symbol for its kind, on a tree branch.
○static (a prerendered file),●static answered by the handler (the app's rules apply to the page),◐ISR,ƒdynamic,◇client,λAPI,≈stream. The legend under the table lists the symbols the build uses. - Kind: the same in words:
static (file),static (via handler),ISR 60s(cached, and rendered again after therevalidatetime),dynamic(rendered on each request),client(ssr = false, the browser renders it),API, orstreamwith the kinds it exports (SSE,WS). - Size: the gzip size of the JavaScript only this page loads (its route chunks).
- First Load JS: the gzip size of the JavaScript a page loads before it is interactive: the shared part (the entry and
the stylesheets) plus the page's own. In a terminal it is green up to 100 kB, yellow up to 170 kB and red above, or red
when the page is over its budget; a page over its budget also carries
⚠so a log without color shows it. - Middleware: how many
_middlewarefiles cover the route (the file of its folder and of the folders above it). The column is left out when no route is covered.
On a narrow terminal the table gives up columns in this order: Middleware, Kind, Size (the symbol still says the kind, and
the legend explains it). If the paths alone are still too wide, a long path is cut at its start (…ection/final-page), so
the end that tells two routes apart stays. No line of the table is wider than the terminal, and no route is left out.
The steps are timed: the client and server bundles, the prerendered pages, the secret scan (the build fails on a secret, and the summary says how many text files it read), and the adapter's output.
dusk build --json prints the same facts as JSON on stdout and nothing else there (warnings and errors stay on stderr),
for a CI check. The summary is the data of the document, and the budget warnings are its warnings:
dusk build --json | jq -e '.warnings | length == 0' > /dev/null
data has version, steps, prerendered, secretScan, adapter, routes (each with path, family, kind,
bytes for the first load, size, middleware and overBudget), shared and ms. Each element of warnings is
{ "code": "DUSK_BUDGET", "message": "..." }. "Output for scripts" below describes the document around it.
Budgets warn for a page that loads too much JavaScript, and fail the build when enforce is set (below). build.budgets in dusk.config.ts
takes gzip sizes in bytes: firstLoadJs for every page, and routes for a page by its path, which replaces the general
budget for that page:
import { defineConfig } from "@shadow-js/dusk/vite";
export default defineConfig({
build: { budgets: { firstLoadJs: 200_000, routes: { "/dashboard": 250_000 } } },
});
A budget for a path that no page has is a warning too, so a typo shows up.
For CI, enforce: true makes a page over its budget fail the build: the summary is printed first, then the exit code is 1 and the message names the pages (DUSK_BUDGET_EXCEEDED in --json, whose data still holds the routes and whose warnings hold each DUSK_BUDGET line). The default is false, so a budget is advice until the project decides it is a gate:
import { defineConfig } from "@shadow-js/dusk/vite";
export default defineConfig({
build: { budgets: { firstLoadJs: 200_000, enforce: true } },
});
Start output#
dusk start prints the same banner as dusk dev with the mode production, and then runs until it is stopped. Request
lines are off by default; --log turns them on:
◆ Dusk 1.0.0 (production)
- Local: http://127.0.0.1:3293/
- Network: not exposed (set HOST or pass --host to listen on the network)
- Environments: .env
- Adapter: node
✓ Ready in 33ms
GET / 200 in 10ms
GET /api/env 200 in 2ms
Stopping the server (SIGINT): finishing requests in flight.
Stopped.
Ctrl+C (SIGINT) and SIGTERM stop the server: it stops accepting connections, finishes the requests in flight, and prints
Stopped.. A second signal exits at once.
Checking a project#
dusk doctor runs seven independent checks and prints one line for each, with a one-line fix for anything that is not
fine. node (22.12 or later), config (the Vite config loads and has dusk()), routes (src/routes exists), env
(no variable with a public prefix, DUSK_PUBLIC_ or NEXT_PUBLIC_, has SECRET, PRIVATE, TOKEN or PASSWORD in its
name; the message names the variable and never prints a value), adapter (a warning when dusk() has none),
lockfile (a warning when no lockfile is found in the project or a parent folder) and duplicates (a warning when two
versions of one @shadow-js package are installed). The exit code is 1 when a check fails and 0 otherwise, so a
warning never fails a CI step. With --json the document is the only thing on stdout: the checks are in data.checks
(id, status, message and fix), each warning is also an element of warnings (DUSK_DOCTOR_LOCKFILE for the
lockfile check, and so on), and a failed check makes ok false with the error DUSK_DOCTOR_FAILED (exit code 1).
npx dusk doctor
npx dusk doctor --json > doctor.json
Output for scripts#
Every command takes --json. Stdout then carries only JSON; the words (logs, plugin warnings, the stack of an
unexpected error) go to stderr, there is no spinner and no color, and the exit code is the one the command has without
the flag. A command that ends prints one document; the same envelope is printed by shadow-ui and create-shadow-app,
and the jq lines in this guide show how to script them.
{
"schema": 1,
"command": "dusk doctor",
"ok": false,
"data": { "checks": [{ "id": "routes", "status": "fail", "message": "src/routes does not exist", "fix": "Create src/routes/index.tsx" }] },
"warnings": [],
"error": { "code": "DUSK_DOCTOR_FAILED", "message": "1 check(s) failed: routes" }
}
| Key | Meaning |
|---|---|
schema |
1. It changes only when a key changes its meaning. |
command |
The command that ran, such as dusk build (preview is dusk start), or dusk for --help, --version and a command line that names no command. |
ok |
true exactly when the exit code is 0. |
data |
The facts of the command; {} when there are none, and what is known when it failed. |
warnings |
{ code, message } objects, each code starting with DUSK_. |
error |
Only when ok is false: { code, message }. The code is stable and SCREAMING_SNAKE; the message is for a person. |
A refused command line is a document too (exit code 2, or 1 for dusk completion with a missing or unknown shell), and so
is a failure that nobody expected (DUSK_UNEXPECTED, exit code 1): a script never has to parse words. dusk --help --json
and dusk build --help --json put the usage text in data.usage, and dusk completion zsh --json puts the script in
data.script.
dusk dev --json and dusk start --json never end on their own, so they print JSON Lines: one compact envelope per
line, each with an event after ok. ready is printed once the server listens, with the address in data.url (and
urls, port, mode, version, adapter, environments and readyMs). Then comes a request line per page or API
request (method, path, status, ms), and a stopped line after a clean stop on SIGINT or SIGTERM. dusk start prints
request lines only with --log, and dusk dev prints the asset requests only with --verbose. A line with ok: false
is the last one, has no event, and comes with exit code 1 (the port is in use, there is no build).
dusk dev --json | jq -r 'select(.event == "ready") | .data.url' | head -n 1
dusk start --json --log | jq -c 'select(.event == "request" and .data.status >= 500)'
The codes: DUSK_UNKNOWN_COMMAND, DUSK_UNKNOWN_OPTION, DUSK_INVALID_OPTION, DUSK_INVALID_PORT, DUSK_INVALID_SHELL
(command lines that do not parse), DUSK_PORT_IN_USE, DUSK_NO_BUILD, DUSK_NO_ADAPTER, DUSK_WRONG_ADAPTER,
DUSK_BAD_BUILD, DUSK_BAD_MANIFEST, DUSK_NO_VERSION (what dusk start needs and dusk build leaves),
DUSK_BUILD_FAILED (the build tool failed on the app), DUSK_BUDGET_EXCEEDED (a page is over its budget and budgets.enforce is on), DUSK_DOCTOR_FAILED, DUSK_FAILED and DUSK_UNEXPECTED.
Flags, configuration and exit codes#
Configuration is the app's config file for the server (the server section) and dusk.config.ts for Dusk's settings.
dusk dev and dusk start read the environment files (.env, .env.local, .env.[mode] and .env.[mode].local) and
name them in the banner.
Exit codes: 0 when the command finishes, or when the server stops on Ctrl+C or SIGTERM; 1 when it fails (a build or
configuration error, a port that is in use with --strictPort, a server that cannot start); 2 for a usage error (an
unknown command or flag, a value that is not a valid port).
dusk start needs two things. First, a build: run dusk build before it. Second, the Node adapter in
vite.config.ts, with node imported from @shadow-js/dusk/node:
import { node } from "@shadow-js/dusk/node";
import { dusk } from "@shadow-js/dusk/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [dusk({ adapter: node() })] });
Ctrl+C (SIGINT) and SIGTERM stop a dusk dev or dusk start server gracefully: it stops accepting connections, lets the
requests in flight finish, and then the process exits.