@ultimat3/cli 29.3.2 → 30.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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +13 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +28 -8
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +42 -138
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +17 -27
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,123 @@
1
+ // What a reader is owed beyond a finding as it was thrown: a fix that runs where `x` is not on
2
+ // PATH, and one line for one cause. Render layer only, so every throw site keeps spelling `x …`,
3
+ // which is what `fix-command.ts` reads to prove the command exists.
4
+
5
+ import type { Finding, JsonValue } from './output';
6
+
7
+ const SEGMENT_END = /^(?:&&|\|\||;)/;
8
+ const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|[^\s'"]*)\s+/;
9
+
10
+ /** Where the command word of a segment starts, past its `A=b` prefix. */
11
+ const commandWordAt = (fix: string, from: number): number => {
12
+ let at = from;
13
+ for (;;) {
14
+ while (fix[at] === ' ') at += 1;
15
+ const assignment = ENV_ASSIGNMENT.exec(fix.slice(at));
16
+ if (assignment === null) return at;
17
+ at += assignment[0].length;
18
+ }
19
+ };
20
+
21
+ const isBareX = (fix: string, at: number): boolean =>
22
+ fix[at] === 'x' && (at + 1 === fix.length || fix[at + 1] === ' ');
23
+
24
+ /**
25
+ * A leading `x ` becomes `bunx x `, and after `&&`, `||` and `;`. Quoted text is an argument and a
26
+ * `#` tail is never run, so both are copied as they are. `bunx x` itself does not match, so a report
27
+ * read back by `x verify merge` and rendered again does not grow.
28
+ */
29
+ export function runnableFix(fix: string): string {
30
+ let out = '';
31
+ let quote: string | undefined;
32
+ let at = 0;
33
+ let segmentStart = true;
34
+ while (at < fix.length) {
35
+ if (segmentStart) {
36
+ const word = commandWordAt(fix, at);
37
+ out += fix.slice(at, word);
38
+ at = word;
39
+ if (isBareX(fix, at)) out += 'bunx ';
40
+ segmentStart = false;
41
+ continue;
42
+ }
43
+ const char = fix[at] ?? '';
44
+ if (quote !== undefined) {
45
+ if (char === quote) quote = undefined;
46
+ } else if (char === "'" || char === '"') {
47
+ quote = char;
48
+ } else if (char === '#' && (at === 0 || fix[at - 1] === ' ')) {
49
+ return out + fix.slice(at);
50
+ } else {
51
+ const end = SEGMENT_END.exec(fix.slice(at));
52
+ if (end !== null) {
53
+ out += end[0];
54
+ at += end[0].length;
55
+ segmentStart = true;
56
+ continue;
57
+ }
58
+ }
59
+ out += char;
60
+ at += 1;
61
+ }
62
+ return out;
63
+ }
64
+
65
+ const keyOf = (finding: Finding): string =>
66
+ [finding.code, finding.cause, finding.fix].join('\u0000');
67
+
68
+ /**
69
+ * One finding per code + cause + fix, with how many reported it and where. A broken `$view` throws
70
+ * from every module importing its entity: 3 findings on one load, 29 on a larger app, one repair.
71
+ * The fix is in the key because a rule whose cause is generic names the file in its fix, and those
72
+ * are separate repairs. `reportedAt` keeps what `at` can no longer say.
73
+ */
74
+ export function collapseFindings(findings: readonly Finding[]): readonly Finding[] {
75
+ const groups = new Map<string, Finding[]>();
76
+ for (const finding of findings) {
77
+ const key = keyOf(finding);
78
+ const group = groups.get(key);
79
+ if (group === undefined) groups.set(key, [finding]);
80
+ else group.push(finding);
81
+ }
82
+ return [...groups.values()].map((group) => {
83
+ const first = group[0] as Finding;
84
+ if (group.length === 1) return first;
85
+ const reportedAt = group.flatMap((one) => (one.at === undefined ? [] : [one.at]));
86
+ const meta: { [key: string]: JsonValue } = { ...first.meta, count: group.length };
87
+ if (reportedAt.length > 0) meta['reportedAt'] = reportedAt;
88
+ return { ...first, meta };
89
+ });
90
+ }
91
+
92
+ /** How many sites the human line names. `--json` carries all of them in `meta.reportedAt`. */
93
+ const SITES_SHOWN = 3;
94
+
95
+ /** The human half of a count: `meta` is never the only home of a fact. */
96
+ export function causeWithCount(finding: Finding): string {
97
+ const count = finding.meta?.['count'];
98
+ if (typeof count !== 'number') return finding.cause;
99
+ const sites = finding.meta?.['reportedAt'];
100
+ const named = Array.isArray(sites) ? sites.slice(0, SITES_SHOWN).join(', ') : '';
101
+ return `${finding.cause} (reported ${String(count)} times${named === '' ? '' : `: ${named}`})`;
102
+ }
103
+
104
+ export interface ReaderOptions {
105
+ /** `x` is not this reader's command: its fixes name `bunx x`. `dispatch` decides. */
106
+ readonly bunxRunner?: boolean;
107
+ }
108
+
109
+ /** A finding read out of a CI log is fenced text and is never turned into an instruction. */
110
+ const withRunnableFix = (finding: Finding): Finding => {
111
+ if (finding.source === 'ci-log') return finding;
112
+ const fix = runnableFix(finding.fix);
113
+ return fix === finding.fix ? finding : { ...finding, fix };
114
+ };
115
+
116
+ /** The list a renderer prints. */
117
+ export const findingsForReader = (
118
+ findings: readonly Finding[],
119
+ options: ReaderOptions,
120
+ ): readonly Finding[] => {
121
+ const collapsed = collapseFindings(findings);
122
+ return options.bunxRunner === true ? collapsed.map(withRunnableFix) : collapsed;
123
+ };
package/src/output.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  // output (axiom 4). The human renderer owns the canonical 3-line error format.
4
4
 
5
5
  import { ERROR_DOCS_URL, renderThrowable, singleLine, stringField } from '@ultimat3/core';
6
+ import { unresolvedImportFinding } from './app-load-unresolved';
6
7
  import { msg } from './messages';
8
+ import { causeWithCount, findingsForReader, type ReaderOptions } from './output-findings';
7
9
  import type { TestCounts } from './test-counts';
8
10
 
9
11
  /** One shard's share of one step's corpus — what `x verify merge` checks the parts against. */
@@ -164,6 +166,9 @@ export function findingFrom(value: unknown): Finding {
164
166
  const docs = stringField(value, 'docs');
165
167
  return docs === undefined ? { code, cause, fix } : { code, cause, fix, docs };
166
168
  }
169
+ // An import that will not resolve, on a path outside the app scan: `bun add`, not `x doctor`.
170
+ const unresolved = unresolvedImportFinding(value, process.cwd());
171
+ if (unresolved !== undefined) return unresolved;
167
172
  // This is the LAST renderer between a thrown value and the terminal: a hostile `toString`, a
168
173
  // throwing `message` getter or a trapped `instanceof` here loses the whole report, not one line
169
174
  // of it. `renderThrowable` is total on all three.
@@ -263,13 +268,19 @@ const why = (step: StepResult): string => {
263
268
  const showsOutput = (step: StepResult, verbose: boolean): boolean =>
264
269
  step.output !== undefined && step.output.length > 0 && (verbose || !step.ok);
265
270
 
266
- export function renderHuman(result: CommandResult, verbose = false): string {
271
+ export function renderHuman(
272
+ result: CommandResult,
273
+ verbose = false,
274
+ reader: ReaderOptions = {},
275
+ ): string {
267
276
  const out: string[] = [];
268
277
  for (const step of result.steps ?? []) {
269
278
  out.push(
270
279
  ` ${mark(step)} ${step.name.padEnd(18)} ${step.durationMs}ms${width(step)}${why(step)}`,
271
280
  );
272
- for (const finding of step.findings) out.push(renderFinding(finding, ' '));
281
+ for (const finding of findingsForReader(step.findings, reader)) {
282
+ out.push(renderFinding({ ...finding, cause: causeWithCount(finding) }, ' '));
283
+ }
273
284
  // Escaped like every other line text reaches fd 1 through: a warning names a file path. A
274
285
  // green terminal run shows the count, not the list: `filesize`'s band alone is ~150 lines,
275
286
  // and a wall of advice on every run is read by nobody. `--json` always carries the list.
@@ -298,19 +309,25 @@ export function renderHuman(result: CommandResult, verbose = false): string {
298
309
  // comment retitled the window and cleared the screen, and a newline in one entry printed a
299
310
  // second line a reader — or the agent this command exists for — takes for the renderer's own.
300
311
  for (const line of result.lines ?? []) out.push(singleLine(line));
301
- for (const finding of result.findings ?? []) out.push(renderFinding(finding, ' '));
312
+ for (const finding of findingsForReader(result.findings ?? [], reader)) {
313
+ out.push(renderFinding({ ...finding, cause: causeWithCount(finding) }, ' '));
314
+ }
302
315
  // The summary is foreign too: `cli.shot.threw` interpolates a page's own error message.
303
316
  out.push(`${result.ok ? '✓' : '✗'} ${singleLine(result.summary)}`);
304
317
  return out.join('\n');
305
318
  }
306
319
 
307
- export function renderJson(result: CommandResult, verbose = false): string {
320
+ export function renderJson(
321
+ result: CommandResult,
322
+ verbose = false,
323
+ reader: ReaderOptions = {},
324
+ ): string {
308
325
  const steps = (result.steps ?? []).map((step) => ({
309
326
  name: step.name,
310
327
  ok: step.ok,
311
328
  durationMs: step.durationMs,
312
329
  skipped: step.skipped === true,
313
- findings: step.findings,
330
+ findings: findingsForReader(step.findings, reader),
314
331
  // Never gated on `ok` or `--verbose`, unlike `output` below: see `StepResult.warnings`.
315
332
  ...(step.warnings === undefined || step.warnings.length === 0
316
333
  ? {}
@@ -348,14 +365,21 @@ export function renderJson(result: CommandResult, verbose = false): string {
348
365
  command: result.command,
349
366
  summary: result.summary,
350
367
  ...(result.steps === undefined ? {} : { steps }),
351
- ...(result.findings === undefined ? {} : { findings: result.findings }),
368
+ ...(result.findings === undefined
369
+ ? {}
370
+ : { findings: findingsForReader(result.findings, reader) }),
352
371
  ...(result.data === undefined ? {} : { data: result.data }),
353
372
  };
354
373
  return JSON.stringify(payload);
355
374
  }
356
375
 
357
- export function render(result: CommandResult, json: boolean, verbose = false): string {
358
- return json ? renderJson(result, verbose) : renderHuman(result, verbose);
376
+ export function render(
377
+ result: CommandResult,
378
+ json: boolean,
379
+ verbose = false,
380
+ reader: ReaderOptions = {},
381
+ ): string {
382
+ return json ? renderJson(result, verbose, reader) : renderHuman(result, verbose, reader);
359
383
  }
360
384
 
361
385
  export function exitCodeFor(result: CommandResult): number {
@@ -0,0 +1,110 @@
1
+ // `packages/` may not import `apps/`: a shared package that depends on one app is no longer shared.
2
+ // Its own rule and its own read, beside `app-boundaries.ts`: `readAppSources` is what the surface
3
+ // graph walks, and a package source joining that graph would change every surface verdict too.
4
+ // No per-package tiers: the layout of an app's `packages/` is the app's.
5
+
6
+ import { existsSync } from 'node:fs'; // why: a synchronous directory probe; Bun.file answers files.
7
+ // why: Bun ships no `Bun.*` path API: `joinPath` reaches a file on disk with the host's separator.
8
+ import { join as joinPath } from 'node:path';
9
+ // why: no Bun path API; the POSIX variants resolve specifiers against repo-relative keys, which
10
+ // are POSIX on every host.
11
+ import { dirname, join, normalize } from 'node:path/posix';
12
+ import { ERROR_DOCS_URL, stringField } from '@ultimat3/core';
13
+ import type { SourceFile } from './app-boundaries';
14
+ import { commentSafe } from './comment-safe';
15
+ import { scanRuntimeImports } from './import-scan';
16
+ import type { Finding } from './output';
17
+ import { hasPathSegment } from './path-segments';
18
+ import { toPosix } from './posix-path';
19
+ import { isEntityModule } from './templates/entity-module';
20
+
21
+ const DB_SOURCES = 'packages/db/src/';
22
+
23
+ /** Each app workspace's package name → its directory: `@acme/web` → `apps/web`. */
24
+ export type AppWorkspaces = ReadonlyMap<string, string>;
25
+
26
+ /** Where a specifier lands inside `apps/`, or `undefined` when it lands anywhere else. */
27
+ const appTarget = (
28
+ from: string,
29
+ specifier: string,
30
+ workspaces: AppWorkspaces,
31
+ ): string | undefined => {
32
+ if (specifier.startsWith('.')) {
33
+ const target = normalize(join(dirname(from), specifier));
34
+ return target.startsWith('apps/') ? target : undefined;
35
+ }
36
+ if (specifier.startsWith('apps/')) return specifier;
37
+ for (const [name, dir] of workspaces) {
38
+ if (specifier === name) return dir;
39
+ if (specifier.startsWith(`${name}/`)) return `${dir}${specifier.slice(name.length)}`;
40
+ }
41
+ return undefined;
42
+ };
43
+
44
+ /**
45
+ * The one generated exception: `x g entity` adds its entity to the typed handle and `x new` to
46
+ * `schema.ts`, both in `packages/db/src/`, reading it from the slice that declares it
47
+ * (`templates/scaffold-db-client.ts`). An entity module, exactly: a `repo` there is a violation.
48
+ */
49
+ const isDbEntry = (from: string, target: string): boolean =>
50
+ from.startsWith(DB_SOURCES) && isEntityModule(`${target}.ts`);
51
+
52
+ /** Pure, so a caller holding the text needs no disk. Runtime imports only: `import type` erases. */
53
+ export function packageToAppFindings(
54
+ files: readonly SourceFile[],
55
+ workspaces: AppWorkspaces,
56
+ ): readonly Finding[] {
57
+ return files.flatMap((file) =>
58
+ scanRuntimeImports(file).flatMap((specifier) => {
59
+ const target = appTarget(file.path, specifier, workspaces);
60
+ if (target === undefined || isDbEntry(file.path, target)) return [];
61
+ return [
62
+ {
63
+ code: 'X_BOUNDARY_PACKAGE_TO_APP',
64
+ cause: `package source imports an app ("${specifier}", in ${target}) — a package an app depends on cannot depend back on one app`,
65
+ fix: `x verify --only boundaries --json # after moving ${commentSafe(target)} into the package, or importing in the opposite direction`,
66
+ docs: ERROR_DOCS_URL,
67
+ at: file.path,
68
+ },
69
+ ];
70
+ }),
71
+ );
72
+ }
73
+
74
+ const PACKAGE_GLOB = 'packages/*/**/*.{ts,tsx}';
75
+
76
+ /** Every package source of an app: never a test, a dependency or a build's output. */
77
+ export async function readPackageSources(root: string): Promise<readonly SourceFile[]> {
78
+ const files: SourceFile[] = [];
79
+ for await (const path of new Bun.Glob(PACKAGE_GLOB).scan({ cwd: root, absolute: false })) {
80
+ if (hasPathSegment(path, 'node_modules') || hasPathSegment(path, 'dist')) continue;
81
+ if (path.includes('.test.') || path.endsWith('.d.ts')) continue;
82
+ const posix = toPosix(path);
83
+ files.push({ path: posix, source: await Bun.file(joinPath(root, posix)).text() });
84
+ }
85
+ return files.sort((left, right) => (left.path < right.path ? -1 : 1));
86
+ }
87
+
88
+ /** `apps/*\/package.json`'s names, or `undefined` for a tree that holds no app at all. */
89
+ const readAppWorkspaces = async (root: string): Promise<AppWorkspaces | undefined> => {
90
+ // A root with no `apps/` at all, before the walk: Bun's glob throws on a missing directory.
91
+ if (!existsSync(joinPath(root, 'apps'))) return undefined;
92
+ const found = new Map<string, string>();
93
+ for await (const path of new Bun.Glob('apps/*/package.json').scan({ cwd: root })) {
94
+ const manifest: unknown = await Bun.file(joinPath(root, path)).json();
95
+ // `stringField`: a parsed manifest is a value this process did not build (`caught-value-reads`).
96
+ const name = stringField(manifest, 'name');
97
+ if (name !== undefined) found.set(name, dirname(toPosix(path)));
98
+ }
99
+ return found;
100
+ };
101
+
102
+ /**
103
+ * The rule over a real tree. A tree with no `apps/` answers nothing: that is the framework's own
104
+ * repository, whose `packages/` is 3,000 files this rule has nothing to say about.
105
+ */
106
+ export async function packageBoundaryFindings(root: string): Promise<readonly Finding[]> {
107
+ const workspaces = await readAppWorkspaces(root);
108
+ if (workspaces === undefined) return [];
109
+ return packageToAppFindings(await readPackageSources(root), workspaces);
110
+ }
@@ -0,0 +1,23 @@
1
+ // The pins a rendered document stamps (`@ultimat3/render`'s `clientPinTags`): every registered
2
+ // action and query that declares `http: { path }`, read off the registries per render, so a
3
+ // browser caller reaching one by NAME goes where the declaration put it — nothing restated.
4
+
5
+ import { actionHttpPinOf, describeActions, getAction } from '@ultimat3/action';
6
+ import type { ActionPin, ClientPins } from '@ultimat3/core';
7
+ import { describeQueries, getQuery, queryHttpPinOf } from '@ultimat3/query';
8
+
9
+ export function servedPins(): ClientPins {
10
+ const actions: Record<string, ActionPin> = {};
11
+ for (const { name } of describeActions()) {
12
+ const target = getAction(name);
13
+ const pin = target === undefined ? undefined : actionHttpPinOf(target);
14
+ if (pin !== undefined) actions[name] = pin;
15
+ }
16
+ const queries: Record<string, string> = {};
17
+ for (const { name } of describeQueries()) {
18
+ const target = getQuery(name);
19
+ const pin = target === undefined ? undefined : queryHttpPinOf(target);
20
+ if (pin !== undefined) queries[name] = pin.path;
21
+ }
22
+ return { actions, queries };
23
+ }
@@ -0,0 +1,13 @@
1
+ // The plain routes both boots mount and `x routes` lists: `apps/<app>/runtime.ts`'s `routes`, then
2
+ // every declared `webhookReceiver()`'s. Declaring a receiver is its one registration; a route the
3
+ // app also named by hand is mounted once, never twice (`X_ROUTE_CONFLICT`).
4
+
5
+ import type { Route } from '@ultimat3/http';
6
+ import { webhookReceiverRoutes } from '@ultimat3/jobs';
7
+
8
+ /** Called after the app's modules loaded: the receivers are read off the job registry. */
9
+ export function plainRoutes(declared: readonly Route[] | undefined): readonly Route[] {
10
+ const own = declared ?? [];
11
+ const named = new Set(own);
12
+ return [...own, ...webhookReceiverRoutes().filter((route) => !named.has(route))];
13
+ }
@@ -0,0 +1,59 @@
1
+ // The pages each public `site/` route's `prerender()` lists, enumerated ONCE by the static export
2
+ // while its in-process API scope and database are still open — and handed to `siteSeo` on the
3
+ // report, so the sitemap never asks `prerender()` again after the build released both.
4
+
5
+ import type { PrerenderEntry, RouteEntry } from '@ultimat3/render';
6
+ import { enumeratePrerenderEntries } from '@ultimat3/render/server';
7
+
8
+ /** Every route's enumeration, by its DECLARED path (`/posts/:slug`). */
9
+ export type RouteEntries = ReadonlyMap<string, readonly PrerenderEntry[]>;
10
+
11
+ export type EnumerateRoute = (entry: RouteEntry) => Promise<readonly PrerenderEntry[]>;
12
+
13
+ /**
14
+ * `enumeratePrerenderEntries`, asked at most once per route: the build hands the one memo to the
15
+ * weigh-only branch (`measurePaths`) and to `enumerateRoutes`, so a budgeted dynamic route's
16
+ * `prerender()` runs once per build.
17
+ */
18
+ export function enumerateOnce(): EnumerateRoute {
19
+ const asked = new Map<string, Promise<readonly PrerenderEntry[]>>();
20
+ return (entry) => {
21
+ const known = asked.get(entry.path);
22
+ if (known !== undefined) return known;
23
+ const answer = enumeratePrerenderEntries(entry);
24
+ asked.set(entry.path, answer);
25
+ return answer;
26
+ };
27
+ }
28
+
29
+ /**
30
+ * A route `siteSeo` would ask: public `site/` with a `prerender()` declared. One without has
31
+ * nothing to enumerate (`enumeratePrerenderEntries` answers from the pattern alone), so an app with
32
+ * none starts no database for it.
33
+ */
34
+ export const asksPrerender = (entry: RouteEntry): boolean =>
35
+ entry.surface === 'site' &&
36
+ entry.config.policy === undefined &&
37
+ entry.config.prerender !== undefined;
38
+
39
+ /**
40
+ * Each of `routes` that `asksPrerender`, enumerated in turn inside `scope` — the build's measuring
41
+ * scope, where a `prerender()` reading the app's own query is answered. A throw is the build's
42
+ * failure, exactly as it was when the sitemap made the call: a `prerender()` that cannot list its
43
+ * pages is the app's bug, never a page silently left out.
44
+ */
45
+ export async function enumerateRoutes(
46
+ routes: readonly RouteEntry[],
47
+ scope: <T>(fn: () => Promise<T>) => Promise<T>,
48
+ before: () => Promise<void>,
49
+ enumerate: EnumerateRoute = enumeratePrerenderEntries,
50
+ ): Promise<RouteEntries> {
51
+ const asked = routes.filter(asksPrerender);
52
+ const entries = new Map<string, readonly PrerenderEntry[]>();
53
+ if (asked.length === 0) return entries;
54
+ await before();
55
+ for (const entry of asked) {
56
+ entries.set(entry.path, await scope(() => enumerate(entry)));
57
+ }
58
+ return entries;
59
+ }
@@ -0,0 +1,27 @@
1
+ // Single responsibility: name the heaviest thing a document boots by the source file an author can
2
+ // open. Split from `prerender.ts`, which stands at its size ceiling.
3
+
4
+ import type { IslandBundle } from './island-bundle';
5
+
6
+ /**
7
+ * The heaviest thing the document actually boots, named by the source file an author can open.
8
+ * An island chunk is content-addressed, so its URL says nothing on its own — mapping it back
9
+ * through the bundle is what turns `X_BUDGET_EXCEEDED` from a number into an instruction.
10
+ */
11
+ export function heaviestSource(
12
+ bundle: IslandBundle,
13
+ entries: readonly { readonly url: string; readonly bytes: number }[],
14
+ ): readonly string[] | undefined {
15
+ const heaviest = entries.reduce<{ url: string; bytes: number } | undefined>(
16
+ (max, entry) => (max === undefined || entry.bytes > max.bytes ? entry : max),
17
+ undefined,
18
+ );
19
+ if (heaviest === undefined) return undefined;
20
+ const chunk = bundle.chunkAt(heaviest.url);
21
+ if (chunk !== undefined) return [chunk.file];
22
+ // A shared chunk has no file of its own: the chain is an island on THIS page that imports it.
23
+ const importer = bundle.chunks.find(
24
+ (one) => one.imports.includes(heaviest.url) && entries.some((entry) => entry.url === one.url),
25
+ );
26
+ return importer === undefined ? [heaviest.url] : [importer.file, heaviest.url];
27
+ }
@@ -0,0 +1,6 @@
1
+ // Preloaded into `x build --target static`'s `bun run apps/web/prerender.ts`: installs the render
2
+ // loader BEFORE Bun parses the script's import graph. A `.tsx` the script reaches statically (a
3
+ // shared module that imports `@ultimat3/ui`) is parsed first, so a loader installed by evaluating
4
+ // `@ultimat3/render` inside that graph arrives too late and Bun compiles the JSX to `jsxDEV`.
5
+
6
+ import '@ultimat3/render/server';
@@ -2,6 +2,8 @@
2
2
  // where the stats and the inventory landed. Split from `prerender.ts`, which decides what is
3
3
  // written; this file only names the shapes the report carries.
4
4
 
5
+ import type { RouteEntries } from './prerender-entries';
6
+ import type { LlmsPages } from './site-llms-pages';
5
7
  import type { SkippedRoute, UnmeasuredRoute } from './static-report';
6
8
 
7
9
  export interface PrerenderedPage {
@@ -55,4 +57,14 @@ export interface PrerenderReport {
55
57
  readonly warnings: readonly string[];
56
58
  /** The origin every absolute URL in the export was built against — hand it to `siteSeo`. */
57
59
  readonly origin: string;
60
+ /**
61
+ * Every page each public `site/` route's `prerender()` listed, enumerated once inside the build's
62
+ * scope — hand it to `siteSeo`, which then never asks `prerender()` after that scope is gone.
63
+ */
64
+ readonly entries: RouteEntries;
65
+ /**
66
+ * Every `llms.txt` page record whose `meta` needs its `load`, read inside the build's scope —
67
+ * hand it to `siteSeo` as `llmsPages`, which then never runs a `load` after that scope is gone.
68
+ */
69
+ readonly llmsPages: LlmsPages;
58
70
  }
@@ -0,0 +1,38 @@
1
+ // What `siteSeo` needs from the static build, read while its API scope and database are still open:
2
+ // each `site/` route's `prerender()` entries and each `llms.txt` page's `load`. Asked after the
3
+ // scope closes, a route that reads the app's own query fails the export.
4
+
5
+ import { routeEntries } from '@ultimat3/render';
6
+ import { type EnumerateRoute, enumerateRoutes, type RouteEntries } from './prerender-entries';
7
+ import { loadSiteSettings } from './site-config';
8
+ import { collectLlmsPages, type LlmsPages } from './site-llms-pages';
9
+
10
+ export interface SeoReadsInput {
11
+ readonly root: string;
12
+ readonly origin: string;
13
+ readonly defaultLocale: string;
14
+ /** Runs one read inside the build's PUBLIC scope: its in-process API authenticates nobody. */
15
+ readonly scope: <T>(fn: () => Promise<T>) => Promise<T>;
16
+ /** Readies the build's database before the first read that needs it. */
17
+ readonly before: () => Promise<void>;
18
+ /** The build's one enumeration memo, so a route's `prerender()` runs once per build. */
19
+ readonly enumerate: EnumerateRoute;
20
+ }
21
+
22
+ export interface SeoReads {
23
+ readonly entries: RouteEntries;
24
+ readonly llmsPages: LlmsPages;
25
+ }
26
+
27
+ export async function collectSeoReads(input: SeoReadsInput): Promise<SeoReads> {
28
+ const entries = await enumerateRoutes(routeEntries(), input.scope, input.before, input.enumerate);
29
+ const llmsPages = await collectLlmsPages({
30
+ baseUrl: input.origin,
31
+ defaultLocale: input.defaultLocale,
32
+ enabled: (await loadSiteSettings(input.root)).llms,
33
+ entries,
34
+ scope: input.scope,
35
+ before: input.before,
36
+ });
37
+ return { entries, llmsPages };
38
+ }
@@ -9,14 +9,21 @@ import type { MeasureOptions } from './budgets';
9
9
  import type { IslandBundle } from './island-bundle';
10
10
  import { isRuntimeChunk } from './island-runtime';
11
11
  import { pageSync } from './page-sync';
12
+ import type { VitalsBuild } from './prerender-vitals';
12
13
  import { realtimeConfigOf } from './runtime-realtime';
13
14
  import { PAGE_BOOT_BASE_PATH } from './worker-bundle';
15
+ import { zoneBoot } from './zone-boot';
14
16
 
15
17
  /** What a served-document measurement adds: to the document's options, and to the measurer's. */
16
18
  export interface ServedMeasure {
17
19
  readonly document: {
18
20
  readonly sync?: ClientSyncHead;
19
21
  readonly persisted?: () => readonly string[];
22
+ /**
23
+ * `/x-client-vitals.js`, when `clientErrors.vitals` lists `app`, and the zone script when
24
+ * `time.detect` is on: served, so charged — both are the app's opt-in.
25
+ */
26
+ readonly appHead?: string;
20
27
  };
21
28
  readonly measure: MeasureOptions;
22
29
  }
@@ -24,25 +31,33 @@ export interface ServedMeasure {
24
31
  /**
25
32
  * `realtime.enabled: false` serves no sync head and no boot (`pageSync`), and then the measurement
26
33
  * adds nothing: such a page's realtime islands load the runtime chunk, and are charged for it.
34
+ *
35
+ * Real-user timings ride the same way: an `app/` document of an app that listed `app` names
36
+ * `/x-client-vitals.js`, which no artifact holds, so its bytes come from `vitals.script`. Charged,
37
+ * not exempted (`budgets.ts`' `FRAMEWORK_SCRIPTS`): the app opted in, and can opt out.
27
38
  */
28
39
  export async function servedMeasure(
29
40
  root: string,
30
41
  buildId: string,
31
42
  islands: IslandBundle,
43
+ vitals: VitalsBuild = {},
32
44
  ): Promise<ServedMeasure> {
33
- const sync = await pageSync(
34
- root,
35
- process.env,
36
- buildId,
37
- realtimeConfigOf(await loadAppConfig(root)),
38
- );
45
+ const config = await loadAppConfig(root);
46
+ const sync = await pageSync(root, process.env, buildId, realtimeConfigOf(config));
47
+ const appHead = (zoneBoot(config?.time)?.head ?? '') + (vitals.appHead ?? '');
39
48
  return {
40
49
  document: {
41
50
  ...(sync.head === undefined ? {} : { sync: sync.head }),
42
51
  persisted: sync.persisted,
52
+ ...(appHead === '' ? {} : { appHead }),
43
53
  },
44
54
  measure: {
45
- served: new Map(sync.scripts.map((script) => [script.url, script.code])),
55
+ served: new Map([
56
+ ...sync.scripts.map((script): [string, string] => [script.url, script.code]),
57
+ ...(vitals.appHead === undefined || vitals.script === undefined
58
+ ? []
59
+ : [[vitals.script.url, vitals.script.code] as [string, string]]),
60
+ ]),
46
61
  boot: {
47
62
  prefix: `${PAGE_BOOT_BASE_PATH}/`,
48
63
  supplies: new Set(islands.shared.map((one) => one.url).filter(isRuntimeChunk)),
@@ -0,0 +1,42 @@
1
+ // What the build adds for real-user timings (`clientErrors.vitals`): the head each surface's
2
+ // documents carry and the script's bytes, so the budgets step weighs a page as it is SERVED. Off,
3
+ // or a surface not listed, adds nothing — and a page that carries nothing is charged nothing.
4
+
5
+ import type { ClientVitalsConfig } from '@ultimat3/core';
6
+ import {
7
+ CLIENT_VITALS_PATH,
8
+ CLIENT_VITALS_SCRIPT_PATH,
9
+ clientVitalsHead,
10
+ clientVitalsScript,
11
+ } from './client-vitals-script';
12
+
13
+ export interface VitalsBuild {
14
+ /** For an `app/` document weighed as served. Absent unless `app` is listed. */
15
+ readonly appHead?: string;
16
+ /** For a `site/` document, exported and weighed. Absent unless `site` is listed. */
17
+ readonly siteHead?: string;
18
+ /** The bytes behind the head, keyed by the URL a document names. Absent when off. */
19
+ readonly script?: { readonly url: string; readonly code: string };
20
+ }
21
+
22
+ export function vitalsBuild(config: ClientVitalsConfig | undefined): VitalsBuild {
23
+ if (config?.enabled !== true) return {};
24
+ const head = clientVitalsHead();
25
+ return {
26
+ ...(config.surfaces.includes('app') ? { appHead: head } : {}),
27
+ ...(config.surfaces.includes('site') ? { siteHead: head } : {}),
28
+ script: { url: CLIENT_VITALS_SCRIPT_PATH, code: clientVitalsScript(config.sampleRate) },
29
+ };
30
+ }
31
+
32
+ /**
33
+ * A `site/` page exported with the script names a beacon route only a served process answers:
34
+ * on a static host the timings go nowhere. Said, never refused — the same export served by the
35
+ * container does land them, and the bytes are charged either way.
36
+ */
37
+ export function clientVitalsStaticWarning(build: VitalsBuild): string[] {
38
+ if (build.siteHead === undefined) return [];
39
+ return [
40
+ `clientErrors.vitals.surfaces lists site, and a static host serves no ${CLIENT_VITALS_PATH} route — exported site/ pages carry ${CLIENT_VITALS_SCRIPT_PATH}, and their beacons land only where the app is served (x dev, or the container)`,
41
+ ];
42
+ }