@ultimat3/cli 22.14.0 → 23.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 (174) hide show
  1. package/CLAUDE.md +25 -8
  2. package/README.md +28 -5
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/api-registration.ts +36 -11
  7. package/src/app-boundaries.ts +6 -20
  8. package/src/app-load.ts +55 -11
  9. package/src/app-manifest.ts +14 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-reload-graph.ts +7 -30
  12. package/src/app-transport.ts +219 -0
  13. package/src/boundary-findings.ts +16 -4
  14. package/src/browser-transport.ts +160 -0
  15. package/src/budgets.ts +35 -3
  16. package/src/cmd-build-spec.ts +10 -3
  17. package/src/cmd-build.ts +56 -9
  18. package/src/cmd-db.ts +60 -11
  19. package/src/cmd-dev.ts +2 -2
  20. package/src/cmd-doctor.ts +35 -11
  21. package/src/cmd-generate.ts +118 -17
  22. package/src/cmd-jobs-spec.ts +19 -3
  23. package/src/cmd-jobs.ts +120 -3
  24. package/src/cmd-new.ts +15 -1
  25. package/src/cmd-routes.ts +15 -1
  26. package/src/cmd-secrets-ring.ts +76 -0
  27. package/src/cmd-secrets-spec.ts +10 -2
  28. package/src/cmd-secrets.ts +48 -12
  29. package/src/cmd-tasks.ts +84 -15
  30. package/src/cmd-verify.ts +10 -2
  31. package/src/comment-safe.ts +16 -0
  32. package/src/coverage-floor.ts +334 -0
  33. package/src/coverage-lcov.ts +193 -0
  34. package/src/coverage-source.ts +136 -0
  35. package/src/db-object-drift.ts +30 -0
  36. package/src/db-replay-engine.ts +115 -0
  37. package/src/db-schema-dump.ts +150 -0
  38. package/src/db-schema-refresh.ts +122 -0
  39. package/src/dev-dashboard.ts +2 -2
  40. package/src/dev-route-table.ts +30 -6
  41. package/src/doctor-coverage.ts +39 -0
  42. package/src/doctor-guards.ts +54 -0
  43. package/src/doctor-sealed.ts +132 -0
  44. package/src/document-graph.ts +82 -0
  45. package/src/document-options.ts +64 -0
  46. package/src/error-codes.ts +32 -0
  47. package/src/framework-schema.ts +15 -0
  48. package/src/generate-files.ts +76 -10
  49. package/src/generate-grant-findings.ts +76 -0
  50. package/src/generate-grants.ts +77 -3
  51. package/src/generate-kinds.ts +19 -0
  52. package/src/generate-write.ts +15 -27
  53. package/src/generated-imports.ts +59 -0
  54. package/src/guard-sources.ts +104 -0
  55. package/src/guards.ts +23 -7
  56. package/src/handle-registration.ts +217 -0
  57. package/src/i18n-registration.ts +36 -0
  58. package/src/image-prepare.ts +51 -8
  59. package/src/import-closure.ts +165 -0
  60. package/src/index.ts +11 -2
  61. package/src/island-store.ts +24 -16
  62. package/src/jobs-drain.ts +14 -7
  63. package/src/jobs-json.ts +50 -0
  64. package/src/jobs-report.ts +3 -0
  65. package/src/json-merge.ts +46 -0
  66. package/src/mcp-errors.ts +29 -1
  67. package/src/messages.ts +33 -0
  68. package/src/metrics-endpoint.ts +71 -24
  69. package/src/migration-extensions.ts +29 -0
  70. package/src/module-imports.ts +47 -0
  71. package/src/output.ts +18 -9
  72. package/src/page-speculation.ts +124 -0
  73. package/src/permission-grants.ts +41 -9
  74. package/src/prerender.ts +9 -5
  75. package/src/publish-closure.ts +101 -0
  76. package/src/role-load.ts +143 -0
  77. package/src/role-start.ts +18 -1
  78. package/src/role-wake.ts +45 -0
  79. package/src/runtime-admin.ts +141 -0
  80. package/src/runtime-hooks.ts +5 -2
  81. package/src/runtime-queue.ts +8 -1
  82. package/src/runtime-render.ts +36 -60
  83. package/src/runtime-services.ts +2 -1
  84. package/src/scaffold-typecheck.ts +1 -0
  85. package/src/schema-dump-drift.ts +64 -0
  86. package/src/serve-boot.ts +85 -147
  87. package/src/serve-prebuilt-paths.ts +19 -0
  88. package/src/serve-prebuilt.ts +84 -0
  89. package/src/serve-web.ts +164 -0
  90. package/src/serve.ts +6 -9
  91. package/src/server-barrels.ts +54 -0
  92. package/src/source-edit.ts +49 -0
  93. package/src/static-document.ts +108 -0
  94. package/src/tasks-facts.ts +4 -1
  95. package/src/templates/action.ts +144 -11
  96. package/src/templates/admin-catalog.ts +58 -0
  97. package/src/templates/admin-page.ts +36 -8
  98. package/src/templates/admin.ts +57 -17
  99. package/src/templates/backfill.ts +121 -87
  100. package/src/templates/entity.ts +140 -44
  101. package/src/templates/guard-animated-layout-property.ts +4 -11
  102. package/src/templates/guard-bare-error.ts +9 -13
  103. package/src/templates/guard-focus-visible.ts +4 -11
  104. package/src/templates/guard-image-dimensions.ts +6 -6
  105. package/src/templates/guard-island-without-states.ts +2 -4
  106. package/src/templates/guard-raw-breakpoint.ts +210 -0
  107. package/src/templates/guard-raw-colour.ts +4 -11
  108. package/src/templates/guard-raw-length.ts +375 -0
  109. package/src/templates/guard-raw-motion.ts +213 -0
  110. package/src/templates/guard-raw-shadow.ts +187 -0
  111. package/src/templates/guard-raw-z-index.ts +166 -0
  112. package/src/templates/guard-repo-raw-sql.ts +345 -0
  113. package/src/templates/guard-semantic-interactive.ts +6 -6
  114. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  115. package/src/templates/guard-undefined-style-class.ts +215 -0
  116. package/src/templates/guard-untranslated-string.ts +6 -6
  117. package/src/templates/guard-unzoned-date.ts +9 -13
  118. package/src/templates/index.ts +3 -2
  119. package/src/templates/island.ts +34 -51
  120. package/src/templates/job-body-test.ts +121 -0
  121. package/src/templates/job.ts +23 -8
  122. package/src/templates/policy.ts +16 -9
  123. package/src/templates/query.ts +242 -26
  124. package/src/templates/resource-create.ts +62 -18
  125. package/src/templates/resource-form-island.ts +58 -80
  126. package/src/templates/resource-page.ts +269 -0
  127. package/src/templates/resource-service.ts +96 -0
  128. package/src/templates/resource.ts +86 -61
  129. package/src/templates/route.ts +38 -29
  130. package/src/templates/scaffold-api.ts +55 -1
  131. package/src/templates/scaffold-app.ts +171 -51
  132. package/src/templates/scaffold-auth.ts +83 -19
  133. package/src/templates/scaffold-browser-client.ts +90 -0
  134. package/src/templates/scaffold-container.ts +16 -0
  135. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  136. package/src/templates/scaffold-dashboard-example.ts +124 -31
  137. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  138. package/src/templates/scaffold-dashboard.ts +1 -2
  139. package/src/templates/scaffold-db-client.ts +142 -0
  140. package/src/templates/scaffold-db-package.ts +90 -8
  141. package/src/templates/scaffold-docs.ts +50 -5
  142. package/src/templates/scaffold-guards.ts +54 -11
  143. package/src/templates/scaffold-http.ts +9 -7
  144. package/src/templates/scaffold-repo.ts +52 -2
  145. package/src/templates/scaffold-roles.ts +56 -9
  146. package/src/templates/scaffold-shell.ts +36 -70
  147. package/src/templates/scaffold-site.ts +45 -19
  148. package/src/templates/scaffold-ui-package.ts +18 -0
  149. package/src/templates/slice-foundation.ts +46 -8
  150. package/src/test-counts.ts +18 -2
  151. package/src/transport-calls.ts +105 -0
  152. package/src/unscanned-admin.ts +54 -0
  153. package/src/verify-checks.ts +26 -7
  154. package/src/verify-coverage-run.ts +190 -0
  155. package/src/verify-deadline.ts +256 -0
  156. package/src/verify-errors.ts +15 -6
  157. package/src/verify-floor.ts +36 -9
  158. package/src/verify-merge.ts +117 -24
  159. package/src/verify-progress.ts +25 -0
  160. package/src/verify-role-load-probe.ts +54 -0
  161. package/src/verify-role-load.ts +148 -0
  162. package/src/verify-run.ts +93 -35
  163. package/src/verify-shard.ts +20 -9
  164. package/src/verify-step.ts +42 -1
  165. package/src/verify-tests.ts +78 -6
  166. package/src/workspace-checks.ts +50 -8
  167. package/src/workspace-dep-edit.ts +127 -0
  168. package/src/cmd-dev-fixture.ts +0 -204
  169. package/src/dev-live-fixture.ts +0 -122
  170. package/src/policy-fixture.ts +0 -78
  171. package/src/realtime-browser-probe-fixture.ts +0 -9
  172. package/src/role-start-fixture.ts +0 -78
  173. package/src/scaffold-fixture.ts +0 -160
  174. package/src/templates/island-fixture.ts +0 -76
@@ -0,0 +1,141 @@
1
+ // Mounting the app's generated admin. `defineAdmin()` declares it; this file is the whole of what
2
+ // serves it — one catch-all under each admin's base path, in `x dev` and in the container alike,
3
+ // through the same document pipeline every page uses. The CLI contributes the HTTP crossing and
4
+ // nothing else: which screen a URL names, who may see it and what it renders are the admin's.
5
+
6
+ import type { AdminApp, AdminRouteConfig } from '@ultimat3/admin';
7
+ import type { Route, RouteMeta } from '@ultimat3/http';
8
+ import { asCtx, html, redirect, routeNotFound } from '@ultimat3/http';
9
+ import type { RouteEntry } from '@ultimat3/render';
10
+ import { compilePattern } from '@ultimat3/render';
11
+ import { renderSsr } from '@ultimat3/render/server';
12
+ import type { DocumentOptions } from './document-options';
13
+ import { routeDocument } from './runtime-render';
14
+
15
+ /**
16
+ * `@ultimat3/admin`'s `ADMIN_MOUNTS`, read off the GLOBAL symbol registry rather than imported.
17
+ * Importing that package loads every screen and registers its stylesheets, so an app that declared
18
+ * no admin would carry one into its documents (axiom 6); asking the registry costs nothing. An app
19
+ * that DID declare one has already loaded the package, which is when `screens()` below imports it.
20
+ */
21
+ const ADMIN_MOUNTS: symbol = Symbol.for('ultimate.admin.mounts');
22
+
23
+ /** The file a mounted screen is attributed to — a package, because no app file declares it. */
24
+ export const ADMIN_MOUNT_FILE = '@ultimat3/admin';
25
+
26
+ const declared = (): ReadonlyMap<string, AdminApp> => {
27
+ const held = (globalThis as { [key: symbol]: unknown })[ADMIN_MOUNTS];
28
+ return held instanceof Map ? (held as ReadonlyMap<string, AdminApp>) : new Map();
29
+ };
30
+
31
+ export interface AdminMountOptions extends Pick<DocumentOptions, 'themeHead' | 'origin'> {
32
+ readonly buildId: string;
33
+ }
34
+
35
+ /**
36
+ * The route table's own entry shape, for a screen nobody registered there: the document builder
37
+ * reads a `RouteEntry`, and an admin screen is a route — `ssr`, gated, `hydrate: 'never'` — whose
38
+ * component is the body the screen just answered with.
39
+ */
40
+ const entryFor = (route: AdminRouteConfig, body: unknown): RouteEntry => ({
41
+ file: ADMIN_MOUNT_FILE,
42
+ path: route.path,
43
+ // An admin screen is behind sign-in: it carries the `app/` surface's stylesheet, never `site/`'s.
44
+ surface: 'app',
45
+ config: route.config,
46
+ suspenseBoundaries: 0,
47
+ islands: [],
48
+ pattern: compilePattern(route.path),
49
+ component: () => body,
50
+ });
51
+
52
+ /**
53
+ * `auth: 'required'` is the pipeline's half — nobody anonymous reaches a screen, and a sign-in
54
+ * redirect is the app's own. `enforcedBy: 'handler'` is the other half: the screen holds the row a
55
+ * row-level rule reads, so deciding in the `authz` stage too would be a second authz system that
56
+ * knows strictly less and answers first.
57
+ */
58
+ const metaFor = (base: string): RouteMeta => ({
59
+ name: `admin:${base}`,
60
+ auth: 'required',
61
+ policy: 'admin:read',
62
+ enforcedBy: 'handler',
63
+ render: 'ssr',
64
+ tags: ['admin'],
65
+ });
66
+
67
+ async function answer(
68
+ base: string,
69
+ method: 'GET' | 'POST',
70
+ options: AdminMountOptions,
71
+ request: Parameters<Route['handler']>[0],
72
+ context: Parameters<Route['handler']>[1],
73
+ ): Promise<Response> {
74
+ // Read per request, never captured: `x dev` re-evaluates the module that declares the admin
75
+ // when it changes, and the admin a request meets is the one the author just saved.
76
+ const app = declared().get(base);
77
+ // Dynamic, and only here: reached only when an admin is mounted, by which point the app has
78
+ // already loaded the package — and after `loadApp` installed the `.tsx` loader its screens need.
79
+ const { adminRouteMatch } = await import('@ultimat3/admin');
80
+ const matched = app === undefined ? null : adminRouteMatch(app, request.url.pathname);
81
+ if (app === undefined || matched === null) throw routeNotFound(method, request.url.pathname);
82
+
83
+ const form = method === 'POST' ? await request.bodyRaw() : null;
84
+ const response = await matched.route.respond({
85
+ ctx: await app.requestCtx(request.raw),
86
+ params: matched.params,
87
+ url: request.url.href,
88
+ method,
89
+ // A POST with no body is a form with no fields — a button-only form posts exactly that.
90
+ form: method === 'POST' ? (isRecord(form) ? form : {}) : null,
91
+ });
92
+ if (response.kind === 'redirect') {
93
+ // 303, always: the browser must GET the page it lands on, not re-POST the form at it.
94
+ const moved = redirect(response.location, 303);
95
+ moved.headers.set('cache-control', 'private, no-store');
96
+ return moved;
97
+ }
98
+
99
+ const entry = entryFor(matched.route, response.body);
100
+ const data = { url: request.url.href, params: matched.params };
101
+ const result = await renderSsr(
102
+ { entry, params: matched.params, url: request.url, ctx: asCtx(context) },
103
+ () =>
104
+ routeDocument(entry, data, {
105
+ ...(options.themeHead === undefined ? {} : { themeHead: options.themeHead }),
106
+ ...(options.origin === undefined ? {} : { origin: options.origin }),
107
+ }),
108
+ { buildId: options.buildId, status: response.status },
109
+ );
110
+ // `renderSsr` answers the string the render function returned; a stream is `stream`'s shape.
111
+ const document = typeof result.body === 'string' ? result.body : '';
112
+ return html(document, { status: result.status, headers: result.headers });
113
+ }
114
+
115
+ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
116
+ typeof value === 'object' && value !== null && !Array.isArray(value);
117
+
118
+ const METHODS: readonly ('GET' | 'POST')[] = ['GET', 'POST'];
119
+
120
+ /**
121
+ * Two paths per admin and two methods on each: the base path itself (the dashboard) and one
122
+ * catch-all under it. The admin's own table is matched INSIDE the handler, against the table as
123
+ * it is now — so a page added in a save is served without this list being rebuilt, and a URL the
124
+ * admin does not declare is the framework's ordinary 404.
125
+ *
126
+ * Empty for an app that declared no admin: nothing is mounted, and nothing is imported.
127
+ */
128
+ export function adminMountRoutes(options: AdminMountOptions): readonly Route[] {
129
+ return [...declared().keys()].flatMap((base) =>
130
+ [base, `${base}/*rest`].flatMap((path) =>
131
+ METHODS.map(
132
+ (method): Route => ({
133
+ method,
134
+ path,
135
+ meta: metaFor(base),
136
+ handler: (request, context) => answer(base, method, options, request, context),
137
+ }),
138
+ ),
139
+ ),
140
+ );
141
+ }
@@ -1,8 +1,8 @@
1
- // The two seams `@ultimat3/http` leaves open, bound to the packages that own them. `authorize`
1
+ // The seams `@ultimat3/http` leaves open, bound to the packages that own them. `authorize`
2
2
  // decides for pages only, from the SAME `Policy` object every other surface evaluates — the route
3
3
  // table's declared permission — so a denial in `x dev` is the one production produces.
4
4
 
5
- import { actorOf } from '@ultimat3/action';
5
+ import { actorOf, explainActionPathMiss } from '@ultimat3/action';
6
6
  import type { AuthzDecision, ServerHooks } from '@ultimat3/http';
7
7
  import { asCtx, configuredAuthenticator } from '@ultimat3/http';
8
8
  import type { KnownPermission, Policy } from '@ultimat3/policy';
@@ -57,6 +57,9 @@ export function devHooks(options: DevHookOptions = {}): ServerHooks {
57
57
  ...(authenticate === undefined ? {} : { authenticate }),
58
58
  ...(devNotices === undefined ? {} : { devNotices }),
59
59
  ...(errorPage === undefined ? {} : { errorPage }),
60
+ // Every web role, `x dev` and the container alike: a caller that derived an action's URL under
61
+ // the wrong `pathStyle` is told which one this app serves, not that no route exists.
62
+ explainMiss: explainActionPathMiss,
60
63
  authorize: (route, _request, ctx): AuthzDecision => {
61
64
  // An action route never arrives here: it carries `enforcedBy: 'handler'`, so the pipeline
62
65
  // never asks. `invoke` is its one evaluation, and the only one holding the row a row-level
@@ -33,6 +33,7 @@ import {
33
33
  setJobsFacade,
34
34
  } from '@ultimat3/jobs';
35
35
  import { applyFrameworkSchema } from './framework-schema';
36
+ import { appExtensions } from './migration-extensions';
36
37
  import type { DevServices } from './runtime-bindings';
37
38
  import type { RuntimeOverrides } from './runtime-overrides';
38
39
  import { attachReplica, type ReplicaEnv, replicaUrlFor } from './runtime-replica';
@@ -93,7 +94,13 @@ export function startDb(services: DevServices, env: ReplicaEnv): StartedDb {
93
94
  binding.mode === 'embedded'
94
95
  ? // `pgliteDataDir` is `@ultimat3/db`'s own reader of the `pglite://` form; a second parser
95
96
  // here is a second thing to keep right when the form changes.
96
- createPgliteClient({ dataDir: pgliteDataDir(binding.url) })
97
+ // Linked at boot, from the app's own migrations: PGlite cannot `create extension` one it
98
+ // was not handed, and the schema dump's scratch replay already asks this same reader — an
99
+ // app that replayed in the gate and failed in `x dev` was the two disagreeing.
100
+ createPgliteClient({
101
+ dataDir: pgliteDataDir(binding.url),
102
+ extensions: () => appExtensions(services.root),
103
+ })
97
104
  : createPostgresClient({ url: binding.url });
98
105
  const attached = attachReplica(client, replicaUrlFor(binding, env));
99
106
  setDbClient(attached.client);
@@ -16,6 +16,7 @@
16
16
 
17
17
  // why: Bun ships no path API; an island's file is its route file's directory joined to its `src`.
18
18
  import { posix } from 'node:path';
19
+ import { actionPathStyle } from '@ultimat3/action';
19
20
  import { clientScopeOf } from '@ultimat3/auth';
20
21
  import type { Ctx } from '@ultimat3/core';
21
22
  import { CLIENT_SCOPE_HEADER } from '@ultimat3/core';
@@ -27,15 +28,10 @@ import type {
27
28
  } from '@ultimat3/http';
28
29
  import { asCtx, html, NO_STORE, redirect, stream, takeRedirect } from '@ultimat3/http';
29
30
  import { currentLocale, localeConfig } from '@ultimat3/i18n';
30
- import type {
31
- ClientSyncHead,
32
- IslandCollector,
33
- RenderResult,
34
- RouteData,
35
- RouteEntry,
36
- } from '@ultimat3/render';
31
+ import type { IslandCollector, RenderResult, RouteData, RouteEntry } from '@ultimat3/render';
37
32
  import {
38
33
  clientBootTags,
34
+ clientPathStyleTags,
39
35
  clientPersistTags,
40
36
  clientScopeTag,
41
37
  clientSyncTags,
@@ -65,6 +61,7 @@ import {
65
61
  staticHeaders,
66
62
  streamResult,
67
63
  } from '@ultimat3/render/server';
64
+ import type { DocumentOptions } from './document-options';
68
65
  import { realtimeIslandFiles } from './island-realtime';
69
66
  import {
70
67
  type NavigationDocumentHead,
@@ -72,64 +69,21 @@ import {
72
69
  navigationTagsOf,
73
70
  principalRelocation,
74
71
  } from './page-navigation';
72
+ import type { StaticResult } from './static-document';
73
+ import { createStaticMemo, staticMemoKey, staticResponse } from './static-document';
75
74
  import { styleBundle } from './style-bundle';
76
75
 
77
- /**
78
- * Specifier → built chunk URL, bound to the route file the specifier is written relative to.
79
- * Supplied by whoever built the islands (`x dev`, the container, the static build); absent means
80
- * no island was built, and a page that renders one then fails by name rather than emitting a
81
- * `data-x-entry` nothing can import.
82
- */
83
- export type IslandResolver = (routeFile: string) => (src: string) => string;
84
-
85
- export interface DocumentOptions {
86
- readonly resolveIsland?: IslandResolver;
87
- /**
88
- * `<link rel="manifest">`, both `theme-color` metas and the apple-touch links — `PwaArtifacts.head`
89
- * from `pwa-artifacts.ts`, or absent when the app is not installable.
90
- *
91
- * A document-level string rather than something a route's `meta()` returns: it is the same three
92
- * elements on every page of the app, an installable app is one whose EVERY page carries them
93
- * (a browser offers the install on whichever page the visitor landed on), and `headFromMeta`
94
- * projects per-route SEO. Passed through `DocumentOptions` for `resolveIsland`'s reason — the
95
- * boot knows it, the renderer cannot ask.
96
- */
97
- readonly pwaHead?: string | ((locale: string) => string);
98
- /**
99
- * The no-flash theme `<script>` from `theme-boot.ts`, or absent for a caller that renders no
100
- * documents a browser paints. Document-level for `pwaHead`'s reason — the same tag on every page,
101
- * decided by `app.config.ts`, which the boot read and the renderer cannot.
102
- */
103
- readonly themeHead?: string;
104
- /**
105
- * The page's sync target — `pageSync(…).head` — rendered as render's `clientSyncTags` on every
106
- * document this process serves. Principal-free, so a shareable document carries it too; absent
107
- * for a caller that serves no socket at all (the static export).
108
- */
109
- readonly sync?: ClientSyncHead;
110
- /**
111
- * The record types the app persists (`entity(…, { persist: true })`), read per render. Rendered
112
- * as `ultimate-persist` beside the scope tag only — persistence is per principal, so a document
113
- * with no scope carries none.
114
- */
115
- readonly persisted?: () => readonly string[];
116
- /**
117
- * The public origin canonical, `og:url` and hreflang are absolute against — `publicOrigin()`
118
- * from `site-config.ts`. Absent, the request's own origin: a relative canonical is one a crawler
119
- * resolves against whatever host it happened to fetch from, a CDN's or a preview's.
120
- */
121
- readonly origin?: string;
122
- /**
123
- * The client router — `pageNavigation(…).head`. A document of a surface listed in
124
- * `navigation.client` names it; every other document carries none of it.
125
- */
126
- readonly navigation?: NavigationDocumentHead;
127
- }
76
+ export type { DocumentOptions, IslandResolver } from './document-options';
128
77
 
129
78
  export interface DevRenderOptions extends DocumentOptions {
130
79
  readonly buildId: string;
131
80
  /** Injected so a test can drive the ISR store without a timer. */
132
81
  readonly isr?: IsrController;
82
+ /**
83
+ * Keep each `static` document after its first render (`static-document.ts`). The container sets
84
+ * it; `x dev` never does — a save changes the page, its stylesheet's URL and its islands'.
85
+ */
86
+ readonly memoStatic?: boolean;
133
87
  }
134
88
 
135
89
  /** What a route's `meta(data)` is given. `url` is a string because that is what `ld.*` embeds. */
@@ -162,6 +116,7 @@ const headFor = async (
162
116
  ): Promise<string> => {
163
117
  const meta = metaContextFor(ctx, data);
164
118
  const url = new URL(ctx.url);
119
+ const router = navigationTagsOf(options.navigation, entry);
165
120
  return (
166
121
  renderHead(
167
122
  headFromMeta(
@@ -177,7 +132,11 @@ const headFor = async (
177
132
  }),
178
133
  [
179
134
  ...(options.sync === undefined ? [] : clientSyncTags(options.sync)),
180
- ...navigationTagsOf(options.navigation, entry),
135
+ // Every document, shareable ones included: the style is the app's, not a visitor's, and
136
+ // it is the only way an island's `rpc()` learns it. Read per render — `defineApi` may run
137
+ // after these routes are built. `'resource'`, the default, is no tag.
138
+ ...clientPathStyleTags(actionPathStyle()),
139
+ ...router,
181
140
  // The page boot rides the scope tag: its whole job — restoring a principal's persisted
182
141
  // records and replaying its queued writes — is per principal, and a shareable document
183
142
  // (no scope tag) has neither. Cheaper than walking the page's islands, and exact.
@@ -188,6 +147,8 @@ const headFor = async (
188
147
  ),
189
148
  ) +
190
149
  (options.themeHead ?? '') +
150
+ // One mechanism per document: the router prefetches for itself, the browser for the rest.
151
+ (router.length === 0 ? (options.speculationHead ?? '') : '') +
191
152
  // Per locale: each links its own locale's manifest (`PwaArtifacts.headFor`).
192
153
  (typeof options.pwaHead === 'function' ? options.pwaHead(meta.locale) : (options.pwaHead ?? ''))
193
154
  );
@@ -469,6 +430,7 @@ const metaOf = (entry: RouteEntry, head?: NavigationDocumentHead): HttpRouteMeta
469
430
  */
470
431
  export function appRoutes(options: DevRenderOptions): readonly Route[] {
471
432
  const isr = options.isr ?? createIsrController({ buildId: options.buildId });
433
+ const memo = createStaticMemo();
472
434
  return routeEntries().map((registered) => ({
473
435
  method: 'GET' as const,
474
436
  path: registered.path,
@@ -481,6 +443,13 @@ export function appRoutes(options: DevRenderOptions): readonly Route[] {
481
443
  // A soft visit onto another principal's document is a real load — answered before `load`.
482
444
  const moved = principalRelocation(entry, request, asCtx(ctx), options.buildId);
483
445
  if (moved !== undefined) return moved;
446
+ // A static page already rendered by this process is answered without `load` or a render.
447
+ const key =
448
+ options.memoStatic === true
449
+ ? staticMemoKey(entry, request.url, asCtx(ctx).locale)
450
+ : undefined;
451
+ const kept = key === undefined ? undefined : memo.get(key);
452
+ if (kept !== undefined) return staticResponse(request, kept);
484
453
  const data: DevRouteData = { url: request.url.href, params: ctx.params };
485
454
  // ONCE per request, before the mode is chosen: every branch of `resultFor` reads this same
486
455
  // object, so a route's `load` runs exactly once however its mode splits head from body.
@@ -490,7 +459,14 @@ export function appRoutes(options: DevRenderOptions): readonly Route[] {
490
459
  // 3xx because a rendered document has no `Location`; this is the path that has one.
491
460
  const to = takeRedirect(ctx);
492
461
  if (to !== undefined) return loadRedirect(entry, to);
493
- return responseOf(await resultFor(entry, data, loaded, options, isr, asCtx(ctx)));
462
+ const result = await resultFor(entry, data, loaded, options, isr, asCtx(ctx));
463
+ if (entry.config.render !== 'static' || typeof result.body !== 'string') {
464
+ return responseOf(result);
465
+ }
466
+ const document: StaticResult = { ...result, body: result.body };
467
+ if (key !== undefined) memo.set(key, document);
468
+ // The ETag was only ever a header: a matching `If-None-Match` is a 304, not the page again.
469
+ return staticResponse(request, document);
494
470
  },
495
471
  }));
496
472
  }
@@ -182,7 +182,8 @@ export function startStorage(services: DevServices, env: Env, override?: Storage
182
182
  bucket,
183
183
  endpoint: binding.url,
184
184
  ...(env['S3_REGION'] === undefined ? {} : { region: env['S3_REGION'] }),
185
- // MinIO needs path style; R2 and AWS do not. Declared, never sniffed from the endpoint.
185
+ // A gateway addressed by host and port needs path style; R2 and AWS do not. Declared,
186
+ // never sniffed from the endpoint.
186
187
  forcePathStyle: env['S3_FORCE_PATH_STYLE'] === '1',
187
188
  }),
188
189
  },
@@ -151,6 +151,7 @@ const overlay = (root: string, app: string): string =>
151
151
  // name, and `cli/serve` as `packages/cli/serve`. Longest prefix wins, so these are
152
152
  // consulted first. `@ultimat3/cli/serve` is what `x new`'s `apps/web/server.ts` imports.
153
153
  '@ultimat3/ui/icons/*': [`${root}/packages/ui/src/icons/glyphs/*`],
154
+ '@ultimat3/query/client': [`${root}/packages/query/src/client`],
154
155
  '@ultimat3/render/server': [`${root}/packages/render/src/server`],
155
156
  '@ultimat3/cli/serve': [`${root}/packages/cli/src/serve-entry`],
156
157
  '@ultimat3/testing/test-types': [`${root}/packages/testing/src/test-types`],
@@ -0,0 +1,64 @@
1
+ // The `drift` step's fourth rail: is the committed schema dump what the migrations produce, and
2
+ // does a database loaded from it come back the same? The other three rails read files and open
3
+ // nothing; this one replays the migrations on a scratch embedded database, because "what the
4
+ // migrations produce" is a fact only a database can state.
5
+
6
+ // why: Bun has no directory probe — `Bun.file().exists()` answers about a file.
7
+ import { existsSync } from 'node:fs';
8
+ // why: Bun ships no path joiner.
9
+ import { join } from 'node:path';
10
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
11
+ import type { Migration } from '@ultimat3/db';
12
+ import type { SchemaDumpDifference } from '@ultimat3/db/schema-dump';
13
+ import { compareSchemaDump, schemaDumpDrift } from '@ultimat3/db/schema-dump';
14
+ import {
15
+ hasSchemaDump,
16
+ type ReplayedSchema,
17
+ readSchemaDump,
18
+ replayFor,
19
+ SCHEMA_DUMP_DIR,
20
+ } from './db-schema-dump';
21
+ import { dumpNeverGenerated, replayFailure } from './db-schema-refresh';
22
+ import { DB_PACKAGE } from './drift';
23
+ import { readMigrations } from './migrations';
24
+ import type { Finding } from './output';
25
+ import { findingFrom } from './output';
26
+
27
+ /** Injected so this module's rules are testable without booting a database per case. */
28
+ export type SchemaReplay = (migrations: readonly Migration[]) => Promise<ReplayedSchema>;
29
+
30
+ const findingFor = (difference: SchemaDumpDifference): Finding => ({
31
+ ...findingFrom(schemaDumpDrift(difference)),
32
+ docs: ERROR_DOCS_URL,
33
+ at: `${SCHEMA_DUMP_DIR}/${difference.path}`,
34
+ });
35
+
36
+ /**
37
+ * Empty result = the dump is the migrations' own.
38
+ *
39
+ * An app with no migration and no dump owes none — zero recorded against zero dumped is agreement,
40
+ * the rule `checkSourceDrift` states for entities, and what keeps `x new --no-example` green. From
41
+ * the first migration on the dump is owed: a missing directory is one finding naming the command
42
+ * that writes it, reported without booting a database.
43
+ *
44
+ * Both halves are reported together. A stale file and a dump that does not round-trip are two
45
+ * repairs, and hiding the second behind the first hands out `x db gen` for a dump regenerating
46
+ * cannot fix.
47
+ */
48
+ export async function checkSchemaDump(
49
+ root: string,
50
+ env: Readonly<Record<string, string | undefined>> = Bun.env,
51
+ replay: SchemaReplay = replayFor(root, env, true),
52
+ ): Promise<readonly Finding[]> {
53
+ if (!existsSync(join(root, DB_PACKAGE))) return [];
54
+ const migrations = await readMigrations(root);
55
+ if (!hasSchemaDump(root)) return migrations.length === 0 ? [] : [dumpNeverGenerated()];
56
+ const committed = await readSchemaDump(root);
57
+ let replayed: ReplayedSchema;
58
+ try {
59
+ replayed = await replay(migrations);
60
+ } catch (error) {
61
+ return [replayFailure(error)];
62
+ }
63
+ return [...compareSchemaDump(committed, replayed.files), ...replayed.reload].map(findingFor);
64
+ }