@ultimat3/cli 23.0.0 → 25.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 (296) hide show
  1. package/CLAUDE.md +28 -13
  2. package/README.md +32 -7
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +30 -36
  6. package/src/api-registration.ts +89 -0
  7. package/src/app-artifacts.ts +1 -1
  8. package/src/app-auth.ts +4 -27
  9. package/src/app-boundaries.ts +17 -4
  10. package/src/app-config-load.ts +51 -0
  11. package/src/app-env.ts +26 -25
  12. package/src/app-load.ts +38 -14
  13. package/src/app-manifest.ts +4 -10
  14. package/src/app-mcp.ts +31 -46
  15. package/src/app-name.ts +52 -0
  16. package/src/app-permissions-borrowed.ts +123 -0
  17. package/src/app-permissions-site.ts +48 -0
  18. package/src/app-permissions.ts +7 -17
  19. package/src/app-root.ts +9 -5
  20. package/src/bin.ts +3 -7
  21. package/src/browser-launcher.ts +4 -2
  22. package/src/budgets-charged.ts +60 -0
  23. package/src/budgets-scan.ts +76 -0
  24. package/src/budgets-unmeasured.ts +91 -0
  25. package/src/budgets.ts +117 -154
  26. package/src/cdp-shot-driver.ts +20 -1
  27. package/src/cdp-shot-watch.ts +35 -31
  28. package/src/ci-log.ts +0 -0
  29. package/src/cmd-auth-spec.ts +16 -0
  30. package/src/cmd-auth.ts +65 -0
  31. package/src/cmd-build-spec.ts +7 -1
  32. package/src/cmd-build.ts +151 -22
  33. package/src/cmd-db-backfill.ts +26 -13
  34. package/src/cmd-db-branch.ts +2 -2
  35. package/src/cmd-db.ts +10 -6
  36. package/src/cmd-deploy-compose.ts +94 -0
  37. package/src/cmd-deploy-drain.ts +48 -0
  38. package/src/cmd-deploy-helm.ts +7 -6
  39. package/src/cmd-deploy-start-first.ts +178 -0
  40. package/src/cmd-deploy.ts +158 -36
  41. package/src/cmd-dev.ts +18 -356
  42. package/src/cmd-doctor-spec.ts +2 -9
  43. package/src/cmd-doctor.ts +57 -80
  44. package/src/cmd-env.ts +55 -16
  45. package/src/cmd-generate.ts +89 -32
  46. package/src/cmd-help.ts +19 -6
  47. package/src/cmd-i18n.ts +21 -17
  48. package/src/cmd-jobs-spec.ts +7 -22
  49. package/src/cmd-jobs.ts +33 -140
  50. package/src/cmd-manifest-spec.ts +1 -1
  51. package/src/cmd-manifest.ts +6 -25
  52. package/src/cmd-mcp.ts +71 -22
  53. package/src/cmd-new.ts +36 -8
  54. package/src/cmd-planned.ts +24 -8
  55. package/src/cmd-routes.ts +15 -5
  56. package/src/cmd-secrets.ts +47 -6
  57. package/src/cmd-shot-matrix.ts +22 -7
  58. package/src/cmd-shot-spec.ts +10 -2
  59. package/src/cmd-shot.ts +46 -41
  60. package/src/cmd-tasks.ts +2 -2
  61. package/src/cmd-test-spec.ts +1 -7
  62. package/src/cmd-test.ts +14 -30
  63. package/src/cmd-verify.ts +4 -4
  64. package/src/coverage-lcov.ts +4 -2
  65. package/src/db-backfill.ts +2 -2
  66. package/src/db-replay-engine.ts +4 -10
  67. package/src/db-subscribes.ts +27 -16
  68. package/src/dev-boot.ts +290 -0
  69. package/src/dev-child-watch.ts +63 -12
  70. package/src/dev-dashboard-guard.ts +76 -0
  71. package/src/dev-dashboard.ts +56 -30
  72. package/src/dev-lock.ts +100 -31
  73. package/src/dev-rebuild.ts +80 -0
  74. package/src/dev-restart-retire.ts +46 -0
  75. package/src/dev-route-table.ts +30 -8
  76. package/src/dev-supervisor.ts +114 -22
  77. package/src/dev-traces.ts +53 -20
  78. package/src/dispatch.ts +17 -13
  79. package/src/doctor-app-url.ts +39 -0
  80. package/src/doctor-auth.ts +95 -0
  81. package/src/doctor-offline.ts +6 -5
  82. package/src/document-options.ts +7 -0
  83. package/src/drift-replica-identity.ts +42 -0
  84. package/src/drift.ts +29 -6
  85. package/src/error-catalog.ts +6 -9
  86. package/src/error-codes.ts +29 -3
  87. package/src/error-page-csp.ts +14 -15
  88. package/src/error-unthrown.ts +29 -11
  89. package/src/errors.ts +54 -83
  90. package/src/exec.ts +8 -0
  91. package/src/favicon.ts +2 -2
  92. package/src/fix-command.ts +9 -1
  93. package/src/framework-env.ts +148 -0
  94. package/src/framework-schema-apply.ts +78 -0
  95. package/src/framework-schema-stamp.ts +96 -0
  96. package/src/framework-schema.ts +25 -3
  97. package/src/generate-catalog-locales.ts +79 -0
  98. package/src/generate-files.ts +4 -3
  99. package/src/generate-kinds.ts +18 -0
  100. package/src/generate-plural.ts +50 -0
  101. package/src/generate-shadow.ts +95 -0
  102. package/src/generate-write.ts +47 -13
  103. package/src/gh-target.ts +12 -7
  104. package/src/hold.ts +56 -1
  105. package/src/i18n-audit.ts +25 -9
  106. package/src/i18n-index.ts +5 -6
  107. package/src/i18n-registration.ts +7 -9
  108. package/src/import-scan.ts +28 -3
  109. package/src/index.ts +2 -6
  110. package/src/island-bundle.ts +57 -79
  111. package/src/island-capture.ts +1 -1
  112. package/src/island-duplicate-refusal.ts +57 -0
  113. package/src/island-duplicates.ts +221 -0
  114. package/src/island-entry-missing.ts +64 -0
  115. package/src/island-harness-route.ts +20 -4
  116. package/src/island-harness-script.ts +23 -2
  117. package/src/island-identity.ts +17 -14
  118. package/src/island-package-dedupe.ts +150 -0
  119. package/src/island-realtime.ts +36 -16
  120. package/src/island-routes.ts +2 -2
  121. package/src/island-runtime.ts +241 -0
  122. package/src/island-sources.ts +105 -0
  123. package/src/island-states-load.ts +6 -0
  124. package/src/island-store.ts +90 -6
  125. package/src/jobs-json.ts +0 -26
  126. package/src/jobs-report.ts +36 -20
  127. package/src/local-cli-handoff.ts +90 -0
  128. package/src/loopback-host.ts +13 -0
  129. package/src/manifest-staleness.ts +62 -0
  130. package/src/mcp-errors.ts +20 -4
  131. package/src/mcp-host.ts +22 -14
  132. package/src/mcp-test-run.ts +38 -0
  133. package/src/mcp-ui-inspect.ts +3 -2
  134. package/src/mcp-ui-interact.ts +3 -2
  135. package/src/mcp-ui.ts +8 -7
  136. package/src/measure-scope.ts +15 -10
  137. package/src/messages.ts +29 -16
  138. package/src/metrics-endpoint.ts +28 -3
  139. package/src/migration-extensions.ts +1 -1
  140. package/src/otlp-export.ts +2 -2
  141. package/src/output.ts +24 -0
  142. package/src/page-navigation.ts +7 -23
  143. package/src/page-speculation.ts +19 -31
  144. package/src/parse.ts +0 -8
  145. package/src/permission-grants.ts +3 -3
  146. package/src/posix-path.ts +30 -0
  147. package/src/prerender-locales.ts +6 -4
  148. package/src/prerender-out.ts +34 -7
  149. package/src/prerender-report.ts +58 -0
  150. package/src/prerender-served.ts +52 -0
  151. package/src/prerender.ts +71 -103
  152. package/src/pwa-artifacts.ts +33 -94
  153. package/src/pwa-manifest-locales.ts +9 -9
  154. package/src/registry.ts +2 -0
  155. package/src/retry-memo.ts +1 -1
  156. package/src/revalidated-response.ts +4 -3
  157. package/src/role-realtime.ts +7 -1
  158. package/src/role-replicator.ts +124 -29
  159. package/src/role-start.ts +69 -85
  160. package/src/role-sync.ts +119 -25
  161. package/src/root-env.ts +157 -28
  162. package/src/route-islands.ts +69 -0
  163. package/src/runtime-admin.ts +4 -1
  164. package/src/runtime-assets.ts +26 -9
  165. package/src/runtime-bindings.ts +27 -7
  166. package/src/runtime-cache.ts +46 -55
  167. package/src/runtime-hooks.ts +2 -1
  168. package/src/runtime-idempotency-scope.ts +23 -0
  169. package/src/runtime-isr-outcome.ts +100 -0
  170. package/src/runtime-isr.ts +36 -0
  171. package/src/runtime-jobs.ts +76 -41
  172. package/src/runtime-live-feed.ts +1 -1
  173. package/src/runtime-mail.ts +26 -0
  174. package/src/runtime-mfa-warning.ts +29 -0
  175. package/src/runtime-notify-retention.ts +10 -46
  176. package/src/runtime-overrides.ts +4 -4
  177. package/src/runtime-purge.ts +38 -16
  178. package/src/runtime-queue.ts +43 -22
  179. package/src/runtime-realtime.ts +11 -45
  180. package/src/runtime-render.ts +136 -114
  181. package/src/runtime-replica.ts +3 -8
  182. package/src/runtime-services.ts +49 -26
  183. package/src/runtime-storage-upload.ts +2 -2
  184. package/src/runtime-storage.ts +13 -8
  185. package/src/schema-diff.ts +16 -0
  186. package/src/schema-drift.ts +45 -15
  187. package/src/secrets-rotation.ts +8 -9
  188. package/src/seo-routes.ts +80 -9
  189. package/src/serve-boot.ts +51 -12
  190. package/src/serve-drain.ts +11 -33
  191. package/src/serve-env.ts +9 -6
  192. package/src/serve-retire.ts +95 -0
  193. package/src/serve-web.ts +31 -15
  194. package/src/serve.ts +27 -10
  195. package/src/server-barrels.ts +6 -3
  196. package/src/shot-browser.ts +1 -1
  197. package/src/shot-locale.ts +6 -33
  198. package/src/shot-settle.ts +3 -3
  199. package/src/shot-theme.ts +1 -1
  200. package/src/shot-viewport.ts +152 -0
  201. package/src/signal-shred.ts +26 -12
  202. package/src/site-asset-routes.ts +2 -2
  203. package/src/site-assets.ts +3 -5
  204. package/src/site-config.ts +9 -48
  205. package/src/site-seo.ts +7 -5
  206. package/src/sitemap-lastmod.ts +5 -0
  207. package/src/solid-loader.ts +12 -3
  208. package/src/source-files.ts +19 -3
  209. package/src/static-report.ts +5 -0
  210. package/src/stored-object-headers.ts +43 -0
  211. package/src/style-routes.ts +2 -2
  212. package/src/sw-artifacts.ts +8 -8
  213. package/src/sw-precache-plan.ts +34 -4
  214. package/src/sync-authenticator.ts +3 -3
  215. package/src/sync-url.ts +3 -3
  216. package/src/tasks-facts.ts +3 -29
  217. package/src/templates/action.ts +7 -4
  218. package/src/templates/backfill.ts +4 -4
  219. package/src/templates/entity.ts +2 -2
  220. package/src/templates/github/ci.yml.ts +28 -18
  221. package/src/templates/github/image.yml.ts +92 -0
  222. package/src/templates/github/runner.ts +18 -0
  223. package/src/templates/guard-bare-error.ts +16 -2
  224. package/src/templates/guard-unzoned-date.ts +56 -6
  225. package/src/templates/index.ts +1 -1
  226. package/src/templates/job-body-test.ts +2 -2
  227. package/src/templates/job.ts +6 -6
  228. package/src/templates/naming.ts +28 -0
  229. package/src/templates/query.ts +9 -8
  230. package/src/templates/resource-page.ts +9 -7
  231. package/src/templates/resource-service.ts +2 -2
  232. package/src/templates/route.ts +26 -14
  233. package/src/templates/scaffold-api.ts +2 -3
  234. package/src/templates/scaffold-app.ts +8 -3
  235. package/src/templates/scaffold-auth.ts +205 -70
  236. package/src/templates/scaffold-bin.ts +122 -0
  237. package/src/templates/scaffold-claude-commands.ts +11 -11
  238. package/src/templates/scaffold-container-compose.ts +10 -1
  239. package/src/templates/scaffold-container.ts +16 -8
  240. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  241. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  242. package/src/templates/scaffold-dashboard-example.ts +112 -172
  243. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  244. package/src/templates/scaffold-dashboard.ts +9 -5
  245. package/src/templates/scaffold-db-client.ts +4 -2
  246. package/src/templates/scaffold-db-package.ts +2 -2
  247. package/src/templates/scaffold-docs.ts +20 -77
  248. package/src/templates/scaffold-domain-package.ts +0 -4
  249. package/src/templates/scaffold-entries.ts +4 -3
  250. package/src/templates/scaffold-env.ts +6 -3
  251. package/src/templates/scaffold-gitattributes.ts +59 -0
  252. package/src/templates/scaffold-helm-fences.ts +76 -0
  253. package/src/templates/scaffold-helm-templates.ts +100 -19
  254. package/src/templates/scaffold-helm.ts +62 -5
  255. package/src/templates/scaffold-http.ts +1 -1
  256. package/src/templates/scaffold-i18n.ts +15 -2
  257. package/src/templates/scaffold-icon.ts +2 -2
  258. package/src/templates/scaffold-repo.ts +27 -16
  259. package/src/templates/scaffold-shell.ts +55 -37
  260. package/src/test-counts.ts +2 -6
  261. package/src/test-ignores.ts +25 -0
  262. package/src/test-only-shipped.ts +193 -0
  263. package/src/test-passes.ts +0 -15
  264. package/src/test-select.ts +7 -15
  265. package/src/test-shards.ts +40 -95
  266. package/src/test-workers.ts +26 -4
  267. package/src/theme-boot.ts +8 -33
  268. package/src/theme-brand.ts +66 -0
  269. package/src/trusted-hops-error.ts +17 -0
  270. package/src/trusted-hops.ts +49 -0
  271. package/src/unscanned-admin.ts +2 -2
  272. package/src/unthrown-codes.ts +26 -0
  273. package/src/verify-checks.ts +29 -48
  274. package/src/verify-coverage-run.ts +3 -2
  275. package/src/verify-deadline.ts +207 -33
  276. package/src/verify-merge.ts +108 -43
  277. package/src/verify-part-step.ts +98 -0
  278. package/src/verify-role-load-probe.ts +12 -2
  279. package/src/verify-role-load.ts +9 -4
  280. package/src/verify-run.ts +32 -8
  281. package/src/verify-shard.ts +13 -2
  282. package/src/verify-stalled.ts +152 -0
  283. package/src/verify-step.ts +2 -0
  284. package/src/verify-test-run.ts +3 -2
  285. package/src/verify-tests.ts +5 -3
  286. package/src/worker-bundle.ts +2 -2
  287. package/src/workspace-checks.ts +53 -7
  288. package/src/workspace-graph.ts +3 -2
  289. package/src/write-line.ts +82 -7
  290. package/src/browser-launcher-fake-html.ts +0 -80
  291. package/src/browser-launcher-fake.ts +0 -168
  292. package/src/build-errors.ts +0 -54
  293. package/src/jobs-drain.ts +0 -163
  294. package/src/scaffold-typecheck.ts +0 -227
  295. package/src/thrown-by.ts +0 -24
  296. package/src/verify-errors.ts +0 -58
@@ -0,0 +1,52 @@
1
+ // How the build weighs an app/ page it renders only to measure: as the document a request is
2
+ // SERVED — the sync head and page boot both boots compose (`pageSync`), and the runtime chunk that
3
+ // boot makes unfetched. An unscoped render measured a runtime chunk no scoped page downloads and
4
+ // never the boot every one of them does, so `/feed` and `/runs` were budgets about nothing (#505).
5
+
6
+ import type { ClientSyncHead } from '@ultimat3/render';
7
+ import { loadAppConfig } from './app-config-load';
8
+ import type { MeasureOptions } from './budgets';
9
+ import type { IslandBundle } from './island-bundle';
10
+ import { isRuntimeChunk } from './island-runtime';
11
+ import { pageSync } from './page-sync';
12
+ import { realtimeConfigOf } from './runtime-realtime';
13
+ import { PAGE_BOOT_BASE_PATH } from './worker-bundle';
14
+
15
+ /** What a served-document measurement adds: to the document's options, and to the measurer's. */
16
+ export interface ServedMeasure {
17
+ readonly document: {
18
+ readonly sync?: ClientSyncHead;
19
+ readonly persisted?: () => readonly string[];
20
+ };
21
+ readonly measure: MeasureOptions;
22
+ }
23
+
24
+ /**
25
+ * `realtime.enabled: false` serves no sync head and no boot (`pageSync`), and then the measurement
26
+ * adds nothing: such a page's realtime islands load the runtime chunk, and are charged for it.
27
+ */
28
+ export async function servedMeasure(
29
+ root: string,
30
+ buildId: string,
31
+ islands: IslandBundle,
32
+ ): Promise<ServedMeasure> {
33
+ const sync = await pageSync(
34
+ root,
35
+ process.env,
36
+ buildId,
37
+ realtimeConfigOf(await loadAppConfig(root)),
38
+ );
39
+ return {
40
+ document: {
41
+ ...(sync.head === undefined ? {} : { sync: sync.head }),
42
+ persisted: sync.persisted,
43
+ },
44
+ measure: {
45
+ served: new Map(sync.scripts.map((script) => [script.url, script.code])),
46
+ boot: {
47
+ prefix: `${PAGE_BOOT_BASE_PATH}/`,
48
+ supplies: new Set(islands.shared.map((one) => one.url).filter(isRuntimeChunk)),
49
+ },
50
+ },
51
+ };
52
+ }
package/src/prerender.ts CHANGED
@@ -5,34 +5,38 @@
5
5
 
6
6
  import { join } from 'node:path';
7
7
  import {
8
- createContext,
8
+ ctxOf,
9
9
  DEFAULT_ENVIRONMENT,
10
10
  isUltimateError,
11
+ localizePath,
11
12
  renderThrowable,
12
13
  runWithContext,
13
14
  tryResolveEnvironment,
14
15
  } from '@ultimat3/core';
15
- import { localeConfig, localizedPath, routedLocales } from '@ultimat3/i18n';
16
16
  import type { RouteEntry } from '@ultimat3/render';
17
- import { describeRoutes, routeEntries } from '@ultimat3/render';
17
+ import { describePages, routeEntries } from '@ultimat3/render';
18
18
  import { renderStatic } from '@ultimat3/render/server';
19
19
  import { loadApp } from './app-load';
20
20
  import { appManifest } from './app-manifest';
21
21
  import type { RouteStats } from './budgets';
22
- import { measureDocumentJs, writeBuildStats } from './budgets';
22
+ import { measureDocumentJs, routeStatsRow, writeBuildStats } from './budgets';
23
23
  import { errorPageDocument, STATIC_ERROR_PAGE } from './error-pages';
24
+ import { PrerenderLoadFailedError } from './errors';
24
25
  import { FAVICON_PATH, faviconBytes } from './favicon';
25
26
  import type { IslandBundle } from './island-bundle';
26
27
  import { buildIslands, writeIslands } from './island-bundle';
27
28
  import { measureDatabase } from './measure-database';
28
29
  import { measurePaths } from './measure-paths';
29
- import { measureScope, withAppUrl } from './measure-scope';
30
+ import { measurementActorLabel, measureScope, withAppUrl } from './measure-scope';
30
31
  import { loadNavigation, pageNavigation } from './page-navigation';
31
- import { loadSpeculation, pageSpeculation } from './page-speculation';
32
+ import { loadSpeculation, otherLocaleSegments, pageSpeculation } from './page-speculation';
32
33
  import { localizedArtifacts } from './prerender-locales';
33
34
  import { clearPrerenderOut } from './prerender-out';
35
+ import type { PrerenderedPage, PrerenderReport } from './prerender-report';
36
+ import type { ServedMeasure } from './prerender-served';
37
+ import { servedMeasure } from './prerender-served';
34
38
  import { loadPwaArtifacts, writePwaIcons } from './pwa-artifacts';
35
- import { routeDocument } from './runtime-render';
39
+ import { routeDocument, servedDocument } from './runtime-render';
36
40
  import { writeSiteAssets } from './site-assets';
37
41
  import { loadSiteSettings, originWarning, publicOrigin } from './site-config';
38
42
  import type { SkippedRoute, UnmeasuredRoute } from './static-report';
@@ -47,9 +51,13 @@ import {
47
51
  serviceWorkerRegistration,
48
52
  } from './sw-artifacts';
49
53
  import { loadThemeMode, themeBoot } from './theme-boot';
54
+ import { loadThemeBrand } from './theme-brand';
55
+ import { writeErrorLine } from './write-line';
50
56
 
57
+ export type { PrerenderedPage, PrerenderReport } from './prerender-report';
51
58
  // Re-exported, never re-declared: `static-report.ts` owns the shape because the report on disk
52
- // carries it, and this file already imports that module.
59
+ // carries it, and this file already imports that module. The report's own shapes are
60
+ // `prerender-report.ts`'s, re-exported here for the same reason: callers ask this module.
53
61
  export type { UnmeasuredRoute };
54
62
 
55
63
  /**
@@ -74,59 +82,6 @@ export interface PrerenderOptions {
74
82
  readonly origin?: string;
75
83
  }
76
84
 
77
- export interface PrerenderedPage {
78
- /** The DECLARED route, `/blog/:slug` — one route can write many pages, and the report groups them. */
79
- readonly route: string;
80
- /** The URL the page is served at — `/en/pricing` for a non-default locale. */
81
- readonly path: string;
82
- /** The locale the page was rendered in. */
83
- readonly locale: string;
84
- /** Relative to `out`, POSIX — under `<locale>/` for a non-default locale. */
85
- readonly file: string;
86
- readonly hash: string;
87
- readonly bytes: number;
88
- }
89
-
90
- export interface PrerenderReport {
91
- readonly out: string;
92
- readonly buildId: string;
93
- readonly pages: readonly PrerenderedPage[];
94
- /**
95
- * Every declared route that wrote no file, WITH the cause. A bare path list was the whole of
96
- * #242: `.x/static/` held a partial site, the report said only which paths were missing, and a
97
- * screenshot tool pointed at the directory filed "the island did not mount" against a route that
98
- * had never been in the artifact. The reason is what tells an author whether an edit exists.
99
- */
100
- readonly skipped: readonly SkippedRoute[];
101
- /**
102
- * Routes whose budget this build could not weigh, with the reason. `X_BUDGET_UNMEASURED` is what
103
- * the gate then reports for each; this is the half that says WHY, which a per-route finding read
104
- * off a stats file cannot know.
105
- */
106
- readonly unmeasured: readonly UnmeasuredRoute[];
107
- /** Where the measured stats landed, for the `budgets` gate step to read. */
108
- readonly stats: string;
109
- /** Where the emitted/skipped inventory landed, for `x build --target static` to read back. */
110
- readonly report: string;
111
- /** Client entries emitted, one chunk each. Reported so "which JS shipped?" needs no unzip. */
112
- readonly islands: readonly string[];
113
- /** Surface stylesheets emitted, one file each — the CSS half of the same question. */
114
- readonly styles: readonly string[];
115
- /**
116
- * What the service worker could not express, and what its precache manifest weighs too much of.
117
- *
118
- * `PrecacheManifest.warnings` had no reader anywhere in the tree — the precache budget was, in
119
- * `wiki/Troubleshooting.md`'s own words, "a designed thing that is not one" (#390). An install
120
- * that stalls on a bad connection is invisible on a laptop and fatal on a phone, so the number
121
- * has to reach the build's own report. Empty for an app with no service worker.
122
- */
123
- readonly serviceWorkerWarnings: readonly string[];
124
- /** What this build could not get right on its own — today, a production build with no origin. */
125
- readonly warnings: readonly string[];
126
- /** The origin every absolute URL in the export was built against — hand it to `siteSeo`. */
127
- readonly origin: string;
128
- }
129
-
130
85
  /**
131
86
  * The heaviest thing the document actually boots, named by the source file an author can open.
132
87
  * An island chunk is content-addressed, so its URL says nothing on its own — mapping it back
@@ -168,15 +123,17 @@ const NOT_FOUND_STATUS = 404;
168
123
  */
169
124
  const declaresBudget = (entry: RouteEntry): boolean => {
170
125
  const budget = entry.config.budget;
171
- return budget !== undefined && (budget.js !== undefined || budget.lcp !== undefined);
126
+ return budget?.js !== undefined;
172
127
  };
173
128
 
174
129
  export async function prerenderSite(options: PrerenderOptions): Promise<PrerenderReport> {
175
- // The same load `x dev` and `x manifest` perform: importing the app's modules IS what fills the
176
- // route registry, so there is no route table to prerender before this runs.
177
- // Emptied FIRST: the export only ever gained files, so a deleted route's HTML kept shipping.
130
+ // The load `x dev` and `x manifest` perform: importing the app's modules fills the route registry.
131
+ // A module that would not import registered nothing, so its page would vanish from the export
132
+ // under a green build — refused BEFORE the export is emptied, so the last good artifact survives.
133
+ const loaded = await loadApp(options.root);
134
+ if (loaded.findings.length > 0) throw new PrerenderLoadFailedError(loaded.findings);
135
+ // Emptied before anything is written: a deleted route's HTML kept shipping when it only grew.
178
136
  await clearPrerenderOut(options.out, options.root);
179
- await loadApp(options.root);
180
137
  const buildId = (await appManifest(options.root)).manifest.buildId;
181
138
  const declaredOrigin =
182
139
  options.origin ?? publicOrigin(process.env, await loadSiteSettings(options.root));
@@ -184,11 +141,12 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
184
141
  // Written to stderr as well as returned: an app's `prerender.ts` prints its report as ONE JSON
185
142
  // line on stdout, and a build that parses it must not meet a warning inside it.
186
143
  const warnings = originWarning(tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT, declaredOrigin);
187
- for (const warning of warnings) process.stderr.write(`warning: ${warning}\n`);
188
- // After `loadApp`: `defineCatalogs()` configures the locales on the app's own import. The
189
- // default first, so every other pass writes beside a tree that already holds the unprefixed one.
190
- const locales = routedLocales();
191
- const defaultLocale = localeConfig().fallback;
144
+ for (const warning of warnings) writeErrorLine(`warning: ${warning}`);
145
+ // The app's locales as `loadApp` read them, ONCE, off `@ultimat3/i18n`'s one reader — the same
146
+ // answer the PWA manifest and the service worker below are built from. The default first, so
147
+ // every other pass writes beside a tree that already holds the unprefixed one.
148
+ const appLocales = loaded.locales;
149
+ const { locales, defaultLocale } = appLocales;
192
150
  const pages: PrerenderedPage[] = [];
193
151
  const skipped: SkippedRoute[] = [];
194
152
  const routes: RouteStats[] = [];
@@ -235,6 +193,8 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
235
193
  // names it either.
236
194
  const pwa = await loadPwaArtifacts(options.root);
237
195
  const theme = themeBoot(await loadThemeMode(options.root));
196
+ // The brand the served process carries, so an exported page and a served one paint alike.
197
+ const brand = await loadThemeBrand(options.root);
238
198
  // The client router, for the surfaces that opted in: written BEFORE any document names it, for
239
199
  // the register's reason below — `measureDocumentJs` weighs it off disk, and it is charged to
240
200
  // the route (the interactivity the app asked for), so it must be there to be weighed.
@@ -244,6 +204,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
244
204
  const speculation = pageSpeculation({
245
205
  config: await loadSpeculation(options.root),
246
206
  client: declared.surfaces,
207
+ localeSegments: otherLocaleSegments(appLocales),
247
208
  });
248
209
  if (navigation.script !== undefined) {
249
210
  await Bun.write(join(options.out, navigation.script.url.slice(1)), navigation.script.code);
@@ -289,11 +250,11 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
289
250
  // build. One context for the build, `role: 'web'` because that is the role serving these
290
251
  // documents, and this build's own id so a component reading `ctx.buildId` stamps the artifact
291
252
  // with the id the report and the stats carry.
292
- // ONE PER LOCALE, and the locale is the context's: `createContext` defaults it to core's `en`, so a
253
+ // ONE PER LOCALE, and the locale is the context's: `ctxOf` defaults it to core's `en`, so a
293
254
  // Spanish-default site was prerendered in English — `<html lang="en">` over English copy — while
294
255
  // the served process answered Spanish. Each `site/` page is rendered once per routed locale.
295
256
  const contexts = new Map(
296
- locales.map((locale) => [locale, createContext({ role: 'web', buildId, locale })]),
257
+ locales.map((locale) => [locale, ctxOf({ role: 'web', buildId, locale })]),
297
258
  );
298
259
  // A SECOND scope, for the branch below that renders only to weigh (`measure-scope.ts`): a
299
260
  // request context holding the app's measurement actor, with the app's own API answered in
@@ -304,23 +265,32 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
304
265
  const measure = await measureScope({ origin, buildId });
305
266
  // Started on the first route that needs weighing, and released however the loop ends.
306
267
  const database = measureDatabase(options.root);
268
+ const documentOptions = {
269
+ resolveIsland: (file: string) => islands.resolverFor(file),
270
+ themeHead: theme.head,
271
+ ...(brand === undefined ? {} : { brandHead: brand.head }),
272
+ ...(speculation === undefined ? {} : { speculationHead: speculation.head }),
273
+ origin,
274
+ ...(navigation.head === undefined ? {} : { navigation: navigation.head }),
275
+ ...(pwa === undefined
276
+ ? {}
277
+ : { pwaHead: (locale: string) => pwa.headFor(locale) + (swHead ?? '') }),
278
+ };
307
279
  const render = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
308
- routeDocument(entry, data, {
309
- resolveIsland: (file: string) => islands.resolverFor(file),
310
- themeHead: theme.head,
311
- ...(speculation === undefined ? {} : { speculationHead: speculation.head }),
312
- origin,
313
- ...(navigation.head === undefined ? {} : { navigation: navigation.head }),
314
- ...(pwa === undefined
315
- ? {}
316
- : { pwaHead: (locale: string) => pwa.headFor(locale) + (swHead ?? '') }),
317
- });
280
+ routeDocument(entry, data, documentOptions);
281
+ // The weigh-only branch measures the document a request is SERVED (`prerender-served.ts`):
282
+ // composed on the first route that needs it, so a build weighing none builds no page boot.
283
+ let weighing: Promise<ServedMeasure> | undefined;
284
+ const served = (): Promise<ServedMeasure> => {
285
+ weighing ??= servedMeasure(options.root, buildId, islands);
286
+ return weighing;
287
+ };
318
288
  const document = (
319
289
  locale: string,
320
290
  entry: RouteEntry,
321
291
  data: { url: string; params: Record<string, string> },
322
292
  ) =>
323
- runWithContext(contexts.get(locale) ?? createContext({ role: 'web', buildId, locale }), () =>
293
+ runWithContext(contexts.get(locale) ?? ctxOf({ role: 'web', buildId, locale }), () =>
324
294
  render(entry, data),
325
295
  );
326
296
 
@@ -349,16 +319,19 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
349
319
  let row: RouteStats | undefined;
350
320
  for (const { path, params } of plan.paths) {
351
321
  const data = { url: new URL(path, origin).href, params };
352
- const html = await withAppUrl(origin, () => measure.run(() => render(entry, data)));
353
- const measured = await measureDocumentJs(html, options.out);
322
+ const as = await served();
323
+ const html = await withAppUrl(origin, () =>
324
+ measure.run(() =>
325
+ servedDocument(entry, data, measure.actor, {
326
+ ...documentOptions,
327
+ ...as.document,
328
+ buildId,
329
+ }),
330
+ ),
331
+ );
332
+ const measured = await measureDocumentJs(html, options.out, as.measure);
354
333
  if (row !== undefined && measured.jsBytes <= row.jsBytes) continue;
355
- const chain = heaviestSource(islands, measured.entries);
356
- row = {
357
- path: entry.path,
358
- jsBytes: measured.jsBytes,
359
- frameworkJsBytes: measured.frameworkBytes,
360
- ...(chain === undefined ? {} : { heaviestChain: chain }),
361
- };
334
+ row = routeStatsRow(entry.path, measured, heaviestSource(islands, measured.entries));
362
335
  }
363
336
  if (row !== undefined) routes.push(row);
364
337
  } catch (error) {
@@ -374,16 +347,17 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
374
347
  ...(isUltimateError(error)
375
348
  ? { code: error.code, cause: error.cause, fix: error.fix }
376
349
  : {}),
350
+ actor: measurementActorLabel(measure.actor),
377
351
  });
378
352
  }
379
353
  continue;
380
354
  }
381
- const artifacts = await localizedArtifacts(locales, defaultLocale, (locale) =>
355
+ const artifacts = await localizedArtifacts(appLocales, (locale) =>
382
356
  renderStatic(
383
357
  entry,
384
358
  ({ path, params }) =>
385
359
  document(locale, entry, {
386
- url: new URL(localizedPath(path, locale, defaultLocale), origin).href,
360
+ url: new URL(localizePath(path, locale, locales, defaultLocale), origin).href,
387
361
  params,
388
362
  }),
389
363
  { buildId },
@@ -428,14 +402,8 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
428
402
  bytes,
429
403
  assets: measured.entries.map((entry) => entry.url),
430
404
  });
431
- const chain = heaviestSource(islands, measured.entries);
432
405
  if (heaviest !== undefined && heaviest.jsBytes >= measured.jsBytes) continue;
433
- heaviest = {
434
- path: entry.path,
435
- jsBytes: measured.jsBytes,
436
- frameworkJsBytes: measured.frameworkBytes,
437
- ...(chain === undefined ? {} : { heaviestChain: chain }),
438
- };
406
+ heaviest = routeStatsRow(entry.path, measured, heaviestSource(islands, measured.entries));
439
407
  }
440
408
  if (heaviest !== undefined) routes.push(heaviest);
441
409
  }
@@ -452,7 +420,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
452
420
  : serviceWorkerArtifacts({
453
421
  pwa,
454
422
  buildId,
455
- routes: describeRoutes(),
423
+ routes: describePages(),
456
424
  islands,
457
425
  styles,
458
426
  documents,
@@ -15,17 +15,13 @@
15
15
  // build behind them — `wiki/PWA-And-Offline.md` says so — and a bad `sw.js` is sticky in a way a
16
16
  // manifest is not, so the worker lands behind a real browser check rather than beside this.
17
17
 
18
- // why: Bun exposes no synchronous file-existence primitive, and this read is the same one
19
- // `app-auth.ts` and `runtime-cache.ts` each make before importing an app's config — a root with no
20
- // `app.config.ts` is an ordinary answer here (a scratch root, `x build` outside an app).
21
- import { existsSync } from 'node:fs';
22
- // why: Bun exposes no path-join primitive, and `APP_CONFIG_FILE` is app-root-relative — the same
23
- // necessity `favicon.ts` and `runtime-assets.ts` each record for their own root-relative constant.
18
+ // why: Bun exposes no path-join primitive, and the export writes each icon under an out directory.
24
19
  import { join } from 'node:path';
25
20
  import type { PwaColors, PwaOfflineConfig } from '@ultimat3/core';
26
- import { localeSegment } from '@ultimat3/core';
21
+ import { escapeHtml, localeSegment } from '@ultimat3/core';
27
22
  import type { CacheHint, RequestContext, Route, UltimateRequest } from '@ultimat3/http';
28
23
  import { applyCacheHeaders } from '@ultimat3/http';
24
+ import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
29
25
  import {
30
26
  appleTouchLinks,
31
27
  generateWebManifest,
@@ -34,9 +30,7 @@ import {
34
30
  } from '@ultimat3/pwa';
35
31
  import type { AssetPath } from '@ultimat3/render';
36
32
  import { assetPathProblem } from '@ultimat3/render';
37
- import { escapeAttribute } from '@ultimat3/seo';
38
- import { APP_CONFIG_EXPORT } from './app-auth';
39
- import { APP_CONFIG_FILE } from './app-root';
33
+ import { loadAppConfig } from './app-config-load';
40
34
  import { hasSourceIcon, iconPlan, iconRenderer } from './icon-assets';
41
35
  import type { AppLocales } from './pwa-manifest-locales';
42
36
  import { appLocales, localeMembers, spellIn } from './pwa-manifest-locales';
@@ -80,45 +74,17 @@ export interface PwaArtifacts {
80
74
  /** The `<head>` for a document in `locale` — it links that locale's manifest. */
81
75
  headFor(locale: string): string;
82
76
  /**
83
- * The three `pwa` keys the SERVICE WORKER needs, carried here because this is the one module
84
- * that reads an app's config file — `sw-artifacts.ts` needs the route table and the island
85
- * bundle as well, and a second `await import` of `app.config.ts` would be a second answer to
86
- * "what did this app declare".
77
+ * The three `pwa` keys the SERVICE WORKER needs, carried here so `sw-artifacts.ts` — which needs
78
+ * the route table and the island bundle as well — reads them off the same resolved block.
87
79
  */
88
80
  readonly offline: PwaOfflineConfig;
89
81
  readonly backgroundSync: boolean;
90
82
  readonly push: boolean;
91
- }
92
-
93
- const isRecord = (value: unknown): value is Record<string, unknown> =>
94
- typeof value === 'object' && value !== null;
95
-
96
- const text = (value: unknown): string | undefined =>
97
- typeof value === 'string' && value.trim() !== '' ? value : undefined;
98
-
99
- /**
100
- * `colors` read structurally, for `loadSignInPath`'s reason: `defineConfig` returns a plain object
101
- * and a config resolved through an older core simply has no such key. `validate()` already refused
102
- * a blank one an `await import` above this line, so anything this rejects is a hand-written config
103
- * object — and the honest answer for one is no manifest at all, never a colour we invented.
104
- */
105
- function colorsOf(value: unknown): PwaColors | undefined {
106
- if (!isRecord(value)) return undefined;
107
- const light = isRecord(value['light']) ? value['light'] : undefined;
108
- const dark = isRecord(value['dark']) ? value['dark'] : undefined;
109
- if (light === undefined || dark === undefined) return undefined;
110
- const [lt, lb, dt, db] = [
111
- text(light['themeColor']),
112
- text(light['backgroundColor']),
113
- text(dark['themeColor']),
114
- text(dark['backgroundColor']),
115
- ];
116
- if (lt === undefined || lb === undefined || dt === undefined || db === undefined)
117
- return undefined;
118
- return {
119
- light: { themeColor: lt, backgroundColor: lb },
120
- dark: { themeColor: dt, backgroundColor: db },
121
- };
83
+ /**
84
+ * The routed locales the manifests above were built from, default first — `appLocaleSet`'s one
85
+ * answer, carried here so the service worker prefixes exactly the locales the manifests name.
86
+ */
87
+ readonly locales: AppLocales;
122
88
  }
123
89
 
124
90
  /** The `pwa` block, as much of it as this file needs, or `undefined` when the app declares none. */
@@ -133,60 +99,30 @@ interface InstallableApp {
133
99
  readonly push: boolean;
134
100
  }
135
101
 
136
- async function loadInstallable(root: string): Promise<InstallableApp | undefined> {
137
- const configPath = join(root, APP_CONFIG_FILE);
138
- if (!existsSync(configPath)) return undefined;
139
- const module = (await import(configPath)) as Record<string, unknown>;
140
- const config = module[APP_CONFIG_EXPORT];
141
- if (!isRecord(config)) return undefined;
142
- const pwa = config['pwa'];
143
- // `pwa.enabled`, read. Not a truthiness test: `enabled` is the key this whole file exists to
144
- // give a reader, and `=== true` is what makes a hand-written `enabled: 'yes'` produce no
145
- // manifest rather than one nobody asked for.
146
- if (!isRecord(pwa) || pwa['enabled'] !== true) return undefined;
147
- const name = text(pwa['name']);
148
- const colors = colorsOf(pwa['colors']);
149
- if (name === undefined || colors === undefined) return undefined;
150
- return {
151
- name,
152
- colors,
153
- block: pwa,
154
- locales: appLocales(config),
155
- offline: offlineOf(pwa['offline']),
156
- ...flags(pwa),
157
- };
158
- }
159
-
160
102
  /**
161
- * The offline block, read structurally for `colorsOf`'s reason: `defineConfig` refuses
162
- * `enabled: true` without an absolute `offline.fallback`, but a HAND-WRITTEN config object never
163
- * passed through it. A missing or relative fallback answers `null`, and `serviceWorkerArtifacts`
164
- * then emits no worker at all — never a path the framework invented, which offline would be a
165
- * cached 404 answering every navigation.
103
+ * `pwa.enabled`, read, off the one loader (`app-config-load.ts`). Core's validator refuses an
104
+ * enabled block without a name, both colour schemes or an absolute `offline.fallback`, so nothing
105
+ * here invents a colour or a fallback route; `colors` is optional only in the type.
166
106
  */
167
- function offlineOf(value: unknown): PwaOfflineConfig {
168
- const block = isRecord(value) ? value : {};
169
- const fallback = text(block['fallback']);
170
- const patterns = block['neverCache'];
107
+ async function loadInstallable(root: string): Promise<InstallableApp | undefined> {
108
+ const config = await loadAppConfig(root);
109
+ const pwa = config?.pwa;
110
+ if (config === undefined || pwa === undefined || !pwa.enabled || pwa.colors === undefined) {
111
+ return undefined;
112
+ }
171
113
  return {
172
- fallback: fallback?.startsWith('/') === true ? fallback : null,
173
- image: text(block['image']) ?? null,
174
- font: text(block['font']) ?? null,
175
- neverCache: Array.isArray(patterns)
176
- ? patterns.filter((entry): entry is string => typeof entry === 'string')
177
- : [],
178
- personalPages: block['personalPages'] === 'last-member' ? 'last-member' : 'never',
114
+ name: pwa.name,
115
+ colors: pwa.colors,
116
+ block: { ...pwa },
117
+ locales: appLocales(await appLocaleSet(root)),
118
+ offline: pwa.offline,
119
+ backgroundSync: pwa.backgroundSync,
120
+ push: pwa.push,
179
121
  };
180
122
  }
181
123
 
182
- /** `=== true` for `enabled`'s reason: a hand-written `backgroundSync: 'yes'` wires nothing. */
183
- const flags = (pwa: Record<string, unknown>): { backgroundSync: boolean; push: boolean } => ({
184
- backgroundSync: pwa['backgroundSync'] === true,
185
- push: pwa['push'] === true,
186
- });
187
-
188
124
  /**
189
- * Resolved ONCE at boot, like `loadSignInPath` and `loadCacheTiers` and unlike `faviconBytes`:
125
+ * Resolved ONCE at boot, like `loadSignInPath` and unlike `faviconBytes`: the loader's
190
126
  * `await import` caches the module, so re-reading per request would answer the same object at a
191
127
  * per-request cost, and the head string has to be available synchronously while a document renders.
192
128
  */
@@ -219,7 +155,7 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
219
155
  path,
220
156
  body: serializeWebManifest(result.manifest),
221
157
  head:
222
- `<link rel="manifest" href="${escapeAttribute(path)}">` +
158
+ `<link rel="manifest" href="${escapeHtml(path)}">` +
223
159
  renderThemeColorMeta(result.themeColorMeta) +
224
160
  (icons === undefined ? '' : appleTouchLinks(icons)),
225
161
  };
@@ -231,6 +167,7 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
231
167
  offline: app.offline,
232
168
  backgroundSync: app.backgroundSync,
233
169
  push: app.push,
170
+ locales: app.locales,
234
171
  body: primary.body,
235
172
  head: primary.head,
236
173
  manifests,
@@ -244,7 +181,9 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
244
181
  * `asset()` does in a page. The site serves an asset ONLY at its hashed URL, and the manifest took
245
182
  * `src` verbatim, so an app had to hash its own screenshots to avoid shipping a 404 in the install
246
183
  * sheet (notificado.co's `pwa-install.ts`). A missing file is `X_ASSET_MISSING` at boot, never a
247
- * broken image in an install prompt; any other `src` (absolute, already hashed) passes untouched.
184
+ * broken image in an install prompt; any other `src` passes untouched — an absolute path outside
185
+ * `/assets/`, an already hashed one, or a shortcut icon's off-site URL (core screens screenshots and
186
+ * shortcut urls to absolute paths, not icon sources).
248
187
  */
249
188
  function withHashedAssets<M extends ReturnType<typeof localeMembers>>(members: M, root: string): M {
250
189
  const resolve = (src: string): string => {
@@ -3,6 +3,7 @@
3
3
  // routed locale from this; the `pwa` block is read structurally, for `colorsOf`'s reason there.
4
4
 
5
5
  import { localeSegment, localizePath } from '@ultimat3/core';
6
+ import type { AppLocaleSet } from '@ultimat3/i18n/app-catalogs';
6
7
  import type {
7
8
  ManifestIcon,
8
9
  ManifestScreenshot,
@@ -10,7 +11,7 @@ import type {
10
11
  WebManifestInput,
11
12
  } from '@ultimat3/pwa';
12
13
 
13
- /** The app's locales as `app.config.ts` declares them, default first. */
14
+ /** The app's locales as its catalogs declare them (`defineCatalogs`), default first. */
14
15
  export interface AppLocales {
15
16
  readonly routed: readonly string[];
16
17
  readonly fallback: string;
@@ -23,15 +24,14 @@ const strings = (value: unknown): readonly string[] =>
23
24
  Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
24
25
 
25
26
  /**
26
- * `locales` and `defaultLocale`, read off the config object. The manifest's `lang` was the
27
- * generator's hardcoded `'en'` for an `es-co` app (notificado.co, 22.3.2). An untyped config with
28
- * neither is core's own default, `en`.
27
+ * The manifests' locales, off `appLocaleSet`'s answer (`@ultimat3/i18n/app-catalogs`) — an app
28
+ * with no catalogs is `UNDECLARED_LOCALES`, the framework default alone. The manifest's `lang` was
29
+ * the generator's hardcoded `'en'` for an `es-co` app (notificado.co, 22.3.2). Deduplicated by URL
30
+ * segment, the unit a locale prefix is spelled in.
29
31
  */
30
- export function appLocales(config: Record<string, unknown>): AppLocales {
31
- const declared = strings(config['locales']);
32
- const named = config['defaultLocale'];
33
- const fallback = typeof named === 'string' && named !== '' ? named : (declared[0] ?? 'en');
34
- const others = declared.filter((locale) => localeSegment(locale) !== localeSegment(fallback));
32
+ export function appLocales(set: AppLocaleSet): AppLocales {
33
+ const fallback = set.defaultLocale;
34
+ const others = set.locales.filter((locale) => localeSegment(locale) !== localeSegment(fallback));
35
35
  return { routed: [fallback, ...others], fallback };
36
36
  }
37
37
 
package/src/registry.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  // dev server it never started (plan 101 slice 12 d).
8
8
 
9
9
  import { affectedSpec } from './cmd-affected-spec';
10
+ import { authSpec } from './cmd-auth-spec';
10
11
  import { buildSpec } from './cmd-build-spec';
11
12
  import { ciSpec } from './cmd-ci-spec';
12
13
  import { dbSpec } from './cmd-db-spec';
@@ -85,6 +86,7 @@ export const LAZY_COMMANDS: readonly LazyCommand[] = [
85
86
  lazy(deploySpec, async () => (await import('./cmd-deploy')).deployCommand),
86
87
  lazy(envSpec, async () => (await import('./cmd-env')).envCommand),
87
88
  lazy(secretsSpec, async () => (await import('./cmd-secrets')).secretsCommand),
89
+ lazy(authSpec, async () => (await import('./cmd-auth')).authCommand),
88
90
  lazy(manifestSpec, async () => (await import('./cmd-manifest')).manifestCommand),
89
91
  lazy(routesSpec, async () => (await import('./cmd-routes')).routesCommand),
90
92
  lazy(actionsSpec, async () => (await import('./cmd-registries')).actionsCommand),
package/src/retry-memo.ts CHANGED
@@ -15,7 +15,7 @@ export interface RetryMemo<T> {
15
15
  }
16
16
 
17
17
  /**
18
- * `start` runs at most once per SUCCESS. `@ultimat3/db`'s `createPgliteClient` states the rule this
18
+ * `start` runs at most once per SUCCESS. `@ultimat3/db`'s `pgliteClient` states the rule this
19
19
  * generalises — "a failed boot must not be cached" — and the clearing handler is attached at
20
20
  * creation for the reason that matters: it therefore runs ahead of every caller's own `await`
21
21
  * continuation, so by the time anyone sees the rejection the slot is already empty and the next
@@ -4,6 +4,7 @@
4
4
  // name that outlives its bytes is a file no client refreshes for a year.
5
5
 
6
6
  import type { UltimateRequest } from '@ultimat3/http';
7
+ import { contentHash } from '@ultimat3/render/server';
7
8
  import { etagMatches } from './runtime-storage';
8
9
 
9
10
  /**
@@ -14,8 +15,8 @@ import { etagMatches } from './runtime-storage';
14
15
  export const REVALIDATE_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
15
16
 
16
17
  /**
17
- * A strong validator from the bytes — xxHash32, the function `contentHash` applies to a document,
18
- * taken here over bytes because an icon is not a string. Revalidating costs a request and no body.
18
+ * A strong validator from the bytes — `@ultimat3/render`'s `contentHash`, the function a document's
19
+ * ETag is, taken here over bytes because an icon is not a string. Revalidating costs a request and no body.
19
20
  */
20
21
  export function revalidatedResponse(
21
22
  request: UltimateRequest,
@@ -25,7 +26,7 @@ export function revalidatedResponse(
25
26
  // Copied, not passed through: a `Uint8Array<ArrayBufferLike>` may be backed by a
26
27
  // `SharedArrayBuffer`, which `Response` does not accept — `runtime-assets.ts`'s rule, verbatim.
27
28
  const bytes = typeof body === 'string' ? new TextEncoder().encode(body) : new Uint8Array(body);
28
- const etag = `"${Bun.hash.xxHash32(bytes).toString(16).padStart(8, '0')}"`;
29
+ const etag = `"${contentHash(bytes)}"`;
29
30
  const headers = {
30
31
  'content-type': contentType,
31
32
  'cache-control': REVALIDATE_CACHE_CONTROL,