@ultimat3/cli 24.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 (242) 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 +64 -85
  130. package/src/role-sync.ts +65 -6
  131. package/src/root-env.ts +157 -28
  132. package/src/route-islands.ts +69 -0
  133. package/src/runtime-admin.ts +4 -1
  134. package/src/runtime-assets.ts +15 -2
  135. package/src/runtime-bindings.ts +23 -3
  136. package/src/runtime-cache.ts +8 -8
  137. package/src/runtime-hooks.ts +2 -1
  138. package/src/runtime-isr-outcome.ts +100 -0
  139. package/src/runtime-isr.ts +2 -2
  140. package/src/runtime-jobs.ts +78 -18
  141. package/src/runtime-live-feed.ts +1 -1
  142. package/src/runtime-mail.ts +26 -0
  143. package/src/runtime-mfa-warning.ts +2 -2
  144. package/src/runtime-overrides.ts +3 -3
  145. package/src/runtime-purge.ts +1 -1
  146. package/src/runtime-queue.ts +12 -12
  147. package/src/runtime-realtime.ts +5 -3
  148. package/src/runtime-render.ts +77 -82
  149. package/src/runtime-replica.ts +3 -8
  150. package/src/runtime-services.ts +33 -13
  151. package/src/runtime-storage-upload.ts +2 -2
  152. package/src/runtime-storage.ts +3 -3
  153. package/src/schema-diff.ts +1 -1
  154. package/src/secrets-rotation.ts +8 -9
  155. package/src/seo-routes.ts +2 -2
  156. package/src/serve-boot.ts +22 -11
  157. package/src/serve-drain.ts +3 -1
  158. package/src/serve-retire.ts +95 -0
  159. package/src/serve-web.ts +19 -8
  160. package/src/serve.ts +27 -10
  161. package/src/server-barrels.ts +6 -3
  162. package/src/shot-browser.ts +1 -1
  163. package/src/shot-locale.ts +6 -15
  164. package/src/shot-settle.ts +3 -3
  165. package/src/shot-theme.ts +1 -1
  166. package/src/shot-viewport.ts +152 -0
  167. package/src/signal-shred.ts +26 -12
  168. package/src/site-asset-routes.ts +2 -2
  169. package/src/site-seo.ts +6 -4
  170. package/src/source-files.ts +13 -3
  171. package/src/static-report.ts +5 -0
  172. package/src/style-routes.ts +2 -2
  173. package/src/sw-artifacts.ts +8 -8
  174. package/src/sw-precache-plan.ts +34 -4
  175. package/src/sync-authenticator.ts +3 -3
  176. package/src/sync-url.ts +1 -1
  177. package/src/templates/action.ts +4 -4
  178. package/src/templates/backfill.ts +4 -4
  179. package/src/templates/entity.ts +2 -2
  180. package/src/templates/github/ci.yml.ts +28 -18
  181. package/src/templates/github/image.yml.ts +92 -0
  182. package/src/templates/github/runner.ts +18 -0
  183. package/src/templates/guard-bare-error.ts +16 -2
  184. package/src/templates/guard-unzoned-date.ts +56 -6
  185. package/src/templates/index.ts +1 -1
  186. package/src/templates/job-body-test.ts +2 -2
  187. package/src/templates/job.ts +6 -6
  188. package/src/templates/query.ts +9 -8
  189. package/src/templates/resource-page.ts +9 -7
  190. package/src/templates/resource-service.ts +2 -2
  191. package/src/templates/route.ts +23 -8
  192. package/src/templates/scaffold-api.ts +2 -3
  193. package/src/templates/scaffold-app.ts +2 -1
  194. package/src/templates/scaffold-auth.ts +205 -70
  195. package/src/templates/scaffold-bin.ts +122 -0
  196. package/src/templates/scaffold-claude-commands.ts +11 -11
  197. package/src/templates/scaffold-container.ts +12 -6
  198. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  199. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  200. package/src/templates/scaffold-dashboard-example.ts +112 -172
  201. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  202. package/src/templates/scaffold-dashboard.ts +9 -5
  203. package/src/templates/scaffold-db-package.ts +2 -2
  204. package/src/templates/scaffold-docs.ts +20 -77
  205. package/src/templates/scaffold-domain-package.ts +0 -4
  206. package/src/templates/scaffold-entries.ts +4 -3
  207. package/src/templates/scaffold-env.ts +6 -3
  208. package/src/templates/scaffold-gitattributes.ts +59 -0
  209. package/src/templates/scaffold-helm-templates.ts +74 -18
  210. package/src/templates/scaffold-helm.ts +31 -4
  211. package/src/templates/scaffold-http.ts +1 -1
  212. package/src/templates/scaffold-i18n.ts +15 -2
  213. package/src/templates/scaffold-icon.ts +2 -2
  214. package/src/templates/scaffold-repo.ts +18 -15
  215. package/src/templates/scaffold-shell.ts +17 -2
  216. package/src/test-only-shipped.ts +193 -0
  217. package/src/test-select.ts +2 -1
  218. package/src/test-shards.ts +1 -4
  219. package/src/theme-boot.ts +2 -2
  220. package/src/theme-brand.ts +66 -0
  221. package/src/trusted-hops.ts +49 -0
  222. package/src/unscanned-admin.ts +2 -2
  223. package/src/verify-checks.ts +12 -2
  224. package/src/verify-coverage-run.ts +3 -2
  225. package/src/verify-deadline.ts +87 -10
  226. package/src/verify-merge.ts +48 -16
  227. package/src/verify-part-step.ts +10 -2
  228. package/src/verify-role-load-probe.ts +12 -2
  229. package/src/verify-role-load.ts +9 -4
  230. package/src/verify-run.ts +3 -0
  231. package/src/verify-step.ts +2 -0
  232. package/src/verify-test-run.ts +3 -2
  233. package/src/verify-tests.ts +0 -1
  234. package/src/worker-bundle.ts +2 -2
  235. package/src/workspace-checks.ts +53 -7
  236. package/src/workspace-graph.ts +3 -2
  237. package/src/write-line.ts +82 -7
  238. package/src/browser-launcher-fake-html.ts +0 -80
  239. package/src/browser-launcher-fake.ts +0 -168
  240. package/src/jobs-drain.ts +0 -230
  241. package/src/scaffold-typecheck.ts +0 -227
  242. package/src/thrown-by.ts +0 -24
@@ -18,12 +18,13 @@ import {
18
18
  pluralVariantsOf,
19
19
  registeredLocales,
20
20
  } from '@ultimat3/i18n';
21
+ import { APP_CATALOGS_PATH } from '@ultimat3/i18n/app-catalogs';
21
22
  import { mountedAdminKeys } from './admin-catalog-keys';
23
+ import type { LoadedApp } from './app-load';
22
24
  import { loadApp } from './app-load';
23
25
  import type { DuplicateProbe } from './duplicate-packages';
24
26
  import { duplicateCause, duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
25
27
  import { auditApp } from './i18n-audit';
26
- import { I18N_INDEX_PATH } from './i18n-index';
27
28
  import type { Finding } from './output';
28
29
  import { findingFrom } from './output';
29
30
  import { CATALOG_ROOT, catalogPath } from './templates/locales';
@@ -37,10 +38,7 @@ const I18N_PKG = '@ultimat3/i18n';
37
38
  * can resolve `@ultimat3/*`. `loadApp` is the production value, and it is the same call
38
39
  * `serveApp` makes: asking a different loader than the server uses would prove nothing.
39
40
  */
40
- export type AppLoader = (root: string) => Promise<{
41
- readonly findings: readonly Finding[];
42
- readonly defaultLocale: string;
43
- }>;
41
+ export type AppLoader = (root: string) => Promise<Pick<LoadedApp, 'findings' | 'locales'>>;
44
42
 
45
43
  export interface RegistrationInput {
46
44
  readonly root: string;
@@ -121,9 +119,9 @@ export async function checkRegistration(input: RegistrationInput): Promise<Regis
121
119
  let locales = gaps.length;
122
120
 
123
121
  if (Object.keys(input.catalogs).length === 0) {
124
- const unresolved = unresolvedUsedKeys(input, app.defaultLocale);
122
+ const unresolved = unresolvedUsedKeys(input, app.locales.defaultLocale);
125
123
  if (unresolved.length > 0) {
126
- findings.push(findingFrom(catalogsNeverRegistered(app.defaultLocale, unresolved)));
124
+ findings.push(findingFrom(catalogsNeverRegistered(app.locales.defaultLocale, unresolved)));
127
125
  unregistered += unresolved.length;
128
126
  locales += 1;
129
127
  }
@@ -182,7 +180,7 @@ function adminKeysFinding(locale: Locale, missing: readonly string[]): Finding {
182
180
  * the half that knows what an app's directories are.
183
181
  */
184
182
  async function indexSource(root: string): Promise<string | undefined> {
185
- const file = Bun.file(join(root, I18N_INDEX_PATH));
183
+ const file = Bun.file(join(root, APP_CATALOGS_PATH));
186
184
  return (await file.exists()) ? file.text() : undefined;
187
185
  }
188
186
 
@@ -208,7 +206,7 @@ export function unregisteredFix(
208
206
  // report a registered locale as unregistered, which is the direction that costs trust.
209
207
  if (index.includes(`${CATALOG_ROOT.split('/').pop() ?? 'catalogs'}/${locale}.json`)) return {};
210
208
  return {
211
- fix: `x i18n sync ${locale} # re-derives ${I18N_INDEX_PATH} from the catalogs on disk`,
209
+ fix: `x i18n sync ${locale} # re-derives ${APP_CATALOGS_PATH} from the catalogs on disk`,
212
210
  };
213
211
  }
214
212
 
@@ -42,12 +42,36 @@ export function scanRuntimeImports(file: ScannedFile): readonly string[] {
42
42
  .map((entry) => entry.path);
43
43
  }
44
44
 
45
+ /**
46
+ * A type-position `import('…')` — `typeof import('@ultimat3/cli')`, `let x: import('a').B` — is no
47
+ * import statement, so the transpiler erases it whole and neither pass above sees it (K16: the tier
48
+ * rule passed one). Read off `masked`, the source with comments and string CONTENTS blanked and
49
+ * delimiters kept (`@ultimat3/core`'s `maskLiterals`): a match there is code, and the specifier is
50
+ * read back from `source` at the same offsets. Taken as an argument because this file is a leaf.
51
+ * A dynamic `import(x)` is reported too, which the runtime pass already holds — callers dedupe.
52
+ */
53
+ const IMPORT_CALL = /(?<![\w$.])import\s*\(\s*(['"])/g;
54
+
55
+ export function typePositionImports(source: string, masked: string): readonly string[] {
56
+ const found: string[] = [];
57
+ for (const match of masked.matchAll(IMPORT_CALL)) {
58
+ const quote = match[1] as string;
59
+ const open = match.index + match[0].length;
60
+ const close = masked.indexOf(quote, open);
61
+ if (close === -1) continue;
62
+ const specifier = source.slice(open, close);
63
+ if (specifier !== '' && !specifier.includes('\n')) found.push(specifier);
64
+ }
65
+ return found;
66
+ }
67
+
45
68
  /**
46
69
  * Every specifier the file names, type-only ones included. Throws when the file does not parse —
47
70
  * a scan that answered `[]` there would read as a file importing nothing. The rewrite pass alone
48
- * is forgiving: a rewrite the parser refuses falls back to the runtime scan.
71
+ * is forgiving: a rewrite the parser refuses falls back to the runtime scan. Hand it `masked`
72
+ * (core's `maskLiterals(file.source)`) and a type-position `import('…')` is read as well.
49
73
  */
50
- export function scanAllImports(file: ScannedFile): readonly string[] {
74
+ export function scanAllImports(file: ScannedFile, masked?: string): readonly string[] {
51
75
  const runtime = scanRuntimeImports(file);
52
76
  let typed: readonly string[] = [];
53
77
  try {
@@ -58,6 +82,7 @@ export function scanAllImports(file: ScannedFile): readonly string[] {
58
82
  } catch {
59
83
  typed = [];
60
84
  }
85
+ const inTypes = masked === undefined ? [] : typePositionImports(file.source, masked);
61
86
  // The rewritten pass first: it holds every import in SOURCE order, which callers report in.
62
- return [...new Set([...typed, ...runtime])];
87
+ return [...new Set([...typed, ...inTypes, ...runtime])];
63
88
  }
package/src/index.ts CHANGED
@@ -115,7 +115,6 @@ export { ICON_BASE_PATH, ICON_SOURCE } from './icon-assets';
115
115
  // `x build`'s and `x dev`'s wiring, and every name here is a semver promise forever.
116
116
  export type { IslandChunk, SharedChunk } from './island-bundle';
117
117
  export { buildIslands } from './island-bundle';
118
- export { JOB_STATES } from './jobs-report';
119
118
  export { renderJobTable } from './jobs-table';
120
119
  export { msg } from './messages';
121
120
  export { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
@@ -123,7 +122,7 @@ export { parseMigrationSql, readMigrations } from './migrations';
123
122
  export type { CommandResult, Finding, JsonValue } from './output';
124
123
  export { exitCodeFor, findingFrom, render, renderFinding, renderHuman } from './output';
125
124
  export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
126
- export { flagBool, flagList, flagString, GLOBAL_FLAGS, nearest, parseArgs } from './parse';
125
+ export { flagBool, flagList, flagString, GLOBAL_FLAGS, parseArgs } from './parse';
127
126
  export type { PrerenderedPage, PrerenderReport } from './prerender';
128
127
  export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
129
128
  export type { PwaArtifacts } from './pwa-artifacts';
@@ -169,7 +168,6 @@ export { readVerifyFloor, skippedSuiteFinding, VERIFY_FLOOR_FILE } from './verif
169
168
  export type { HostCheck, StepOutcome, VerifyStep, VerifyStepName } from './verify-step';
170
169
  export { VERIFY_STEP_NAMES } from './verify-step';
171
170
  export type { TestType } from './verify-tests';
172
- export { TEST_TYPES } from './verify-tests';
173
171
  export {
174
172
  checkFileSizes,
175
173
  checkLockstep,
@@ -15,25 +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';
24
+ import { ISLAND_EXTENSION, islandModuleId } from '@ultimat3/render';
25
25
  import { loadAppConfig } from './app-config-load';
26
26
  import { IslandBuildFailedError } from './errors';
27
+ import { refuseDuplicatedModules } from './island-duplicate-refusal';
28
+ import { entryMissing, onlyMissing } from './island-entry-missing';
27
29
  import { describeBuildError, sourcesContentOf, stableChunk, stripDebugId } from './island-identity';
28
30
  import type { BuiltOutput, LinkedFile } from './island-link';
29
31
  import { linkOutputs, sharedChunkName } from './island-link';
32
+ import { frameworkDedupePlugins } from './island-package-dedupe';
30
33
  import {
31
34
  islandRealtimePlugin,
32
35
  REALTIME_ISLAND_ENTRY,
33
36
  reachesRealtime,
34
37
  realtimeIslandEntry,
35
38
  } from './island-realtime';
39
+ import type { Runtimes } from './island-runtime';
40
+ import { loadingRuntime, mergeBuilds, runtimeBuilder, runtimeShared } from './island-runtime';
36
41
  import { solidDedupePlugin } from './island-solid-dedupe';
42
+ import type { SourcePaths } from './island-sources';
43
+ import { sourcesOnDisk } from './island-sources';
37
44
  import { islandStylesPlugin } from './island-styles';
38
45
  import { hasPathSegment } from './path-segments';
39
46
  import { solidJsxPlugin } from './solid-loader';
@@ -70,6 +77,8 @@ export interface IslandChunk {
70
77
  * beyond `bytes`, what a precached page must carry with it. Empty for an island sharing nothing.
71
78
  */
72
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;
73
82
  }
74
83
 
75
84
  /** A module two or more islands import, served once beside them. */
@@ -80,6 +89,7 @@ export interface SharedChunk {
80
89
  readonly bytes: number;
81
90
  /** The islands (app-root-relative) that load it — for a finding that names what an author edits. */
82
91
  readonly importers: readonly string[];
92
+ readonly sources?: SourcePaths;
83
93
  }
84
94
 
85
95
  export interface IslandBundle {
@@ -119,6 +129,7 @@ async function buildAll(
119
129
  root: string,
120
130
  files: readonly string[],
121
131
  splitting: boolean,
132
+ runtimes: Runtimes,
122
133
  ): Promise<{ readonly chunks: readonly IslandChunk[]; readonly shared: readonly SharedChunk[] }> {
123
134
  if (files.length === 0) return { chunks: [], shared: [] };
124
135
  // Inside the refusal: the realtime probe PARSES the island's graph, and a file that will not
@@ -134,6 +145,7 @@ async function buildAll(
134
145
  // Only an island whose own graph reaches `@ultimat3/realtime` is wrapped (`island-realtime.ts`);
135
146
  // every other one is built from its own file, byte for byte what it was.
136
147
  const live = files.filter((_, index) => realtime[index] === true);
148
+ const runtime = live[0] === undefined ? undefined : await runtimes(live[0]);
137
149
  const entrypoints = files.map((file, index) =>
138
150
  realtime[index] === true ? realtimeIslandEntry(file) : join(root, file),
139
151
  );
@@ -161,10 +173,13 @@ async function buildAll(
161
173
  //
162
174
  // The dedupe goes FIRST: it answers `solid-js` specifiers before either plugin loads a file,
163
175
  // so the `solid-js/web` helpers the JSX transform writes into a symlinked package resolve
164
- // 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.
165
179
  plugins: [
166
- ...(live[0] === undefined ? [] : [islandRealtimePlugin(root, live[0])]),
180
+ ...(runtime === undefined ? [] : [islandRealtimePlugin(root, runtime)]),
167
181
  solidDedupePlugin(root),
182
+ ...frameworkDedupePlugins(root),
168
183
  solidJsxPlugin,
169
184
  islandStylesPlugin,
170
185
  ],
@@ -187,9 +202,13 @@ async function buildAll(
187
202
  // output has. See `graphHash`. Measured on 1.4.0 against a 131 kB island: 277ms with it and
188
203
  // 276ms without, so the map costs nothing worth naming.
189
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,
190
209
  });
191
210
  } catch (error) {
192
- throw await blame(root, files, error, splitting);
211
+ throw await blame(root, files, error, splitting, runtimes);
193
212
  }
194
213
  if (!built.success) {
195
214
  throw new IslandBuildFailedError({
@@ -197,9 +216,18 @@ async function buildAll(
197
216
  logs: built.logs.map((log) => String(log)).join('; '),
198
217
  });
199
218
  }
200
- const linked = linkOutputs(await builtOutputs(files, built.outputs));
201
- const chunks = entryChunks(files, linked);
202
- 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)] };
203
231
  }
204
232
 
205
233
  /**
@@ -212,11 +240,12 @@ async function blame(
212
240
  files: readonly string[],
213
241
  error: unknown,
214
242
  splitting: boolean,
243
+ runtimes: Runtimes,
215
244
  ): Promise<IslandBuildFailedError> {
216
245
  if (files.length > 1) {
217
246
  for (const file of files) {
218
247
  try {
219
- await buildAll(root, [file], splitting);
248
+ await buildAll(root, [file], splitting, runtimes);
220
249
  } catch (alone) {
221
250
  if (alone instanceof IslandBuildFailedError) return alone;
222
251
  }
@@ -241,8 +270,10 @@ const withoutExtension = (file: string): string =>
241
270
 
242
271
  /** Every code output paired with its source map's inputs and, for an entry, its island. */
243
272
  async function builtOutputs(
273
+ root: string,
244
274
  files: readonly string[],
245
275
  artifacts: readonly Bun.BuildArtifact[],
276
+ sourcePaths: Map<string, SourcePaths>,
246
277
  ): Promise<readonly BuiltOutput[]> {
247
278
  const byStem = new Map(files.map((file) => [withoutExtension(file), file]));
248
279
  const maps = new Map<string, string>();
@@ -255,7 +286,9 @@ async function builtOutputs(
255
286
  const key = outputKey(artifact.path);
256
287
  const file = artifact.kind === 'entry-point' ? byStem.get(withoutExtension(key)) : undefined;
257
288
  const map = maps.get(`${artifact.path}.map`);
258
- 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));
259
292
  if (sources === undefined || (artifact.kind === 'entry-point' && file === undefined)) {
260
293
  throw new IslandBuildFailedError({
261
294
  file: file ?? key,
@@ -276,6 +309,7 @@ async function builtOutputs(
276
309
  function entryChunks(
277
310
  files: readonly string[],
278
311
  linked: readonly LinkedFile[],
312
+ sources: ReadonlyMap<string, SourcePaths>,
279
313
  ): readonly IslandChunk[] {
280
314
  const shared = new Map(
281
315
  linked
@@ -311,6 +345,7 @@ function entryChunks(
311
345
  // different files at one address. `bytes` is measured on THAT code, never on this build's.
312
346
  ...stableChunk(file, entry.identity, entry.code),
313
347
  imports: closure(entry.imports),
348
+ sources: sources.get(entry.path) ?? [],
314
349
  });
315
350
  }
316
351
  return chunks;
@@ -320,6 +355,7 @@ function entryChunks(
320
355
  function sharedChunks(
321
356
  chunks: readonly IslandChunk[],
322
357
  linked: readonly LinkedFile[],
358
+ sources: ReadonlyMap<string, SourcePaths>,
323
359
  ): readonly SharedChunk[] {
324
360
  const out: SharedChunk[] = [];
325
361
  for (const one of linked) {
@@ -332,6 +368,7 @@ function sharedChunks(
332
368
  .filter((chunk) => chunk.imports.includes(url))
333
369
  .map((chunk) => chunk.file)
334
370
  .sort(),
371
+ sources: sources.get(one.path) ?? [],
335
372
  });
336
373
  }
337
374
  return out.sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
@@ -373,43 +410,15 @@ export async function buildIslands(
373
410
  // an empty bundle here would surface two steps later, as a chunk table with no entry for a file
374
411
  // the caller can see on disk.
375
412
  if (only !== undefined && files.length === 0) throw onlyMissing(only, discovered);
413
+ const runtimes = runtimeBuilder(root, ISLAND_BASE_PATH);
376
414
  if (options.sharedChunks ?? (await loadSharedChunks(root))) {
377
- const built = await buildAll(root, files, true);
415
+ const built = await buildAll(root, files, true, runtimes);
378
416
  return islandBundle(built.chunks, built.shared);
379
417
  }
380
- // One build per island, splitting off: each chunk is self-contained, so its size is the whole
381
- // answer to "what does booting this island cost?" and nothing is shaken for another island.
382
- const solo = await Promise.all(files.map((file) => buildAll(root, [file], false)));
383
- return islandBundle(solo.flatMap((one) => one.chunks));
384
- }
385
-
386
- /**
387
- * Same code as an unbuildable `src`: "this path cannot become a client entry" is one condition.
388
- *
389
- * Two fixes, because there are two causes and only one of them can be repaired by naming a path.
390
- * The line was `pass only: '<app-root-relative path>.island.tsx'` — a placeholder nobody can run,
391
- * which no gate could see: `fixProblem` fails a fix only for ADVICE with no command token, and a
392
- * sentence with neither is not advice. Both forms below are constructed from what the caller
393
- * already handed in, so neither can name a path this app does not have.
394
- */
395
- function onlyMissing(only: string, discovered: readonly string[]): IslandInvalidError {
396
- const cause =
397
- `buildIslands was asked for ${JSON.stringify(only)} alone, which is not one of the ` +
398
- `${discovered.length} islands this app has (${discovered.length === 0 ? 'none' : discovered.join(', ')})`;
399
- // The basename match first: a filter that misses normally missed on the PREFIX — a route-relative
400
- // specifier where `discoverIslands`' app-root-relative path was wanted — and the filename
401
- // survives that. Falling back to the first keeps the fix a real path rather than a shape.
402
- const nearest =
403
- discovered.find((file) => posix.basename(file) === posix.basename(only)) ?? discovered[0];
404
- // An app with no islands cannot be pointed at one, so the fix WRITES the file that was asked
405
- // for — the same command `entryMissing` hands back, split off the same path.
406
- if (nearest === undefined) {
407
- return new IslandInvalidError(
408
- cause,
409
- `x g island ${posix.basename(only, ISLAND_EXTENSION)} --at ${posix.dirname(only)}`,
410
- );
411
- }
412
- 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));
413
422
  }
414
423
 
415
424
  export function islandBundle(
@@ -437,25 +446,6 @@ export function islandBundle(
437
446
  };
438
447
  }
439
448
 
440
- /**
441
- * The specifier named no file the build could bundle. `X_ISLAND_INVALID` is render's and is
442
- * borrowed rather than renamed here: "this src cannot become a client entry" is the condition that
443
- * code already means, and a second name for it is a second thing to look up.
444
- */
445
- function entryMissing(
446
- routeFile: string,
447
- src: string,
448
- target: string,
449
- chunks: readonly IslandChunk[],
450
- ): IslandInvalidError {
451
- const known = chunks.map((chunk) => chunk.file);
452
- return new IslandInvalidError(
453
- `${routeFile} declares island src ${JSON.stringify(src)}, which resolves to ${target} — a ` +
454
- `file this build did not bundle (${known.length === 0 ? 'it found no islands at all' : `it found ${known.join(', ')}`})`,
455
- `x g island ${posix.basename(target, ISLAND_EXTENSION)} --at ${posix.dirname(target)}`,
456
- );
457
- }
458
-
459
449
  /** Write every chunk under the static export, at the same URL the documents already carry. */
460
450
  export async function writeIslands(bundle: IslandBundle, out: string): Promise<void> {
461
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
+ }