@ultimat3/cli 24.0.0 → 25.1.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 (243) hide show
  1. package/CLAUDE.md +15 -8
  2. package/README.md +30 -5
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +3 -1
  6. package/src/app-boundaries.ts +17 -4
  7. package/src/app-config-load.ts +3 -3
  8. package/src/app-env.ts +21 -14
  9. package/src/app-load.ts +38 -14
  10. package/src/app-manifest.ts +2 -2
  11. package/src/app-mcp.ts +25 -27
  12. package/src/app-name.ts +52 -0
  13. package/src/app-permissions-borrowed.ts +2 -8
  14. package/src/app-permissions-site.ts +31 -7
  15. package/src/app-root.ts +9 -5
  16. package/src/bin.ts +3 -7
  17. package/src/browser-launcher.ts +4 -2
  18. package/src/budgets-charged.ts +60 -0
  19. package/src/budgets-scan.ts +76 -0
  20. package/src/budgets-unmeasured.ts +91 -0
  21. package/src/budgets.ts +115 -121
  22. package/src/cdp-shot-driver.ts +17 -0
  23. package/src/cdp-shot-watch.ts +35 -31
  24. package/src/ci-log.ts +0 -0
  25. package/src/cmd-auth.ts +2 -2
  26. package/src/cmd-build-spec.ts +7 -1
  27. package/src/cmd-build.ts +89 -9
  28. package/src/cmd-db-backfill.ts +1 -1
  29. package/src/cmd-db-branch.ts +2 -2
  30. package/src/cmd-db.ts +5 -2
  31. package/src/cmd-deploy-compose.ts +94 -0
  32. package/src/cmd-deploy-drain.ts +48 -0
  33. package/src/cmd-deploy-start-first.ts +178 -0
  34. package/src/cmd-deploy.ts +158 -36
  35. package/src/cmd-dev.ts +18 -357
  36. package/src/cmd-doctor.ts +16 -66
  37. package/src/cmd-env.ts +29 -14
  38. package/src/cmd-generate.ts +12 -2
  39. package/src/cmd-i18n.ts +11 -8
  40. package/src/cmd-jobs-spec.ts +7 -22
  41. package/src/cmd-jobs.ts +18 -137
  42. package/src/cmd-mcp.ts +25 -7
  43. package/src/cmd-new.ts +36 -8
  44. package/src/cmd-planned.ts +9 -0
  45. package/src/cmd-routes.ts +3 -3
  46. package/src/cmd-shot-matrix.ts +21 -4
  47. package/src/cmd-shot-spec.ts +10 -2
  48. package/src/cmd-shot.ts +35 -39
  49. package/src/cmd-test.ts +1 -1
  50. package/src/coverage-lcov.ts +4 -2
  51. package/src/db-backfill.ts +2 -2
  52. package/src/db-replay-engine.ts +4 -10
  53. package/src/db-subscribes.ts +19 -16
  54. package/src/dev-boot.ts +290 -0
  55. package/src/dev-child-watch.ts +63 -12
  56. package/src/dev-dashboard-guard.ts +1 -1
  57. package/src/dev-dashboard.ts +5 -1
  58. package/src/dev-lock.ts +96 -24
  59. package/src/dev-rebuild.ts +80 -0
  60. package/src/dev-restart-retire.ts +46 -0
  61. package/src/dev-route-table.ts +30 -8
  62. package/src/dev-supervisor.ts +114 -22
  63. package/src/dev-traces.ts +53 -20
  64. package/src/dispatch.ts +14 -7
  65. package/src/doctor-auth.ts +4 -4
  66. package/src/doctor-offline.ts +6 -5
  67. package/src/document-options.ts +7 -0
  68. package/src/drift-replica-identity.ts +2 -2
  69. package/src/drift.ts +29 -6
  70. package/src/error-codes.ts +6 -0
  71. package/src/error-page-csp.ts +14 -15
  72. package/src/exec.ts +8 -0
  73. package/src/favicon.ts +2 -2
  74. package/src/fix-command.ts +9 -1
  75. package/src/framework-env.ts +148 -0
  76. package/src/framework-schema.ts +13 -2
  77. package/src/generate-catalog-locales.ts +79 -0
  78. package/src/generate-kinds.ts +3 -17
  79. package/src/generate-plural.ts +50 -0
  80. package/src/generate-shadow.ts +50 -1
  81. package/src/hold.ts +56 -1
  82. package/src/i18n-audit.ts +21 -8
  83. package/src/i18n-index.ts +5 -6
  84. package/src/i18n-registration.ts +7 -9
  85. package/src/import-scan.ts +28 -3
  86. package/src/index.ts +1 -3
  87. package/src/island-bundle.ts +54 -64
  88. package/src/island-capture.ts +1 -1
  89. package/src/island-duplicate-refusal.ts +57 -0
  90. package/src/island-duplicates.ts +221 -0
  91. package/src/island-entry-missing.ts +64 -0
  92. package/src/island-harness-route.ts +20 -4
  93. package/src/island-harness-script.ts +23 -2
  94. package/src/island-identity.ts +17 -14
  95. package/src/island-package-dedupe.ts +150 -0
  96. package/src/island-realtime.ts +36 -16
  97. package/src/island-routes.ts +2 -2
  98. package/src/island-runtime.ts +241 -0
  99. package/src/island-sources.ts +105 -0
  100. package/src/island-states-load.ts +6 -0
  101. package/src/island-store.ts +90 -6
  102. package/src/jobs-json.ts +0 -26
  103. package/src/jobs-report.ts +32 -16
  104. package/src/local-cli-handoff.ts +90 -0
  105. package/src/mcp-errors.ts +8 -1
  106. package/src/mcp-host.ts +6 -5
  107. package/src/mcp-ui-inspect.ts +3 -2
  108. package/src/mcp-ui-interact.ts +3 -2
  109. package/src/mcp-ui.ts +8 -7
  110. package/src/measure-scope.ts +15 -10
  111. package/src/messages.ts +27 -16
  112. package/src/metrics-endpoint.ts +3 -2
  113. package/src/migration-extensions.ts +1 -1
  114. package/src/otlp-export.ts +2 -2
  115. package/src/output.ts +18 -0
  116. package/src/page-speculation.ts +14 -11
  117. package/src/parse.ts +0 -8
  118. package/src/permission-grants.ts +3 -3
  119. package/src/posix-path.ts +30 -0
  120. package/src/prerender-locales.ts +6 -4
  121. package/src/prerender-report.ts +58 -0
  122. package/src/prerender-served.ts +52 -0
  123. package/src/prerender.ts +63 -98
  124. package/src/pwa-artifacts.ts +8 -1
  125. package/src/pwa-manifest-locales.ts +9 -9
  126. package/src/retry-memo.ts +1 -1
  127. package/src/role-realtime.ts +7 -1
  128. package/src/role-replicator.ts +110 -31
  129. package/src/role-start.ts +66 -87
  130. package/src/role-sync.ts +65 -6
  131. package/src/role-wake.ts +2 -3
  132. package/src/root-env.ts +157 -28
  133. package/src/route-islands.ts +69 -0
  134. package/src/runtime-admin.ts +4 -1
  135. package/src/runtime-assets.ts +15 -2
  136. package/src/runtime-bindings.ts +23 -3
  137. package/src/runtime-cache.ts +8 -8
  138. package/src/runtime-hooks.ts +2 -1
  139. package/src/runtime-isr-outcome.ts +100 -0
  140. package/src/runtime-isr.ts +2 -2
  141. package/src/runtime-jobs.ts +78 -18
  142. package/src/runtime-live-feed.ts +1 -1
  143. package/src/runtime-mail.ts +26 -0
  144. package/src/runtime-mfa-warning.ts +2 -2
  145. package/src/runtime-overrides.ts +3 -3
  146. package/src/runtime-purge.ts +1 -1
  147. package/src/runtime-queue.ts +19 -30
  148. package/src/runtime-realtime.ts +5 -3
  149. package/src/runtime-render.ts +77 -82
  150. package/src/runtime-replica.ts +3 -8
  151. package/src/runtime-services.ts +36 -15
  152. package/src/runtime-storage-upload.ts +2 -2
  153. package/src/runtime-storage.ts +3 -3
  154. package/src/schema-diff.ts +1 -1
  155. package/src/secrets-rotation.ts +8 -9
  156. package/src/seo-routes.ts +2 -2
  157. package/src/serve-boot.ts +22 -11
  158. package/src/serve-drain.ts +3 -1
  159. package/src/serve-retire.ts +95 -0
  160. package/src/serve-web.ts +19 -8
  161. package/src/serve.ts +27 -10
  162. package/src/server-barrels.ts +6 -3
  163. package/src/shot-browser.ts +1 -1
  164. package/src/shot-locale.ts +6 -15
  165. package/src/shot-settle.ts +3 -3
  166. package/src/shot-theme.ts +1 -1
  167. package/src/shot-viewport.ts +152 -0
  168. package/src/signal-shred.ts +26 -12
  169. package/src/site-asset-routes.ts +2 -2
  170. package/src/site-seo.ts +6 -4
  171. package/src/source-files.ts +13 -3
  172. package/src/static-report.ts +5 -0
  173. package/src/style-routes.ts +2 -2
  174. package/src/sw-artifacts.ts +8 -8
  175. package/src/sw-precache-plan.ts +34 -4
  176. package/src/sync-authenticator.ts +3 -3
  177. package/src/sync-url.ts +1 -1
  178. package/src/templates/action.ts +4 -4
  179. package/src/templates/backfill.ts +4 -4
  180. package/src/templates/entity.ts +2 -2
  181. package/src/templates/github/ci.yml.ts +28 -18
  182. package/src/templates/github/image.yml.ts +92 -0
  183. package/src/templates/github/runner.ts +18 -0
  184. package/src/templates/guard-bare-error.ts +16 -2
  185. package/src/templates/guard-unzoned-date.ts +56 -6
  186. package/src/templates/index.ts +1 -1
  187. package/src/templates/job-body-test.ts +2 -2
  188. package/src/templates/job.ts +6 -6
  189. package/src/templates/query.ts +9 -8
  190. package/src/templates/resource-page.ts +9 -7
  191. package/src/templates/resource-service.ts +2 -2
  192. package/src/templates/route.ts +23 -8
  193. package/src/templates/scaffold-api.ts +2 -3
  194. package/src/templates/scaffold-app.ts +2 -1
  195. package/src/templates/scaffold-auth.ts +205 -70
  196. package/src/templates/scaffold-bin.ts +122 -0
  197. package/src/templates/scaffold-claude-commands.ts +11 -11
  198. package/src/templates/scaffold-container.ts +12 -6
  199. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  200. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  201. package/src/templates/scaffold-dashboard-example.ts +112 -172
  202. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  203. package/src/templates/scaffold-dashboard.ts +9 -5
  204. package/src/templates/scaffold-db-package.ts +2 -2
  205. package/src/templates/scaffold-docs.ts +20 -77
  206. package/src/templates/scaffold-domain-package.ts +0 -4
  207. package/src/templates/scaffold-entries.ts +4 -3
  208. package/src/templates/scaffold-env.ts +6 -3
  209. package/src/templates/scaffold-gitattributes.ts +59 -0
  210. package/src/templates/scaffold-helm-templates.ts +74 -18
  211. package/src/templates/scaffold-helm.ts +31 -4
  212. package/src/templates/scaffold-http.ts +1 -1
  213. package/src/templates/scaffold-i18n.ts +15 -2
  214. package/src/templates/scaffold-icon.ts +2 -2
  215. package/src/templates/scaffold-repo.ts +18 -15
  216. package/src/templates/scaffold-shell.ts +17 -2
  217. package/src/test-only-shipped.ts +193 -0
  218. package/src/test-select.ts +2 -1
  219. package/src/test-shards.ts +1 -4
  220. package/src/theme-boot.ts +2 -2
  221. package/src/theme-brand.ts +66 -0
  222. package/src/trusted-hops.ts +49 -0
  223. package/src/unscanned-admin.ts +2 -2
  224. package/src/verify-checks.ts +12 -2
  225. package/src/verify-coverage-run.ts +3 -2
  226. package/src/verify-deadline.ts +87 -10
  227. package/src/verify-merge.ts +48 -16
  228. package/src/verify-part-step.ts +10 -2
  229. package/src/verify-role-load-probe.ts +12 -2
  230. package/src/verify-role-load.ts +9 -4
  231. package/src/verify-run.ts +3 -0
  232. package/src/verify-step.ts +2 -0
  233. package/src/verify-test-run.ts +3 -2
  234. package/src/verify-tests.ts +0 -1
  235. package/src/worker-bundle.ts +2 -2
  236. package/src/workspace-checks.ts +53 -7
  237. package/src/workspace-graph.ts +3 -2
  238. package/src/write-line.ts +82 -7
  239. package/src/browser-launcher-fake-html.ts +0 -80
  240. package/src/browser-launcher-fake.ts +0 -168
  241. package/src/jobs-drain.ts +0 -230
  242. package/src/scaffold-typecheck.ts +0 -227
  243. package/src/thrown-by.ts +0 -24
@@ -0,0 +1,221 @@
1
+ // One module, one copy per island chunk — read off the build's metafile. Bun keys a module by its
2
+ // path STRING, so one file reached under two spellings ships twice: a drive letter in two cases, `\`
3
+ // beside `/`, a junction beside its target. Measured on the windows job, the scaffold's `/posts`
4
+ // island shipped 81,306 B against Linux's 59,849 B from the same source, with `success: true`.
5
+
6
+ // why: Bun ships no path API and no realpath; `win32`/`posix` let a Windows-shaped metafile be
7
+ // judged on any host, and `realpathSync` folds a link into the file it names.
8
+ import { realpathSync } from 'node:fs';
9
+ import { posix, win32 } from 'node:path';
10
+
11
+ /** The part of `Bun.BuildMetafile` the guard reads — `Bun.BuildMetafile` satisfies it as is. */
12
+ export interface IslandMetafile {
13
+ readonly outputs: Readonly<
14
+ Record<
15
+ string,
16
+ { readonly inputs: Readonly<Record<string, { readonly bytesInOutput: number }>> }
17
+ >
18
+ >;
19
+ }
20
+
21
+ /** The filesystem the guard asks, injectable so a Windows tree can be judged on a Linux runner. */
22
+ export interface ModuleIdentityIo {
23
+ /** What Bun's metafile keys are relative to: the BUILDING process's cwd (`island-sources.ts`). */
24
+ readonly cwd: string;
25
+ /** win32 compares case-insensitively and treats `\` and `/` as one separator. */
26
+ readonly platform: NodeJS.Platform;
27
+ /** Throws for a path with no file behind it — a virtual module keeps its own spelling. */
28
+ readonly realpath: (path: string) => string;
29
+ /** A file's text, or `undefined` when there is none. */
30
+ readonly readText: (path: string) => Promise<string | undefined>;
31
+ }
32
+
33
+ export const hostIo = (): ModuleIdentityIo => ({
34
+ cwd: process.cwd(),
35
+ platform: process.platform,
36
+ realpath: (path) => realpathSync(path),
37
+ readText: async (path) => {
38
+ const file = Bun.file(path);
39
+ return (await file.exists()) ? file.text() : undefined;
40
+ },
41
+ });
42
+
43
+ /**
44
+ * `path` as `island-package-dedupe.ts` spells every framework file it answers: the realpath. What a
45
+ * resolver answered under another spelling (a Windows junction) is renamed through this before a
46
+ * plugin hands it to `Bun.build`, so one file is one module string. A path with no file stays.
47
+ */
48
+ export function realSpelling(path: string): string {
49
+ try {
50
+ return realpathSync(path);
51
+ } catch {
52
+ return path;
53
+ }
54
+ }
55
+
56
+ /** One module an output carries more than once. */
57
+ export interface DuplicatedModule {
58
+ /** The output path as the metafile names it. */
59
+ readonly output: string;
60
+ /** Every spelling it was bundled under, absolute, in the metafile's order. */
61
+ readonly spellings: readonly string[];
62
+ /** Bytes the output carries beyond its largest copy. */
63
+ readonly wastedBytes: number;
64
+ /** One file under several spellings (`true`), or byte-identical copies of one package (`false`). */
65
+ readonly sameFile: boolean;
66
+ /** `name@version` when the copies are a package's. */
67
+ readonly package?: string;
68
+ /** The package's `name` alone — what `bun why` is asked about. */
69
+ readonly packageName?: string;
70
+ }
71
+
72
+ interface Input {
73
+ readonly spelling: string;
74
+ readonly bytes: number;
75
+ readonly canonical: string;
76
+ }
77
+
78
+ interface PackageOf {
79
+ readonly id: string;
80
+ readonly name: string;
81
+ readonly subpath: string;
82
+ }
83
+
84
+ /** The realpath, `/`-separated, lowercased where the filesystem is case-insensitive. */
85
+ function canonicalOf(absolute: string, io: ModuleIdentityIo): string {
86
+ let real = absolute;
87
+ try {
88
+ real = io.realpath(absolute);
89
+ } catch {
90
+ // No file behind it: a virtual module, judged by its spelling alone.
91
+ }
92
+ const slashed = real.replace(/^\\\\\?\\/, '').replaceAll('\\', '/');
93
+ return io.platform === 'win32' ? slashed.toLowerCase() : slashed;
94
+ }
95
+
96
+ /** The nearest `package.json` with a `name` above `file`, as `name@version` + the file under it. */
97
+ async function packageOf(
98
+ file: string,
99
+ io: ModuleIdentityIo,
100
+ manifests: Map<string, Promise<{ name: string; version: string } | null>>,
101
+ ): Promise<PackageOf | undefined> {
102
+ const path = io.platform === 'win32' ? win32 : posix;
103
+ for (let dir = path.dirname(file); ; dir = path.dirname(dir)) {
104
+ let pending = manifests.get(dir);
105
+ if (pending === undefined) {
106
+ pending = io.readText(path.join(dir, 'package.json')).then(namedManifest);
107
+ manifests.set(dir, pending);
108
+ }
109
+ const found = await pending;
110
+ if (found !== null) {
111
+ const subpath = path.relative(dir, file).replaceAll('\\', '/');
112
+ const id = `${found.name}@${found.version}`;
113
+ return {
114
+ id,
115
+ name: found.name,
116
+ subpath: io.platform === 'win32' ? subpath.toLowerCase() : subpath,
117
+ };
118
+ }
119
+ if (path.dirname(dir) === dir) return undefined;
120
+ }
121
+ }
122
+
123
+ function namedManifest(text: string | undefined): { name: string; version: string } | null {
124
+ if (text === undefined) return null;
125
+ try {
126
+ const parsed: unknown = JSON.parse(text);
127
+ if (typeof parsed !== 'object' || parsed === null) return null;
128
+ const { name, version } = parsed as Record<string, unknown>;
129
+ return typeof name === 'string'
130
+ ? { name, version: typeof version === 'string' ? version : '' }
131
+ : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Every module an output carries twice: first by canonical path, then — between inputs that are
139
+ * still distinct — by package. Two copies count as one module only when they are the same file of
140
+ * the same `name@version` AND hold the same bytes, so a patched or differently-versioned copy is
141
+ * left alone. Contents are read only for a package collision, never for every input.
142
+ */
143
+ export async function duplicatedModules(
144
+ metafile: IslandMetafile,
145
+ io: ModuleIdentityIo,
146
+ ): Promise<readonly DuplicatedModule[]> {
147
+ const path = io.platform === 'win32' ? win32 : posix;
148
+ const manifests = new Map<string, Promise<{ name: string; version: string } | null>>();
149
+ const found: DuplicatedModule[] = [];
150
+ for (const [output, { inputs }] of Object.entries(metafile.outputs)) {
151
+ const byPath = new Map<string, Input[]>();
152
+ for (const [key, { bytesInOutput }] of Object.entries(inputs)) {
153
+ // A namespaced key (`x-realtime-island:…`) is a virtual module: no file, no second spelling.
154
+ if (/^[a-z][\w-]+:(?![\\/])/i.test(key)) continue;
155
+ const spelling = path.resolve(io.cwd, key);
156
+ const canonical = canonicalOf(spelling, io);
157
+ const group = byPath.get(canonical) ?? [];
158
+ group.push({ spelling, bytes: bytesInOutput, canonical });
159
+ byPath.set(canonical, group);
160
+ }
161
+ for (const group of byPath.values()) {
162
+ if (group.length > 1) found.push(duplicate(output, group, true));
163
+ }
164
+ found.push(...(await packageCopies(output, [...byPath.values()], io, manifests)));
165
+ }
166
+ return found;
167
+ }
168
+
169
+ /** Distinct files that are one package's one file, byte for byte, under two install paths. */
170
+ async function packageCopies(
171
+ output: string,
172
+ groups: readonly (readonly Input[])[],
173
+ io: ModuleIdentityIo,
174
+ manifests: Map<string, Promise<{ name: string; version: string } | null>>,
175
+ ): Promise<readonly DuplicatedModule[]> {
176
+ const byPackage = new Map<string, { pkg: PackageOf; inputs: Input[] }>();
177
+ for (const group of groups) {
178
+ const first = group[0];
179
+ if (first === undefined) continue;
180
+ const pkg = await packageOf(first.spelling, io, manifests);
181
+ if (pkg === undefined) continue;
182
+ const key = `${pkg.id}/${pkg.subpath}`;
183
+ const entry = byPackage.get(key) ?? { pkg, inputs: [] };
184
+ entry.inputs.push(first);
185
+ byPackage.set(key, entry);
186
+ }
187
+ const found: DuplicatedModule[] = [];
188
+ for (const { pkg, inputs } of byPackage.values()) {
189
+ if (inputs.length < 2) continue;
190
+ const byHash = new Map<string, Input[]>();
191
+ for (const input of inputs) {
192
+ const text = await io.readText(input.spelling);
193
+ if (text === undefined) continue;
194
+ const hash = String(Bun.hash(text));
195
+ byHash.set(hash, [...(byHash.get(hash) ?? []), input]);
196
+ }
197
+ for (const same of byHash.values()) {
198
+ if (same.length > 1) found.push(duplicate(output, same, false, pkg.id, pkg.name));
199
+ }
200
+ }
201
+ return found;
202
+ }
203
+
204
+ function duplicate(
205
+ output: string,
206
+ inputs: readonly Input[],
207
+ sameFile: boolean,
208
+ id?: string,
209
+ name?: string,
210
+ ): DuplicatedModule {
211
+ const bytes = inputs.map((one) => one.bytes);
212
+ const wastedBytes = bytes.reduce((sum, one) => sum + one, 0) - Math.max(...bytes);
213
+ return {
214
+ output,
215
+ spellings: inputs.map((one) => one.spelling),
216
+ wastedBytes,
217
+ sameFile,
218
+ ...(id === undefined ? {} : { package: id }),
219
+ ...(name === undefined ? {} : { packageName: name }),
220
+ };
221
+ }
@@ -0,0 +1,64 @@
1
+ // The refusals for an island the build cannot answer: a `buildIslands({ only })` naming no island, and
2
+ // a route's `src` resolving to a file the build did not bundle. Split from `island-bundle.ts`, which
3
+ // keeps the build; both refusals are render's `X_ISLAND_INVALID` (see `entryMissing`).
4
+
5
+ // why: Bun ships no path API; `posix` does the specifier arithmetic on app-relative paths.
6
+ import { posix } from 'node:path';
7
+ import { ISLAND_EXTENSION, IslandInvalidError } from '@ultimat3/render';
8
+ import { quoteArg } from './shell-quote';
9
+
10
+ /**
11
+ * Same code as an unbuildable `src`: "this path cannot become a client entry" is one condition.
12
+ *
13
+ * Two fixes, because there are two causes and only one of them can be repaired by naming a path.
14
+ * The line was `pass only: '<app-root-relative path>.island.tsx'` — a placeholder nobody can run,
15
+ * which no gate could see: `fixProblem` fails a fix only for ADVICE with no command token, and a
16
+ * sentence with neither is not advice. Both forms below are constructed from what the caller
17
+ * already handed in, so neither can name a path this app does not have.
18
+ */
19
+ export function onlyMissing(only: string, discovered: readonly string[]): IslandInvalidError {
20
+ const cause =
21
+ `buildIslands was asked for ${JSON.stringify(only)} alone, which is not one of the ` +
22
+ `${discovered.length} islands this app has (${discovered.length === 0 ? 'none' : discovered.join(', ')})`;
23
+ // The basename match first: a filter that misses normally missed on the PREFIX — a route-relative
24
+ // specifier where `discoverIslands`' app-root-relative path was wanted — and the filename
25
+ // survives that. Falling back to the first keeps the fix a real path rather than a shape.
26
+ const nearest =
27
+ discovered.find((file) => posix.basename(file) === posix.basename(only)) ?? discovered[0];
28
+ // An app with no islands cannot be pointed at one, so the fix WRITES the file that was asked
29
+ // for — the same command `entryMissing` hands back, split off the same path.
30
+ if (nearest === undefined) {
31
+ return new IslandInvalidError(cause, generateIslandFix(only));
32
+ }
33
+ return new IslandInvalidError(cause, `buildIslands(root, { only: '${nearest}' })`);
34
+ }
35
+
36
+ /**
37
+ * The specifier named no file the build could bundle. `X_ISLAND_INVALID` is render's and is
38
+ * borrowed rather than renamed here: "this src cannot become a client entry" is the condition that
39
+ * code already means, and a second name for it is a second thing to look up.
40
+ */
41
+ export function entryMissing(
42
+ routeFile: string,
43
+ src: string,
44
+ target: string,
45
+ chunks: readonly { readonly file: string }[],
46
+ ): IslandInvalidError {
47
+ const known = chunks.map((chunk) => chunk.file);
48
+ return new IslandInvalidError(
49
+ `${routeFile} declares island src ${JSON.stringify(src)}, which resolves to ${target} — a ` +
50
+ `file this build did not bundle (${known.length === 0 ? 'it found no islands at all' : `it found ${known.join(', ')}`})`,
51
+ generateIslandFix(target),
52
+ );
53
+ }
54
+
55
+ /**
56
+ * `x g island <name> --at <dir>`, split off a path the caller or a route file supplied. Each half is
57
+ * quoted (security audit of plan 101 sweep 1c, M1), and one opening with `-` is the placeholder:
58
+ * quoting does not stop `x` reading `--json` as a flag (L3).
59
+ */
60
+ function generateIslandFix(path: string): string {
61
+ const name = posix.basename(path, ISLAND_EXTENSION);
62
+ const dir = posix.dirname(path);
63
+ return `x g island ${quoteArg(name.startsWith('-') ? '<name>' : name)} --at ${quoteArg(dir.startsWith('-') ? '<dir>' : dir)}`;
64
+ }
@@ -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
+ }