@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
@@ -15,26 +15,32 @@
15
15
  // `globalThis` object created lazily by the first transport call or realtime hook. Measured before
16
16
  // that decision on `examples/dummy`, a core-importing wrapper cost contact-sales 875 → 8,827 B.
17
17
  // What IS prepended is the realtime install, and only where the island's graph reaches
18
- // `@ultimat3/realtime` (`island-realtime.ts`). Measured on a fixture, As of 2026-09-22: a Solid
19
- // island reading `useConnection` 64,964 → 65,067 B (+103 B); an island not reaching realtime +0.
18
+ // `@ultimat3/realtime` (`island-realtime.ts`); the page runtime it awaits is ONE more output,
19
+ // `page-runtime.<identity>.js` beside the entries (`island-runtime.ts`), never inlined into one (#505).
20
20
 
21
21
  // Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
22
22
  // POSIX by construction), `join`/`basename` the filesystem side.
23
23
  import { basename, join, posix, relative, sep } from 'node:path';
24
- import { ISLAND_EXTENSION, IslandInvalidError, islandModuleId } from '@ultimat3/render';
25
- import { APP_CONFIG_EXPORT } from './app-auth';
26
- import { APP_CONFIG_FILE } from './app-root';
24
+ import { ISLAND_EXTENSION, islandModuleId } from '@ultimat3/render';
25
+ import { loadAppConfig } from './app-config-load';
27
26
  import { IslandBuildFailedError } from './errors';
27
+ import { refuseDuplicatedModules } from './island-duplicate-refusal';
28
+ import { entryMissing, onlyMissing } from './island-entry-missing';
28
29
  import { describeBuildError, sourcesContentOf, stableChunk, stripDebugId } from './island-identity';
29
30
  import type { BuiltOutput, LinkedFile } from './island-link';
30
31
  import { linkOutputs, sharedChunkName } from './island-link';
32
+ import { frameworkDedupePlugins } from './island-package-dedupe';
31
33
  import {
32
34
  islandRealtimePlugin,
33
35
  REALTIME_ISLAND_ENTRY,
34
36
  reachesRealtime,
35
37
  realtimeIslandEntry,
36
38
  } from './island-realtime';
39
+ import type { Runtimes } from './island-runtime';
40
+ import { loadingRuntime, mergeBuilds, runtimeBuilder, runtimeShared } from './island-runtime';
37
41
  import { solidDedupePlugin } from './island-solid-dedupe';
42
+ import type { SourcePaths } from './island-sources';
43
+ import { sourcesOnDisk } from './island-sources';
38
44
  import { islandStylesPlugin } from './island-styles';
39
45
  import { hasPathSegment } from './path-segments';
40
46
  import { solidJsxPlugin } from './solid-loader';
@@ -71,6 +77,8 @@ export interface IslandChunk {
71
77
  * beyond `bytes`, what a precached page must carry with it. Empty for an island sharing nothing.
72
78
  */
73
79
  readonly imports: readonly string[];
80
+ /** The files on disk it was built from (`island-sources.ts`). Absent on a chunk not built here. */
81
+ readonly sources?: SourcePaths;
74
82
  }
75
83
 
76
84
  /** A module two or more islands import, served once beside them. */
@@ -81,6 +89,7 @@ export interface SharedChunk {
81
89
  readonly bytes: number;
82
90
  /** The islands (app-root-relative) that load it — for a finding that names what an author edits. */
83
91
  readonly importers: readonly string[];
92
+ readonly sources?: SourcePaths;
84
93
  }
85
94
 
86
95
  export interface IslandBundle {
@@ -120,6 +129,7 @@ async function buildAll(
120
129
  root: string,
121
130
  files: readonly string[],
122
131
  splitting: boolean,
132
+ runtimes: Runtimes,
123
133
  ): Promise<{ readonly chunks: readonly IslandChunk[]; readonly shared: readonly SharedChunk[] }> {
124
134
  if (files.length === 0) return { chunks: [], shared: [] };
125
135
  // Inside the refusal: the realtime probe PARSES the island's graph, and a file that will not
@@ -135,6 +145,7 @@ async function buildAll(
135
145
  // Only an island whose own graph reaches `@ultimat3/realtime` is wrapped (`island-realtime.ts`);
136
146
  // every other one is built from its own file, byte for byte what it was.
137
147
  const live = files.filter((_, index) => realtime[index] === true);
148
+ const runtime = live[0] === undefined ? undefined : await runtimes(live[0]);
138
149
  const entrypoints = files.map((file, index) =>
139
150
  realtime[index] === true ? realtimeIslandEntry(file) : join(root, file),
140
151
  );
@@ -162,10 +173,13 @@ async function buildAll(
162
173
  //
163
174
  // The dedupe goes FIRST: it answers `solid-js` specifiers before either plugin loads a file,
164
175
  // so the `solid-js/web` helpers the JSX transform writes into a symlinked package resolve
165
- // to the app's one copy. See `island-solid-dedupe.ts` for the measurement.
176
+ // to the app's one copy. See `island-solid-dedupe.ts` for the measurement. `@ultimat3/*`
177
+ // gets the same rule (`island-package-dedupe.ts`): a nested copy is a second module where no
178
+ // symlink folds it, which is every `file:` install on Windows.
166
179
  plugins: [
167
- ...(live[0] === undefined ? [] : [islandRealtimePlugin(root, live[0])]),
180
+ ...(runtime === undefined ? [] : [islandRealtimePlugin(root, runtime)]),
168
181
  solidDedupePlugin(root),
182
+ ...frameworkDedupePlugins(root),
169
183
  solidJsxPlugin,
170
184
  islandStylesPlugin,
171
185
  ],
@@ -188,9 +202,13 @@ async function buildAll(
188
202
  // output has. See `graphHash`. Measured on 1.4.0 against a 131 kB island: 277ms with it and
189
203
  // 276ms without, so the map costs nothing worth naming.
190
204
  sourcemap: 'external',
205
+ // The fifth: which inputs each output carries, so one module bundled twice is refused below
206
+ // rather than shipped (`island-duplicates.ts`). Measured on 1.4.2 against examples/dummy's
207
+ // feed island, five interleaved builds each way: 44 ms without, 48 ms with, noise-level.
208
+ metafile: true,
191
209
  });
192
210
  } catch (error) {
193
- throw await blame(root, files, error, splitting);
211
+ throw await blame(root, files, error, splitting, runtimes);
194
212
  }
195
213
  if (!built.success) {
196
214
  throw new IslandBuildFailedError({
@@ -198,9 +216,18 @@ async function buildAll(
198
216
  logs: built.logs.map((log) => String(log)).join('; '),
199
217
  });
200
218
  }
201
- const linked = linkOutputs(await builtOutputs(files, built.outputs));
202
- const chunks = entryChunks(files, linked);
203
- return { chunks, shared: sharedChunks(chunks, linked) };
219
+ if (built.metafile === undefined) {
220
+ throw new IslandBuildFailedError({
221
+ file: files.join(', '),
222
+ logs: 'the bundler returned no metafile',
223
+ });
224
+ }
225
+ await refuseDuplicatedModules(files.join(', '), built.metafile);
226
+ const sources = new Map<string, SourcePaths>();
227
+ const linked = linkOutputs(await builtOutputs(root, files, built.outputs, sources));
228
+ const chunks = loadingRuntime(entryChunks(files, linked, sources), live, runtime);
229
+ const shared = sharedChunks(chunks, linked, sources);
230
+ return { chunks, shared: [...shared, ...runtimeShared(runtime, live)] };
204
231
  }
205
232
 
206
233
  /**
@@ -213,11 +240,12 @@ async function blame(
213
240
  files: readonly string[],
214
241
  error: unknown,
215
242
  splitting: boolean,
243
+ runtimes: Runtimes,
216
244
  ): Promise<IslandBuildFailedError> {
217
245
  if (files.length > 1) {
218
246
  for (const file of files) {
219
247
  try {
220
- await buildAll(root, [file], splitting);
248
+ await buildAll(root, [file], splitting, runtimes);
221
249
  } catch (alone) {
222
250
  if (alone instanceof IslandBuildFailedError) return alone;
223
251
  }
@@ -242,8 +270,10 @@ const withoutExtension = (file: string): string =>
242
270
 
243
271
  /** Every code output paired with its source map's inputs and, for an entry, its island. */
244
272
  async function builtOutputs(
273
+ root: string,
245
274
  files: readonly string[],
246
275
  artifacts: readonly Bun.BuildArtifact[],
276
+ sourcePaths: Map<string, SourcePaths>,
247
277
  ): Promise<readonly BuiltOutput[]> {
248
278
  const byStem = new Map(files.map((file) => [withoutExtension(file), file]));
249
279
  const maps = new Map<string, string>();
@@ -256,7 +286,9 @@ async function builtOutputs(
256
286
  const key = outputKey(artifact.path);
257
287
  const file = artifact.kind === 'entry-point' ? byStem.get(withoutExtension(key)) : undefined;
258
288
  const map = maps.get(`${artifact.path}.map`);
259
- const sources = map === undefined ? undefined : sourcesContentOf(JSON.parse(map), true);
289
+ const parsed: unknown = map === undefined ? undefined : JSON.parse(map);
290
+ const sources = parsed === undefined ? undefined : sourcesContentOf(parsed, true);
291
+ sourcePaths.set(key, await sourcesOnDisk(root, parsed));
260
292
  if (sources === undefined || (artifact.kind === 'entry-point' && file === undefined)) {
261
293
  throw new IslandBuildFailedError({
262
294
  file: file ?? key,
@@ -277,6 +309,7 @@ async function builtOutputs(
277
309
  function entryChunks(
278
310
  files: readonly string[],
279
311
  linked: readonly LinkedFile[],
312
+ sources: ReadonlyMap<string, SourcePaths>,
280
313
  ): readonly IslandChunk[] {
281
314
  const shared = new Map(
282
315
  linked
@@ -312,6 +345,7 @@ function entryChunks(
312
345
  // different files at one address. `bytes` is measured on THAT code, never on this build's.
313
346
  ...stableChunk(file, entry.identity, entry.code),
314
347
  imports: closure(entry.imports),
348
+ sources: sources.get(entry.path) ?? [],
315
349
  });
316
350
  }
317
351
  return chunks;
@@ -321,6 +355,7 @@ function entryChunks(
321
355
  function sharedChunks(
322
356
  chunks: readonly IslandChunk[],
323
357
  linked: readonly LinkedFile[],
358
+ sources: ReadonlyMap<string, SourcePaths>,
324
359
  ): readonly SharedChunk[] {
325
360
  const out: SharedChunk[] = [];
326
361
  for (const one of linked) {
@@ -333,6 +368,7 @@ function sharedChunks(
333
368
  .filter((chunk) => chunk.imports.includes(url))
334
369
  .map((chunk) => chunk.file)
335
370
  .sort(),
371
+ sources: sources.get(one.path) ?? [],
336
372
  });
337
373
  }
338
374
  return out.sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
@@ -357,20 +393,9 @@ export interface BuildIslandsOptions {
357
393
  readonly sharedChunks?: boolean;
358
394
  }
359
395
 
360
- /**
361
- * `islands.sharedChunks` as `defineConfig` validated it — `false` for an app with no config file or
362
- * none of it. Structural, never `instanceof`, for `loadNavigation`'s reason.
363
- */
396
+ /** `islands.sharedChunks` off the one loader (`app-config-load.ts`) — `false` with no config file. */
364
397
  export async function loadSharedChunks(root: string): Promise<boolean> {
365
- const configPath = join(root, APP_CONFIG_FILE);
366
- if (!(await Bun.file(configPath).exists())) return false;
367
- const config: unknown = ((await import(configPath)) as Record<string, unknown>)[
368
- APP_CONFIG_EXPORT
369
- ];
370
- if (typeof config !== 'object' || config === null) return false;
371
- const islands: unknown = (config as Record<string, unknown>)['islands'];
372
- if (typeof islands !== 'object' || islands === null) return false;
373
- return (islands as Record<string, unknown>)['sharedChunks'] === true;
398
+ return (await loadAppConfig(root))?.islands.sharedChunks ?? false;
374
399
  }
375
400
 
376
401
  /** Build every island in the app. An app with none returns an empty bundle and costs one glob. */
@@ -385,43 +410,15 @@ export async function buildIslands(
385
410
  // an empty bundle here would surface two steps later, as a chunk table with no entry for a file
386
411
  // the caller can see on disk.
387
412
  if (only !== undefined && files.length === 0) throw onlyMissing(only, discovered);
413
+ const runtimes = runtimeBuilder(root, ISLAND_BASE_PATH);
388
414
  if (options.sharedChunks ?? (await loadSharedChunks(root))) {
389
- const built = await buildAll(root, files, true);
415
+ const built = await buildAll(root, files, true, runtimes);
390
416
  return islandBundle(built.chunks, built.shared);
391
417
  }
392
- // One build per island, splitting off: each chunk is self-contained, so its size is the whole
393
- // answer to "what does booting this island cost?" and nothing is shaken for another island.
394
- const solo = await Promise.all(files.map((file) => buildAll(root, [file], false)));
395
- return islandBundle(solo.flatMap((one) => one.chunks));
396
- }
397
-
398
- /**
399
- * Same code as an unbuildable `src`: "this path cannot become a client entry" is one condition.
400
- *
401
- * Two fixes, because there are two causes and only one of them can be repaired by naming a path.
402
- * The line was `pass only: '<app-root-relative path>.island.tsx'` — a placeholder nobody can run,
403
- * which no gate could see: `fixProblem` fails a fix only for ADVICE with no command token, and a
404
- * sentence with neither is not advice. Both forms below are constructed from what the caller
405
- * already handed in, so neither can name a path this app does not have.
406
- */
407
- function onlyMissing(only: string, discovered: readonly string[]): IslandInvalidError {
408
- const cause =
409
- `buildIslands was asked for ${JSON.stringify(only)} alone, which is not one of the ` +
410
- `${discovered.length} islands this app has (${discovered.length === 0 ? 'none' : discovered.join(', ')})`;
411
- // The basename match first: a filter that misses normally missed on the PREFIX — a route-relative
412
- // specifier where `discoverIslands`' app-root-relative path was wanted — and the filename
413
- // survives that. Falling back to the first keeps the fix a real path rather than a shape.
414
- const nearest =
415
- discovered.find((file) => posix.basename(file) === posix.basename(only)) ?? discovered[0];
416
- // An app with no islands cannot be pointed at one, so the fix WRITES the file that was asked
417
- // for — the same command `entryMissing` hands back, split off the same path.
418
- if (nearest === undefined) {
419
- return new IslandInvalidError(
420
- cause,
421
- `x g island ${posix.basename(only, ISLAND_EXTENSION)} --at ${posix.dirname(only)}`,
422
- );
423
- }
424
- return new IslandInvalidError(cause, `buildIslands(root, { only: '${nearest}' })`);
418
+ // One build per island, splitting off: each chunk is self-contained but for the page runtime,
419
+ // the ONE file beside it every realtime island loads, counted once however many load it.
420
+ const solo = await Promise.all(files.map((file) => buildAll(root, [file], false, runtimes)));
421
+ return islandBundle(...mergeBuilds(solo));
425
422
  }
426
423
 
427
424
  export function islandBundle(
@@ -449,25 +446,6 @@ export function islandBundle(
449
446
  };
450
447
  }
451
448
 
452
- /**
453
- * The specifier named no file the build could bundle. `X_ISLAND_INVALID` is render's and is
454
- * borrowed rather than renamed here: "this src cannot become a client entry" is the condition that
455
- * code already means, and a second name for it is a second thing to look up.
456
- */
457
- function entryMissing(
458
- routeFile: string,
459
- src: string,
460
- target: string,
461
- chunks: readonly IslandChunk[],
462
- ): IslandInvalidError {
463
- const known = chunks.map((chunk) => chunk.file);
464
- return new IslandInvalidError(
465
- `${routeFile} declares island src ${JSON.stringify(src)}, which resolves to ${target} — a ` +
466
- `file this build did not bundle (${known.length === 0 ? 'it found no islands at all' : `it found ${known.join(', ')}`})`,
467
- `x g island ${posix.basename(target, ISLAND_EXTENSION)} --at ${posix.dirname(target)}`,
468
- );
469
- }
470
-
471
449
  /** Write every chunk under the static export, at the same URL the documents already carry. */
472
450
  export async function writeIslands(bundle: IslandBundle, out: string): Promise<void> {
473
451
  for (const chunk of [...bundle.chunks, ...bundle.shared]) {
@@ -265,7 +265,7 @@ export async function captureIslandState(
265
265
  state: target.state,
266
266
  theme: target.theme,
267
267
  reason: `produced ${bytes.byteLength} bytes, under the ${floor}-byte floor — that is not an image`,
268
- fix: `x shot --island ${target.name} --browser /usr/bin/chromium --json`,
268
+ fix: `x shot --island ${target.name} --browser /usr/bin/chromium --json # macOS: --browser "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; Windows: --browser "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"`,
269
269
  });
270
270
  }
271
271
  await Bun.write(join(options.outDir, target.file), bytes);
@@ -0,0 +1,57 @@
1
+ // What a module bundled twice costs the build. A framework bug refuses it: one file under two
2
+ // spellings, or a second `@ultimat3/*` copy, is the framework's resolver or dedupe failing. A
3
+ // third-party package installed twice with identical bytes is the app's lockfile, which the app
4
+ // did not write by hand — so it is a warning naming the bytes, never a broken build.
5
+
6
+ import { logger, UltimateError } from '@ultimat3/core';
7
+ import type { DuplicatedModule, IslandMetafile, ModuleIdentityIo } from './island-duplicates';
8
+ import { duplicatedModules, hostIo } from './island-duplicates';
9
+ import { quoteArg } from './shell-quote';
10
+
11
+ /** The framework's own scope: a second copy of one of these is ours to fold (`island-package-dedupe.ts`). */
12
+ const FRAMEWORK_SCOPE = '@ultimat3/';
13
+
14
+ /** A duplicate only the framework can have caused. */
15
+ export const isFrameworkDuplicate = (one: DuplicatedModule): boolean =>
16
+ one.sameFile || (one.packageName?.startsWith(FRAMEWORK_SCOPE) ?? false);
17
+
18
+ /**
19
+ * Its own code, not `X_BUILD_FAILED`: that one's fix re-runs `bun build`, which SUCCEEDS here — the
20
+ * build did not fail, it shipped one module twice and said `success: true`. Every refusal is a
21
+ * framework defect, so the fix is the report that gets it fixed.
22
+ */
23
+ export class IslandModuleDuplicatedError extends UltimateError {
24
+ constructor(island: string, found: readonly DuplicatedModule[]) {
25
+ const wasted = found.reduce((sum, one) => sum + one.wastedBytes, 0);
26
+ const listed = found
27
+ .slice(0, 3)
28
+ .map((one) => `${one.package ?? 'one file'} as ${one.spellings.join(' and ')}`)
29
+ .join('; ');
30
+ super({
31
+ code: 'X_ISLAND_MODULE_DUPLICATED',
32
+ cause:
33
+ `${island} bundles ${found.length} module(s) more than once in ${found[0]?.output ?? 'its chunk'} ` +
34
+ `(${wasted} B shipped twice): ${listed}${found.length > 3 ? `; and ${found.length - 3} more` : ''}`,
35
+ fix: `gh issue create --repo developerz-ai/ultimate --title ${quoteArg(`X_ISLAND_MODULE_DUPLICATED in ${island}`)} — paste this error's --json as the body: the framework bundled one of its own modules twice`,
36
+ });
37
+ }
38
+ }
39
+
40
+ /** The one line a third-party copy costs: the island, the package, the bytes, and what to run. */
41
+ export const thirdPartyWarning = (island: string, one: DuplicatedModule): string =>
42
+ `X_ISLAND_MODULE_DUPLICATED (warning): ${island} bundles ${one.package ?? 'a package'} twice ` +
43
+ `(${one.wastedBytes} B shipped twice, as ${one.spellings.join(' and ')}) — fix: bun why ` +
44
+ `${quoteArg(one.packageName ?? '<package>')}, then bun install --force to fold the copies`;
45
+
46
+ /** Refuse a framework duplicate; warn about each third-party one. */
47
+ export async function refuseDuplicatedModules(
48
+ island: string,
49
+ metafile: IslandMetafile,
50
+ io: ModuleIdentityIo = hostIo(),
51
+ warn: (line: string) => void = (line) => logger.warn(line),
52
+ ): Promise<void> {
53
+ const found = await duplicatedModules(metafile, io);
54
+ const fatal = found.filter(isFrameworkDuplicate);
55
+ if (fatal.length > 0) throw new IslandModuleDuplicatedError(island, fatal);
56
+ for (const one of found) warn(thirdPartyWarning(island, one));
57
+ }
@@ -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
+ }