@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
@@ -4,7 +4,7 @@
4
4
  // can only be one of those per checkout.
5
5
 
6
6
  import type { Route, UltimateRequest } from '@ultimat3/http';
7
- import { html, json } from '@ultimat3/http';
7
+ import { html, jsonResponse } from '@ultimat3/http';
8
8
  import type { IslandStatesManifest } from '@ultimat3/testing';
9
9
  import {
10
10
  islandShotFile,
@@ -13,8 +13,10 @@ import {
13
13
  islandStatesNames,
14
14
  parseIslandAddress,
15
15
  } from '@ultimat3/testing';
16
+ import { hostRefusal } from './dev-dashboard-guard';
16
17
  import { harnessPage, ISLAND_HARNESS_PATH } from './island-harness';
17
18
  import type { IslandSource } from './island-routes';
19
+ import { quoteArg } from './shell-quote';
18
20
 
19
21
  /**
20
22
  * A getter, never the manifests: `x dev` rebuilds on the watcher tick, and a set captured when the
@@ -25,14 +27,20 @@ import type { IslandSource } from './island-routes';
25
27
  export type IslandStatesSource = () => Promise<readonly IslandStatesManifest[]>;
26
28
 
27
29
  const refused = (cause: string, fix: string): Response =>
28
- json(
30
+ jsonResponse(
29
31
  { ok: false, error: { code: 'X_SHOT_ISLAND_UNPHOTOGRAPHABLE', cause, fix } },
30
32
  { status: 404 },
31
33
  );
32
34
 
35
+ /** A value opening with `-` reads as a flag however it is quoted, so it becomes the placeholder. */
36
+ const unflagged = (value: string, placeholder: string): string =>
37
+ value.startsWith('-') ? placeholder : value;
38
+
33
39
  export interface HarnessRouteInput {
34
40
  readonly islands: IslandSource;
35
41
  readonly states: IslandStatesSource;
42
+ /** This dev server's URL, read per request — only what a refused Host's `fix:` line names. */
43
+ readonly devUrl: () => string;
36
44
  }
37
45
 
38
46
  /**
@@ -44,6 +52,10 @@ export interface HarnessRouteInput {
44
52
  * An island or a state this process does not know is the one case that IS refused, and it can only
45
53
  * mean the two processes disagree: `x shot` computed its picture list from the states files on disk
46
54
  * and this server was booted against an older set.
55
+ *
56
+ * Before any of that, the Host: this is a `/_x` route, so it answers a loopback Host only, exactly
57
+ * as the dashboard does (`hostRefusal`, `X_DEV_HOST_REFUSED`). Without it a DNS-rebinding page
58
+ * read an island rendered with its declared props — and imported states files — same-origin.
47
59
  */
48
60
  export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[] {
49
61
  return [
@@ -52,6 +64,8 @@ export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[]
52
64
  path: ISLAND_HARNESS_PATH,
53
65
  meta: { name: 'dev._x.island', auth: 'public', tags: ['dev'] },
54
66
  handler: async (request: UltimateRequest): Promise<Response> => {
67
+ const misdirected = hostRefusal(request, input.devUrl());
68
+ if (misdirected !== undefined) return misdirected;
55
69
  const address = parseIslandAddress(request.url.search);
56
70
  const all = await input.states();
57
71
  const manifest = islandStatesMatching(all, address.island)[0];
@@ -71,7 +85,9 @@ export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[]
71
85
  if (declared === undefined || target === undefined) {
72
86
  return refused(
73
87
  `${manifest.island} declares no state ${wanted === '' ? '(none named)' : wanted} in theme ${address.theme}`,
74
- `x shot --island ${manifest.name} --state ${manifest.states[0]?.id ?? '<id>'} --json`,
88
+ // Every value here is read off a states file on disk: quoted where it enters the line,
89
+ // and one opening with `-` is the placeholder — quoting does not stop a flag (L3).
90
+ `x shot --island ${quoteArg(unflagged(manifest.name, '<island>'))} --state ${quoteArg(unflagged(manifest.states[0]?.id ?? '<id>', '<id>'))} --json`,
75
91
  );
76
92
  }
77
93
  // The chunk is looked up rather than built here: `x dev` already built every island at
@@ -81,7 +97,7 @@ export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[]
81
97
  if (chunk === undefined) {
82
98
  return refused(
83
99
  `${manifest.island} is declared as an island's states file but this build has no chunk for it`,
84
- `x g island ${manifest.name} --at ${manifest.island.split('/').slice(0, -1).join('/')}`,
100
+ `x g island ${quoteArg(unflagged(manifest.name, '<island>'))} --at ${quoteArg(unflagged(manifest.island.split('/').slice(0, -1).join('/'), '<dir>'))}`,
85
101
  );
86
102
  }
87
103
  return html(harnessPage({ target, state: declared, entry: chunk.url }));
@@ -6,6 +6,7 @@
6
6
  // the path that serves a page. `@ultimat3/testing`'s `sealNetwork()` patches THIS process's
7
7
  // `globalThis.fetch` and cannot reach the page's realm.
8
8
 
9
+ import { encodeRecordEnvelope, RECORDS_HEADER } from '@ultimat3/core';
9
10
  import type { IslandRouteStub } from '@ultimat3/testing';
10
11
 
11
12
  /**
@@ -26,6 +27,25 @@ export const HARNESS_GLOBAL = '__xShot';
26
27
  */
27
28
  const embed = (value: unknown): string => JSON.stringify(value ?? null).replaceAll('<', '\\u003c');
28
29
 
30
+ /**
31
+ * A stub as the page script answers it: `records` becomes the envelope core's own
32
+ * `encodeRecordEnvelope` builds, behind core's own `RECORDS_HEADER` — the server's two halves,
33
+ * never a second spelling of either in the browser string below (#683).
34
+ */
35
+ const wireStub = (stub: IslandRouteStub): unknown => {
36
+ const { respond } = stub;
37
+ if (respond.kind !== 'records') return stub;
38
+ return {
39
+ match: stub.match,
40
+ respond: {
41
+ kind: 'json',
42
+ ...(respond.status === undefined ? {} : { status: respond.status }),
43
+ body: encodeRecordEnvelope(respond.data, respond.records, respond.removed),
44
+ headers: { [RECORDS_HEADER]: '1' },
45
+ },
46
+ };
47
+ };
48
+
29
49
  export interface HarnessScriptOptions {
30
50
  readonly stubs: readonly IslandRouteStub[];
31
51
  /** The frozen instant, with an explicit offset — the manifest's `now`. */
@@ -44,7 +64,7 @@ export interface HarnessScriptOptions {
44
64
  * a `pending` fixture, which is a state an author declares on purpose.
45
65
  */
46
66
  const sealScript = (stubs: readonly IslandRouteStub[]): string => `
47
- var W=window.${HARNESS_GLOBAL};var STUBS=${embed(stubs)};
67
+ var W=window.${HARNESS_GLOBAL};var STUBS=${embed(stubs.map(wireStub))};
48
68
  function bump(){W.activity+=1}
49
69
  function stubFor(method,path){var k=method.toUpperCase()+' '+path;
50
70
  for(var i=0;i<STUBS.length;i+=1){if(k.indexOf(STUBS[i].match)===0)return STUBS[i].respond}
@@ -57,8 +77,9 @@ function answer(respond,k){
57
77
  if(respond===null)return Promise.reject(refuse(k));
58
78
  if(respond.kind==='pending')return new Promise(function(){});
59
79
  if(respond.kind==='offline')return Promise.reject(new TypeError('x shot: offline fixture for '+k));
80
+ var h=Object.assign({'content-type':'application/json'},respond.headers||{});
60
81
  return Promise.resolve(new Response(JSON.stringify(respond.body===undefined?null:respond.body),
61
- {status:respond.status||200,headers:{'content-type':'application/json'}}))}
82
+ {status:respond.status||200,headers:h}))}
62
83
  window.fetch=function(input,init){
63
84
  var url=typeof input==='string'?input:(input&&input.url)||String(input);
64
85
  var method=(init&&init.method)||(typeof input==='object'&&input&&input.method)||'GET';
@@ -24,14 +24,16 @@ export function stripDebugId(code: string): string {
24
24
  /**
25
25
  * The chunk's identity, computed from what went IN rather than from what came out.
26
26
  *
27
- * `Bun.build` is not byte-deterministic under `minify`. Measured on 1.4.0, one entry point, no
28
- * source file touched: a 131,589-byte island alternated between two outputs of IDENTICAL length
29
- * differing only in minified identifier names (`var ca=Object.defineProperty` against
30
- * `var la=…`) — roughly one build in ten, which is a race in the renamer and not anything a caller
31
- * can order. Hashing that output made the URL flap: ten distinct `session-console-*.js` names in
32
- * ten minutes, so a service worker's precache manifest named a chunk that already 404ed and a
33
- * browser's `immutable` cache never hit on a 131 kB download. Twelve consecutive builds hash
34
- * identically here.
27
+ * `Bun.build` was not byte-deterministic under `minify` before 1.4.1 (oven-sh/bun#40657). Measured
28
+ * on 1.4.0, one entry point, no source file touched: a 131,589-byte island alternated between two
29
+ * outputs of IDENTICAL length differing only in minified identifier names
30
+ * (`var ca=Object.defineProperty` against `var la=…`) — roughly one build in ten, a race in the
31
+ * renamer and not anything a caller can order. Hashing that output made the URL flap: ten distinct
32
+ * `session-console-*.js` names in ten minutes, so a service worker's precache manifest named a
33
+ * chunk that already 404ed and a browser's `immutable` cache never hit on a 131 kB download.
34
+ * Fixed upstream in 1.4.1; on 1.4.2, 192 child-process build pairs of `examples/dummy`'s islands
35
+ * under 12- and 16-way contention emitted one byte string per island (2026-10-05, issue #354), and
36
+ * `island-bytes.test.ts` now demands byte identity outright.
35
37
  *
36
38
  * `sourcesContent`, hashed per file and SORTED, so the identity is independent of the order the
37
39
  * bundler happened to visit the graph in. The PATHS are deliberately not in it: they are absolute
@@ -41,12 +43,13 @@ export function stripDebugId(code: string): string {
41
43
  * version are, because both decide the emitted bytes while no source file moves — an upgrade must
42
44
  * mint a new URL rather than leave a stale chunk pinned in a browser for a year.
43
45
  *
44
- * What this gives up, stated plainly: the URL is source-addressed, not byte-addressed, so two
45
- * processes building the same sources can serve two byte-strings at one URL. They are the same
46
- * program under different local identifier names. That is the trade a nondeterministic bundler
47
- * forces, and the alternative — `minify: { identifiers: false }`, which IS deterministic — was
48
- * measured at 193,590 bytes against 131,649, +47% raw and +20% gzipped, on every island of every
49
- * app. Delete this the day `Bun.build` is deterministic.
46
+ * What this gives up, stated plainly: the URL is source-addressed, not byte-addressed. On a Bun
47
+ * with the renamer race (< 1.4.1) two processes building the same sources served two byte-strings
48
+ * at one URL — the same program under different local identifier names — and the alternative,
49
+ * `minify: { identifiers: false }`, was +47% raw and +20% gzipped. The `engines` floor is now past
50
+ * the fix, so the reason this function exists is gone; it is kept for now because moving every
51
+ * island URL to a content hash re-keys every browser cache and every precache manifest, which is a
52
+ * change of its own to make deliberately (issue #354's follow-up), not a comment edit.
50
53
  */
51
54
  export function graphHash(file: string, map: string): string {
52
55
  const parsed: unknown = JSON.parse(map);
@@ -0,0 +1,150 @@
1
+ // One copy of each framework package per island chunk: the app's. `island-solid-dedupe.ts`'s rule,
2
+ // for `@ultimat3/*`. The framework ships in lockstep, so the copy the APP installed is the right
3
+ // one for every importer, and a second copy is a second registry, never a feature.
4
+ //
5
+ // Without it `Bun.build` resolves each import from the importing file, so a package nested under
6
+ // another (`node_modules/@ultimat3/ui/node_modules/@ultimat3/core`) is a second module. Linux hid
7
+ // it: Bun links a `file:` package file by file with symlinks, and a realpath folds every nest into
8
+ // one. Windows links without symlinks, so the scaffold smoke's `/posts` island shipped `core` and
9
+ // `schema` more than once: 82,106 B against 60,649 B on Linux, over a 64kb budget.
10
+ // Reproduced on Linux by replacing those links with copies: 75,023 B, and 59,926 B with this plugin.
11
+
12
+ // why: Bun ships no path API and no existence check; `dirname` walks up the way Node resolution
13
+ // does, `join` builds each candidate, and `existsSync` asks whether one is installed.
14
+ import { existsSync, lstatSync, readdirSync, realpathSync } from 'node:fs';
15
+ import { dirname, join } from 'node:path';
16
+ import type { BunPlugin } from 'bun';
17
+ import { conditionTarget } from './island-solid-dedupe';
18
+
19
+ /** `@ultimat3/<name>` and every subpath of it. */
20
+ export const FRAMEWORK_SPECIFIER = /^@ultimat3\/[^/]+(?:\/.*)?$/;
21
+
22
+ /** `@ultimat3/core/page` → `{ name: '@ultimat3/core', subpath: './page' }`. */
23
+ export function splitFrameworkSpecifier(specifier: string): {
24
+ readonly name: string;
25
+ readonly subpath: string;
26
+ } {
27
+ const [scope = '', pkg = '', ...rest] = specifier.split('/');
28
+ return { name: `${scope}/${pkg}`, subpath: rest.length === 0 ? '.' : `./${rest.join('/')}` };
29
+ }
30
+
31
+ export interface AppPackage {
32
+ /** The installed package's directory — every export target is joined onto it. */
33
+ readonly dir: string;
34
+ /** Its `exports` map, verbatim. */
35
+ readonly exports: unknown;
36
+ }
37
+
38
+ /**
39
+ * The copy the app's own entry would find: `node_modules/<name>` in the app root, then each
40
+ * directory above it. `null` when none is installed there — a package only nested under another
41
+ * is left to Bun, which resolves it from its importer as before.
42
+ */
43
+ export async function resolveAppPackage(root: string, name: string): Promise<AppPackage | null> {
44
+ for (let dir = root; ; dir = dirname(dir)) {
45
+ const manifest = join(dir, 'node_modules', name, 'package.json');
46
+ if (existsSync(manifest)) {
47
+ const parsed = (await Bun.file(manifest).json()) as { readonly exports?: unknown };
48
+ // The REAL directory: Bun keys a module by its realpath, so returning the `node_modules/…`
49
+ // spelling of a symlinked package would bundle it a second time beside every file reached
50
+ // through its real path — the duplication this plugin exists to remove.
51
+ return { dir: realpathSync(dirname(manifest)), exports: parsed.exports };
52
+ }
53
+ if (dirname(dir) === dir) return null;
54
+ }
55
+ }
56
+
57
+ /** Where `subpath` lands in the app's copy, under the browser conditions, or `undefined`. */
58
+ export function appPackagePath(pkg: AppPackage, subpath: string): string | undefined {
59
+ const exports = pkg.exports;
60
+ if (exports === null || typeof exports !== 'object' || !Object.hasOwn(exports, subpath)) {
61
+ return undefined;
62
+ }
63
+ const target = conditionTarget((exports as Record<string, unknown>)[subpath]);
64
+ return target === undefined ? undefined : join(pkg.dir, target);
65
+ }
66
+
67
+ /** Installed beside `solidDedupePlugin`, before any plugin loads a file. One lookup per package. */
68
+ export function frameworkDedupePlugin(root: string): BunPlugin {
69
+ const found = new Map<string, Promise<AppPackage | null>>();
70
+ return {
71
+ name: 'ultimate-framework-dedupe',
72
+ setup(build): void {
73
+ build.onResolve({ filter: FRAMEWORK_SPECIFIER }, async (args) => {
74
+ const { name, subpath } = splitFrameworkSpecifier(args.path);
75
+ let pending = found.get(name);
76
+ if (pending === undefined) {
77
+ pending = resolveAppPackage(root, name);
78
+ found.set(name, pending);
79
+ }
80
+ const pkg = await pending;
81
+ const path = pkg === null ? undefined : appPackagePath(pkg, subpath);
82
+ if (path === undefined) return undefined;
83
+ // Only when Bun's own answer is a SECOND copy, or this file under a second spelling
84
+ // (`sameModule`). A path a plugin returns loses its package's `sideEffects`, so taking over
85
+ // every import shipped ~2.2 KB of untree-shaken core in each island of examples/dummy
86
+ // (21,945 → 24,202 B on /pricing) while folding nothing on Linux.
87
+ return sameModule(path, args.path, args.importer) ? undefined : { path };
88
+ });
89
+ },
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Whether Bun, left alone, resolves `specifier` from `importer` to `path` itself — as the same
95
+ * STRING, because Bun keys a module by its path string, never by the file. This compared realpaths
96
+ * once, which is right on Linux (Bun's resolver answers a realpath, so the strings agree) and wrong
97
+ * on Windows, where it answers the junction spelling: both sides realpath to one file, Bun kept its
98
+ * own spelling, every nest this plugin folded got the realpath one, and one file shipped as two
99
+ * modules (the windows job's `/posts`, 81,306 B against Linux's 59,849 B). `resolve` is injectable
100
+ * so that spelling can be handed in on a host whose resolver never produces it.
101
+ */
102
+ export function sameModule(
103
+ path: string,
104
+ specifier: string,
105
+ importer: string,
106
+ resolve: (specifier: string, from: string) => string = Bun.resolveSync,
107
+ ): boolean {
108
+ try {
109
+ return resolve(specifier, dirname(importer)) === path;
110
+ } catch {
111
+ // Unresolvable from the importer: the app's copy is the only answer there is.
112
+ return false;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Whether any installed `@ultimat3/*` package carries a NESTED framework package that is a real
118
+ * directory rather than a symlink — a second copy no realpath folds, which is a `file:` install on
119
+ * Windows. Only then is the plugin installed: Bun stops honouring a package's `sideEffects` for
120
+ * every import an `onResolve` hook sees, even one it answers `undefined` to, so a hook on a tree
121
+ * with nothing to fold cost each island of examples/dummy ~2.2 KB (21,945 → 24,202 B on /pricing).
122
+ */
123
+ export function hasNestedFrameworkCopies(root: string): boolean {
124
+ for (let dir = root; ; dir = dirname(dir)) {
125
+ const scope = join(dir, 'node_modules', '@ultimat3');
126
+ if (existsSync(scope)) {
127
+ for (const pkg of readdirSync(scope)) {
128
+ const nested = join(scope, pkg, 'node_modules', '@ultimat3');
129
+ if (!existsSync(nested)) continue;
130
+ for (const inner of readdirSync(nested)) {
131
+ if (!lstatSync(join(nested, inner)).isSymbolicLink()) return true;
132
+ }
133
+ }
134
+ }
135
+ if (dirname(dir) === dir) return false;
136
+ }
137
+ }
138
+
139
+ /** The plugin list entry: the dedupe when a nested copy exists, nothing otherwise. */
140
+ export function frameworkDedupePlugins(
141
+ root: string,
142
+ platform: NodeJS.Platform = process.platform,
143
+ ): readonly BunPlugin[] {
144
+ // Windows always: a `file:` install there links packages through junctions Bun does not fold,
145
+ // so the same file is reached under two spellings — the windows job's `/posts` shipped 80.2kb
146
+ // against Linux's 60kb with only the nested-copy check deciding.
147
+ return platform === 'win32' || hasNestedFrameworkCopies(root)
148
+ ? [frameworkDedupePlugin(root)]
149
+ : [];
150
+ }
@@ -1,12 +1,14 @@
1
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).
2
+ // graph reaches `@ultimat3/realtime` is built from a virtual entry that first awaits the page
3
+ // runtime (`island-runtime.ts`), then calls `installRealtime({ signal: createSignal })` with THIS
4
+ // bundle's solid-js, then re-exports the island whole. Every other island pays nothing.
5
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';
6
+ // why: Bun ships no path API; the entry is joined to the root, the runtime named by its basename.
7
+ import { basename, join } from 'node:path';
8
8
  import type { BunPlugin } from 'bun';
9
+ import { RUNTIME_CHUNK_SPECIFIER, type RuntimeChunk } from './island-runtime';
9
10
  import { firstInGraph } from './live-routes';
11
+ import { PAGE_BOOT_BASE_PATH } from './worker-bundle';
10
12
 
11
13
  /**
12
14
  * What `Bun.build` is handed for a realtime island: this prefix plus the island's app-root-relative
@@ -22,6 +24,7 @@ const REALTIME = '@ultimat3/realtime';
22
24
  const NAMESPACE = 'ultimate-island';
23
25
  const INSTALL = 'ultimate:island-realtime';
24
26
  const MODULE = 'ultimate:island-module:';
27
+ const WAIT = 'ultimate:island-page-wait';
25
28
 
26
29
  /**
27
30
  * Whether the island's own graph value-imports realtime. Relative specifiers only, which is the
@@ -71,19 +74,31 @@ const entrySource = (file: string): string =>
71
74
  * source is part of the chunk's `sourcesContent`, which is what `graphHash` names the URL from, so
72
75
  * a path in it gave the same island a different URL in every checkout and every image build.
73
76
  * `solid-js` goes through `island-solid-dedupe.ts` like every other import in the graph.
77
+ *
78
+ * The await comes first: no hook may run before the page runtime is installed, and the module
79
+ * graph is not done — so `mount` is not called — until it is. The page boot carries the runtime
80
+ * on a document rendered for a principal; anywhere else the island loads the runtime chunk, by a
81
+ * path relative to its own URL, so it resolves under `/islands/` and beside it in `mountIsland`.
82
+ * The chunk's URL is in this source, so a new runtime is a new URL for every island loading it.
83
+ * Then the first-paint hold (#506): `mount` waits for the restored records and the open outbox
84
+ * (capped), so a reload's first render already carries a queued write's overlay.
74
85
  */
75
- const INSTALL_SOURCE =
76
- `import { installRealtime } from '${REALTIME}';\n` +
86
+ const installSource = (runtimeUrl: string): string =>
87
+ `import { holdFirstPaint, installRealtime } from '${REALTIME}';\n` +
88
+ `import { awaitPageRuntime } from '${WAIT}';\n` +
77
89
  `import { createSignal } from 'solid-js';\n` +
90
+ `await awaitPageRuntime({ boot: '${PAGE_BOOT_BASE_PATH}/', load: () => import('./${basename(runtimeUrl)}') });\n` +
91
+ `await holdFirstPaint();\n` +
78
92
  `installRealtime({ signal: createSignal });\n`;
79
93
 
80
94
  /**
81
- * `file` is the island whose directory every bare `@ultimat3/realtime` resolves from — the first
82
- * realtime island of the build. One directory for all of them, because one copy in the bundle is
83
- * the requirement: the signal the install sets is only read by hooks from the same module instance.
95
+ * `runtime` names the realtime every bare `@ultimat3/realtime` of the build resolves to — the copy
96
+ * the runtime chunk was built against, resolved from the first realtime island's directory. One
97
+ * copy for all of them, because one copy in the bundle is the requirement: the signal the install
98
+ * sets is only read by hooks from the same module instance, and the page object it reads is the
99
+ * shape that copy's runtime installs.
84
100
  */
85
- export function islandRealtimePlugin(root: string, file: string): BunPlugin {
86
- const island = join(root, file);
101
+ export function islandRealtimePlugin(root: string, runtime: RuntimeChunk): BunPlugin {
87
102
  return {
88
103
  name: 'ultimate-island-realtime',
89
104
  setup(build) {
@@ -98,16 +113,21 @@ export function islandRealtimePlugin(root: string, file: string): BunPlugin {
98
113
  build.onResolve({ filter: /^ultimate:island-module:/ }, (args) => ({
99
114
  path: join(root, args.path.slice(MODULE.length)),
100
115
  }));
116
+ // The wait is realtime's, from the copy the hooks are bundled from; the runtime chunk is
117
+ // another file of this build's output, fetched by the browser and never bundled in.
118
+ build.onResolve({ filter: /^ultimate:island-page-wait$/ }, () => ({ path: runtime.wait }));
119
+ build.onResolve({ filter: RUNTIME_CHUNK_SPECIFIER }, (args) => ({
120
+ path: args.path,
121
+ external: true,
122
+ }));
101
123
  // Every bare `@ultimat3/realtime` in this build, the virtual install's included — a virtual
102
124
  // module has no directory to resolve from. The island's directory for all of them, which is
103
125
  // what the island itself would get.
104
- build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({
105
- path: Bun.resolveSync(REALTIME, dirname(island)),
106
- }));
126
+ build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({ path: runtime.realtime }));
107
127
  build.onLoad({ filter: /.*/, namespace: NAMESPACE }, (args) => ({
108
128
  contents: args.path.startsWith('entry:')
109
129
  ? entrySource(args.path.slice('entry:'.length))
110
- : INSTALL_SOURCE,
130
+ : installSource(runtime.url),
111
131
  loader: 'js',
112
132
  }));
113
133
  },
@@ -3,7 +3,7 @@
3
3
  // would be a page that boots in `x dev` and 404s in the image.
4
4
 
5
5
  import type { Route, UltimateRequest } from '@ultimat3/http';
6
- import { applyCacheHeaders, json, NO_STORE } from '@ultimat3/http';
6
+ import { applyCacheHeaders, jsonResponse, NO_STORE } from '@ultimat3/http';
7
7
  import type { IslandBundle } from './island-bundle';
8
8
  import { ISLAND_BASE_PATH } from './island-bundle';
9
9
 
@@ -30,7 +30,7 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
30
30
  // have to answer at the one base path, under the one cache rule.
31
31
  const chunk = source().assetAt(request.pathname);
32
32
  if (chunk === undefined) {
33
- const missing = json(
33
+ const missing = jsonResponse(
34
34
  {
35
35
  ok: false,
36
36
  error: {