@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,64 @@
1
+ // `/llms.txt`'s pages, off the route table: every public `site/` page in the default locale by the
2
+ // title and description its own `meta` gives it. A dynamic route (or one with a `load`) is expanded
3
+ // through `prerender()` and its `load`, page by page, exactly as the static export renders it — or
4
+ // read off what the export already collected inside its API scope (`site-llms-pages.ts`).
5
+
6
+ import type { Environment } from '@ultimat3/core';
7
+ import { DEFAULT_ENVIRONMENT, tryResolveEnvironment } from '@ultimat3/core';
8
+ import { unlocalizedPath } from '@ultimat3/i18n';
9
+ import type { RouteRecord } from '@ultimat3/seo';
10
+ import { buildLlmsTxt, disallowMatcher, isDynamic, isIndexable } from '@ultimat3/seo';
11
+ import type { Enumerate, LlmsPages } from './site-llms-pages';
12
+ import { llmsRoutes, pageRecords } from './site-llms-pages';
13
+
14
+ export interface SiteLlmsOptions {
15
+ readonly baseUrl: string;
16
+ readonly environment?: Environment | undefined;
17
+ /** `seo.llms`; `false` answers an empty document. */
18
+ readonly enabled?: boolean | undefined;
19
+ readonly disallow?: readonly string[] | undefined;
20
+ /** The static export's emitted pages per route, as the sitemap reads them (`SiteSeoOptions`). */
21
+ readonly pagesFor?: ((routePath: string) => readonly string[]) | undefined;
22
+ /** The app's default locale: every `meta` runs in it, and its paths are unprefixed. */
23
+ readonly defaultLocale: string;
24
+ /** `siteSeo`'s one enumeration per route, so the sitemap and this file never call `prerender()` twice. */
25
+ readonly enumerate?: Enumerate | undefined;
26
+ /**
27
+ * What the static export read inside its API scope (`collectLlmsPages`), by declared route path.
28
+ * A route listed here never runs its `load` again: by the time `llms.txt` is written the build's
29
+ * scope is gone, and a `load` reading the app's own query failed the export
30
+ * (`X_CLIENT_TRANSPORT_FAILED`).
31
+ */
32
+ readonly pages?: LlmsPages | undefined;
33
+ }
34
+
35
+ /**
36
+ * Empty outside production and under `seo.llms: false` BEFORE any `prerender()` or `load` runs: a
37
+ * staging web role must not read every post to answer a document it then throws away.
38
+ */
39
+ export async function siteLlms(options: SiteLlmsOptions): Promise<string> {
40
+ const environment = options.environment ?? tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT;
41
+ if (options.enabled === false || !isIndexable(environment)) return '';
42
+ const disallowed = disallowMatcher(options.disallow ?? []);
43
+ const { resolved, loaded } = await llmsRoutes(options.defaultLocale);
44
+ const expanded: RouteRecord[] = [];
45
+ for (const entry of loaded) {
46
+ const records = options.pages?.get(entry.path) ?? (await pageRecords(entry, options));
47
+ expanded.push(...emittedOnly(entry.path, records, options));
48
+ }
49
+ return buildLlmsTxt(
50
+ [...resolved, ...expanded].filter((record) => !disallowed(record.path)),
51
+ { baseUrl: options.baseUrl, environment },
52
+ );
53
+ }
54
+
55
+ /** A dynamic route's pages, less those the export did not emit — the sitemap's own rule. */
56
+ function emittedOnly(
57
+ routePath: string,
58
+ records: readonly RouteRecord[],
59
+ options: SiteLlmsOptions,
60
+ ): readonly RouteRecord[] {
61
+ if (options.pagesFor === undefined || !isDynamic(routePath)) return records;
62
+ const emitted = new Set(options.pagesFor(routePath).map((path) => unlocalizedPath(path)));
63
+ return records.filter((record) => emitted.has(record.path));
64
+ }
@@ -1,8 +1,9 @@
1
1
  // Single responsibility: `siteSeo()`'s answer as files in a static export — `/sitemap.xml`, every
2
- // part an index names, and `robots.txt` — so a CDN serves the bytes a running web role answers.
2
+ // part an index names, `robots.txt` and `llms.txt` — so a CDN serves the bytes a running web role answers.
3
3
 
4
4
  // why: Bun exposes no path-join primitive, and every file path is relative to the output root.
5
5
  import { join } from 'node:path';
6
+ import { LLMS_PATH } from '@ultimat3/seo';
6
7
  import { ROBOTS_PATH, type SiteSeo } from './site-seo';
7
8
 
8
9
  /**
@@ -12,5 +13,6 @@ import { ROBOTS_PATH, type SiteSeo } from './site-seo';
12
13
  export async function writeSiteSeo(out: string, seo: SiteSeo): Promise<readonly string[]> {
13
14
  for (const file of seo.sitemaps) await Bun.write(join(out, file.path), file.xml);
14
15
  await Bun.write(join(out, ROBOTS_PATH), seo.robots);
15
- return [...seo.sitemaps.map((file) => file.path), ROBOTS_PATH];
16
+ await Bun.write(join(out, LLMS_PATH), seo.llms);
17
+ return [...seo.sitemaps.map((file) => file.path), ROBOTS_PATH, LLMS_PATH];
16
18
  }
package/src/site-seo.ts CHANGED
@@ -1,4 +1,4 @@
1
- // `robots.txt` and `sitemap.xml`, off the route table: ONE answer for the static export
1
+ // `robots.txt`, `sitemap.xml` and `llms.txt`, off the route table: ONE answer for the static export
2
2
  // (`apps/web/prerender.ts` writes it into the artifact) and the running web role (`seo-routes.ts`
3
3
  // serves it), so a crawler reads the same two files from a CDN and from a container.
4
4
 
@@ -6,12 +6,15 @@ import type { Environment, SeoRobotsGroup, SeoSitemapConfig } from '@ultimat3/co
6
6
  import { UltimateError } from '@ultimat3/core';
7
7
  import { localizedPath, unlocalizedPath } from '@ultimat3/i18n';
8
8
  import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
9
- import type { RouteEntry } from '@ultimat3/render';
9
+ import type { PrerenderEntry, RouteEntry } from '@ultimat3/render';
10
10
  import { routeEntries } from '@ultimat3/render';
11
11
  import { enumeratePrerenderEntries, fillPath } from '@ultimat3/render/server';
12
12
  import type { RouteRecord, SitemapFile, SitemapPage } from '@ultimat3/seo';
13
13
  import { buildRobots, buildSitemap, isDynamic } from '@ultimat3/seo';
14
+ import type { RouteEntries } from './prerender-entries';
14
15
  import { readSiteMeta } from './seo-meta';
16
+ import { siteLlms } from './site-llms';
17
+ import type { LlmsPages } from './site-llms-pages';
15
18
  import { lastmodOf } from './sitemap-lastmod';
16
19
 
17
20
  export const ROBOTS_PATH = '/robots.txt';
@@ -47,6 +50,20 @@ export interface SiteSeoOptions {
47
50
  readonly sitemap?: SiteSeoSitemap | undefined;
48
51
  /** The app root the route files are relative to — read for `lastmod: 'git' | 'mtime'`. */
49
52
  readonly root?: string | undefined;
53
+ /** `seo.llms` from `app.config.ts`. Omitted: on, like the sitemap. */
54
+ readonly llms?: boolean | undefined;
55
+ /**
56
+ * What the static export's `prerender()` calls already answered, by declared route path
57
+ * (`PrerenderReport.entries`). A route listed here is never asked again: the build enumerated
58
+ * it inside its API scope, which is gone by the time the sitemap is written, so a `prerender()`
59
+ * reading the app's own query would fail the export (`X_CLIENT_TRANSPORT_FAILED`).
60
+ */
61
+ readonly entries?: RouteEntries | undefined;
62
+ /**
63
+ * `llms.txt`'s page records the static export read inside its API scope
64
+ * (`PrerenderReport.llmsPages`). A route listed here never runs its `load` again.
65
+ */
66
+ readonly llmsPages?: LlmsPages | undefined;
50
67
  }
51
68
 
52
69
  /**
@@ -72,6 +89,8 @@ export class SitemapExtraInvalidError extends UltimateError {
72
89
 
73
90
  export interface SiteSeo {
74
91
  readonly robots: string;
92
+ /** `/llms.txt`: empty outside production and under `seo.llms: false`. */
93
+ readonly llms: string;
75
94
  /** Every sitemap file: `/sitemap.xml` alone, or the index at that path first and its parts after. */
76
95
  readonly sitemaps: readonly SitemapFile[];
77
96
  }
@@ -89,8 +108,9 @@ const isPublicSite = (entry: RouteEntry): boolean =>
89
108
  const pagesOf = async (
90
109
  entry: RouteEntry,
91
110
  options: SiteSeoOptions,
111
+ enumerate: Enumerate,
92
112
  ): Promise<readonly SitemapPage[]> => {
93
- const said = (await enumeratePrerenderEntries(entry)).map(
113
+ const said = (await enumerate(entry)).map(
94
114
  (page): SitemapPage => ({
95
115
  path: fillPath(entry.pattern.source, page.params),
96
116
  ...(page.lastmod === undefined ? {} : { lastmod: page.lastmod }),
@@ -147,6 +167,7 @@ async function publicSiteRoutes(
147
167
  options: SiteSeoOptions,
148
168
  stamp: Stamp,
149
169
  locales: readonly string[],
170
+ enumerate: Enumerate,
150
171
  ): Promise<readonly RouteRecord[]> {
151
172
  const indexed = await indexability(locales);
152
173
  return routeEntries()
@@ -158,7 +179,7 @@ async function publicSiteRoutes(
158
179
  render: entry.config.render,
159
180
  ...indexed(entry.path),
160
181
  ...(isDynamic(entry.path) || entry.config.prerender !== undefined
161
- ? { prerender: () => pagesOf(entry, options) }
182
+ ? { prerender: () => pagesOf(entry, options, enumerate) }
162
183
  : {}),
163
184
  ...stamp(entry.file),
164
185
  }));
@@ -166,6 +187,22 @@ async function publicSiteRoutes(
166
187
 
167
188
  type Stamp = (file: string) => { readonly lastmod?: string };
168
189
 
190
+ /** `enumeratePrerenderEntries`, asked once per route per answer: the sitemap and `llms.txt` share it. */
191
+ type Enumerate = (entry: RouteEntry) => Promise<readonly PrerenderEntry[]>;
192
+
193
+ const enumerateOnce = (given: RouteEntries | undefined): Enumerate => {
194
+ const asked = new Map<RouteEntry, Promise<readonly PrerenderEntry[]>>();
195
+ return (entry) => {
196
+ const enumerated = given?.get(entry.path);
197
+ if (enumerated !== undefined) return Promise.resolve(enumerated);
198
+ const known = asked.get(entry);
199
+ if (known !== undefined) return known;
200
+ const answer = enumeratePrerenderEntries(entry);
201
+ asked.set(entry, answer);
202
+ return answer;
203
+ };
204
+ };
205
+
169
206
  /**
170
207
  * The app's `seo.sitemap.extra` paths as records, each resolved to the route that answers it — the
171
208
  * most specific match, the router's own tie-break. REFUSED, never skipped, when it names no route,
@@ -216,8 +253,9 @@ export async function siteSeo(options: SiteSeoOptions): Promise<SiteSeo> {
216
253
  const lastmod = lastmodOf(file, settings.lastmod, root);
217
254
  return lastmod === undefined ? {} : { lastmod };
218
255
  };
256
+ const enumerate = enumerateOnce(options.entries);
219
257
  const routes = [
220
- ...(await publicSiteRoutes(options, stamp, locales)),
258
+ ...(await publicSiteRoutes(options, stamp, locales, enumerate)),
221
259
  ...extraRoutes(settings.extra, stamp),
222
260
  ];
223
261
  const sitemap = await buildSitemap(routes, {
@@ -245,5 +283,16 @@ export async function siteSeo(options: SiteSeoOptions): Promise<SiteSeo> {
245
283
  ...(options.robotsGroups === undefined ? {} : { groups: options.robotsGroups }),
246
284
  ...(options.environment === undefined ? {} : { environment: options.environment }),
247
285
  });
248
- return { robots, sitemaps };
286
+ // The same pages, default locale, less what `robots.txt` keeps crawlers out of (`site-llms.ts`).
287
+ const llms = await siteLlms({
288
+ baseUrl: options.baseUrl,
289
+ environment: options.environment,
290
+ enabled: options.llms,
291
+ disallow: options.disallow,
292
+ pagesFor: options.pagesFor,
293
+ defaultLocale,
294
+ enumerate,
295
+ pages: options.llmsPages,
296
+ });
297
+ return { robots, sitemaps, llms };
249
298
  }
@@ -0,0 +1,50 @@
1
+ // The newest mtime over what a static build reads, for `budgets`' freshness: the app's source files
2
+ // (tests and declarations ship no bytes), the config and the lockfile, and every directory those
3
+ // files sit in — a deleted page changes no remaining file's mtime, only its directory's.
4
+
5
+ // why: Bun exposes no path-join or dirname primitive.
6
+ import { dirname, join } from 'node:path';
7
+ import { APP_CONFIG_FILE } from '@ultimat3/core';
8
+ import { eachSourceFile, isGenerated, isTest } from './source-files';
9
+
10
+ /**
11
+ * Files outside `SOURCE_GLOBS` that still move a route's bytes: the config (pwa, theme, surfaces)
12
+ * and the lockfile, whose change is a framework or dependency upgrade.
13
+ */
14
+ const BYTE_INPUTS = [APP_CONFIG_FILE, 'bun.lock'] as const;
15
+
16
+ /** `0` for a path that vanished between the glob and the stat: it cannot be newer than anything. */
17
+ const mtimeOf = async (path: string): Promise<number> => {
18
+ const file = Bun.file(path);
19
+ try {
20
+ return (await file.stat()).mtimeMs;
21
+ } catch {
22
+ return 0;
23
+ }
24
+ };
25
+
26
+ /** Every directory between `path` and the root, the root itself excluded (`.x/` lives there). */
27
+ const ancestorsOf = (path: string): readonly string[] => {
28
+ const out: string[] = [];
29
+ for (let dir = dirname(path); dir !== '.' && dir !== '' && dir !== '/'; dir = dirname(dir)) {
30
+ out.push(dir);
31
+ }
32
+ return out;
33
+ };
34
+
35
+ /** The newest mtime, in ms, or `undefined` for an app with no source at all. */
36
+ export async function newestSourceMtime(root: string): Promise<number | undefined> {
37
+ let newest: number | undefined;
38
+ const consider = (ms: number): void => {
39
+ if (ms > 0 && (newest === undefined || ms > newest)) newest = ms;
40
+ };
41
+ const dirs = new Set<string>();
42
+ for await (const path of eachSourceFile(root)) {
43
+ if (isTest(path) || isGenerated(path)) continue;
44
+ consider(await mtimeOf(join(root, path)));
45
+ for (const dir of ancestorsOf(path)) dirs.add(dir);
46
+ }
47
+ for (const dir of dirs) consider(await mtimeOf(join(root, dir)));
48
+ for (const path of BYTE_INPUTS) consider(await mtimeOf(join(root, path)));
49
+ return newest;
50
+ }
@@ -0,0 +1,41 @@
1
+ // The three ways a static export's FILES — image variants and social cards — cannot be made: a
2
+ // `src` the export does not hold, a card path no exported page answers, and no browser to take the
3
+ // card with. Codes and titles are `error-codes.ts`'s; these are the constructors.
4
+
5
+ import { renderCauseValue, UltimateError } from '@ultimat3/core';
6
+
7
+ const listed = (values: readonly string[]): string =>
8
+ values.map((value) => renderCauseValue(value)).join(', ');
9
+
10
+ export class ImageSourceUnreadableError extends UltimateError {
11
+ constructor(srcs: readonly string[]) {
12
+ super({
13
+ code: 'X_IMAGE_SOURCE_UNREADABLE',
14
+ cause: `responsiveImage() on an exported page named ${listed(srcs)}, and the static export holds no such file, so no variant of it can be written — a CDN would answer every <source> with a 404`,
15
+ fix: "x build --target static --json # lists the export; put the file under apps/web/site/assets/ and pass asset('assets/<file>') as src, or give an absolute https:// URL",
16
+ meta: { srcs: [...srcs] },
17
+ });
18
+ }
19
+ }
20
+
21
+ export class CardRouteUnknownError extends UltimateError {
22
+ constructor(paths: readonly string[], declaredIn?: string) {
23
+ super({
24
+ code: 'X_SEO_CARD_ROUTE_UNKNOWN',
25
+ cause: `og.capture names ${listed(paths)}${declaredIn === undefined ? '' : ` (in ${declaredIn})`}, and no exported site/ page answers that path, so there is no page to photograph`,
26
+ fix: 'x routes --json # list the site/ routes; add apps/web/site/<the capture path>/page.tsx at 1200 x 630 with robots: { index: false }, or correct the og.capture path',
27
+ meta: { paths: [...paths] },
28
+ });
29
+ }
30
+ }
31
+
32
+ export class CardBrowserMissingError extends UltimateError {
33
+ constructor(paths: readonly string[]) {
34
+ super({
35
+ code: 'X_SEO_CARD_BROWSER_MISSING',
36
+ cause: `x build --target static photographs ${listed(paths)} as social cards, and no browser was named or found — no CHROME_PATH, PUPPETEER_EXECUTABLE_PATH or SCRAPE_CDP_URL is set and no probed path holds one`,
37
+ fix: 'export CHROME_PATH=/usr/bin/google-chrome # any Chrome, Chromium or Edge binary; or drop og.capture from the route; the gate build passes --no-capture and needs none',
38
+ meta: { paths: [...paths] },
39
+ });
40
+ }
41
+ }
@@ -0,0 +1,56 @@
1
+ // Single responsibility: install `@ultimat3/seo`'s static seam for one export, and after the render
2
+ // loop write the files the exported documents promised — image variants (`static-images.ts`) and
3
+ // social cards (`static-cards.ts`). The prerenderer's one touch point for both.
4
+
5
+ // why: Bun exposes no path-join primitive, and every page file is relative to the export root.
6
+ import { join } from 'node:path';
7
+ import { setStaticExport } from '@ultimat3/seo';
8
+ import type { ShotDriver } from './browser-launcher-port';
9
+ import { BUILD_CAPTURE_ENV } from './build-capture-env';
10
+ import { CARD_SIZE, staticCards } from './static-cards';
11
+ import { staticImages } from './static-images';
12
+
13
+ export interface StaticCaptureFiles {
14
+ readonly images: readonly string[];
15
+ readonly cards: readonly string[];
16
+ }
17
+
18
+ export interface StaticCapture {
19
+ /** Uninstalls the seam. Idempotent; runs in the render loop's `finally`. */
20
+ close(): void;
21
+ write(
22
+ pages: readonly { readonly path: string; readonly file: string }[],
23
+ ): Promise<StaticCaptureFiles>;
24
+ }
25
+
26
+ /** The CLI's own CDP driver, or `undefined` when nothing can be launched or attached to. */
27
+ async function cardBrowser(): Promise<ShotDriver | undefined> {
28
+ const { appBrowser, cdpUrlFrom, executablePathFrom } = await import('./browser-launcher');
29
+ const cdpUrl = cdpUrlFrom(undefined, Bun.env);
30
+ const executablePath = executablePathFrom(undefined, Bun.env);
31
+ if (cdpUrl === undefined && executablePath === undefined) return undefined;
32
+ return appBrowser({ cdpUrl, executablePath, viewport: CARD_SIZE });
33
+ }
34
+
35
+ export async function staticCapture(
36
+ out: string,
37
+ capture: boolean = Bun.env[BUILD_CAPTURE_ENV] !== 'off',
38
+ browser: () => Promise<ShotDriver | undefined> = cardBrowser,
39
+ ): Promise<StaticCapture> {
40
+ const images = await staticImages(out);
41
+ const cards = staticCards();
42
+ setStaticExport({ imageUrl: images.imageUrl, cardImage: cards.cardImage });
43
+ return {
44
+ close: () => setStaticExport(undefined),
45
+ async write(pages) {
46
+ const documents = await Promise.all(
47
+ pages.map((page) => Bun.file(join(out, page.file)).text()),
48
+ );
49
+ const exported = new Set(pages.map((page) => page.path));
50
+ return {
51
+ images: await images.write(documents),
52
+ cards: await cards.write(out, documents, exported, { capture, browser }),
53
+ };
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,39 @@
1
+ // Single responsibility: the export, served on loopback for the length of a card capture — the
2
+ // dumb file server a CDN is, so a card page is photographed with exactly the stylesheet, fonts and
3
+ // assets the published page links. Never the app: a static export has no process behind it.
4
+
5
+ // why: Bun exposes no path-join primitive, and every file served is relative to the export root.
6
+ import { join, normalize, sep } from 'node:path';
7
+
8
+ export interface ServedExport {
9
+ readonly origin: string;
10
+ stop(): void;
11
+ }
12
+
13
+ /** `/og/home` → `og/home/index.html`, then the bare file — the two shapes an export holds. */
14
+ function fileFor(out: string, pathname: string): readonly string[] {
15
+ let decoded: string;
16
+ try {
17
+ decoded = decodeURIComponent(pathname);
18
+ } catch {
19
+ return [];
20
+ }
21
+ const relative = normalize(decoded).replace(/^[/\\]+/, '');
22
+ if (relative.split(sep).includes('..')) return [];
23
+ return [join(out, relative, 'index.html'), join(out, relative)];
24
+ }
25
+
26
+ export function serveExport(out: string): ServedExport {
27
+ const server = Bun.serve({
28
+ hostname: '127.0.0.1',
29
+ port: 0,
30
+ async fetch(request) {
31
+ for (const file of fileFor(out, new URL(request.url).pathname)) {
32
+ const body = Bun.file(file);
33
+ if (await body.exists()) return new Response(body);
34
+ }
35
+ return new Response(null, { status: 404 });
36
+ },
37
+ });
38
+ return { origin: `http://127.0.0.1:${String(server.port)}`, stop: () => void server.stop(true) };
39
+ }
@@ -0,0 +1,93 @@
1
+ // Single responsibility: social cards as FILES in a static export. While it renders, `og.capture`
2
+ // is answered as `/_og/<path>.png` at 1200 x 630 and recorded; after it, each card an exported
3
+ // document names is photographed from its exported page over the CLI's own CDP driver. Under
4
+ // `--no-capture` (the gate's build) the tags are stamped and no PNG is written.
5
+
6
+ // why: Bun exposes no path-join primitive, and every card path is relative to the export root.
7
+ import { join } from 'node:path';
8
+ import { probeImage, transformImageBytes } from '@ultimat3/core';
9
+ import type { OpenGraphImage } from '@ultimat3/seo';
10
+ import type { ShotDriver } from './browser-launcher-port';
11
+ import { DEFAULT_PAGE_TIMEOUT_MS, systemShotClock } from './cdp-shot-clock';
12
+ import { CardBrowserMissingError, CardRouteUnknownError } from './static-capture-errors';
13
+ import { serveExport } from './static-cards-serve';
14
+
15
+ /** The size every platform crops a link preview from. */
16
+ export const CARD_SIZE = Object.freeze({ width: 1200, height: 630 });
17
+
18
+ export const STATIC_CARD_DIR = '_og';
19
+
20
+ /** `/og/post/hello` → `/_og/og/post/hello.png`; `/` → `/_og/index.png`. */
21
+ export function cardUrl(capturePath: string): string {
22
+ const trimmed = capturePath.replace(/\/+$/, '');
23
+ return `/${STATIC_CARD_DIR}${trimmed === '' ? '/index' : trimmed}.png`;
24
+ }
25
+
26
+ export interface CardCaptureOptions {
27
+ /** `false` under `--no-capture`: the tags are stamped, no browser is asked, no PNG is written. */
28
+ readonly capture: boolean;
29
+ /** The browser, asked only when a card has to be taken. `undefined` means none is available. */
30
+ readonly browser: () => Promise<ShotDriver | undefined>;
31
+ }
32
+
33
+ export interface StaticCards {
34
+ /** The `og:image` the seo seam stamps for one `og.capture`. Never throws. */
35
+ cardImage(capturePath: string): OpenGraphImage;
36
+ /**
37
+ * The cards exported documents name: each capture path must be an exported page
38
+ * (`X_SEO_CARD_ROUTE_UNKNOWN`), then each is photographed unless capture is off. Answers the
39
+ * PNG site paths written.
40
+ */
41
+ write(
42
+ out: string,
43
+ documents: readonly string[],
44
+ exportedPaths: ReadonlySet<string>,
45
+ options: CardCaptureOptions,
46
+ ): Promise<readonly string[]>;
47
+ }
48
+
49
+ /** Exactly 1200 x 630 on disk, whatever the device pixel ratio did: the stamped size is a promise. */
50
+ async function sized(png: Uint8Array): Promise<Uint8Array> {
51
+ const info = probeImage(png);
52
+ if (info.width === CARD_SIZE.width && info.height === CARD_SIZE.height) return png;
53
+ return transformImageBytes(png, { ...CARD_SIZE, fit: 'cover', format: 'png' });
54
+ }
55
+
56
+ export function staticCards(): StaticCards {
57
+ const captures = new Map<string, string>();
58
+ return {
59
+ cardImage(capturePath) {
60
+ const url = cardUrl(capturePath);
61
+ captures.set(url, capturePath);
62
+ return { url, ...CARD_SIZE, type: 'image/png' };
63
+ },
64
+ async write(out, documents, exportedPaths, options) {
65
+ const named = [...captures].filter(([url]) => documents.some((html) => html.includes(url)));
66
+ const unknown = named.map(([, path]) => path).filter((path) => !exportedPaths.has(path));
67
+ if (unknown.length > 0) throw new CardRouteUnknownError(unknown);
68
+ if (!options.capture || named.length === 0) return [];
69
+ const driver = await options.browser();
70
+ if (driver === undefined) throw new CardBrowserMissingError(named.map(([, path]) => path));
71
+ const served = serveExport(out);
72
+ const session = await driver.open({
73
+ name: 'x build cards',
74
+ rules: { allowHosts: ['127.0.0.1'] },
75
+ clock: systemShotClock,
76
+ timeoutMs: DEFAULT_PAGE_TIMEOUT_MS,
77
+ viewport: CARD_SIZE,
78
+ });
79
+ try {
80
+ const written: string[] = [];
81
+ for (const [url, path] of named) {
82
+ await session.page.goto(new URL(path, served.origin).href);
83
+ await Bun.write(join(out, url.slice(1)), await sized(await session.page.screenshot({})));
84
+ written.push(url);
85
+ }
86
+ return written;
87
+ } finally {
88
+ await session.close();
89
+ served.stop();
90
+ }
91
+ },
92
+ };
93
+ }
@@ -4,9 +4,8 @@
4
4
  // per request, nor a body per revalidation. No other mode comes through here.
5
5
 
6
6
  import type { UltimateRequest } from '@ultimat3/http';
7
- import { html } from '@ultimat3/http';
7
+ import { html, notModified } from '@ultimat3/http';
8
8
  import type { RenderResult, RouteEntry } from '@ultimat3/render';
9
- import { etagMatches } from './runtime-storage';
10
9
 
11
10
  /** A rendered static document: always a string body, never a stream. */
12
11
  export type StaticResult = RenderResult & { readonly body: string };
@@ -78,19 +77,24 @@ export function createStaticMemo(
78
77
  /**
79
78
  * The key a request's document is kept under, or `undefined` when it must be rendered: a query
80
79
  * string is part of what `meta` reads (`data.url`), and a dynamic segment is an unbounded set an
81
- * anonymous visitor chooses. The origin is in the key because canonical and `og:url` are absolute
82
- * against the request's own when the app declares none. `url` is `addressedUrl`'s: keyed on the
83
- * pathname as written, any visitor filled the memo with spellings of one page (`/about//`, …) and
84
- * every page that arrived after the cap was rendered per request for the life of the process.
80
+ * anonymous visitor chooses. The request's origin is in the key only when the app declares none,
81
+ * because canonical and `og:url` are then absolute against it; declared, they are absolute against
82
+ * `origin` (`runtime-render-document.ts`'s `baseUrl`), the document is one whatever the `Host`, and
83
+ * keying on the request's let rotated `Host` headers fill the memo. `url` is `addressedUrl`'s:
84
+ * keyed on the pathname as written, any visitor filled the memo with spellings of one page
85
+ * (`/about//`, …) and every page that arrived after the cap was rendered per request for the life
86
+ * of the process.
85
87
  */
86
88
  export function staticMemoKey(
87
89
  entry: Pick<RouteEntry, 'config' | 'pattern'>,
88
90
  url: URL,
89
91
  locale: string,
92
+ /** The app's declared origin — the same value the document's head is built against. */
93
+ origin?: string,
90
94
  ): string | undefined {
91
95
  if (entry.config.render !== 'static') return undefined;
92
96
  if (entry.pattern.keys.length > 0 || url.search !== '') return undefined;
93
- return `${locale}\n${url.origin}${url.pathname}`;
97
+ return `${locale}\n${origin ?? url.origin}${url.pathname}`;
94
98
  }
95
99
 
96
100
  /**
@@ -100,13 +104,10 @@ export function staticMemoKey(
100
104
  */
101
105
  export function staticResponse(request: UltimateRequest, result: StaticResult): Response {
102
106
  const etag = result.headers['etag'];
103
- if (
104
- result.status === 200 &&
105
- etag !== undefined &&
106
- etagMatches(request.header('if-none-match'), etag)
107
- ) {
107
+ if (result.status === 200 && etag !== undefined) {
108
108
  const { 'content-type': _dropped, ...headers } = result.headers;
109
- return new Response(null, { status: 304, headers });
109
+ const fresh = notModified(request, etag, headers);
110
+ if (fresh !== undefined) return fresh;
110
111
  }
111
112
  return html(result.body, { status: result.status, headers: result.headers });
112
113
  }