@ultimat3/cli 20.2.1 → 22.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 (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
@@ -3,6 +3,13 @@
3
3
  // byte-deterministic), plus the resolver that turns a page's `src` specifier into the URL its
4
4
  // `data-x-entry` carries. One entry point per island is axiom 6 made mechanical — the page's graph
5
5
  // never reaches an island, so a `site/` document stays at 0kb whatever the island imports.
6
+ //
7
+ // No page-client bootstrap is prepended (plan 101, decided 2026-09-22): the handle is one
8
+ // `globalThis` object created lazily by the first transport call or realtime hook. Measured before
9
+ // that decision on `examples/dummy`, a core-importing wrapper cost contact-sales 875 → 8,827 B.
10
+ // What IS prepended is the realtime install, and only where the island's graph reaches
11
+ // `@ultimat3/realtime` (`island-realtime.ts`). Measured on a fixture, As of 2026-09-22: a Solid
12
+ // island reading `useConnection` 64,964 → 65,067 B (+103 B); an island not reaching realtime +0.
6
13
 
7
14
  // Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
8
15
  // POSIX by construction), `join`/`basename` the filesystem side.
@@ -11,6 +18,7 @@ import { frameworkVersion, renderThrowable } from '@ultimat3/core';
11
18
  import { ISLAND_EXTENSION, IslandInvalidError, islandModuleId } from '@ultimat3/render';
12
19
  import { contentHash } from '@ultimat3/render/server';
13
20
  import { IslandBuildFailedError } from './errors';
21
+ import { islandRealtimePlugin, REALTIME_ISLAND_ENTRY, reachesRealtime } from './island-realtime';
14
22
  import { solidDedupePlugin } from './island-solid-dedupe';
15
23
  import { islandStylesPlugin } from './island-styles';
16
24
  import { hasPathSegment } from './path-segments';
@@ -75,10 +83,18 @@ export async function discoverIslands(root: string): Promise<readonly string[]>
75
83
  async function buildOne(root: string, file: string): Promise<IslandChunk> {
76
84
  // `Bun.build` REJECTS on a failed bundle, it does not answer `success: false` — so the catch is
77
85
  // the real path here and the `success` test below is the belt for a future default.
86
+ // Only an island whose own graph reaches `@ultimat3/realtime` is wrapped (`island-realtime.ts`);
87
+ // every other one is built from its own file, byte for byte what it was.
88
+ // Inside the refusal too: the realtime probe PARSES the island's graph, and a file that will not
89
+ // parse rejected with a raw `AggregateError: Failed to scan imports` — out of the web boot, with
90
+ // no code, no file and no fix — before `Bun.build` ever ran to raise the one below.
91
+ const realtime = await reachesRealtime(root, file).catch((error: unknown) => {
92
+ throw new IslandBuildFailedError({ file, logs: describeBuildError(error) });
93
+ });
78
94
  let built: Awaited<ReturnType<typeof Bun.build>>;
79
95
  try {
80
96
  built = await Bun.build({
81
- entrypoints: [join(root, file)],
97
+ entrypoints: [realtime ? REALTIME_ISLAND_ENTRY : join(root, file)],
82
98
  target: 'browser',
83
99
  format: 'esm',
84
100
  splitting: false,
@@ -96,7 +112,12 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
96
112
  // The dedupe goes FIRST: it answers `solid-js` specifiers before either plugin loads a file,
97
113
  // so the `solid-js/web` helpers the JSX transform writes into a symlinked package resolve
98
114
  // to the app's one copy. See `island-solid-dedupe.ts` for the measurement.
99
- plugins: [solidDedupePlugin(root), solidJsxPlugin, islandStylesPlugin],
115
+ plugins: [
116
+ ...(realtime ? [islandRealtimePlugin(root, file)] : []),
117
+ solidDedupePlugin(root),
118
+ solidJsxPlugin,
119
+ islandStylesPlugin,
120
+ ],
100
121
  // The third one, and it is a `define` rather than the plugin this used to be: Bun selects
101
122
  // the `development`/`production` export condition from the BUILD PROCESS's own `NODE_ENV`,
102
123
  // and a defined `process.env.NODE_ENV` overrides it. Measured on 1.4.0, `solid-js` plus
@@ -138,9 +159,8 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
138
159
  // The FIRST bytes this process emitted for these inputs, so a URL served `immutable` answers
139
160
  // one byte string for as long as the process lives. Without it `x dev` re-mints the chunk on
140
161
  // every watcher tick and a browser holding the previous one under `max-age=31536000` has two
141
- // different files at one address.
142
- code: stableCode(file, hash, code),
143
- bytes: new TextEncoder().encode(code).byteLength,
162
+ // different files at one address. `bytes` is measured on THAT code, never on this build's.
163
+ ...stableChunk(file, hash, code),
144
164
  };
145
165
  }
146
166
 
@@ -153,7 +173,7 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
153
173
  */
154
174
  const DEBUG_ID_COMMENT = '\n//# debugId=';
155
175
 
156
- function stripDebugId(code: string): string {
176
+ export function stripDebugId(code: string): string {
157
177
  const at = code.lastIndexOf(DEBUG_ID_COMMENT);
158
178
  return at === -1 ? code : code.slice(0, at);
159
179
  }
@@ -185,7 +205,7 @@ function stripDebugId(code: string): string {
185
205
  * measured at 193,590 bytes against 131,649, +47% raw and +20% gzipped, on every island of every
186
206
  * app. Delete this the day `Bun.build` is deterministic.
187
207
  */
188
- function graphHash(file: string, map: string): string {
208
+ export function graphHash(file: string, map: string): string {
189
209
  const parsed: unknown = JSON.parse(map);
190
210
  const contents = sourcesContentOf(parsed);
191
211
  if (contents === undefined) {
@@ -236,11 +256,18 @@ export function clearIslandChunkCache(): void {
236
256
  * value called `hash` is a digest an attacker may be probing. This one is a build input's
237
257
  * identity — the same reason `pr-threads.ts` calls a review state `wanted`.
238
258
  */
239
- function stableCode(file: string, graph: string, code: string): string {
259
+ export function stableChunk(
260
+ file: string,
261
+ graph: string,
262
+ code: string,
263
+ ): { readonly code: string; readonly bytes: number } {
240
264
  const hit = emitted.get(file);
241
- if (hit !== undefined && hit.graph === graph) return hit.code;
242
- emitted.set(file, { graph, code });
243
- return code;
265
+ const served = hit !== undefined && hit.graph === graph ? hit.code : code;
266
+ if (served === code) emitted.set(file, { graph, code });
267
+ // Measured on the code that is SERVED. It was measured on this build's output, which under a
268
+ // minifier that renames differently between builds is a second size for one URL in one process
269
+ // — and a budget weighed on bytes no browser receives.
270
+ return { code: served, bytes: new TextEncoder().encode(served).byteLength };
244
271
  }
245
272
 
246
273
  /**
@@ -5,10 +5,10 @@
5
5
  // why: no Bun native joins a path; `Bun.write` takes one already joined.
6
6
  import { join } from 'node:path';
7
7
  import { finiteCount } from '@ultimat3/core';
8
- import type { CaptureClip, ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
9
- import { systemScrapeClock } from '@ultimat3/scraping';
10
8
  import type { IslandShotTarget, IslandViewport } from '@ultimat3/testing';
11
9
  import { islandStatesFile } from '@ultimat3/testing';
10
+ import type { CaptureClip, ShotDriver, ShotSession } from './browser-launcher-port';
11
+ import { systemShotClock } from './cdp-shot-clock';
12
12
  import { ISLAND_HARNESS_PATH } from './island-harness';
13
13
  import { readinessProbe } from './island-harness-script';
14
14
  import { IslandRequestUnstubbedError, IslandUnphotographableError } from './island-shot-errors';
@@ -43,7 +43,7 @@ export const ISLAND_CROP_MARGIN_PX = 8;
43
43
  * takes the viewport as a LAUNCH option (`LocalBrowserOptions.options`) and a state declares its
44
44
  * own — so "photograph this state at 480x320" is a different browser, not a different call.
45
45
  */
46
- export type IslandBrowser = (viewport: IslandViewport) => Promise<ScrapeDriver>;
46
+ export type IslandBrowser = (viewport: IslandViewport) => Promise<ShotDriver>;
47
47
 
48
48
  interface Refusal {
49
49
  readonly reason: string;
@@ -181,13 +181,13 @@ export async function captureIslandState(
181
181
  floor: number,
182
182
  ): Promise<IslandStateShot> {
183
183
  const url = new URL(`${ISLAND_HARNESS_PATH}${target.query}`, server.url).toString();
184
- let session: ScrapeSession | undefined;
184
+ let session: ShotSession | undefined;
185
185
  try {
186
186
  const driver = await options.driver(target.viewport);
187
187
  session = await driver.open({
188
188
  name: 'x shot --island',
189
189
  rules: { allowHosts: allowHostsFrom(server.url, options.extraHosts) },
190
- clock: systemScrapeClock,
190
+ clock: systemShotClock,
191
191
  timeoutMs: options.timeoutMs,
192
192
  });
193
193
  const page = session.page;
@@ -237,7 +237,7 @@ export async function captureIslandState(
237
237
  // around, and which nothing passed until 2026-08-26 (issue #338).
238
238
  //
239
239
  // The clip ALONE. `fullPage: false` beside it is accepted — `assertCaptureFraming` refuses only
240
- // `=== true`, and `cdp-target.ts` sends `{ clip }` and nothing else either way — but it is a
240
+ // `=== true`, and `cdp-shot-page.ts` sends `{ clip }` and nothing else either way — but it is a
241
241
  // field that says nothing: the two are exclusive, and spelling out the default of the one you
242
242
  // did not ask for reads as a choice.
243
243
  // `??` screens null and undefined and NOTHING else, so `cropMarginPx: NaN` reached `clipFor`,
@@ -147,12 +147,16 @@ Intl.DateTimeFormat=ShotDTF;
147
147
 
148
148
  /**
149
149
  * Ready is quiet, not idle. Fonts first — a picture taken mid-swap photographs the fallback face —
150
- * then `QUIET_FRAMES` consecutive frames in which nothing started and nothing settled.
150
+ * then `QUIET_FRAMES` consecutive frames in which nothing started and nothing settled, counted only
151
+ * once the island has MOUNTED or FAILED (#474): an `idle` island mounts in idle time, after frames
152
+ * that were quiet only because nothing had started yet. A page with no island host is not waited on.
151
153
  */
152
154
  const readyScript = (): string => `
153
155
  var last=-1;var still=0;
156
+ function answered(){var h=document.querySelector("[data-x-island]");
157
+ return !h||h.hasAttribute("data-x-mounted")||h.hasAttribute("data-x-failed")}
154
158
  function tick(){var seen=W.activity;
155
- if(seen===last)still+=1;else{still=0;last=seen}
159
+ if(!answered()){still=0;last=seen}else if(seen===last)still+=1;else{still=0;last=seen}
156
160
  if(still>=${QUIET_FRAMES}){W.ready=true;return}
157
161
  requestAnimationFrame(tick)}
158
162
  (document.fonts?document.fonts.ready:Promise.resolve()).then(function(){requestAnimationFrame(tick)});
@@ -0,0 +1,98 @@
1
+ // Realtime installed for the author, on exactly the islands that use it: an island whose own import
2
+ // graph reaches `@ultimat3/realtime` is built from a virtual entry that first calls
3
+ // `installRealtime({ signal: createSignal })` with THIS bundle's solid-js, then re-exports the
4
+ // island whole. Every other island is built from its own file and pays nothing (plan 101 slice 14).
5
+
6
+ // why: Bun ships no path API; the entry is joined to the root and resolved from its directory.
7
+ import { dirname, join } from 'node:path';
8
+ import type { BunPlugin } from 'bun';
9
+ import { firstInGraph } from './live-routes';
10
+
11
+ /** What `Bun.build` is handed for a realtime island; resolved by `islandRealtimePlugin`. */
12
+ export const REALTIME_ISLAND_ENTRY = 'ultimate:island-entry';
13
+
14
+ const REALTIME = '@ultimat3/realtime';
15
+ const NAMESPACE = 'ultimate-island';
16
+ const INSTALL = 'ultimate:island-realtime';
17
+ const MODULE = 'ultimate:island-module';
18
+
19
+ /**
20
+ * Whether the island's own graph value-imports realtime. Relative specifiers only, which is the
21
+ * one blind spot: a PACKAGE importing realtime for the island is not seen, and a hook it calls
22
+ * then throws `X_REALTIME_UNINSTALLED` by name — loud, never silent.
23
+ */
24
+ export async function reachesRealtime(root: string, file: string): Promise<boolean> {
25
+ const found = await firstInGraph(root, file, (source, path) => {
26
+ // The transpiler, not a regex: it erases `import type` and reads a re-export as an import.
27
+ const scanned = new Bun.Transpiler({ loader: path.endsWith('x') ? 'tsx' : 'ts' }).scanImports(
28
+ source,
29
+ );
30
+ return scanned.some((entry) => entry.path === REALTIME) ? true : undefined;
31
+ });
32
+ // Recorded with the answer, so the document renderer can ask it of a page's islands without a
33
+ // second graph walk per request. Every build re-asks, so an edit that drops realtime drops it.
34
+ if (found === true) realtimeIslands.add(file);
35
+ else realtimeIslands.delete(file);
36
+ return found === true;
37
+ }
38
+
39
+ /** App-root-relative island files whose graph reaches realtime, as the last build answered. */
40
+ const realtimeIslands = new Set<string>();
41
+
42
+ /**
43
+ * The islands (app-root-relative POSIX paths) the last build found reaching realtime. A page needs
44
+ * realtime's page boot only if one of ITS islands does — `settings` paid 34.9 kB of boot script
45
+ * for an island that never touched a record.
46
+ */
47
+ export function realtimeIslandFiles(): ReadonlySet<string> {
48
+ return realtimeIslands;
49
+ }
50
+
51
+ /**
52
+ * `export *` and never a named list: the hydration runtime reads `mount` off the module, and the
53
+ * wrapper must not decide which of an island's names survive. The install is imported FIRST, so it
54
+ * has run before the island's module body — and every hook the island calls — does.
55
+ */
56
+ const ENTRY_SOURCE = `import '${INSTALL}';\nexport * from '${MODULE}';\n`;
57
+
58
+ /**
59
+ * The install names `@ultimat3/realtime` BARE, and `islandRealtimePlugin` resolves it from the
60
+ * ISLAND's own directory — the resolution the island itself gets — so the bundle holds one copy
61
+ * and the signal lands where the hooks read it. Never the resolved absolute path spliced in: this
62
+ * source is part of the chunk's `sourcesContent`, which is what `graphHash` names the URL from, so
63
+ * a path in it gave the same island a different URL in every checkout and every image build.
64
+ * `solid-js` goes through `island-solid-dedupe.ts` like every other import in the graph.
65
+ */
66
+ const INSTALL_SOURCE =
67
+ `import { installRealtime } from '${REALTIME}';\n` +
68
+ `import { createSignal } from 'solid-js';\n` +
69
+ `installRealtime({ signal: createSignal });\n`;
70
+
71
+ export function islandRealtimePlugin(root: string, file: string): BunPlugin {
72
+ const island = join(root, file);
73
+ return {
74
+ name: 'ultimate-island-realtime',
75
+ setup(build) {
76
+ build.onResolve({ filter: /^ultimate:island-entry$/ }, () => ({
77
+ path: 'entry',
78
+ namespace: NAMESPACE,
79
+ }));
80
+ build.onResolve({ filter: /^ultimate:island-realtime$/ }, () => ({
81
+ path: 'install',
82
+ namespace: NAMESPACE,
83
+ }));
84
+ build.onResolve({ filter: /^ultimate:island-module$/ }, () => ({ path: island }));
85
+ // Every bare `@ultimat3/realtime` in this build, the virtual install's included — a virtual
86
+ // module has no directory to resolve from. The island's directory for all of them, which is
87
+ // what the island itself would get, and one copy in the bundle is the requirement anyway:
88
+ // the signal the install sets is only read by hooks from the same module instance.
89
+ build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({
90
+ path: Bun.resolveSync(REALTIME, dirname(island)),
91
+ }));
92
+ build.onLoad({ filter: /.*/, namespace: NAMESPACE }, (args) => ({
93
+ contents: args.path === 'entry' ? ENTRY_SOURCE : INSTALL_SOURCE,
94
+ loader: 'js',
95
+ }));
96
+ },
97
+ };
98
+ }
@@ -8,6 +8,8 @@
8
8
  // and the index are written, and only then does the missing-picture gate turn the reasons into a
9
9
  // non-zero exit. One island that will not mount must not cost a reader the other nineteen.
10
10
 
11
+ // why: Bun has no file delete that tolerates an absent file.
12
+ import { rm } from 'node:fs/promises';
11
13
  // why: no Bun native joins a path; `Bun.write` and `Bun.file` both take one already joined.
12
14
  import { join } from 'node:path';
13
15
  import { finiteCount } from '@ultimat3/core';
@@ -95,6 +97,9 @@ export async function runIslandSweep(options: IslandSweepRun): Promise<IslandSwe
95
97
  // for each one it cannot, and the missing-shot gate below is what turns those reasons into
96
98
  // a non-zero exit.
97
99
  try {
100
+ // Deleted FIRST: the missing-shot gate reads the disk, and a picture an earlier run left
101
+ // there made a capture that failed today read as taken.
102
+ await rm(join(options.outDir, target.file), { force: true });
98
103
  shots.push(await captureIslandState(options, server, target, floor));
99
104
  } catch (error) {
100
105
  failures.push(error);
@@ -0,0 +1,131 @@
1
+ // The island chunks a container serves, built ONCE by `x build --target docker` into
2
+ // `.x/islands/` and read back at boot. Without it every role's boot ran `Bun.build` — Babel, the
3
+ // JSX transform, a minifier — for every island, in the image, on every start, and a chunk whose
4
+ // URL is a hash of its sources was rebuilt from sources that could not have changed.
5
+ //
6
+ // Load-and-VERIFY, never load-and-trust: a store written by another framework or Bun version, one
7
+ // that names a different set of islands than the app has, or a chunk whose bytes do not hash to
8
+ // what the index recorded is refused, and the boot builds instead — with the reason logged.
9
+
10
+ import { join } from 'node:path'; // why: Bun ships no path-join primitive.
11
+ import { frameworkVersion, logger } from '@ultimat3/core';
12
+ import { contentHash } from '@ultimat3/render/server';
13
+ import type { IslandBundle, IslandChunk } from './island-bundle';
14
+ import { buildIslands, discoverIslands, islandBundle } from './island-bundle';
15
+
16
+ /** App-root-relative: `COPY . .` carries it into the image with the rest of `.x/`. */
17
+ export const ISLAND_STORE_DIR = '.x/islands';
18
+ const INDEX = 'index.json';
19
+
20
+ interface StoredChunk {
21
+ readonly file: string;
22
+ readonly moduleId: string;
23
+ readonly url: string;
24
+ readonly identity: string;
25
+ }
26
+
27
+ interface StoreIndex {
28
+ readonly framework: string;
29
+ readonly bun: string;
30
+ readonly chunks: readonly StoredChunk[];
31
+ }
32
+
33
+ const chunkFile = (url: string): string => url.slice(url.lastIndexOf('/') + 1);
34
+
35
+ /** Writes every chunk and the index that verifies them. Answers the files written, app-relative. */
36
+ export async function writeIslandStore(
37
+ root: string,
38
+ bundle: IslandBundle,
39
+ ): Promise<readonly string[]> {
40
+ const dir = join(root, ISLAND_STORE_DIR);
41
+ const chunks: StoredChunk[] = [];
42
+ const written: string[] = [];
43
+ for (const chunk of bundle.chunks) {
44
+ await Bun.write(join(dir, chunkFile(chunk.url)), chunk.code);
45
+ written.push(`${ISLAND_STORE_DIR}/${chunkFile(chunk.url)}`);
46
+ chunks.push({
47
+ file: chunk.file,
48
+ moduleId: chunk.moduleId,
49
+ url: chunk.url,
50
+ identity: contentHash(chunk.code),
51
+ });
52
+ }
53
+ const index: StoreIndex = { framework: frameworkVersion(), bun: Bun.version, chunks };
54
+ await Bun.write(join(dir, INDEX), `${JSON.stringify(index, null, 2)}\n`);
55
+ return [...written, `${ISLAND_STORE_DIR}/${INDEX}`];
56
+ }
57
+
58
+ const isString = (value: unknown): value is string => typeof value === 'string';
59
+
60
+ /** The index, narrowed field by field — a file on disk is input, never a trusted shape. */
61
+ function parseIndex(value: unknown): StoreIndex | undefined {
62
+ if (typeof value !== 'object' || value === null) return undefined;
63
+ const record = value as Record<string, unknown>;
64
+ const chunks = record['chunks'];
65
+ if (!isString(record['framework']) || !isString(record['bun']) || !Array.isArray(chunks)) {
66
+ return undefined;
67
+ }
68
+ const parsed: StoredChunk[] = [];
69
+ for (const entry of chunks) {
70
+ if (typeof entry !== 'object' || entry === null) return undefined;
71
+ const chunk = entry as Record<string, unknown>;
72
+ const { file, moduleId, url, identity } = chunk;
73
+ if (!isString(file) || !isString(moduleId) || !isString(url) || !isString(identity))
74
+ return undefined;
75
+ parsed.push({ file, moduleId, url, identity });
76
+ }
77
+ return { framework: record['framework'], bun: record['bun'], chunks: parsed };
78
+ }
79
+
80
+ /** A verified store, or the one sentence saying why it cannot be served. */
81
+ export type StoreRead =
82
+ | { readonly bundle: IslandBundle; readonly stale?: undefined }
83
+ | { readonly bundle?: undefined; readonly stale: string };
84
+
85
+ export async function readIslandStore(root: string): Promise<StoreRead> {
86
+ const dir = join(root, ISLAND_STORE_DIR);
87
+ const file = Bun.file(join(dir, INDEX));
88
+ if (!(await file.exists())) return { stale: `no ${ISLAND_STORE_DIR}/${INDEX}` };
89
+ const index = parseIndex(await file.json().catch(() => undefined));
90
+ if (index === undefined) return { stale: `${ISLAND_STORE_DIR}/${INDEX} does not parse` };
91
+ if (index.framework !== frameworkVersion() || index.bun !== Bun.version) {
92
+ return {
93
+ stale: `built by framework ${index.framework} on Bun ${index.bun}, serving ${frameworkVersion()} on Bun ${Bun.version}`,
94
+ };
95
+ }
96
+ const stored = index.chunks.map((chunk) => chunk.file).sort();
97
+ const present = [...(await discoverIslands(root))];
98
+ if (stored.join('\n') !== present.join('\n')) {
99
+ return { stale: 'the stored islands are not the islands this app has' };
100
+ }
101
+ const chunks: IslandChunk[] = [];
102
+ for (const entry of index.chunks) {
103
+ const bytes = Bun.file(join(dir, chunkFile(entry.url)));
104
+ const code = (await bytes.exists()) ? await bytes.text() : undefined;
105
+ if (code === undefined || contentHash(code) !== entry.identity) {
106
+ return { stale: `${entry.url} is missing or does not match its recorded hash` };
107
+ }
108
+ chunks.push({
109
+ file: entry.file,
110
+ moduleId: entry.moduleId,
111
+ url: entry.url,
112
+ code,
113
+ bytes: new TextEncoder().encode(code).byteLength,
114
+ });
115
+ }
116
+ return { bundle: islandBundle(chunks) };
117
+ }
118
+
119
+ /**
120
+ * The container's islands: the verified store, or a build when there is none to trust. A build is
121
+ * correct and only slower, so a stale store is a warning with its reason, never a refusal.
122
+ */
123
+ export async function loadOrBuildIslands(root: string): Promise<IslandBundle> {
124
+ const read = await readIslandStore(root);
125
+ if (read.bundle !== undefined) return read.bundle;
126
+ logger.warn('islands built at boot', {
127
+ reason: read.stale,
128
+ fix: 'x build --target docker',
129
+ });
130
+ return buildIslands(root);
131
+ }
@@ -5,8 +5,8 @@
5
5
 
6
6
  import type { StandardSchemaV1 } from '@ultimat3/schema';
7
7
  import { t, validate } from '@ultimat3/schema';
8
- import type { ConsoleLine, PageError } from '@ultimat3/scraping';
9
8
  import type { IslandShotTarget } from '@ultimat3/testing';
9
+ import type { ConsoleLine, PageError } from './browser-launcher-port';
10
10
  import { msg } from './messages';
11
11
  import type { JsonValue } from './output';
12
12
 
@@ -57,7 +57,7 @@ export interface IslandReadiness {
57
57
  /**
58
58
  * `"WS <url>"` / `"SSE <url>"` for every socket a component constructed — recorded, never
59
59
  * gating. The harness's stand-in is inert (constructs, never opens, `close()` is a no-op), so
60
- * a component dialing `@ultimat3/realtime`'s `LiveClient.connect()` does not fail the state it
60
+ * a component whose page socket dials `/_x/sync` (`@ultimat3/realtime`) does not fail the state it
61
61
  * is mounted in; this is the fact a picture cannot carry about that.
62
62
  */
63
63
  readonly sockets: readonly string[];
@@ -0,0 +1,42 @@
1
+ // The `manifest` step's job half: a job or a task still carrying the positional name `job()` or
2
+ // `task()` minted (`anonymous-job-2`) was never handed to `defineApi`. That name is what the queue
3
+ // row, `x.manifest.json`, `x jobs show` and every dead-letter trace carry, and it moves whenever the
4
+ // import order does — so a retry scheduled under one deploy's name lands on another deploy's job.
5
+
6
+ import { registeredJobs, registeredTasks } from '@ultimat3/jobs';
7
+ import { loadApp } from './app-load';
8
+ import type { AppLoader } from './app-permissions';
9
+ import type { Finding } from './output';
10
+
11
+ /** What `job()` and `task()` name a declaration nobody named (`packages/jobs/src/job.ts`). */
12
+ const POSITIONAL = /^anonymous-(job|task)-\d+$/;
13
+
14
+ /** Every registered job and task still under its positional name. */
15
+ export function positionalNames(
16
+ names: readonly string[] = [...registeredJobs(), ...registeredTasks()].map((one) => one.name),
17
+ ): readonly string[] {
18
+ return names.filter((name) => POSITIONAL.test(name)).sort();
19
+ }
20
+
21
+ export function unregisteredJobFinding(name: string): Finding {
22
+ const kind = name.startsWith('anonymous-task-') ? 'tasks' : 'jobs';
23
+ return {
24
+ code: 'X_JOB_UNREGISTERED',
25
+ cause: `${name} is a ${kind === 'tasks' ? 'task' : 'job'} no defineApi() call names, so it carries the positional name ${kind === 'tasks' ? 'task()' : 'job()'} minted — on the queue row, in the manifest and in every dead-letter trace, and it moves when the import order does`,
26
+ fix: `add import * as <module> from its file and <module> to ${kind}: [...] in apps/web/api/index.ts, then x manifest`,
27
+ at: 'apps/web/api/index.ts',
28
+ };
29
+ }
30
+
31
+ /**
32
+ * The step's answer. Loads the app the way every other gate step does — the registries are only
33
+ * whole after the scan — and says nothing when the load failed: a short registry is the load's
34
+ * finding, reported by the step that loaded it.
35
+ */
36
+ export async function unregisteredJobFindings(
37
+ root: string,
38
+ load: AppLoader = loadApp,
39
+ ): Promise<readonly Finding[]> {
40
+ if ((await load(root)).findings.length > 0) return [];
41
+ return positionalNames().map(unregisteredJobFinding);
42
+ }
@@ -5,9 +5,9 @@
5
5
  import type { JobDriver } from '@ultimat3/jobs';
6
6
  import { jobDriver } from '@ultimat3/jobs';
7
7
  import type { CommandContext } from './command';
8
- import { startQueue } from './dev-queue';
9
- import { resolveServices } from './dev-services';
10
8
  import type { CommandResult } from './output';
9
+ import { resolveServices } from './runtime-bindings';
10
+ import { startQueue } from './runtime-queue';
11
11
 
12
12
  /**
13
13
  * `x jobs` needs the app's real driver. Reuse an already-running one first — inside `x dev` or