@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,170 @@
1
+ // The `api` shape's key slice: an org's API key, issued by an admin, resolved by the `/v1` bearer
2
+ // mount. The framework's own `issueApiKey` / `apiKeyResolver` over `x_api_keys`; no second store.
3
+
4
+ import type { GeneratedFile } from '../naming';
5
+
6
+ const DIR = 'apps/web/app/keys';
7
+
8
+ const keys = (): string => `/**
9
+ * An org's API key: a machine caller's credential for the actions and queries a browser would call.
10
+ * No second API — the \`/v1\` mount (\`api/index.ts\`) re-serves them behind \`resolveApiKey\`.
11
+ *
12
+ * A key's scopes ARE its permissions (\`apiKeyActor\`), so a key issued with \`post:write\` passes
13
+ * \`canPostWrite\` for the org it was issued in and for nothing else.
14
+ */
15
+
16
+ import type { ApiKeyVerifyStore } from '@ultimat3/auth';
17
+ import {
18
+ apiKeyResolver,
19
+ issueApiKey,
20
+ memoryAuthAdapter,
21
+ postgresAuthAdapter,
22
+ } from '@ultimat3/auth';
23
+ import { type Clock, resolveEnvironment, storeMode } from '@ultimat3/core';
24
+
25
+ /** Every scope a key can carry; the map below says what each one reaches. */
26
+ export const apiKeyScopes = ['post:read', 'post:write'] as const;
27
+
28
+ export type ApiKeyScope = (typeof apiKeyScopes)[number];
29
+
30
+ /** What a key may do, and the one map the \`/v1\` mount cuts its routes by. */
31
+ export const API_KEY_SCOPES = {
32
+ 'post:read': ['postList'],
33
+ 'post:write': ['createPost', 'archivePost'],
34
+ } as const satisfies Record<ApiKeyScope, readonly string[]>;
35
+
36
+ /**
37
+ * \`x_api_keys\` through the framework's adapter, except under \`bun test\`, where no database client
38
+ * is installed — core's \`storeMode\`, the answer \`packages/db\`'s driver reads too. Built on first
39
+ * use: the adapter takes the process client, which the boot installs after this module loads.
40
+ */
41
+ let store: ApiKeyVerifyStore | undefined;
42
+ const keyStore = (): ApiKeyVerifyStore => {
43
+ store ??= storeMode(Bun.env) === 'memory' ? memoryAuthAdapter() : postgresAuthAdapter();
44
+ return store;
45
+ };
46
+
47
+ /** The mount's \`resolveToken\`: every wrong key is one \`null\` (401), a fault propagates. */
48
+ export const resolveApiKey = apiKeyResolver(keyStore);
49
+
50
+ export interface IssuedKey {
51
+ readonly id: string;
52
+ readonly prefix: string;
53
+ /** The plaintext. Stored nowhere: the record holds a hash of its secret half. */
54
+ readonly key: string;
55
+ }
56
+
57
+ /** A key for one org, with every scope unless \`scopes\` narrows it. Owned by the org, not a user. */
58
+ export async function issueKeyFor(issuer: {
59
+ readonly orgId: string;
60
+ readonly clock: Clock;
61
+ readonly scopes?: readonly ApiKeyScope[] | undefined;
62
+ }): Promise<IssuedKey> {
63
+ const issued = issueApiKey({
64
+ env: resolveEnvironment({ env: Bun.env }) === 'production' ? 'prod' : 'dev',
65
+ scopes: issuer.scopes ?? apiKeyScopes,
66
+ orgId: issuer.orgId,
67
+ clock: issuer.clock,
68
+ });
69
+ await keyStore().putApiKey(issued.record);
70
+ return { id: issued.record.id, prefix: issued.record.prefix, key: issued.plaintext };
71
+ }
72
+ `;
73
+
74
+ const keysTest =
75
+ (): string => `// A key, issued and presented back: who it resolves to, and that a wrong one is nobody.
76
+ import { systemClock } from '@ultimat3/core';
77
+ import { expect, unitTest } from '@ultimat3/testing';
78
+ import { apiKeyScopes, issueKeyFor, resolveApiKey } from './keys';
79
+
80
+ const ORG = '00000000-0000-4000-8000-0000000000a1';
81
+
82
+ unitTest('an issued key resolves to its org, carrying every scope', async () => {
83
+ const issued = await issueKeyFor({ orgId: ORG, clock: systemClock });
84
+ const caller = await resolveApiKey(issued.key);
85
+ expect(caller?.actor.orgId).toBe(ORG);
86
+ expect([...(caller?.scopes ?? [])].sort()).toEqual([...apiKeyScopes].sort());
87
+ });
88
+
89
+ unitTest('a key narrowed to reads carries reads only', async () => {
90
+ const issued = await issueKeyFor({ orgId: ORG, clock: systemClock, scopes: ['post:read'] });
91
+ expect([...((await resolveApiKey(issued.key))?.scopes ?? [])]).toEqual(['post:read']);
92
+ });
93
+
94
+ unitTest('a key nobody issued is nobody, never an error', async () => {
95
+ expect(await resolveApiKey('sk_dev_0000000000000000_not-a-real-secret')).toBeNull();
96
+ });
97
+ `;
98
+
99
+ const policy =
100
+ (): string => `// Who may mint an org's API key: an admin of that org. A key is a credential that outlives the
101
+ // session that issued it, so it is its own grant rather than a side effect of \`post:write\`.
102
+
103
+ import { can, definePermissions } from '@ultimat3/policy';
104
+
105
+ declare module '@ultimat3/policy' {
106
+ interface PermissionRegistry {
107
+ 'key:issue': true;
108
+ }
109
+ }
110
+
111
+ export const keyPermissions = definePermissions(['key:issue']);
112
+
113
+ /** Issue takes no org from its input — the key is the actor's own org's — so the actor must HAVE one. */
114
+ export const canKeyIssue = can(
115
+ 'key:issue',
116
+ ({ actor }) => actor !== null && typeof actor.orgId === 'string' && actor.orgId !== '',
117
+ );
118
+ `;
119
+
120
+ const issueAction =
121
+ (): string => `// Mint an API key for the caller's org. The answer is a credential shown once, so there is no
122
+ // \`mcp\` block: a tool result is kept in a transcript.
123
+
124
+ import { action, t } from '@ultimat3/action';
125
+ import { apiKeyScopes, issueKeyFor } from '../keys';
126
+ import { canKeyIssue } from '../policy';
127
+
128
+ export const issueKey = action({
129
+ input: t.object({ scopes: t.array(t.enum(apiKeyScopes)).optional() }),
130
+ output: t.object({ id: t.string, prefix: t.string, key: t.string }),
131
+ policy: canKeyIssue,
132
+ async handle({ input, ctx }) {
133
+ return issueKeyFor({ orgId: ctx.actor?.orgId ?? '', clock: ctx.clock, scopes: input.scopes });
134
+ },
135
+ });
136
+ `;
137
+
138
+ const issueActionTest = (): string => `// Who may mint a key, and what the key can then do.
139
+ import { testActor } from '@ultimat3/policy';
140
+ import { expect, unitTest } from '@ultimat3/testing';
141
+ import { resolveApiKey } from '../keys';
142
+ import { issueKey } from './issue-key';
143
+
144
+ const ORG = '00000000-0000-4000-8000-0000000000a1';
145
+ const target = issueKey.named('issueKey');
146
+
147
+ unitTest('a member without the grant is refused', async () => {
148
+ const member = testActor('member', { orgId: ORG, permissions: ['post:read'] }).actor;
149
+ await expect(target.as(member, {})).rejects.toBeUltimateError('X_FORBIDDEN');
150
+ });
151
+
152
+ unitTest("an admin's key resolves to the admin's org, with the scopes asked for", async () => {
153
+ const admin = testActor('admin', { orgId: ORG, permissions: ['key:issue'] }).actor;
154
+ const issued = await target.as(admin, { scopes: ['post:read'] });
155
+ const caller = await resolveApiKey(issued.key);
156
+ expect(caller?.actor.orgId).toBe(ORG);
157
+ expect([...(caller?.scopes ?? [])]).toEqual(['post:read']);
158
+ });
159
+ `;
160
+
161
+ /** `apps/web/app/keys/`: the key store, its policy, the issuing action and their tests. */
162
+ export function apiKeyFiles(): readonly GeneratedFile[] {
163
+ return [
164
+ { path: `${DIR}/keys.ts`, contents: keys() },
165
+ { path: `${DIR}/keys.test.ts`, contents: keysTest() },
166
+ { path: `${DIR}/policy.ts`, contents: policy() },
167
+ { path: `${DIR}/actions/issue-key.ts`, contents: issueAction() },
168
+ { path: `${DIR}/actions/issue-key.test.ts`, contents: issueActionTest() },
169
+ ];
170
+ }
@@ -0,0 +1,101 @@
1
+ // `x new <name> --shape api`: the example `post` slice as an API — actions, a live query and a job,
2
+ // served over a session, an org API key on `/v1`, OpenAPI and MCP. No page beyond the error
3
+ // documents and the offline fallback, no island, no app UI. `/admin` stays: it is the operator's
4
+ // console. The PWA block stays too: a page `x g` writes into this app is tested offline.
5
+
6
+ import type { GeneratedFile, NameSet } from '../naming';
7
+ import { apiKeyFiles } from './api-keys';
8
+ import type { ShapeEdit, ShapeOverlay } from './overlay';
9
+ import { claudeRows, shapeLines } from './shared-edits';
10
+
11
+ const apiIndex = (app: NameSet): string => `/**
12
+ * The API surface: every action, query and job this app exposes, registered in one call. From this
13
+ * list the framework projects HTTP routes, \`openapi.json\`, the typed client, job handles and MCP
14
+ * tools. \`defineApi\` takes whole MODULES, so the export name IS the primitive's name.
15
+ *
16
+ * Two doors onto the same handlers: a session (or \`x dev\`'s development viewer) on \`/api\`, and an
17
+ * org's API key on \`/v1\` — cut by \`API_KEY_SCOPES\`, so a read-only key reaches \`postList\` only.
18
+ */
19
+
20
+ import { defineApi } from '@ultimat3/action';
21
+ import * as issueKey from '../app/keys/actions/issue-key';
22
+ import { API_KEY_SCOPES, resolveApiKey } from '../app/keys/keys';
23
+ import * as archivePost from '../app/post/actions/archive-post';
24
+ import * as createPost from '../app/post/actions/create-post';
25
+ import * as reindexPost from '../app/post/jobs/reindex-post';
26
+ import * as postList from '../app/post/live/post-list';
27
+ import * as health from './health';
28
+
29
+ export const api = defineApi({
30
+ actions: [health, createPost, archivePost, issueKey],
31
+ queries: [postList],
32
+ jobs: [reindexPost],
33
+ http: { mounts: [{ prefix: '/v1', scopes: API_KEY_SCOPES, resolveToken: resolveApiKey }] },
34
+ // Declared, so \`openapi.json\` is the complete document: every operation, both security schemes.
35
+ openapi: { title: '${app.pascal}', version: '1.0.0' },
36
+ });
37
+
38
+ /** What a typed client is shaped from. */
39
+ export type Api = typeof api;
40
+ `;
41
+
42
+ const apiIndexTest =
43
+ (): string => `// The registration, read back: an action left out of the list is a route that answers 404 with
44
+ // every test of the action itself still green.
45
+ import { expect, unitTest } from '@ultimat3/testing';
46
+ import { API_KEY_SCOPES } from '../app/keys/keys';
47
+ import { api } from './index';
48
+
49
+ unitTest('every action, the live query and the job are registered under their export names', () => {
50
+ for (const name of ['health', 'createPost', 'archivePost', 'issueKey'] as const) {
51
+ expect(api.actions[name].describe().name).toBe(name);
52
+ }
53
+ expect(api.queries.postList.isLive).toBe(true);
54
+ expect(api.jobs.reindexPost.name).toBe('reindexPost');
55
+ });
56
+
57
+ unitTest('every operation a key scope names is registered', () => {
58
+ const registered = new Set([...Object.keys(api.actions), ...Object.keys(api.queries)]);
59
+ for (const names of Object.values(API_KEY_SCOPES)) {
60
+ for (const name of names)
61
+ expect({ name, registered: registered.has(name) }).toEqual({ name, registered: true });
62
+ }
63
+ });
64
+ `;
65
+
66
+ const edits = (): readonly ShapeEdit[] => [
67
+ {
68
+ path: 'apps/web/shared/roles.ts',
69
+ find: " grants: [\n 'admin:read',",
70
+ replace: " grants: [\n 'key:issue',\n 'admin:read',",
71
+ },
72
+ ...claudeRows({
73
+ page: 'none in this shape: `bunx x g route <name>` writes one with its test',
74
+ island: 'none in this shape: `bunx x g island <name>` writes one with its states',
75
+ }),
76
+ ...shapeLines(
77
+ 'api',
78
+ 'an API — `apps/web/api/index.ts` registers every primitive, `/v1` takes an org API key (`apps/web/app/keys/`)',
79
+ ),
80
+ ];
81
+
82
+ export const apiShape: ShapeOverlay = {
83
+ example: true,
84
+ drop: [
85
+ 'apps/web/app/dashboard/',
86
+ 'apps/web/app/posts/',
87
+ 'apps/web/app/post/post-form.island.',
88
+ 'apps/web/app/post/ui',
89
+ 'apps/web/shared/shell',
90
+ 'apps/web/shared/theme-toggle.island',
91
+ 'apps/web/shared/browser-client',
92
+ 'apps/web/site/page.',
93
+ ],
94
+ dropKeys: ['app.dashboard', 'app.post', 'app.posts', 'shell', 'site'],
95
+ edits,
96
+ files: (app): readonly GeneratedFile[] => [
97
+ ...apiKeyFiles(),
98
+ { path: 'apps/web/api/index.ts', contents: apiIndex(app) },
99
+ { path: 'apps/web/api/index.test.ts', contents: apiIndexTest() },
100
+ ],
101
+ };
@@ -0,0 +1,340 @@
1
+ // The `blog` shape's site: the index at `/` and one page per post at `/posts/<slug>`, both `isr`
2
+ // with zero JavaScript, read over the app's own typed query client — `site/` never imports `app/`,
3
+ // so a page reaches the slice the way a stranger's client would, and the build prerenders it.
4
+
5
+ import { catalogJson } from '../catalog-json';
6
+ import { catalogPath } from '../locales';
7
+ import type { GeneratedFile, NameSet } from '../naming';
8
+
9
+ const client =
10
+ (): string => `// The typed read client a \`site/\` page loads through. \`Api\` is a TYPE import, so no module edge
11
+ // reaches \`app/\`: the page asks its own server over HTTP, and \`x build\` answers it in process.
12
+
13
+ import { queryClient } from '@ultimat3/query';
14
+ import type { Api } from '../api';
15
+
16
+ type Env = Readonly<Record<string, string | undefined>>;
17
+
18
+ /** The origin the server reads itself through: \`APP_URL\`, or this process's own port. */
19
+ export const appUrl = (env: Env): string => env.APP_URL || \`http://localhost:\${env.PORT || '3000'}\`;
20
+
21
+ export const queries = queryClient<Api['queries']>({
22
+ get baseUrl() {
23
+ return appUrl(Bun.env);
24
+ },
25
+ });
26
+ `;
27
+
28
+ const clientTest =
29
+ (): string => `// Where the server reads itself: the one origin rule the pages' client depends on.
30
+ import { expect, unitTest } from '@ultimat3/testing';
31
+ import { appUrl } from './client';
32
+
33
+ unitTest('the server reads itself at APP_URL, or at its own port when none is set', () => {
34
+ expect(appUrl({ APP_URL: 'https://blog.example' })).toBe('https://blog.example');
35
+ expect(appUrl({ PORT: '4000' })).toBe('http://localhost:4000');
36
+ expect(appUrl({})).toBe('http://localhost:3000');
37
+ });
38
+ `;
39
+
40
+ const rows =
41
+ (): string => `// The one row a detail page renders. A read answers rows, \`limit(1)\` included; the empty answer is
42
+ // a 404 decided here rather than in every \`load\`.
43
+
44
+ import { assertCoded } from '@ultimat3/core';
45
+
46
+ export function oneRow<TRow>(found: readonly TRow[], reference: string): TRow {
47
+ const [row] = found;
48
+ assertCoded(
49
+ row !== undefined,
50
+ 'X_NOT_FOUND',
51
+ \`no published post at "\${reference}"\`,
52
+ 'bunx x db seed # or publish the post at /admin',
53
+ );
54
+ return row;
55
+ }
56
+ `;
57
+
58
+ const rowsTest = (): string => `// A detail page's one row: the row, or the 404 the route answers.
59
+ import { expect, unitTest } from '@ultimat3/testing';
60
+ import { oneRow } from './rows';
61
+
62
+ unitTest('one row is the row, and none is X_NOT_FOUND', () => {
63
+ expect(oneRow(['a'], 'a')).toBe('a');
64
+ expect(() => oneRow([], 'missing')).toThrow(expect.objectContaining({ code: 'X_NOT_FOUND' }));
65
+ });
66
+ `;
67
+
68
+ /** One zone for every reader: an \`isr\` page is ONE document, rendered once for all of them. */
69
+ const ISR_HEAD = ` render: 'isr',
70
+ // A post edited at /admin reaches readers within five minutes. No query parameter is part of an
71
+ // \`isr\` page by default, so \`?utm_source=…\` cannot mint another stored copy.
72
+ revalidate: { ttl: '5m' },
73
+ offline: 'runtime',
74
+ hydrate: 'never',
75
+ budget: { js: '0kb' },`;
76
+
77
+ const indexPage = (
78
+ app: NameSet,
79
+ ): string => `// The blog index: every published post, newest first. Zero JavaScript.
80
+
81
+ import { useT } from '@${app.kebab}/i18n';
82
+ import { defineRoute } from '@ultimat3/render';
83
+ import { DateTime } from '@ultimat3/ui';
84
+ import type { JSX } from 'solid-js';
85
+ import { For, Show } from 'solid-js';
86
+ import { queries } from '../shared/client';
87
+ import styles from './page.module.scss';
88
+
89
+ export const config = defineRoute({
90
+ ${ISR_HEAD}
91
+ load: async () => await queries.publishedPosts({}),
92
+ meta: ({ t }) => ({ title: t('site.blog.title'), description: t('site.blog.description') }),
93
+ });
94
+
95
+ export type BlogIndex = Awaited<ReturnType<typeof queries.publishedPosts>>;
96
+
97
+ export function Page(props: { readonly data: BlogIndex }): JSX.Element {
98
+ const t = useT();
99
+ return (
100
+ <main class={styles.page}>
101
+ <h1>{t('site.blog.title')}</h1>
102
+ <Show when={props.data.length > 0} fallback={<p>{t('site.blog.empty')}</p>}>
103
+ <ul class={styles.list}>
104
+ <For each={props.data}>
105
+ {(post) => (
106
+ <li class={styles.item}>
107
+ <a href={\`/posts/\${encodeURIComponent(post.slug)}\`}>{post.title}</a>
108
+ <Show when={post.publishedAt}>
109
+ {(at) => <DateTime value={at()} timeZone="UTC" dateStyle="long" />}
110
+ </Show>
111
+ </li>
112
+ )}
113
+ </For>
114
+ </ul>
115
+ </Show>
116
+ </main>
117
+ );
118
+ }
119
+ `;
120
+
121
+ /** The test helper both page tests share: the app's own HTTP answer, as the query route sends it. */
122
+ const ANSWER = `/** Every read the page makes, answered in process with \`rows\` — the query route's JSON body. */
123
+ const answering = (rows: readonly unknown[]) => async (): Promise<Response> => Response.json(rows);
124
+ `;
125
+
126
+ const indexTest = (
127
+ app: NameSet,
128
+ ): string => `// The blog index: what it links, its empty state, its zero-JS contract and its read.
129
+ import { useT } from '@${app.kebab}/i18n';
130
+ import { withInProcessFetch } from '@ultimat3/core';
131
+ import { expect, renderRoute, renderView, unitTest } from '@ultimat3/testing';
132
+ import * as page from './page';
133
+ import { config, Page } from './page';
134
+
135
+ ${ANSWER}
136
+ const post = { slug: 'hello world', title: 'Hello', body: 'b', id: 'p1' };
137
+
138
+ unitTest('the index links every post by its slug, newest first as the read answers', async () => {
139
+ const at = new Date('2026-01-01T00:00:00Z');
140
+ const view = await renderView(Page, {
141
+ data: [
142
+ { ...post, publishedAt: at, createdAt: at },
143
+ { ...post, slug: 'two', title: 'Two', publishedAt: at, createdAt: at },
144
+ ],
145
+ });
146
+ const hrefs = [...view.html.matchAll(/href="([^"]*)"/g)].map((match) => match[1]);
147
+ expect(hrefs).toEqual(['/posts/hello%20world', '/posts/two']);
148
+ expect(view.text).toContain('Hello');
149
+ });
150
+
151
+ unitTest('no post is an empty state, not an empty list', async () => {
152
+ const view = await renderView(Page, { data: [] });
153
+ expect(view.text).toContain(useT()('site.blog.empty'));
154
+ expect(view.html).not.toContain('<ul');
155
+ });
156
+
157
+ unitTest('it is isr with zero JavaScript', () => {
158
+ expect(config.render).toBe('isr');
159
+ expect(config.hydrate).toBe('never');
160
+ expect(config.budget.js).toBe('0kb');
161
+ });
162
+
163
+ unitTest('load reads the published posts through the typed client', async () => {
164
+ const rows = [{ ...post, publishedAt: null, createdAt: null }];
165
+ const view = await withInProcessFetch(answering(rows), () => renderRoute(page));
166
+ expect(view.data.map((row) => row.slug)).toEqual(['hello world']);
167
+ expect(view.meta.title).toBe(useT()('site.blog.title'));
168
+ });
169
+ `;
170
+
171
+ const postPage = (
172
+ app: NameSet,
173
+ ): string => `// One published post. Prerendered at build for every published slug, then regenerated on read.
174
+
175
+ import { useT } from '@${app.kebab}/i18n';
176
+ import { defineRoute } from '@ultimat3/render';
177
+ import { DateTime } from '@ultimat3/ui';
178
+ import type { JSX } from 'solid-js';
179
+ import { Show } from 'solid-js';
180
+ import { queries } from '../../../shared/client';
181
+ import { oneRow } from '../../../shared/rows';
182
+ import styles from './page.module.scss';
183
+
184
+ export const config = defineRoute({
185
+ ${ISR_HEAD}
186
+ prerender: async () => (await queries.publishedPosts({ limit: 50 })).map((post) => post.slug),
187
+ load: async ({ params }) => {
188
+ const slug = params.slug ?? '';
189
+ return oneRow(await queries.publishedPost({ slug }), slug);
190
+ },
191
+ meta: ({ data }) => ({ title: data.title, description: data.body.slice(0, 160) }),
192
+ });
193
+
194
+ export type BlogPost = Awaited<ReturnType<typeof queries.publishedPost>>[number];
195
+
196
+ export function Page(props: { readonly data: BlogPost }): JSX.Element {
197
+ const t = useT();
198
+ return (
199
+ <main class={styles.page}>
200
+ <article>
201
+ <h1>{props.data.title}</h1>
202
+ <Show when={props.data.publishedAt}>
203
+ {(at) => <DateTime value={at()} timeZone="UTC" dateStyle="long" />}
204
+ </Show>
205
+ <p class={styles.body}>{props.data.body}</p>
206
+ </article>
207
+ <a href="/">{t('site.blog.back')}</a>
208
+ </main>
209
+ );
210
+ }
211
+ `;
212
+
213
+ const postTest = (
214
+ app: NameSet,
215
+ ): string => `// One post: what it renders, which slugs the build prerenders, and the 404 for the rest.
216
+ import { useT } from '@${app.kebab}/i18n';
217
+ import { withInProcessFetch } from '@ultimat3/core';
218
+ import { expect, renderRoute, renderView, unitTest } from '@ultimat3/testing';
219
+ import * as page from './page';
220
+ import { config, Page } from './page';
221
+
222
+ ${ANSWER}
223
+ unitTest('a post renders its title, its body and the way back', async () => {
224
+ const at = new Date('2026-01-01T00:00:00Z');
225
+ const data = {
226
+ id: 'p1',
227
+ slug: 'hello',
228
+ title: 'Hello',
229
+ body: 'The body.',
230
+ publishedAt: at,
231
+ createdAt: at,
232
+ };
233
+ const view = await renderView(Page, { data });
234
+ expect(view.html.match(/<h1\\b/g)).toHaveLength(1);
235
+ expect(view.text).toContain('The body.');
236
+ expect(view.text).toContain(useT()('site.blog.back'));
237
+ });
238
+
239
+ unitTest('it is isr with zero JavaScript', () => {
240
+ expect(config.render).toBe('isr');
241
+ expect(config.budget.js).toBe('0kb');
242
+ });
243
+
244
+ unitTest('prerender lists every published slug, and load renders one post', async () => {
245
+ const row = {
246
+ id: 'p1',
247
+ slug: 'hello',
248
+ title: 'Hello',
249
+ body: 'b',
250
+ publishedAt: null,
251
+ createdAt: null,
252
+ };
253
+ const slugs = await withInProcessFetch(answering([row]), () => config.prerender?.());
254
+ expect(slugs).toEqual(['hello']);
255
+ const view = await withInProcessFetch(answering([row]), () =>
256
+ renderRoute(page, { params: { slug: 'hello' } }),
257
+ );
258
+ expect(view.meta.title).toBe('Hello');
259
+ });
260
+
261
+ unitTest('a slug no published post has is X_NOT_FOUND', async () => {
262
+ const missing = withInProcessFetch(answering([]), () =>
263
+ renderRoute(page, { params: { slug: 'gone' } }),
264
+ );
265
+ await expect(missing).rejects.toThrow(expect.objectContaining({ code: 'X_NOT_FOUND' }));
266
+ });
267
+ `;
268
+
269
+ const style = (): string => `@use '@ultimat3/ui/tokens' as tokens;
270
+
271
+ .page {
272
+ display: grid;
273
+ gap: tokens.space(4);
274
+ padding: tokens.space(8);
275
+ background: tokens.role('bg');
276
+ color: tokens.role('fg');
277
+ }
278
+
279
+ .list {
280
+ display: grid;
281
+ gap: tokens.space(3);
282
+ padding: 0;
283
+ list-style: none;
284
+ }
285
+
286
+ .item {
287
+ display: grid;
288
+ gap: tokens.space(1);
289
+ color: tokens.role('fg-muted');
290
+ }
291
+ `;
292
+
293
+ const postStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
294
+
295
+ .page {
296
+ display: grid;
297
+ gap: tokens.space(4);
298
+ padding: tokens.space(8);
299
+ background: tokens.role('bg');
300
+ color: tokens.role('fg');
301
+ }
302
+
303
+ .body {
304
+ white-space: pre-line;
305
+ }
306
+ `;
307
+
308
+ /** `shared/` helpers and the two pages, with the catalog keys they read. */
309
+ export function blogSiteFiles(app: NameSet): readonly GeneratedFile[] {
310
+ return [
311
+ { path: 'apps/web/shared/client.ts', contents: client() },
312
+ { path: 'apps/web/shared/client.test.ts', contents: clientTest() },
313
+ { path: 'apps/web/shared/rows.ts', contents: rows() },
314
+ { path: 'apps/web/shared/rows.test.ts', contents: rowsTest() },
315
+ { path: 'apps/web/site/page.tsx', contents: indexPage(app) },
316
+ { path: 'apps/web/site/page.test.ts', contents: indexTest(app) },
317
+ { path: 'apps/web/site/page.module.scss', contents: style() },
318
+ { path: 'apps/web/site/posts/[slug]/page.tsx', contents: postPage(app) },
319
+ { path: 'apps/web/site/posts/[slug]/page.test.ts', contents: postTest(app) },
320
+ { path: 'apps/web/site/posts/[slug]/page.module.scss', contents: postStyle() },
321
+ {
322
+ path: catalogPath('en'),
323
+ merge: 'json',
324
+ contents: catalogJson({
325
+ 'site.blog.title': app.pascal,
326
+ 'site.blog.description': `Posts from ${app.pascal}.`,
327
+ 'site.blog.empty': 'Nothing published yet. Write the first post at /admin.',
328
+ 'site.blog.back': 'All posts',
329
+ // The labels \`/admin\` renders for the \`posts\` screens it derives from the entity.
330
+ 'admin.posts.title': 'Posts',
331
+ 'admin.posts.field.id': 'ID',
332
+ 'admin.posts.field.slug': 'Slug',
333
+ 'admin.posts.field.title': 'Title',
334
+ 'admin.posts.field.body': 'Body',
335
+ 'admin.posts.field.publishedAt': 'Published at',
336
+ 'admin.posts.field.createdAt': 'Created at',
337
+ }),
338
+ },
339
+ ];
340
+ }