@ultimat3/cli 20.2.1 → 22.0.0

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 (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
package/src/prerender.ts CHANGED
@@ -12,13 +12,16 @@ import { loadApp } from './app-load';
12
12
  import { appManifest } from './app-manifest';
13
13
  import type { RouteStats } from './budgets';
14
14
  import { measureDocumentJs, writeBuildStats } from './budgets';
15
- import { routeDocument } from './dev-render';
16
15
  import { errorPageDocument, STATIC_ERROR_PAGE } from './error-pages';
17
16
  import { FAVICON_PATH, faviconBytes } from './favicon';
18
17
  import type { IslandBundle } from './island-bundle';
19
18
  import { buildIslands, writeIslands } from './island-bundle';
20
- import { measurementActor } from './measurement-actor';
19
+ import { measureDatabase } from './measure-database';
20
+ import { measurePaths } from './measure-paths';
21
+ import { measureScope, withAppUrl } from './measure-scope';
22
+ import { clearPrerenderOut } from './prerender-out';
21
23
  import { loadPwaArtifacts, WEB_MANIFEST_PATH, writePwaIcons } from './pwa-artifacts';
24
+ import { routeDocument } from './runtime-render';
22
25
  import type { SkippedRoute, UnmeasuredRoute } from './static-report';
23
26
  import { skippedRoute, skipReasonFor, writeStaticReport } from './static-report';
24
27
  import { styleBundle, writeStyles } from './style-bundle';
@@ -142,6 +145,8 @@ const declaresBudget = (entry: RouteEntry): boolean => {
142
145
  export async function prerenderSite(options: PrerenderOptions): Promise<PrerenderReport> {
143
146
  // The same load `x dev` and `x manifest` perform: importing the app's modules IS what fills the
144
147
  // route registry, so there is no route table to prerender before this runs.
148
+ // Emptied FIRST: the export only ever gained files, so a deleted route's HTML kept shipping.
149
+ await clearPrerenderOut(options.out, options.root);
145
150
  await loadApp(options.root);
146
151
  const buildId = (await appManifest(options.root)).manifest.buildId;
147
152
  const origin = options.origin ?? DEFAULT_ORIGIN;
@@ -218,7 +223,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
218
223
  }
219
224
 
220
225
  // Every render below goes through `routeDocument`, which is the function a REQUEST reaches — and
221
- // a request arrives inside `runWithContext`, installed by the HTTP pipeline (`dev-render.ts`).
226
+ // a request arrives inside `runWithContext`, installed by the HTTP pipeline (`runtime-render.ts`).
222
227
  // Called bare, any route whose component, `load` or `meta` reads `useContext()` threw
223
228
  // `X_NO_CONTEXT`: measured against `examples/dummy`, `/posts/new` and `/settings` were filed
224
229
  // unmeasured for that reason alone, and a `render: 'static'` route reading it failed the whole
@@ -226,115 +231,129 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
226
231
  // documents, and this build's own id so a component reading `ctx.buildId` stamps the artifact
227
232
  // with the id the report and the stats carry.
228
233
  const ctx = createContext({ role: 'web', buildId });
229
- // A SECOND context, for the branch below that renders only to weigh. Its actor holds every
230
- // permission (`measurement-actor.ts`), because an `app/` page's `load` calls policy-guarded
231
- // queries and denied the anonymous one with `X_UNAUTHENTICATED` — every authed page unmeasured.
232
- // `renderStatic` keeps `ctx`: its output is a published file, and a `site/` load that a policy
233
- // refuses must fail the build, never render another actor's rows into it.
234
- const measureCtx = createContext({ role: 'web', buildId, actor: measurementActor() });
235
- const documentAs = (
236
- as: typeof ctx,
237
- entry: RouteEntry,
238
- data: { url: string; params: Record<string, string> },
239
- ) =>
240
- runWithContext(as, () =>
241
- routeDocument(entry, data, {
242
- resolveIsland: (file: string) => islands.resolverFor(file),
243
- themeHead: theme.head,
244
- ...(pwa === undefined ? {} : { pwaHead: pwa.head + (swHead ?? '') }),
245
- }),
246
- );
234
+ // A SECOND scope, for the branch below that renders only to weigh (`measure-scope.ts`): a
235
+ // request context holding the app's measurement actor, with the app's own API answered in
236
+ // process, because an `app/` page's `load` calls policy-guarded queries over its typed client
237
+ // and no server is listening during a build. `renderStatic` keeps `ctx`: its output is a
238
+ // published file, and a `site/` load a policy refuses must fail the build, never render another
239
+ // actor's rows into it.
240
+ const measure = await measureScope({ origin, buildId });
241
+ // Started on the first route that needs weighing, and released however the loop ends.
242
+ const database = measureDatabase(options.root);
243
+ const render = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
244
+ routeDocument(entry, data, {
245
+ resolveIsland: (file: string) => islands.resolverFor(file),
246
+ themeHead: theme.head,
247
+ ...(pwa === undefined ? {} : { pwaHead: pwa.head + (swHead ?? '') }),
248
+ });
247
249
  const document = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
248
- documentAs(ctx, entry, data);
250
+ runWithContext(ctx, () => render(entry, data));
249
251
 
250
- for (const entry of routeEntries()) {
251
- const facts = { surface: entry.surface, render: entry.config.render, route: entry.path };
252
- const reason = skipReasonFor(facts);
253
- if (reason !== null) {
254
- skipped.push(skippedRoute(facts, reason));
255
- if (!declaresBudget(entry)) continue;
256
- // Non-fatal, and that is deliberate: an ssr page's `load` may want a request, a session or a
257
- // database this build does not have, and a `x build --target static` that started failing on
258
- // routes it never used to touch would be a worse regression than the gap it closes. A route
259
- // that will not render here is reported, gets no stats entry, and stays `X_BUDGET_UNMEASURED`.
260
- try {
261
- const html = await documentAs(measureCtx, entry, {
262
- url: new URL(entry.path, origin).href,
263
- params: {},
252
+ try {
253
+ for (const entry of routeEntries()) {
254
+ const facts = { surface: entry.surface, render: entry.config.render, route: entry.path };
255
+ const reason = skipReasonFor(facts);
256
+ if (reason !== null) {
257
+ skipped.push(skippedRoute(facts, reason));
258
+ if (!declaresBudget(entry)) continue;
259
+ // Non-fatal, and that is deliberate: an ssr page's `load` may want a request, a session or a
260
+ // database this build does not have, and a `x build --target static` that started failing on
261
+ // routes it never used to touch would be a worse regression than the gap it closes. A route
262
+ // that will not render here is reported, gets no stats entry, and stays `X_BUDGET_UNMEASURED`.
263
+ try {
264
+ // A dynamic route is rendered at the paths its own `prerender()` lists, and the row holds
265
+ // the heaviest — a static route's rule. One that lists none is its own finding.
266
+ // Inside the measuring scope: a route's `prerender()` lists its paths by reading the app's
267
+ // own data, the same way its `load` does.
268
+ await database.ready();
269
+ const plan = await withAppUrl(origin, () => measure.run(() => measurePaths(entry)));
270
+ if ('unmeasured' in plan) {
271
+ unmeasured.push(plan.unmeasured);
272
+ continue;
273
+ }
274
+ let row: RouteStats | undefined;
275
+ for (const { path, params } of plan.paths) {
276
+ const data = { url: new URL(path, origin).href, params };
277
+ const html = await withAppUrl(origin, () => measure.run(() => render(entry, data)));
278
+ const measured = await measureDocumentJs(html, options.out);
279
+ if (row !== undefined && measured.jsBytes <= row.jsBytes) continue;
280
+ const chain = heaviestSource(islands, measured.entries);
281
+ row = {
282
+ path: entry.path,
283
+ jsBytes: measured.jsBytes,
284
+ frameworkJsBytes: measured.frameworkBytes,
285
+ ...(chain === undefined ? {} : { heaviestChain: chain }),
286
+ };
287
+ }
288
+ if (row !== undefined) routes.push(row);
289
+ } catch (error) {
290
+ // `renderThrowable`, never `String(error)`: this is a caught unknown, and a hostile
291
+ // `toString` here would take the whole build down instead of one route's measurement.
292
+ // A framework error rides along with its code, cause and fix: `checkBudgets` reports
293
+ // `X_ISLAND_PROPS_INVALID` under its own name, because that sentence — the island, the
294
+ // prop, its bytes — is the finding, and `X_BUDGET_UNMEASURED` pointing at this list was
295
+ // a second command between the author and it.
296
+ unmeasured.push({
297
+ path: entry.path,
298
+ reason: renderThrowable(error),
299
+ ...(isUltimateError(error)
300
+ ? { code: error.code, cause: error.cause, fix: error.fix }
301
+ : {}),
302
+ });
303
+ }
304
+ continue;
305
+ }
306
+ const artifacts = await renderStatic(
307
+ entry,
308
+ ({ path, params }) => document(entry, { url: new URL(path, origin).href, params }),
309
+ { buildId },
310
+ );
311
+ // `enumeratePrerender` answers `[]` for a dynamic route with no `prerender()`, so a
312
+ // `render: 'static'` route with a param writes nothing and used to be reported NOWHERE — past
313
+ // the skip branch by its mode, absent from `pages` by its zero artifacts. A route in neither
314
+ // list is the defect this report exists to close, wearing its other shape.
315
+ if (artifacts.length === 0) {
316
+ skipped.push(skippedRoute(facts, 'no-prerender-paths'));
317
+ continue;
318
+ }
319
+ // One stats row per ROUTE, holding its heaviest page. `checkBudgets` looks a route up by
320
+ // `route.url`, which is the manifest's DECLARED pattern (`/blog/:slug`), and this pushed the
321
+ // FILLED path (`/blog/hello`) — so no dynamic static route has ever been weighed: every one
322
+ // was `X_BUDGET_UNMEASURED` and `X_BUDGET_EXCEEDED` could not fire for the whole class. The
323
+ // heaviest page and not the first, because a budget is a ceiling: the page that breaks it is
324
+ // the one the route has to answer for. `pages` below still names every filled path.
325
+ let heaviest: RouteStats | undefined;
326
+ for (const artifact of artifacts) {
327
+ const file = join(options.out, artifact.outputPath);
328
+ const bytes = await Bun.write(file, artifact.html);
329
+ pages.push({
330
+ route: entry.path,
331
+ path: artifact.path,
332
+ file: artifact.outputPath,
333
+ hash: artifact.hash,
334
+ bytes,
264
335
  });
265
- const measured = await measureDocumentJs(html, options.out);
336
+ // `artifact.hash` is `contentHash(html)` — the same identity that becomes this page's ETag,
337
+ // so the precache revision and the HTTP validator can never disagree about one document.
338
+ // Keyed by the FILLED path, which for a non-dynamic route is the declared one; a dynamic
339
+ // route is not precached as a single URL anyway (`buildPrecacheManifest` skips it).
340
+ documents.set(artifact.path, { revision: artifact.hash, bytes });
341
+ // Measured from the document that was just written, so the `budgets` step compares a
342
+ // declared budget against bytes that exist on disk rather than against a graph's estimate.
343
+ const measured = await measureDocumentJs(artifact.html, options.out);
266
344
  const chain = heaviestSource(islands, measured.entries);
267
- routes.push({
345
+ if (heaviest !== undefined && heaviest.jsBytes >= measured.jsBytes) continue;
346
+ heaviest = {
268
347
  path: entry.path,
269
348
  jsBytes: measured.jsBytes,
270
349
  frameworkJsBytes: measured.frameworkBytes,
271
350
  ...(chain === undefined ? {} : { heaviestChain: chain }),
272
- });
273
- } catch (error) {
274
- // `renderThrowable`, never `String(error)`: this is a caught unknown, and a hostile
275
- // `toString` here would take the whole build down instead of one route's measurement.
276
- // A framework error rides along with its code, cause and fix: `checkBudgets` reports
277
- // `X_ISLAND_PROPS_INVALID` under its own name, because that sentence — the island, the
278
- // prop, its bytes — is the finding, and `X_BUDGET_UNMEASURED` pointing at this list was
279
- // a second command between the author and it.
280
- unmeasured.push({
281
- path: entry.path,
282
- reason: renderThrowable(error),
283
- ...(isUltimateError(error)
284
- ? { code: error.code, cause: error.cause, fix: error.fix }
285
- : {}),
286
- });
351
+ };
287
352
  }
288
- continue;
289
- }
290
- const artifacts = await renderStatic(
291
- entry,
292
- ({ path, params }) => document(entry, { url: new URL(path, origin).href, params }),
293
- { buildId },
294
- );
295
- // `enumeratePrerender` answers `[]` for a dynamic route with no `prerender()`, so a
296
- // `render: 'static'` route with a param writes nothing and used to be reported NOWHERE — past
297
- // the skip branch by its mode, absent from `pages` by its zero artifacts. A route in neither
298
- // list is the defect this report exists to close, wearing its other shape.
299
- if (artifacts.length === 0) {
300
- skipped.push(skippedRoute(facts, 'no-prerender-paths'));
301
- continue;
302
- }
303
- // One stats row per ROUTE, holding its heaviest page. `checkBudgets` looks a route up by
304
- // `route.url`, which is the manifest's DECLARED pattern (`/blog/:slug`), and this pushed the
305
- // FILLED path (`/blog/hello`) — so no dynamic static route has ever been weighed: every one
306
- // was `X_BUDGET_UNMEASURED` and `X_BUDGET_EXCEEDED` could not fire for the whole class. The
307
- // heaviest page and not the first, because a budget is a ceiling: the page that breaks it is
308
- // the one the route has to answer for. `pages` below still names every filled path.
309
- let heaviest: RouteStats | undefined;
310
- for (const artifact of artifacts) {
311
- const file = join(options.out, artifact.outputPath);
312
- const bytes = await Bun.write(file, artifact.html);
313
- pages.push({
314
- route: entry.path,
315
- path: artifact.path,
316
- file: artifact.outputPath,
317
- hash: artifact.hash,
318
- bytes,
319
- });
320
- // `artifact.hash` is `contentHash(html)` — the same identity that becomes this page's ETag,
321
- // so the precache revision and the HTTP validator can never disagree about one document.
322
- // Keyed by the FILLED path, which for a non-dynamic route is the declared one; a dynamic
323
- // route is not precached as a single URL anyway (`buildPrecacheManifest` skips it).
324
- documents.set(artifact.path, { revision: artifact.hash, bytes });
325
- // Measured from the document that was just written, so the `budgets` step compares a
326
- // declared budget against bytes that exist on disk rather than against a graph's estimate.
327
- const measured = await measureDocumentJs(artifact.html, options.out);
328
- const chain = heaviestSource(islands, measured.entries);
329
- if (heaviest !== undefined && heaviest.jsBytes >= measured.jsBytes) continue;
330
- heaviest = {
331
- path: entry.path,
332
- jsBytes: measured.jsBytes,
333
- frameworkJsBytes: measured.frameworkBytes,
334
- ...(chain === undefined ? {} : { heaviestChain: chain }),
335
- };
353
+ if (heaviest !== undefined) routes.push(heaviest);
336
354
  }
337
- if (heaviest !== undefined) routes.push(heaviest);
355
+ } finally {
356
+ await database.close();
338
357
  }
339
358
  // The worker, LAST: every document it precaches has now been rendered, hashed and weighed. A
340
359
  // static host runs no route table, so both files go into the artifact — a
@@ -7,7 +7,7 @@
7
7
  // `<link rel="manifest">`, no `theme-color` and no apple-touch icon — and no browser has ever
8
8
  // offered to install one, however the config was written.
9
9
  //
10
- // WHY HERE. `dev-assets.ts`'s reason exactly: three packages declare what an installable app is and
10
+ // WHY HERE. `runtime-assets.ts`'s reason exactly: three packages declare what an installable app is and
11
11
  // none of them can read a config file off disk. This one composes tier 0's `pwa` block with tier
12
12
  // 4's generator and hands both served surfaces and the static export the same two strings.
13
13
  //
@@ -16,11 +16,11 @@
16
16
  // manifest is not, so the worker lands behind a real browser check rather than beside this.
17
17
 
18
18
  // why: Bun exposes no synchronous file-existence primitive, and this read is the same one
19
- // `app-auth.ts` and `dev-cache.ts` each make before importing an app's config — a root with no
19
+ // `app-auth.ts` and `runtime-cache.ts` each make before importing an app's config — a root with no
20
20
  // `app.config.ts` is an ordinary answer here (a scratch root, `x build` outside an app).
21
21
  import { existsSync } from 'node:fs';
22
22
  // why: Bun exposes no path-join primitive, and `APP_CONFIG_FILE` is app-root-relative — the same
23
- // necessity `favicon.ts` and `dev-assets.ts` each record for their own root-relative constant.
23
+ // necessity `favicon.ts` and `runtime-assets.ts` each record for their own root-relative constant.
24
24
  import { join } from 'node:path';
25
25
  import type { PwaColors, PwaOfflineConfig } from '@ultimat3/core';
26
26
  import type { CacheHint, Route, UltimateRequest } from '@ultimat3/http';
@@ -4,6 +4,6 @@
4
4
  // Bundled AND imported by `realtime-browser-barrel.test.ts` — the import is what gives it an lcov
5
5
  // record, since `Bun.build()` reads this file without evaluating it.
6
6
 
7
- import { useLive } from '@ultimat3/realtime';
7
+ import { useQuery } from '@ultimat3/realtime';
8
8
 
9
- export const probeUseLive = useLive;
9
+ export const probeUseQuery = useQuery;
@@ -3,7 +3,8 @@
3
3
  // none: its length is a function of the package's API size, so the ceiling measures the wrong
4
4
  // thing there. One added statement of logic disqualifies it and re-arms the ceiling on the spot.
5
5
 
6
- import { CLOSERS, maskLiterals, OPENERS } from './ts-scan';
6
+ import { maskLiterals } from '@ultimat3/core';
7
+ import { CLOSERS, OPENERS } from './ts-scan';
7
8
 
8
9
  /** Every statement a manifest may hold begins with one of these two words. */
9
10
  const IMPORT_OR_EXPORT = /^(?:import|export)\b/;
package/src/registry.ts CHANGED
@@ -1,34 +1,42 @@
1
1
  // The command registry: the one list the parser, the help catalogue and the dispatcher all read.
2
2
  // A command that is not here does not exist — there is no second place to register one.
3
+ //
4
+ // LAZY, `As of 2026-09-23`: every declaration is static (`cmd-<name>-spec.ts`) and every body is
5
+ // `await import()`ed when that command runs. Importing all 28 bodies to answer one of them was
6
+ // the CLI's whole startup: `x --help` paid for `sass`, Babel, every framework package and the
7
+ // dev server it never started (plan 101 slice 12 d).
3
8
 
4
- import { affectedCommand } from './cmd-affected';
5
- import { buildCommand } from './cmd-build';
6
- import { ciCommand } from './cmd-ci';
7
- import { dbCommand } from './cmd-db';
8
- import { deployCommand } from './cmd-deploy';
9
- import { devCommand } from './cmd-dev';
10
- import { docsCommand } from './cmd-docs';
11
- import { doctorCommand } from './cmd-doctor';
12
- import { envCommand } from './cmd-env';
13
- import { errorsCommand } from './cmd-errors';
14
- import { fixCommand } from './cmd-fix';
15
- import { generateCommand } from './cmd-generate';
9
+ import { affectedSpec } from './cmd-affected-spec';
10
+ import { buildSpec } from './cmd-build-spec';
11
+ import { ciSpec } from './cmd-ci-spec';
12
+ import { dbSpec } from './cmd-db-spec';
13
+ import { deploySpec } from './cmd-deploy-spec';
14
+ import { devSpec } from './cmd-dev-spec';
15
+ import { docsSpec } from './cmd-docs-spec';
16
+ import { doctorSpec } from './cmd-doctor-spec';
17
+ import { envSpec } from './cmd-env-spec';
18
+ import { errorsSpec } from './cmd-errors-spec';
19
+ import { fixSpec } from './cmd-fix-spec';
20
+ import { generateSpec } from './cmd-generate-spec';
21
+ // Registered here, not by whichever command body happens to load: `dispatch` renders a CLI code's
22
+ // title for a refusal raised before any body is imported.
23
+ import './error-codes';
16
24
  import { createHelpCommand, createVersionCommand } from './cmd-help';
17
- import { i18nCommand } from './cmd-i18n';
18
- import { jobsCommand } from './cmd-jobs';
19
- import { manifestCommand } from './cmd-manifest';
20
- import { mcpCommand } from './cmd-mcp';
21
- import { newCommand } from './cmd-new';
25
+ import { i18nSpec } from './cmd-i18n-spec';
26
+ import { jobsSpec } from './cmd-jobs-spec';
27
+ import { manifestSpec } from './cmd-manifest-spec';
28
+ import { mcpSpec } from './cmd-mcp-spec';
29
+ import { newSpec } from './cmd-new-spec';
22
30
  import { plannedCommands } from './cmd-planned';
23
- import { policyCommand } from './cmd-policy';
24
- import { prCommand } from './cmd-pr';
25
- import { actionsCommand, entitiesCommand, queriesCommand } from './cmd-registries';
26
- import { routesCommand } from './cmd-routes';
27
- import { secretsCommand } from './cmd-secrets';
28
- import { shotCommand } from './cmd-shot';
29
- import { tasksCommand } from './cmd-tasks';
30
- import { testCommand } from './cmd-test';
31
- import { verifyCommand } from './cmd-verify';
31
+ import { policySpec } from './cmd-policy-spec';
32
+ import { prSpec } from './cmd-pr-spec';
33
+ import { actionsSpec, entitiesSpec, queriesSpec } from './cmd-registries-spec';
34
+ import { routesSpec } from './cmd-routes-spec';
35
+ import { secretsSpec } from './cmd-secrets-spec';
36
+ import { shotSpec } from './cmd-shot-spec';
37
+ import { tasksSpec } from './cmd-tasks-spec';
38
+ import { testSpec } from './cmd-test-spec';
39
+ import { verifySpec } from './cmd-verify-spec';
32
40
  import type { CliCommand } from './command';
33
41
  import type { CommandSpec } from './parse';
34
42
  import { loadVersion } from './version-loader';
@@ -48,35 +56,51 @@ export function cliVersion(): string {
48
56
  return cliVersionCache;
49
57
  }
50
58
 
51
- const CORE: readonly CliCommand[] = [
52
- newCommand,
53
- devCommand,
54
- buildCommand,
55
- testCommand,
56
- verifyCommand,
57
- generateCommand,
58
- dbCommand,
59
- mcpCommand,
60
- doctorCommand,
61
- deployCommand,
62
- envCommand,
63
- secretsCommand,
64
- manifestCommand,
65
- routesCommand,
66
- actionsCommand,
67
- queriesCommand,
68
- entitiesCommand,
69
- jobsCommand,
70
- tasksCommand,
71
- policyCommand,
72
- i18nCommand,
73
- errorsCommand,
74
- docsCommand,
75
- fixCommand,
76
- affectedCommand,
77
- shotCommand,
78
- prCommand,
79
- ciCommand,
59
+ /**
60
+ * A command whose body loads on first run. The spec is the module's own (`cmd-<name>.ts` reads
61
+ * the same `cmd-<name>-spec.ts`), so the parser and the body can never describe two commands.
62
+ */
63
+ export interface LazyCommand extends CliCommand {
64
+ /** The body module's own command — what `run` delegates to, exposed so a test can hold them equal. */
65
+ load(): Promise<CliCommand>;
66
+ }
67
+
68
+ const lazy = (spec: CommandSpec, load: () => Promise<CliCommand>): LazyCommand => ({
69
+ spec,
70
+ load,
71
+ run: async (ctx) => (await load()).run(ctx),
72
+ });
73
+
74
+ /** The shipped commands, each a static declaration plus the body it loads on first run. */
75
+ export const LAZY_COMMANDS: readonly LazyCommand[] = [
76
+ lazy(newSpec, async () => (await import('./cmd-new')).newCommand),
77
+ lazy(devSpec, async () => (await import('./cmd-dev')).devCommand),
78
+ lazy(buildSpec, async () => (await import('./cmd-build')).buildCommand),
79
+ lazy(testSpec, async () => (await import('./cmd-test')).testCommand),
80
+ lazy(verifySpec, async () => (await import('./cmd-verify')).verifyCommand),
81
+ lazy(generateSpec, async () => (await import('./cmd-generate')).generateCommand),
82
+ lazy(dbSpec, async () => (await import('./cmd-db')).dbCommand),
83
+ lazy(mcpSpec, async () => (await import('./cmd-mcp')).mcpCommand),
84
+ lazy(doctorSpec, async () => (await import('./cmd-doctor')).doctorCommand),
85
+ lazy(deploySpec, async () => (await import('./cmd-deploy')).deployCommand),
86
+ lazy(envSpec, async () => (await import('./cmd-env')).envCommand),
87
+ lazy(secretsSpec, async () => (await import('./cmd-secrets')).secretsCommand),
88
+ lazy(manifestSpec, async () => (await import('./cmd-manifest')).manifestCommand),
89
+ lazy(routesSpec, async () => (await import('./cmd-routes')).routesCommand),
90
+ lazy(actionsSpec, async () => (await import('./cmd-registries')).actionsCommand),
91
+ lazy(queriesSpec, async () => (await import('./cmd-registries')).queriesCommand),
92
+ lazy(entitiesSpec, async () => (await import('./cmd-registries')).entitiesCommand),
93
+ lazy(jobsSpec, async () => (await import('./cmd-jobs')).jobsCommand),
94
+ lazy(tasksSpec, async () => (await import('./cmd-tasks')).tasksCommand),
95
+ lazy(policySpec, async () => (await import('./cmd-policy')).policyCommand),
96
+ lazy(i18nSpec, async () => (await import('./cmd-i18n')).i18nCommand),
97
+ lazy(errorsSpec, async () => (await import('./cmd-errors')).errorsCommand),
98
+ lazy(docsSpec, async () => (await import('./cmd-docs')).docsCommand),
99
+ lazy(fixSpec, async () => (await import('./cmd-fix')).fixCommand),
100
+ lazy(affectedSpec, async () => (await import('./cmd-affected')).affectedCommand),
101
+ lazy(shotSpec, async () => (await import('./cmd-shot')).shotCommand),
102
+ lazy(prSpec, async () => (await import('./cmd-pr')).prCommand),
103
+ lazy(ciSpec, async () => (await import('./cmd-ci')).ciCommand),
80
104
  ];
81
105
 
82
106
  /**
@@ -85,7 +109,7 @@ const CORE: readonly CliCommand[] = [
85
109
  * facts, and only one of them is true — see `cmd-planned.ts`.
86
110
  */
87
111
  export const COMMANDS: readonly CliCommand[] = [
88
- ...CORE,
112
+ ...LAZY_COMMANDS,
89
113
  ...plannedCommands(),
90
114
  createHelpCommand(() => SPECS),
91
115
  createVersionCommand(cliVersion),
@@ -0,0 +1,36 @@
1
+ // `realtime.enabled`, obeyed where the roles are chosen. Until 22.0.0 no boot read it: `x dev`
2
+ // started a `sync` node and `ROLE=sync` served one whatever the key said, so `enabled: false` was
3
+ // documentation. Off now means no `sync` node, no replicator and — with no node to feed — no live
4
+ // feed wiring (`runtime-live-feed.ts` starts nothing for a null node).
5
+
6
+ import type { RealtimeConfig, Role } from '@ultimat3/core';
7
+ import { ConfigInvalidError, logger } from '@ultimat3/core';
8
+
9
+ /** The roles that exist only to serve realtime. `web` publishes too, but serves pages first. */
10
+ const REALTIME_ROLES: readonly Role[] = ['sync', 'replicator'];
11
+
12
+ /**
13
+ * The roles this process may start. Disabled drops the realtime roles and keeps the rest, which is
14
+ * what `x dev`'s default set needs on an app that opted out (`enabled: false`; the default is on).
15
+ * A selection that was NOTHING but realtime roles is refused instead: that is `ROLE=sync` on an app
16
+ * that turned realtime off, a container that would bind nothing and restart behind its probe.
17
+ */
18
+ export function rolesUnderRealtime(
19
+ selected: readonly Role[],
20
+ realtime: Pick<RealtimeConfig, 'enabled'>,
21
+ ): readonly Role[] {
22
+ // The key is read only when a realtime role was asked for, so a selection without one never
23
+ // depends on it — a hand-built runtime in a test that starts `web` alone included.
24
+ const dropped = selected.filter((role) => REALTIME_ROLES.includes(role));
25
+ if (dropped.length === 0 || realtime.enabled) return selected;
26
+ const kept = selected.filter((role) => !REALTIME_ROLES.includes(role));
27
+ if (kept.length === 0) {
28
+ throw new ConfigInvalidError({
29
+ cause: `role ${dropped.join(',')} was asked for and realtime.enabled is false in app.config.ts, so this process would serve nothing`,
30
+ fix: `set realtime: { enabled: true } in app.config.ts, or run a role that is not realtime: x dev --role web,worker,scheduler`,
31
+ meta: { key: 'realtime.enabled', roles: dropped },
32
+ });
33
+ }
34
+ logger.info(`realtime.enabled is false in app.config.ts: not starting ${dropped.join(', ')}`);
35
+ return kept;
36
+ }
@@ -7,8 +7,8 @@ import { describeEntities } from '@ultimat3/entity';
7
7
  import { ReplicatorSlotHeldError } from '@ultimat3/realtime';
8
8
  import type { Replicator, Transport } from '@ultimat3/realtime/server';
9
9
  import { createReplicator, replicatorLockKey, selectChangeFeed } from '@ultimat3/realtime/server';
10
- import type { DevServices, Env } from './dev-services';
11
10
  import { BadFlagError } from './errors';
11
+ import type { DevServices, Env } from './runtime-bindings';
12
12
 
13
13
  export interface StartReplicatorOptions {
14
14
  readonly services: DevServices;
@@ -1,4 +1,4 @@
1
- // The one `RunningServices` every `dev-roles` test file boots roles against, and the one reset
1
+ // The one `RunningServices` every `role-start` test file boots roles against, and the one reset
2
2
  // between them. Shared rather than copied, for the reason `policy-fixture.ts` gives: three files
3
3
  // start the same roles, and a second copy of the runtime drifts while each file keeps passing.
4
4
  //
@@ -6,7 +6,7 @@
6
6
  // `*.test.ts` — a fixture written there is one `tsc` never reads.
7
7
 
8
8
  import { noopPurgeDriver } from '@ultimat3/cache';
9
- import { resetLifecycle } from '@ultimat3/core';
9
+ import { type RealtimeConfig, resetLifecycle } from '@ultimat3/core';
10
10
  import { resetHttpConfig } from '@ultimat3/http';
11
11
  import {
12
12
  createMemoryDriver,
@@ -19,8 +19,8 @@ import {
19
19
  import { createMemoryDriver as createMemoryMailDriver } from '@ultimat3/mail';
20
20
  import { DEFAULT_PRESENCE_TTL_MS, InProcessTransport } from '@ultimat3/realtime/server';
21
21
  import { defineStorage, localDriver } from '@ultimat3/storage';
22
- import type { RunningServices } from './dev-runtime';
23
- import { resolveServices } from './dev-services';
22
+ import { resolveServices } from './runtime-bindings';
23
+ import type { RunningServices } from './runtime-services';
24
24
 
25
25
  /**
26
26
  * Every service a role touches, embedded but real — no PGlite boot for a role-wiring test.
@@ -28,7 +28,10 @@ import { resolveServices } from './dev-services';
28
28
  * `root` is a parameter and not a constant: each test file owns its own directory and deletes it,
29
29
  * so two files sharing one on-disk storage root cannot leave the other's fixture half-removed.
30
30
  */
31
- export function fixtureRuntime(root: string): RunningServices {
31
+ /** Enabled, in-process: the section every role test before 22.0.0 was implicitly booting. */
32
+ const REALTIME_ON: RealtimeConfig = { enabled: true, transport: 'memory', urlEnv: undefined };
33
+
34
+ export function fixtureRuntime(root: string, realtime = REALTIME_ON): RunningServices {
32
35
  const services = resolveServices(root, {});
33
36
  const transport = new InProcessTransport();
34
37
  return {
@@ -41,6 +44,7 @@ export function fixtureRuntime(root: string): RunningServices {
41
44
  outbox: createMemoryOutboxStore(),
42
45
  events: createMemoryEventBus(),
43
46
  transport,
47
+ realtime,
44
48
  transportDetail: 'in-process fanout',
45
49
  // The sync role reads this to build its `PresenceRegistry`; the default is what a boot with no
46
50
  // `NATS_URL` resolves to, so the fixture is the real number rather than a rounder one.