@ultimat3/cli 19.2.0 → 19.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CLAUDE.md +108 -6
  2. package/package.json +29 -29
  3. package/src/app-boundaries.ts +11 -2
  4. package/src/app-load.ts +5 -1
  5. package/src/budgets.ts +17 -6
  6. package/src/cmd-dev.ts +29 -39
  7. package/src/cmd-doctor.ts +61 -23
  8. package/src/cmd-generate.ts +5 -2
  9. package/src/cmd-i18n.ts +10 -3
  10. package/src/cmd-jobs.ts +56 -10
  11. package/src/cmd-test.ts +15 -10
  12. package/src/db-seed.ts +2 -1
  13. package/src/dev-queue.ts +16 -2
  14. package/src/dev-reload.ts +46 -0
  15. package/src/dev-runtime.ts +4 -1
  16. package/src/dev-sync.ts +11 -3
  17. package/src/dev-watch-tree.ts +226 -0
  18. package/src/dev-watch.ts +59 -37
  19. package/src/doctor-offline.ts +122 -0
  20. package/src/error-catalog.ts +4 -5
  21. package/src/fix-command.ts +40 -1
  22. package/src/fix-path.ts +10 -11
  23. package/src/flag-number.ts +15 -0
  24. package/src/generate-kinds.ts +54 -4
  25. package/src/generate-write.ts +25 -2
  26. package/src/gitignore.ts +145 -0
  27. package/src/hold.ts +50 -17
  28. package/src/index.ts +1 -1
  29. package/src/island-bundle.ts +2 -1
  30. package/src/island-states-load.ts +2 -1
  31. package/src/jobs-driver.ts +4 -1
  32. package/src/mcp-host.ts +18 -9
  33. package/src/parse.ts +17 -0
  34. package/src/path-segments.ts +14 -0
  35. package/src/prerender.ts +46 -20
  36. package/src/retry-memo.ts +37 -0
  37. package/src/serve.ts +5 -1
  38. package/src/source-files.ts +3 -1
  39. package/src/sw-artifacts.ts +71 -7
  40. package/src/templates/admin-page.ts +49 -1
  41. package/src/templates/scaffold-container.ts +12 -0
  42. package/src/templates/scaffold-repo.ts +7 -2
  43. package/src/test-passes.ts +79 -0
  44. package/src/test-shards.ts +110 -36
  45. package/src/verify-checks.ts +6 -6
  46. package/src/verify-step.ts +4 -4
  47. package/src/verify-tests.ts +14 -2
package/CLAUDE.md CHANGED
@@ -8,12 +8,13 @@ Tier 5. May import tiers 0–4. Nothing imports this except `create-ultimate`.
8
8
  | stdout | `write-line.ts`'s `writeLine` — synchronous fd 1, never `process.stdout.write`, which truncates at the 64KB pipe buffer when `process.exit` follows. Exported, because `create-ultimate`'s entry point needs the same one |
9
9
  | stderr | `write-line.ts`'s `writeErrorLine` — the same loop on fd 2, for a line that is not the command's answer. A `CommandResult` declaring `stream: 'stderr'` is routed there by `dispatch.ts`'s `sinkFor`, and `x mcp serve --transport stdio` is the one case: its fd 1 carries JSON-RPC frames, so the `✓ mcp stdio serving 13 tools` line rendered after the loop was a malformed frame. Neither renderer carries `stream`, exactly like `hold` |
10
10
  | Boot logs under `--json` | `dispatch.ts` calls core's `setLogStream('stderr')` when `args.json` is set, once, for all thirty commands. `x db migrate --json` printed the boot logger's `ultimate migrate applied` and then the command's own object, so `json.load` raised on the second document. A server's stdout stays its log stream; this is the CLI process only |
11
- | Test execution | `test-shards.ts`'s `testArgs` — ONE `bun test --parallel=N`, never N processes this repo packs itself. It did pack them, largest-first greedy over file SIZE, and the packer was deleted for buying **nothing**: four interleaved runs each on the 1296-file unit corpus gave 58.2/60.0/65.0/66.5s hand-packed against 54.5/57.8/61.7/64.5s under `--parallel=8`, within noise, because both are work-bound — 436.7s of file time is a 54.6s floor on 8 workers and the slowest single file is 20.5s. A greedy pack of 1296 small items lands near-optimal by accident. `--timings` is refused on the same evidence (#342). `--parallel` implies `--isolate`, so the per-file module registry is unchanged, and the per-worker database is too: `@ultimat3/testing`'s `workerId` already read `BUN_TEST_WORKER_ID`, which Bun sets 1..N. `ULTIMATE_TEST_WORKER` is set only for a single-shard `x test --worker I` rerun, which is one process |
11
+ | Test execution | `test-shards.ts`'s `testArgs` — ONE `bun test --parallel=N` per PASS (`test-passes.ts`; a selection mixing `live`/`e2e` files with the rest is two passes, the serial one at `--parallel=1`), never N processes this repo packs itself. It did pack them, largest-first greedy over file SIZE, and the packer was deleted for buying **nothing**: four interleaved runs each on the 1296-file unit corpus gave 58.2/60.0/65.0/66.5s hand-packed against 54.5/57.8/61.7/64.5s under `--parallel=8`, within noise, because both are work-bound — 436.7s of file time is a 54.6s floor on 8 workers and the slowest single file is 20.5s. A greedy pack of 1296 small items lands near-optimal by accident. `--timings` is refused on the same evidence (#342). `--parallel` implies `--isolate`, so the per-file module registry is unchanged, and the per-worker database is too: `@ultimat3/testing`'s `workerId` already read `BUN_TEST_WORKER_ID`, which Bun sets 1..N. `ULTIMATE_TEST_WORKER` is set only for a single-shard `x test --worker I` rerun, which is one process |
12
12
  | Numeric flags | `flag-number.ts` — one reader for `--port` / `--workers` / `--shard`. A bare `Number.parseInt` accepts `4abc` and answers `NaN`, which turned three checks into ones that cannot fail |
13
13
  | Shell quoting | `shell-quote.ts`'s `quoteArg` — every value the CLI pastes into a `fix:` or a reproduce line, `exec.ts`'s missing-program refusal and `test-shards.ts`'s reproduce command both. A name holding a space or a `;` interpolated bare is an instruction that runs something else |
14
14
  | Missing positionals | `MissingPositionalError`, never `BadFlagError` (names a flag that does not exist) and never `UnknownCommandError` (says a known command form is not one). Its `example` is a REAL invocation — `x g route <name>` in a shell is a redirect |
15
15
  | Bare subcommands | `CommandSpec.defaultSubcommand`, **declared**. The parser answered `subcommands[0]` until 1.2.0, so `x db` ran `gen` — the migration GENERATOR — because it sorted first, and `x mcp` started a server. A command with no defensible default declares none and `MissingSubcommandError` refuses the bare form; `parse.test.ts` pins the set at exactly `db` and `mcp`. Its fix is `x help <command>`. Both forms answer now: `--help` is read off the flag loop and `readSubcommand` is SKIPPED when it is set, so `x db --help`, `x mcp --help` and `x pr --help` print usage instead of exiting 1 with this same refusal — which is what they did on every command taking a subcommand until 2026-08 (`parse.test.ts` pins it across the shipped registry) |
16
16
  | Closed flag values | a flag whose values are a closed set is READ through a function that refuses the rest — `cmd-build.ts`'s `readTarget`, `cmd-deploy.ts`'s `readMethod`, `cmd-routes.ts`'s `readSurfaceFilter`, `cmd-mcp.ts`'s `isTransport`. `=== 'helm' ? 'helm' : 'compose'` made `x deploy --method helmm` a COMPOSE deploy reporting `method: "compose"`, and `--surface App` reported `0 routes` and exit 0 — a typo and an empty table rendering identically. The set is the framework's own where one exists (`SURFACES` from `@ultimat3/render`), never a list restated here |
17
+ | Passthrough | `CommandSpec.passthrough`, **declared**. `ParsedArgs.passthrough` says "handed to the underlying tool verbatim" and had no reader anywhere, so `x test unit -- --coverage --bail` parsed both flags and dropped both — a run that measured nothing reporting what a coverage run reports. `x test` forwards them to `bun test` (before the file list) and prints them back in its reproduce line; every other command refuses a non-empty `--` tail with `X_CLI_BAD_FLAG` rather than dropping it. `help`/`version` short-circuit ahead of the command and are exempt |
17
18
  | App root | `CommandSpec.requiresApp`, **enforced by `dispatch.ts`** before `target.run` — the field's doc said so for 17 commands and nothing read it, so the promise was kept only by each command remembering to call `requireAppRoot` itself. Those 17 calls stay (they hand the command its root, and name subcommands the dispatcher cannot see), but the DECLARATION is what decides, ahead of any check a command makes about its own arguments; `--help` is exempt, because `target` is the help command by then |
18
19
  | Result helpers | `command.ts`'s `ok()` / `failed()` write `ok` **after** the `extra` spread: the function's name is the verdict and nothing a caller passes can overturn it. Spread last, `failed('verify', '1 of 20 steps failed', { ok: true })` answered `ok: true` and `exitCodeFor` exited **0** on it — a green CI over a red command (`command.test.ts`) |
19
20
  | I/O | only `dispatch.ts` renders or exits; commands return `CommandResult` |
@@ -81,7 +82,7 @@ source only: a test file's import is not judged, because `packages/*` here decla
81
82
  read is its own finding rather than a silent skip — a skipped workspace is a hiding place for the
82
83
  very edge the rule is looking for.
83
84
 
84
- `app-permissions.ts` is the `policy` step, and it is the twentieth. Two references in the whole
85
+ `app-permissions.ts` is the `policy` step. Two references in the whole
85
86
  framework are bare strings nothing checks — `RoleDef.grants` and `RouteGuard.permission` — while
86
87
  `can()` calls `assertPermission` and throws `X_PERMISSION_UNKNOWN` on the first request that
87
88
  reaches the route. So `x new` shipped an app that granted `dashboard:read`, required it on
@@ -108,9 +109,19 @@ halves are `undefined`/empty with no replica configured, and an EMBEDDED binding
108
109
  PGlite has no standby. Not `@ultimat3/http`'s pipeline, which would make the HTTP tier know what a
109
110
  database is; the boot is the only tier that may know about a request and a pool.
110
111
 
112
+ `flag-number.ts`'s `portPairAfter` is the one suggestion for a caller that binds a PAIR, and
113
+ `neighbouringPort` is for a caller that binds one port. `x dev` takes `PORT` **and** `PORT + 1`, so
114
+ the neighbour of a taken web port is the sync port — both `x doctor`'s `X_PORT_IN_USE` and
115
+ `dev-sync.ts`'s handed back the port the finding had just named as taken, each under a test that
116
+ said it ended the failure. It answers downward at the top of the range for the reason
117
+ `neighbouringPort` does, and one further: the suggestion's own neighbour has to exist, or
118
+ `syncPortFor` refuses it with `X_PORT_INVALID`.
119
+
111
120
  `port-probe.ts` is the one `portFree`, because two commands ask it and must not disagree:
112
- `x doctor` reports it as a finding for BOTH ports `x dev` binds — the web port and the `PORT + 1`
113
- sync port, each labelled with the role that wants it — and `startSync` asks it after a failed
121
+ `x doctor` reports it as a finding for BOTH ports `x dev` binds — the web port and the sync port,
122
+ which it derives with `syncPortFor` (the boot's own rule: a `--port` at the top of the range has no
123
+ sync port at all, so the probe reports `X_PORT_INVALID` instead of probing the port BELOW it, which
124
+ is what `neighbouringPort` answered there) — each labelled with the role that wants it — and `startSync` asks it after a failed
114
125
  `listenSyncNode` so a taken neighbour is `X_PORT_IN_USE` rather than `X_CLI_UNEXPECTED` over
115
126
  `Bun.serve`'s own English rendered into a `cause:`. It is ASKED, never read off the caught value,
116
127
  which is what `scripts/catch-render.ts` refuses; anything else the listener failed on is re-thrown
@@ -119,6 +130,36 @@ untouched. `x doctor` also probes `DATABASE_URL` with a real `select 1` through
119
130
  credentials, which is the case an operator most needs told about — and reports `X_DB_UNAVAILABLE`
120
131
  with that package's own two-branch fix. An EMBEDDED binding is not probed: that lock is `x dev`'s.
121
132
 
133
+ `doctor-offline.ts` is `x doctor`'s offline-fallback check, and it is the shape of a diagnostic
134
+ that could not be closed. The probe was the literal `apps/web/app/offline.tsx` — a filename
135
+ `assertRouteFilename` REFUSES (`X_ROUTE_FILE_INVALID`: the directory is the URL, so a page is
136
+ `page.tsx`) — while `x new` scaffolds `apps/web/site/offline/page.tsx` and the finding's own `fix:`
137
+ writes `apps/web/app/offline/page.tsx`. Three different paths, so every app the framework has ever
138
+ produced reported `X_PWA_NO_OFFLINE_FALLBACK` from its first `x doctor`, running the fix changed
139
+ nothing, and the app that HAD the route was told it did not. It now reads the DECLARED
140
+ `pwa.offline.fallback` through `loadPwaArtifacts` and matches it against `describeRoutes()` —
141
+ **`site/` only**, `As of 2026-09`. It accepted `app/` too, on the argument that both surfaces answer
142
+ the same URL; true, and not the question. `SURFACE_SPECS` allows `app/` exactly `stream | ssr`, only
143
+ a `static` route is prerendered, and `serviceWorkerArtifacts` precaches a rendered DOCUMENT
144
+ (`documents.get(fallback)`) — so an `app/` fallback has nothing to precache and the offline
145
+ navigation reaches the network it exists to survive without. The check and its own `fix:` disagreed
146
+ about one code. Not closed by this: a `site/` route declaring `render: 'ssr'` is not prerendered
147
+ either, and `NavigableRoute` carries no render mode. An app whose modules will not import is not
148
+ judged at all, which is
149
+ `appEntities`' rule (`schema-drift.ts`) one registry over. The fix is `x g route <name>
150
+ --surface site` only where the fallback is one path segment the generator can really produce: a
151
+ nested path slugifies to a DIFFERENT url, so there it is the config edit instead — a `fix:` that
152
+ runs and leaves the finding where it was is the defect this file exists to end. **`site`, not
153
+ `app`**: the document that answers a lost network has to render with no network, no session and no
154
+ database, which `app/` (`ssr | stream`) cannot promise, and it is the line `@ultimat3/pwa`'s own
155
+ `X_PWA_NO_OFFLINE_FALLBACK` hands out for the same code — two fixes for one code are two answers.
156
+
157
+ `X_ENV_MISSING`'s fix is the file write, for the same reason. It was `x new --force`, which cannot
158
+ run where the reader is standing: `x new` takes a `<name>` positional (`X_CLI_BAD_FLAG` without
159
+ one) and with one it scaffolds a SECOND app beside the broken one. `cp .env.example
160
+ .env.development` — the committed projection of `envSchema`, which is what `x env example` writes
161
+ and `X_ENV_EXAMPLE_DRIFT` keeps honest.
162
+
122
163
  `i18n-index.ts` is the one writer of an app's `packages/i18n/src/index.ts`, shared by `x g` and
123
164
  `x i18n add|sync`. A catalog on disk and a SELECTABLE locale were two different sets: `x i18n add
124
165
  fr` wrote the file, exited 0, and left `x verify --only i18n` red with `X_CATALOG_UNREGISTERED`
@@ -406,7 +447,26 @@ cannot be missing from the precache manifest.
406
447
  |---|---|
407
448
  | `cmd-dev.ts` | mounts `/sw.js` and `/x-sw-register.js`; built ONCE at boot and deliberately not rebuilt on the watcher tick — a worker that changes under a page it already controls is the update path, and re-emitting one per keystroke exercises it on every save |
408
449
  | `serve.ts` | the same two routes in the container, from the same call |
409
- | `prerender.ts` | writes both as FILES into the export — a static host runs no route table, so a `<script src="/x-sw-register.js">` in every document is a 404 unless the bytes are in the artifact |
450
+ | `prerender.ts` | writes both as FILES into the export — a static host runs no route table, so a `<script src="/x-sw-register.js">` in every document is a 404 unless the bytes are in the artifact. In TWO halves, `As of 2026-09-06`: `serviceWorkerHead(pwa)` before the render loop, because every document has to name the script, and `serviceWorkerArtifacts` AFTER it, because a precache revision is the content hash of a document that does not exist yet |
451
+
452
+ **A precache revision is the DOCUMENT's content hash, and until 2026-09-06 it was the build id.**
453
+ `pwaRoutes` projected four of `PwaRoute`'s eight fields, so every route entry read
454
+ `{"url":"/","revision":"<buildId>","bytes":0}`: two deploys of a byte-identical site re-fetched
455
+ every precached page, which is the one thing `packages/pwa/src/precache.ts`' own header says must
456
+ never happen, and `DEFAULT_PRECACHE_WARN_BYTES` was a 5 MB budget over a total that could not count
457
+ one byte of HTML. The cause was ORDER — `prerender.ts` emitted the worker before the render loop —
458
+ so the emission moved after it and `ServiceWorkerInput.documents` carries `renderStatic`'s own
459
+ `hash` and byte count per rendered path. `x dev` and `serve.ts` pass none and keep the build id:
460
+ neither has rendered a page at boot, and inventing a hash for bytes that do not exist is a revision
461
+ that never changes when the page does. **The offline document is fed through its own pair**, because
462
+ `buildPrecacheManifest` adds it ITSELF as `reason: 'fallback'` ahead of every route and `add()`
463
+ keeps the first entry per url — so the route entry of the same url is shadowed and only
464
+ `offlineFallbackRevision` / `offlineFallbackBytes` (`ServiceWorkerConfig`, `@ultimat3/pwa`) can
465
+ decide it. That pair was declared and fed by nobody for one commit; `serviceWorkerArtifacts` reads
466
+ the fallback path out of the same `documents` map, so the one page an offline navigation depends on
467
+ is content-addressed like every other. A fallback this pass did not render — no route serves it,
468
+ which `x doctor` reports as `X_PWA_NO_OFFLINE_FALLBACK` — is absent from the map and keeps the
469
+ build id.
410
470
 
411
471
  **Registration is an EXTERNAL script, never inline**, and that is a CSP fact rather than a
412
472
  preference: `startWeb` computes a `script-src` sha256 per inline script, so an unhashed one is
@@ -996,7 +1056,11 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
996
1056
  | `statement-loop.ts` | one verdict → the finding, the panel fact, the overlay notice and the log line |
997
1057
  | `dev-policy.ts` | which actors to ask about, and which capability each policy gates |
998
1058
  | `cmd-dev.ts` | boot order, mounting `/_x`, installing the span exporter, the file watcher |
999
- | `dev-watch.ts` | which writes under the app root are a source change. Seven directories are never one — `.x`, `node_modules`, `.git`, `.personal`, `.claude`, `dist`, `coverage` — and each is matched as a path SEGMENT: the rule was `filename.includes('.x/') \|\| filename.includes('node_modules')`, so `git status`, an agent's scratch file and a coverage run each ran a full `appManifest()` + `buildIslands()` (measured in ai-maxxing, which keeps two whole copies of the app under `.claude/worktrees/`), while a directory named `my-node_modules-notes/` got no reload at all |
1059
+ | `dev-watch.ts` | which paths under the app root `x dev` may not watch: the app's own `.gitignore` (`gitignore.ts`) plus a floor of five directory names an ignore file cannot be relied on to carry — `.git`, `.x`, `node_modules`, `.personal`, `.claude` — every one of them dotted or an install, matched as a path SEGMENT at any depth |
1060
+ | `dev-watch-tree.ts` | the watch SET: one `watch(dir, { recursive: false })` per ADMITTED directory, a new directory picked up on `rename`, a removed one giving its descriptor back, and the 30ms trailing debounce |
1061
+ | `dev-reload.ts` | one rebuild at a time — a tick arriving mid-build coalesces into ONE trailing rebuild, for the newest file |
1062
+ | `gitignore.ts` | what git ignores, as data: parse, the ancestor chain up to the repository, and last-match-wins |
1063
+ | `path-segments.ts` | `hasPathSegment` — a directory of that exact name, never a substring of one |
1000
1064
  | `style-bundle.ts` / `style-routes.ts` | a surface's CSS as one content-hashed file under `/styles/`, served `immutable` — `island-bundle.ts` / `island-routes.ts`' shape one asset over. It was an inline `<style>` until 2026-09-06: 156,738 bytes, identical on every page, inside a `private, no-store` document. The URL is the hash alone, no surface in the name: a surface is not a property of the bytes, and an app whose only CSS is its global layer would otherwise write three identical files into its static export and three entries into a precache manifest that has a budget |
1001
1065
  | `mcp-host.ts` | the `DevCapabilities` half of `@ultimat3/mcp`'s `DevHost` — db, tests, logs, verify |
1002
1066
  | `mcp-db-target.ts` | which database the host is pointed at: whether it is a branch, and whether it is production |
@@ -1005,6 +1069,44 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
1005
1069
  | `mcp-test-output.ts` | reading `bun test`'s own summary back into a `TestRun` |
1006
1070
  | `cmd-mcp.ts` | `x mcp serve`: the two transports, and the local developer's caller |
1007
1071
 
1072
+ **The watch set is a REGISTRATION decision, not a filter, `As of 2026-09-06`.** `watch(root, {
1073
+ recursive: true })` takes one inotify descriptor per directory in the tree — measured on `x dev`
1074
+ against `examples/dummy`: **110 descriptors, 39 of them (35%) under `.x/` and `node_modules/`**, and
1075
+ on a monorepo root **1901, of which 1490 (78%) were `.git/` and `node_modules/`**, where one
1076
+ `git status` delivered 5 `.git/index` events into the JS callback. `isIgnoredPath` answered
1077
+ correctly every time and answered too late: the kernel queue entry, the callback and the slot out of
1078
+ `max_user_watches` (8192 on many distributions) were already spent. Bun 1.4.0's `fs.watch` has no
1079
+ ignore option, so `dev-watch-tree.ts` walks the root itself and registers one non-recursive watcher
1080
+ per admitted directory, pruning at descent.
1081
+
1082
+ **And the ignore set is the app's own `.gitignore`.** Seven hand-listed directory names were wrong
1083
+ in both directions. Nothing read the ignore file, so `touch tsconfig.tsbuildinfo` — the file every
1084
+ `bun run typecheck` rewrites, named by `x new`'s own scaffolded `.gitignore` — logged
1085
+ `reloaded tsconfig.tsbuildinfo in 113ms`, a full `appManifest()` plus `buildIslands()`; on the
1086
+ framework root 54 git-ignored directories were unfiltered. And `dist` and `coverage` were matched at
1087
+ ANY depth, so an app's own `/dist` or `/coverage` ROUTE never reloaded — silently, which the file's
1088
+ own header calls worse than a spurious rebuild. `gitignore.ts` reads it with git's own anchoring
1089
+ (unanchored at any depth, a leading `/` or an inner slash where it is written, `!` re-including,
1090
+ a trailing `/` directory-only) and walks ANCESTOR ignore files up to the directory holding `.git` —
1091
+ `examples/dummy` carries none of its own and every rule about it lives in the repository root's. The
1092
+ file is re-read, and the whole watch set re-walked, on any write naming `.gitignore`; nothing ever
1093
+ spawns `git check-ignore`. `x new`'s scaffold writes `/dist/` and `/coverage/` **root-anchored** for
1094
+ this reason, plus `packages/*/dist/`. It is `fix-path.ts`'s parser, lifted: two readers of one file
1095
+ are two answers to what an app committed.
1096
+
1097
+ **A watcher event carries no filename when the WATCHED directory itself moves.** Bun's recursive
1098
+ watcher delivers `undefined` — not `null` — on `mv myapp myapp2`, a re-clone or a volume remount,
1099
+ and `isIgnoredPath(undefined)` threw a `TypeError` inside an fs callback, outside any `try`, with no
1100
+ `uncaughtException` handler: `x dev` died with a stack trace. The listener is total over
1101
+ `string | Buffer | null | undefined` and logs `dev.watch.unnamed_event` once.
1102
+
1103
+ **The reload has an in-flight guard, and it is the state that needed it.** A 45ms drip — a slow
1104
+ `git checkout`, a formatter walking files, `x db gen` — measured **40 reloads for 40 files**, each
1105
+ launching `Promise.all([appManifest, buildIslands])` while the previous still ran and assigning
1106
+ `state.manifest` / `state.islands` in COMPLETION order, so a slower earlier tick could land on top
1107
+ of a newer one. `coalesceReloads` (`dev-reload.ts`) keeps the LAST tick that arrived during a
1108
+ rebuild and starts exactly one more.
1109
+
1008
1110
  `api-routes.ts` is the app's own API surface, composed **once** and mounted by both `cmd-dev.ts`
1009
1111
  and `serve.ts`: `listActions().map(toRoute)` from `@ultimat3/action` plus
1010
1112
  `listQueries().map(toQueryRoute)` from `@ultimat3/query`. Two lists is how `query.client()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "19.2.0",
3
+ "version": "19.3.1",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,34 +37,34 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
40
- "@ultimat3/action": "19.2.0",
41
- "@ultimat3/admin": "19.2.0",
42
- "@ultimat3/ai": "19.2.0",
43
- "@ultimat3/auth": "19.2.0",
44
- "@ultimat3/cache": "19.2.0",
45
- "@ultimat3/core": "19.2.0",
46
- "@ultimat3/db": "19.2.0",
47
- "@ultimat3/entity": "19.2.0",
48
- "@ultimat3/flags": "19.2.0",
49
- "@ultimat3/http": "19.2.0",
50
- "@ultimat3/i18n": "19.2.0",
51
- "@ultimat3/jobs": "19.2.0",
52
- "@ultimat3/mail": "19.2.0",
53
- "@ultimat3/manifest": "19.2.0",
54
- "@ultimat3/mcp": "19.2.0",
55
- "@ultimat3/money": "19.2.0",
56
- "@ultimat3/notify": "19.2.0",
57
- "@ultimat3/policy": "19.2.0",
58
- "@ultimat3/pwa": "19.2.0",
59
- "@ultimat3/query": "19.2.0",
60
- "@ultimat3/realtime": "19.2.0",
61
- "@ultimat3/render": "19.2.0",
62
- "@ultimat3/schema": "19.2.0",
63
- "@ultimat3/scraping": "19.2.0",
64
- "@ultimat3/seo": "19.2.0",
65
- "@ultimat3/storage": "19.2.0",
66
- "@ultimat3/testing": "19.2.0",
67
- "@ultimat3/time": "19.2.0",
40
+ "@ultimat3/action": "19.3.1",
41
+ "@ultimat3/admin": "19.3.1",
42
+ "@ultimat3/ai": "19.3.1",
43
+ "@ultimat3/auth": "19.3.1",
44
+ "@ultimat3/cache": "19.3.1",
45
+ "@ultimat3/core": "19.3.1",
46
+ "@ultimat3/db": "19.3.1",
47
+ "@ultimat3/entity": "19.3.1",
48
+ "@ultimat3/flags": "19.3.1",
49
+ "@ultimat3/http": "19.3.1",
50
+ "@ultimat3/i18n": "19.3.1",
51
+ "@ultimat3/jobs": "19.3.1",
52
+ "@ultimat3/mail": "19.3.1",
53
+ "@ultimat3/manifest": "19.3.1",
54
+ "@ultimat3/mcp": "19.3.1",
55
+ "@ultimat3/money": "19.3.1",
56
+ "@ultimat3/notify": "19.3.1",
57
+ "@ultimat3/policy": "19.3.1",
58
+ "@ultimat3/pwa": "19.3.1",
59
+ "@ultimat3/query": "19.3.1",
60
+ "@ultimat3/realtime": "19.3.1",
61
+ "@ultimat3/render": "19.3.1",
62
+ "@ultimat3/schema": "19.3.1",
63
+ "@ultimat3/scraping": "19.3.1",
64
+ "@ultimat3/seo": "19.3.1",
65
+ "@ultimat3/storage": "19.3.1",
66
+ "@ultimat3/testing": "19.3.1",
67
+ "@ultimat3/time": "19.3.1",
68
68
  "babel-preset-solid": "^1.9.15"
69
69
  }
70
70
  }
@@ -18,6 +18,7 @@ import { ERROR_DOCS_URL } from '@ultimat3/core';
18
18
  import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
19
19
  import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
20
20
  import type { Finding } from './output';
21
+ import { hasPathSegment } from './path-segments';
21
22
  import { quoteArg } from './shell-quote';
22
23
 
23
24
  export const BOUNDARY_CODES = [
@@ -56,8 +57,16 @@ const isDbSpecifier = (specifier: string): boolean =>
56
57
  specifier.endsWith('/db') ||
57
58
  /^@[^/]+\/db$/.test(specifier) ||
58
59
  specifier === 'drizzle-orm';
60
+ /**
61
+ * `node:http` and `node:https` are HTTP too, and the anchor could not reach them: `http` had to
62
+ * follow a `/` or start the string, and a `node:` specifier has a colon there. So the one spelling
63
+ * that needs no dependency at all — a service reaching straight for the runtime's own client — was
64
+ * the one spelling this rule could not see. Matched on the boundary character rather than by
65
+ * listing every prefix, and still anchored at both ends: `node:http2-fake` and
66
+ * `@ultimat3/https-client` are names, not HTTP.
67
+ */
59
68
  const isHttpSpecifier = (specifier: string): boolean =>
60
- specifier === '@ultimat3/http' || /(^|\/)http($|\/)/.test(specifier);
69
+ specifier === '@ultimat3/http' || /(^|[/:])https?($|\/)/.test(specifier);
61
70
 
62
71
  /** The transpiler rejects a shebang, and an app's `bin/` entry points legitimately have one. */
63
72
  export const stripShebang = (source: string): string =>
@@ -236,7 +245,7 @@ export async function readAppSources(root: string): Promise<readonly SourceFile[
236
245
  for (const pattern of APP_GLOBS) {
237
246
  const glob = new Bun.Glob(pattern);
238
247
  for await (const path of glob.scan({ cwd: root, absolute: false })) {
239
- if (path.includes('node_modules') || path.includes('.test.')) continue;
248
+ if (hasPathSegment(path, 'node_modules') || path.includes('.test.')) continue;
240
249
  const posix = path.split('\\').join('/');
241
250
  files.push({ path: posix, source: await Bun.file(joinPath(root, posix)).text() });
242
251
  }
package/src/app-load.ts CHANGED
@@ -21,6 +21,7 @@ import '@ultimat3/render/server';
21
21
  import { collectDeclaredCodes } from './error-contract';
22
22
  import type { Finding } from './output';
23
23
  import { findingFrom } from './output';
24
+ import { hasPathSegment } from './path-segments';
24
25
 
25
26
  /** Every place an app keeps code the framework has to see. */
26
27
  const APP_GLOBS = [
@@ -95,7 +96,10 @@ export async function loadApp(root: string): Promise<LoadedApp> {
95
96
 
96
97
  for (const pattern of APP_GLOBS) {
97
98
  for await (const absolute of new Bun.Glob(pattern).scan({ cwd: root, absolute: true })) {
98
- if (absolute.includes('node_modules') || absolute.includes('.test.')) continue;
99
+ // A SEGMENT, never a substring: an app checked out under
100
+ // `~/dev/node_modules-experiments/myapp` answered `includes('node_modules')` for every
101
+ // file it holds, so this loop imported none of them and the app registered nothing.
102
+ if (hasPathSegment(absolute, 'node_modules') || absolute.includes('.test.')) continue;
99
103
  const file = relative(root, absolute).split(sep).join('/');
100
104
  if (ENTRY_POINT.test(file) || CLIENT_ENTRY_POINT.test(file) || STATES_FILE.test(file)) {
101
105
  continue;
package/src/budgets.ts CHANGED
@@ -227,9 +227,24 @@ export interface MeasuredJs {
227
227
  export async function measureDocumentJs(html: string, out: string): Promise<MeasuredJs> {
228
228
  let jsBytes = 0;
229
229
  const entries: MeasuredEntry[] = [];
230
+ // Deduped ONCE, across both readers below, and the unit is the FETCH: a browser downloads a URL
231
+ // once however many times the document names it, so `budget.js` — a byte budget — counts it
232
+ // once. Two instances of one island are two wrappers and one chunk; so are a `<script src>`
233
+ // repeated by a page and its layout, and a src that is also an island entry. Only the island
234
+ // half was deduped, so a document naming one script twice was charged twice and could fail a
235
+ // budget it clears.
236
+ //
237
+ // EXECUTION is a different count and this is deliberately not it. A repeated classic
238
+ // `<script src>` runs once per element (a module runs once per document, off the module map), so
239
+ // the layout-plus-page case above really does execute twice — for zero extra bytes. That is a
240
+ // CPU cost, and this gate is a bound on what the browser downloads and parses. An INLINE script
241
+ // is not in this set for the same reason: two identical inline bodies are two copies of the
242
+ // bytes in the document, so both are charged.
243
+ const fetched = new Set<string>();
230
244
  const weigh = async (url: string): Promise<void> => {
231
245
  // Only a path inside the artifact can be weighed; a cross-origin script is not this build's.
232
- if (!url.startsWith('/')) return;
246
+ if (!url.startsWith('/') || fetched.has(url)) return;
247
+ fetched.add(url);
233
248
  const file = Bun.file(join(out, url.slice(1)));
234
249
  const bytes = (await file.exists()) ? file.size : 0;
235
250
  entries.push({ url, bytes });
@@ -246,13 +261,9 @@ export async function measureDocumentJs(html: string, out: string): Promise<Meas
246
261
  }
247
262
  await weigh(src);
248
263
  }
249
- // Deduped: two instances of one island are two wrappers and one chunk, and a browser that
250
- // imports the same module twice fetches and executes it once.
251
- const booted = new Set<string>();
252
264
  for (const match of html.matchAll(ENTRY_ATTR)) {
253
265
  const url = match.groups?.['url'];
254
- if (url === undefined || booted.has(url)) continue;
255
- booted.add(url);
266
+ if (url === undefined) continue;
256
267
  await weigh(url);
257
268
  }
258
269
  return { jsBytes, entries };
package/src/cmd-dev.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  // alongside it — mounted, never re-implemented — so an agent can introspect the running app.
5
5
  // No Docker, no env setup: an unset variable means the embedded default.
6
6
 
7
- import { watch } from 'node:fs';
8
7
  import { join } from 'node:path';
9
8
  import { devShellStyle } from '@ultimat3/admin/dev';
10
9
  import type { Role } from '@ultimat3/core';
@@ -29,6 +28,7 @@ import { devDashboardRoutes, devPanels } from './dev-dashboard';
29
28
  import { liveFeedLabel } from './dev-live-feed';
30
29
  import { clearLock, preflight, writeLock } from './dev-lock';
31
30
  import { createStatementLedger } from './dev-n-plus-one';
31
+ import { coalesceReloads } from './dev-reload';
32
32
  import { appRoutes } from './dev-render';
33
33
  import { replicaOverrides } from './dev-replica';
34
34
  import type { RunningRoles } from './dev-roles';
@@ -39,7 +39,7 @@ import type { DevServices } from './dev-services';
39
39
  import { describeServices, reportedUrls, resolveServices } from './dev-services';
40
40
  import { storageRoutes } from './dev-storage';
41
41
  import { createTraceRecorder } from './dev-traces';
42
- import { isIgnoredPath } from './dev-watch';
42
+ import { watchTree } from './dev-watch-tree';
43
43
  import { intFlagOr, PORT_RANGE } from './flag-number';
44
44
  import { holdUntilShutdown } from './hold';
45
45
  import type { IslandBundle } from './island-bundle';
@@ -96,26 +96,6 @@ interface DevState {
96
96
  islands: IslandBundle;
97
97
  }
98
98
 
99
- /**
100
- * Debounced: a save that touches five files is one reload, not five. What counts as a save at all
101
- * is `dev-watch.ts` — a reload is a full `appManifest()` plus a `buildIslands()` over every island,
102
- * so a write this cannot rule out is the most expensive no-op the dev loop has.
103
- */
104
- function watchApp(root: string, onChange: (file: string) => void): () => void {
105
- let timer: ReturnType<typeof setTimeout> | undefined;
106
- let last = '';
107
- const watcher = watch(root, { recursive: true }, (_event, filename) => {
108
- if (filename === null || isIgnoredPath(filename)) return;
109
- last = filename;
110
- if (timer !== undefined) clearTimeout(timer);
111
- timer = setTimeout(() => onChange(last), 30);
112
- });
113
- return () => {
114
- if (timer !== undefined) clearTimeout(timer);
115
- watcher.close();
116
- };
117
- }
118
-
119
99
  export interface StartDevOptions {
120
100
  readonly root: string;
121
101
  readonly port: number;
@@ -294,22 +274,32 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
294
274
  ...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
295
275
  });
296
276
 
297
- const stopWatching = watchApp(options.root, (file) => {
298
- const started = performance.now();
299
- void Promise.all([appManifest(options.root), buildIslands(options.root)])
300
- .then(([{ manifest }, islands]) => {
301
- state.manifest = manifest;
302
- state.islands = islands;
303
- state.reloads += 1;
304
- state.reloadFinding = undefined;
305
- options.onReload?.(file, Math.round(performance.now() - started));
306
- })
307
- // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
308
- // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
309
- .catch((error: unknown) => {
310
- state.reloadFinding = { ...findingFrom(error), at: file };
311
- });
312
- });
277
+ // One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
278
+ // trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
279
+ const rebuild = coalesceReloads(
280
+ async (file) => {
281
+ const started = performance.now();
282
+ const [{ manifest }, islands] = await Promise.all([
283
+ appManifest(options.root),
284
+ buildIslands(options.root),
285
+ ]);
286
+ state.manifest = manifest;
287
+ state.islands = islands;
288
+ state.reloads += 1;
289
+ state.reloadFinding = undefined;
290
+ options.onReload?.(file, Math.round(performance.now() - started));
291
+ },
292
+ // Same rule as a module that will not import: a save the manifest cannot be rebuilt from is
293
+ // a finding on `/_x`, never an unhandled rejection that takes the dev server down.
294
+ (error: unknown, file: string) => {
295
+ state.reloadFinding = { ...findingFrom(error), at: file };
296
+ },
297
+ );
298
+ // Watched one directory at a time, so an ignored one costs no descriptor at all — `dev-watch.ts`
299
+ // decides which, from the app's own `.gitignore`. A recursive watch on the root registered one
300
+ // inotify descriptor per directory in the tree, `.git/`, `node_modules/` and the `.x/` this
301
+ // process writes to included, and filtered the events afterwards.
302
+ const watcher = watchTree({ root: options.root, onChange: rebuild });
313
303
 
314
304
  server = {
315
305
  url: running.url ?? `http://localhost:${options.port}`,
@@ -333,7 +323,7 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
333
323
  runtime,
334
324
  panels,
335
325
  async stop() {
336
- stopWatching();
326
+ watcher.close();
337
327
  await running.stop();
338
328
  await runtime.stop();
339
329
  // Released after the roles: a span opened by an in-flight request still has an exporter to
package/src/cmd-doctor.ts CHANGED
@@ -4,16 +4,25 @@
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
- import { ERROR_DOCS_URL, tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
7
+ import {
8
+ ENV_EXAMPLE_PATH,
9
+ ERROR_DOCS_URL,
10
+ tryResolveEnvironment,
11
+ usesDevCursorSecret,
12
+ } from '@ultimat3/core';
8
13
  import { checkDb, createPostgresClient } from '@ultimat3/db';
9
14
  import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
10
15
  import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
11
16
  import type { CliCommand, CommandContext } from './command';
12
17
  import { checkMigrationSnapshots } from './db-snapshot';
13
- import { intFlagOr, neighbouringPort, PORT_RANGE } from './flag-number';
18
+ import { syncPortFor } from './dev-sync';
19
+ import type { OfflineFallbackFact } from './doctor-offline';
20
+ import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
21
+ import { intFlagOr, PORT_RANGE, portPairAfter } from './flag-number';
14
22
  import { ICON_SOURCE } from './icon-assets';
15
23
  import { msg } from './messages';
16
24
  import type { CommandResult, Finding } from './output';
25
+ import { findingFrom } from './output';
17
26
  import type { ParsedArgs } from './parse';
18
27
  import { portFree } from './port-probe';
19
28
  import { checkMigrationDrift } from './schema-drift';
@@ -59,6 +68,12 @@ export interface DoctorProbe {
59
68
  * separate remedies — one is "generate a migration", the other is "this migration is incomplete".
60
69
  */
61
70
  snapshots(): Promise<readonly Finding[]>;
71
+ /**
72
+ * What the app declared as its offline fallback, and which routes it really serves. A FACT and
73
+ * not a finding, because deciding is `offlineFallbackFinding`'s and this seam's whole job is
74
+ * reaching the disk — the same split `drift()` makes one question over.
75
+ */
76
+ offlineFallback(): Promise<OfflineFallbackFact>;
62
77
  }
63
78
 
64
79
  const finding = (code: string, cause: string, fix: string, at?: string): Finding =>
@@ -66,7 +81,8 @@ const finding = (code: string, cause: string, fix: string, at?: string): Finding
66
81
  ? { code, cause, fix, docs: ERROR_DOCS_URL }
67
82
  : { code, cause, fix, docs: ERROR_DOCS_URL, at };
68
83
 
69
- export const OFFLINE_FALLBACK = 'apps/web/app/offline.tsx';
84
+ /** The file `x doctor` reports missing, and the one the reader creates. */
85
+ export const ENV_DEVELOPMENT = '.env.development';
70
86
 
71
87
  /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
72
88
  const DEFAULT_DOCTOR_PORT = 3000;
@@ -80,12 +96,30 @@ const DEFAULT_DOCTOR_PORT = 3000;
80
96
  * derives `PORT = .port - 1` from it, so it is part of the contract `x dev` runs by.
81
97
  *
82
98
  * The suggested port moves BOTH: `x dev --port N` occupies N and N+1, so a free N beside a taken
83
- * N+1 is still not a runnable command.
99
+ * N+1 is still not a runnable command. It did not move both until 2026-09 — the line was
100
+ * `neighbouringPort(probe.port)`, which for the sync finding IS the port the finding is about, and
101
+ * a docblock claiming otherwise is how it survived. `portPairAfter` is the one reader of that rule
102
+ * and `dev-sync.ts`'s own refusal shares it.
103
+ *
104
+ * The sync port is `syncPortFor`, never `neighbouringPort` again: that helper answers 65534 for a
105
+ * web port of 65535 — BELOW the web port, and a port `x dev` never binds — where the boot refuses
106
+ * the run outright with `X_PORT_INVALID`. A probe that reports on a socket the command would never
107
+ * open is answering a question nobody asked, so the boot's own rule decides, and its refusal is
108
+ * reported instead of a probe. Alone, and ahead of the web port: there is no runnable `x dev` at
109
+ * this port whatever the other one answers, and a second finding about it is noise over the cause.
84
110
  */
85
111
  async function portFindings(probe: DoctorProbe): Promise<readonly Finding[]> {
112
+ let syncPort: number;
113
+ try {
114
+ syncPort = syncPortFor(probe.port);
115
+ } catch (error) {
116
+ // The boot's own error, carried whole — `findingFrom` reads its code, cause and fix off the
117
+ // value rather than rendering it, which is what `scripts/catch-render.ts` requires.
118
+ return [findingFrom(error)];
119
+ }
86
120
  const wanted = [
87
121
  { port: probe.port, role: 'web' },
88
- { port: neighbouringPort(probe.port), role: 'sync' },
122
+ { port: syncPort, role: 'sync' },
89
123
  ] as const;
90
124
  const findings: Finding[] = [];
91
125
  for (const entry of wanted) {
@@ -94,10 +128,7 @@ async function portFindings(probe: DoctorProbe): Promise<readonly Finding[]> {
94
128
  finding(
95
129
  'X_PORT_IN_USE',
96
130
  `port ${entry.port} is already listening, and \`x dev --port ${probe.port}\` binds it for the ${entry.role} role`,
97
- // Unchanged, and deliberately: the neighbour below the top of the range is the one port
98
- // `x dev` is guaranteed to accept (`X_CLI_BAD_FLAG` otherwise), which is what
99
- // `cmd-doctor.test.ts` pins by parsing this line with `x dev`'s own flag reader.
100
- `x dev --port ${neighbouringPort(probe.port)}`,
131
+ `x dev --port ${portPairAfter(probe.port)}`,
101
132
  ),
102
133
  );
103
134
  }
@@ -125,13 +156,20 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
125
156
  );
126
157
  return findings;
127
158
  }
128
- if (!probe.exists('.env.development')) {
159
+ if (!probe.exists(ENV_DEVELOPMENT)) {
129
160
  findings.push(
130
161
  finding(
131
162
  'X_ENV_MISSING',
132
- '.env.development is missing, so committed defaults cannot be read',
133
- 'x new --force to restore the committed defaults, or create .env.development',
134
- '.env.development',
163
+ `${ENV_DEVELOPMENT} is missing, so committed defaults cannot be read`,
164
+ // The file write, named — `X_PWA_ICON_MISSING`'s shape below, and for its reason. This
165
+ // said `x new --force`, which cannot run where the reader is standing: `x new` takes a
166
+ // <name> positional (reproduced: `x new --force --json` inside an app answers
167
+ // `X_CLI_BAD_FLAG`), and with one it scaffolds a SECOND app beside the broken one.
168
+ // `.env.example` is the committed projection of `envSchema`, so the copy lands every
169
+ // declared key with its default and blank secrets; `x env example` writes it where an app
170
+ // has none, and `X_ENV_EXAMPLE_DRIFT` is what reports that.
171
+ `cp ${ENV_EXAMPLE_PATH} ${ENV_DEVELOPMENT}`,
172
+ ENV_DEVELOPMENT,
135
173
  ),
136
174
  );
137
175
  }
@@ -183,16 +221,12 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
183
221
  ),
184
222
  );
185
223
  }
186
- if (!probe.exists(OFFLINE_FALLBACK)) {
187
- findings.push(
188
- finding(
189
- 'X_PWA_NO_OFFLINE_FALLBACK',
190
- `${OFFLINE_FALLBACK} is missing, so an offline navigation falls back to the browser error page`,
191
- 'x g route offline --surface app',
192
- OFFLINE_FALLBACK,
193
- ),
194
- );
195
- }
224
+ // The DECLARED fallback against the ROUTE TABLE, never a filename: this probed
225
+ // `apps/web/app/offline.tsx`, which is not a route file at all (`assertRouteFilename` refuses
226
+ // it), so no app could clear the finding and its own `fix:` did not either. `doctor-offline.ts`
227
+ // holds the rule and the reasons.
228
+ const offline = offlineFallbackFinding(await probe.offlineFallback());
229
+ if (offline !== undefined) findings.push(offline);
196
230
  const database = await probe.database();
197
231
  if (database !== null) findings.push(database);
198
232
  findings.push(...(await probe.drift()));
@@ -264,6 +298,10 @@ export function probeFor(cwd: string, bunVersion: string, port: number): DoctorP
264
298
  database: () => probeDatabase(process.env['DATABASE_URL']),
265
299
  drift: async () => (root === undefined ? [] : checkMigrationDrift(root)),
266
300
  snapshots: async () => (root === undefined ? [] : checkMigrationSnapshots(root)),
301
+ // `routes: undefined` outside an app is "not judged", which is what the caller already is:
302
+ // `runDoctor` returns on `X_NOT_IN_APP` before this can be asked.
303
+ offlineFallback: async () =>
304
+ root === undefined ? { fallback: null, routes: undefined } : offlineFallbackProbe(root),
267
305
  };
268
306
  }
269
307