@ultimat3/cli 19.1.2 → 19.2.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.
package/src/prerender.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // only which routes qualify and where the bytes land.
5
5
 
6
6
  import { join } from 'node:path';
7
- import { createContext, renderThrowable, runWithContext } from '@ultimat3/core';
7
+ import { createContext, isUltimateError, renderThrowable, runWithContext } from '@ultimat3/core';
8
8
  import type { RouteEntry } from '@ultimat3/render';
9
9
  import { describeRoutes, routeEntries } from '@ultimat3/render';
10
10
  import { renderStatic } from '@ultimat3/render/server';
@@ -21,6 +21,7 @@ import { measurementActor } from './measurement-actor';
21
21
  import { loadPwaArtifacts, WEB_MANIFEST_PATH, writePwaIcons } from './pwa-artifacts';
22
22
  import type { SkippedRoute, UnmeasuredRoute } from './static-report';
23
23
  import { skippedRoute, skipReasonFor, writeStaticReport } from './static-report';
24
+ import { styleBundle, writeStyles } from './style-bundle';
24
25
  import { SERVICE_WORKER_PATH, SW_REGISTER_PATH, serviceWorkerArtifacts } from './sw-artifacts';
25
26
 
26
27
  // Re-exported, never re-declared: `static-report.ts` owns the shape because the report on disk
@@ -78,6 +79,8 @@ export interface PrerenderReport {
78
79
  readonly report: string;
79
80
  /** Client entries emitted, one chunk each. Reported so "which JS shipped?" needs no unzip. */
80
81
  readonly islands: readonly string[];
82
+ /** Surface stylesheets emitted, one file each — the CSS half of the same question. */
83
+ readonly styles: readonly string[];
81
84
  /**
82
85
  * What the service worker could not express, and what its precache manifest weighs too much of.
83
86
  *
@@ -144,6 +147,12 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
144
147
  // behind it, so the artifact carries every byte the browser will ask for.
145
148
  const islands = await buildIslands(options.root);
146
149
  await writeIslands(islands, options.out);
150
+ // And the stylesheet every document below LINKS. Derived after the island build on purpose:
151
+ // `islandStylesPlugin` registers an island's own `.module.scss` during `Bun.build`, so a bundle
152
+ // minted before it would hash CSS the documents do not carry — and the export would publish
153
+ // pages whose `<link>` names a file the artifact does not have.
154
+ const styles = styleBundle();
155
+ await writeStyles(styles, options.out);
147
156
  // Same rule, one asset further: a browser asks for `/favicon.ico` on the first page it loads,
148
157
  // and a static export has no route to answer it — so the bytes the served surfaces would have
149
158
  // returned go into the artifact instead of leaving a 404 in every visitor's console.
@@ -172,7 +181,13 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
172
181
  const serviceWorker =
173
182
  pwa === undefined
174
183
  ? undefined
175
- : serviceWorkerArtifacts({ pwa, buildId, routes: describeRoutes(), islands });
184
+ : serviceWorkerArtifacts({
185
+ pwa,
186
+ buildId,
187
+ routes: describeRoutes(),
188
+ islands,
189
+ styles,
190
+ });
176
191
  if (serviceWorker !== undefined) {
177
192
  await Bun.write(join(options.out, SERVICE_WORKER_PATH.slice(1)), serviceWorker.source);
178
193
  await Bun.write(join(options.out, SW_REGISTER_PATH.slice(1)), serviceWorker.register);
@@ -240,7 +255,17 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
240
255
  } catch (error) {
241
256
  // `renderThrowable`, never `String(error)`: this is a caught unknown, and a hostile
242
257
  // `toString` here would take the whole build down instead of one route's measurement.
243
- unmeasured.push({ path: entry.path, reason: renderThrowable(error) });
258
+ // A framework error rides along with its code, cause and fix: `checkBudgets` reports
259
+ // `X_ISLAND_PROPS_INVALID` under its own name, because that sentence — the island, the
260
+ // prop, its bytes — is the finding, and `X_BUDGET_UNMEASURED` pointing at this list was
261
+ // a second command between the author and it.
262
+ unmeasured.push({
263
+ path: entry.path,
264
+ reason: renderThrowable(error),
265
+ ...(isUltimateError(error)
266
+ ? { code: error.code, cause: error.cause, fix: error.fix }
267
+ : {}),
268
+ });
244
269
  }
245
270
  continue;
246
271
  }
@@ -313,6 +338,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
313
338
  stats,
314
339
  report,
315
340
  islands: islands.chunks.map((chunk) => chunk.file),
341
+ styles: styles.chunks.map((chunk) => chunk.url),
316
342
  serviceWorkerWarnings: serviceWorker?.warnings ?? [],
317
343
  };
318
344
  }
package/src/serve.ts CHANGED
@@ -49,6 +49,8 @@ import { readMigrations } from './migrations';
49
49
  import { startOtlpExport } from './otlp-export';
50
50
  import { loadPwaArtifacts } from './pwa-artifacts';
51
51
  import type { RuntimeOverrides } from './runtime-overrides';
52
+ import { styleBundle } from './style-bundle';
53
+ import { styleRoutes } from './style-routes';
52
54
  import { serviceWorkerArtifacts } from './sw-artifacts';
53
55
  import { serviceWorkerRoutes } from './sw-routes';
54
56
 
@@ -329,7 +331,13 @@ async function bootRoles(boot: {
329
331
  const serviceWorker =
330
332
  pwa === undefined
331
333
  ? undefined
332
- : serviceWorkerArtifacts({ pwa, buildId, routes: describeRoutes(), islands });
334
+ : serviceWorkerArtifacts({
335
+ pwa,
336
+ buildId,
337
+ routes: describeRoutes(),
338
+ islands,
339
+ styles: styleBundle(),
340
+ });
333
341
  // The app's own MCP endpoint, through the same call `x dev` makes — see `app-mcp.ts`.
334
342
  const mcpMount = await mountAppMcp(options.root);
335
343
  const routes: readonly Route[] = [
@@ -344,6 +352,9 @@ async function bootRoles(boot: {
344
352
  }),
345
353
  ...storageRoutes({ storage: runtime.storage }),
346
354
  ...islandRoutes(() => islands),
355
+ // The surface stylesheets the documents link. Built from the registry the `loadApp` above
356
+ // filled, so this process serves exactly the CSS it renders against.
357
+ ...styleRoutes(() => styleBundle()),
347
358
  ...appRoutes({
348
359
  buildId,
349
360
  resolveIsland: (file) => islands.resolverFor(file),
@@ -8,6 +8,7 @@ import {
8
8
  cdpUrlFrom,
9
9
  cdpUrlProblem,
10
10
  executablePathFrom,
11
+ ShotChromeMissingError,
11
12
  } from './browser-launcher';
12
13
  import { BadFlagError } from './errors';
13
14
 
@@ -21,7 +22,10 @@ const CDP_FIX = 'x shot / --cdp-url wss://cdp.example.com/session/abc';
21
22
  export interface ShotBrowserChoice {
22
23
  /** Attach here. When set, nothing about a local executable was read. */
23
24
  readonly cdpUrl?: string | undefined;
24
- /** Launch this. Already proved to exist on disk. */
25
+ /**
26
+ * Launch this. Already proved to exist on disk, and never absent alongside an absent `cdpUrl`:
27
+ * a run that reached this type has a browser, because the alternative is refused above.
28
+ */
25
29
  readonly executablePath?: string | undefined;
26
30
  }
27
31
 
@@ -31,6 +35,12 @@ export interface ShotBrowserInput {
31
35
  /** `--browser` as typed, before `PUPPETEER_EXECUTABLE_PATH` / `CHROME_PATH`. */
32
36
  readonly browserFlag?: string | undefined;
33
37
  readonly env: Readonly<Record<string, string | undefined>>;
38
+ /**
39
+ * Test seam: how a path is proved to be on disk. Injected rather than stubbed globally, so the
40
+ * probe below is asserted identically on a machine that has Chrome and on one that does not —
41
+ * an assertion whose verdict depends on the box it runs on is not an assertion.
42
+ */
43
+ readonly exists?: (path: string) => boolean;
34
44
  }
35
45
 
36
46
  /**
@@ -47,8 +57,14 @@ export interface ShotBrowserInput {
47
57
  * the flag IS read.
48
58
  * 3. **On an attach, no executable is read at all.** Checking the filesystem for a binary this run
49
59
  * will never execute is how a correct remote capture gets refused on a box with no Chrome.
60
+ * 4. **A local run with no browser anywhere is refused HERE.** `puppeteer-core` has no bundled
61
+ * browser and no default, so "nothing named" is not "the library finds its own" — it is a throw
62
+ * from inside somebody else's library, after `runShot` has already booted an embedded Postgres,
63
+ * saying ``An `executablePath` or `channel` must be specified``. The same fact is knowable from
64
+ * the environment and four filesystem probes before anything starts.
50
65
  */
51
66
  export function shotBrowserChoice(input: ShotBrowserInput): ShotBrowserChoice {
67
+ const exists = input.exists ?? browserBinaryExists;
52
68
  if (input.cdpFlag !== undefined && input.browserFlag !== undefined) {
53
69
  throw new BadFlagError({
54
70
  flag: 'cdp-url',
@@ -71,8 +87,11 @@ export function shotBrowserChoice(input: ShotBrowserInput): ShotBrowserChoice {
71
87
  }
72
88
  return { cdpUrl };
73
89
  }
74
- const executablePath = executablePathFrom(input.browserFlag, input.env);
75
- if (executablePath !== undefined && !browserBinaryExists(executablePath)) {
90
+ const executablePath = executablePathFrom(input.browserFlag, input.env, exists);
91
+ // Nothing named and nothing probed: not a bad flag, because no flag was typed — a machine with no
92
+ // browser on it, which is a configuration to repair rather than a value to correct.
93
+ if (executablePath === undefined) throw new ShotChromeMissingError();
94
+ if (!exists(executablePath)) {
76
95
  throw new BadFlagError({
77
96
  flag: 'browser',
78
97
  command: 'shot',
@@ -80,5 +99,5 @@ export function shotBrowserChoice(input: ShotBrowserInput): ShotBrowserChoice {
80
99
  fix: 'x shot / --browser /usr/bin/chromium',
81
100
  });
82
101
  }
83
- return executablePath === undefined ? {} : { executablePath };
102
+ return { executablePath };
84
103
  }
@@ -60,6 +60,17 @@ export type UnmeasuredRoute = {
60
60
  /** The DECLARED path, as `X_BUDGET_UNMEASURED`'s `at:` spells it, so the two rows join. */
61
61
  readonly path: string;
62
62
  readonly reason: string;
63
+ /**
64
+ * The throwable's own `code`, `cause` and `fix` when the render failed with an `UltimateError`
65
+ * — absent for a bare `TypeError`. Carried so the `budgets` step can report a failure that IS
66
+ * an instruction under its own code rather than under `X_BUDGET_UNMEASURED`: an island handed
67
+ * props over `ISLAND_PROPS_MAX_BYTES` throws `X_ISLAND_PROPS_INVALID` naming the prop and its
68
+ * bytes, and "run x build, its unmeasured list says why" sent the reader to a second command to
69
+ * read the sentence this build had already composed.
70
+ */
71
+ readonly code?: string;
72
+ readonly cause?: string;
73
+ readonly fix?: string;
63
74
  };
64
75
 
65
76
  /** One HTML file in the artifact, and the declared route that produced it. */
@@ -171,8 +182,17 @@ const isSkipped = (value: unknown): value is SkippedRoute =>
171
182
  typeof value['why'] === 'string' &&
172
183
  inDomain(SKIP_REASONS, value['reason']);
173
184
 
185
+ const optionalString = (value: unknown): boolean =>
186
+ value === undefined || typeof value === 'string';
187
+
188
+ /** The three coded fields are optional TOGETHER on the way in; a present one must be a string. */
174
189
  const isUnmeasured = (value: unknown): value is UnmeasuredRoute =>
175
- isRecord(value) && typeof value['path'] === 'string' && typeof value['reason'] === 'string';
190
+ isRecord(value) &&
191
+ typeof value['path'] === 'string' &&
192
+ typeof value['reason'] === 'string' &&
193
+ optionalString(value['code']) &&
194
+ optionalString(value['cause']) &&
195
+ optionalString(value['fix']);
176
196
 
177
197
  const isEmitted = (value: unknown): value is EmittedPage =>
178
198
  isRecord(value) &&
@@ -0,0 +1,124 @@
1
+ // The surface stylesheet table: the CSS a document on `site/` or `app/` carries, content-hashed
2
+ // and addressed as a FILE rather than inlined into every response. The island table's shape one
3
+ // asset over (`island-bundle.ts`), because the two answer the same question — "what does this
4
+ // document make the browser fetch, and can it keep it?" — and one mechanism is the whole point.
5
+ //
6
+ // Why it stopped being an inline `<style>`: measured against ai-maxxing on 2026-09-06, every
7
+ // `app/` document carried one 156,738-byte block — 1,336 rules from 406 module sources, byte
8
+ // identical across `/`, `/fleet`, `/fleet/[host]` and the session page — inside a response the
9
+ // pipeline sends `Cache-Control: private, no-store`. 92% of the dashboard document and 89% of the
10
+ // session document, re-sent and re-parsed on every navigation, cacheable by nothing.
11
+
12
+ // Bun ships no path API; `join` is the filesystem side of writing a chunk into a static export.
13
+ // why: Bun exposes no path API — nothing native joins a directory to a URL-shaped path.
14
+ import { join } from 'node:path';
15
+ import type { Surface } from '@ultimat3/render';
16
+ import { SURFACES } from '@ultimat3/render';
17
+ import { contentHash, stylesFor, stylesheetsRevision } from '@ultimat3/render/server';
18
+
19
+ /**
20
+ * The surfaces a stylesheet is minted for. `api/` is dropped for `documentSurfaces`' reason — it
21
+ * emits no document, so a file no `<link>` can ever name would be bytes in the export and an entry
22
+ * in the precache manifest with no reader. `shared/` stays: `x shot --island` renders an island
23
+ * that lives there, and `surfaceOf` answers `shared` for it.
24
+ */
25
+ const STYLED_SURFACES: readonly Surface[] = SURFACES.filter((surface) => surface !== 'api');
26
+
27
+ /**
28
+ * Where a surface stylesheet is served from, in `x dev`, in the container and in a static export.
29
+ * Sits beside `ISLAND_BASE_PATH`, `ICON_BASE_PATH` and `MEDIA_BASE_PATH`, and outside the dev-only
30
+ * `/_x` namespace for their reason: the URL is baked into documents a static export publishes, so
31
+ * a dev-only path would be a page that renders in `x dev` and 404s on a CDN.
32
+ */
33
+ export const STYLE_BASE_PATH = '/styles';
34
+
35
+ export interface StyleChunk {
36
+ /**
37
+ * Every surface whose documents link it, sorted. Usually one — `site/` and `app/` carry
38
+ * different modules — but an app whose only CSS is its global layer produces one byte string for
39
+ * all three, and shipping it three times would put three copies in the static export and three
40
+ * entries in the precache manifest, which has a budget.
41
+ */
42
+ readonly surfaces: readonly Surface[];
43
+ /** Immutable, content-addressed URL. What `<link rel="stylesheet">` carries. */
44
+ readonly url: string;
45
+ readonly css: string;
46
+ readonly bytes: number;
47
+ }
48
+
49
+ export interface StyleBundle {
50
+ readonly chunks: readonly StyleChunk[];
51
+ /** The `href` a document on this surface links, or `undefined` when the surface has no CSS. */
52
+ hrefFor(surface: Surface | null): string | undefined;
53
+ /** The chunk a URL names — for serving it, and for writing it into a static export. */
54
+ chunkAt(url: string): StyleChunk | undefined;
55
+ }
56
+
57
+ /**
58
+ * `stylesFor(null)` and `stylesFor('shared')` select the same sheets by construction — a `null`
59
+ * surface matches only the package sheets, which `'shared'` already carries — so the one chunk
60
+ * answers both. `island-harness.ts` is the caller that can hold a `null`.
61
+ */
62
+ const surfaceKey = (surface: Surface | null): Surface => surface ?? 'shared';
63
+
64
+ /**
65
+ * Derived, never cached across a change: `stylesheetsRevision()` moves when a sheet's rules move,
66
+ * and island CSS registers on every `buildIslands` — which `x dev` re-runs on every watcher tick.
67
+ * Memoised on that revision rather than recomputed per request, because the derivation is a filter
68
+ * over every registered sheet plus a hash of the ~150 kB it joins.
69
+ */
70
+ let memo: { readonly revision: number; readonly bundle: StyleBundle } | undefined;
71
+
72
+ export function styleBundle(): StyleBundle {
73
+ const revision = stylesheetsRevision();
74
+ if (memo !== undefined && memo.revision === revision) return memo.bundle;
75
+ const bundle = styleBundleOf(
76
+ STYLED_SURFACES.map((surface) => ({ surface, css: stylesFor(surface) })).filter(
77
+ (sheet) => sheet.css.length > 0,
78
+ ),
79
+ );
80
+ memo = { revision, bundle };
81
+ return bundle;
82
+ }
83
+
84
+ /**
85
+ * Test seam, and the shape `islandBundle` has: a table built from what the caller supplies.
86
+ *
87
+ * The URL is the content hash and nothing else — no surface in the name — because a surface is not
88
+ * a property of the BYTES. Two surfaces with identical CSS are one file, one precache entry and
89
+ * one download, which is what the name would otherwise prevent. The same `contentHash` that stamps
90
+ * an ETag, a precache revision and an island chunk: one identity for a byte string, not a fourth.
91
+ */
92
+ export function styleBundleOf(
93
+ sheets: readonly { readonly surface: Surface; readonly css: string }[],
94
+ ): StyleBundle {
95
+ const byCss = new Map<string, Surface[]>();
96
+ for (const sheet of sheets) {
97
+ const held = byCss.get(sheet.css);
98
+ if (held === undefined) byCss.set(sheet.css, [sheet.surface]);
99
+ else held.push(sheet.surface);
100
+ }
101
+ const chunks: readonly StyleChunk[] = [...byCss].map(([css, surfaces]) => ({
102
+ surfaces: [...surfaces].sort(),
103
+ url: `${STYLE_BASE_PATH}/${contentHash(css)}.css`,
104
+ css,
105
+ bytes: new TextEncoder().encode(css).byteLength,
106
+ }));
107
+ const bySurface = new Map(
108
+ chunks.flatMap((chunk) => chunk.surfaces.map((surface) => [surface, chunk] as const)),
109
+ );
110
+ const byUrl = new Map(chunks.map((chunk) => [chunk.url, chunk]));
111
+ return {
112
+ chunks,
113
+ hrefFor: (surface: Surface | null): string | undefined =>
114
+ bySurface.get(surfaceKey(surface))?.url,
115
+ chunkAt: (url: string): StyleChunk | undefined => byUrl.get(url),
116
+ };
117
+ }
118
+
119
+ /** Write every surface stylesheet under a static export, at the URL the documents already carry. */
120
+ export async function writeStyles(bundle: StyleBundle, out: string): Promise<void> {
121
+ for (const chunk of bundle.chunks) {
122
+ await Bun.write(join(out, chunk.url.slice(1)), chunk.css);
123
+ }
124
+ }
package/src/style-csp.ts CHANGED
@@ -1,19 +1,21 @@
1
- // Every inline `<style>` body a served process can put in a document, as the `style-src` sources
2
- // that admit it. Read from the stylesheet registry at boot rather than checked in as a constant:
3
- // importing the app's modules IS what fills that registry, so a committed hash would describe a
4
- // stylesheet the document no longer carries — and the CSP would block the framework's own CSS.
1
+ // Every inline `<style>` body a served process can still put in a document, as the `style-src`
2
+ // sources that admit it. The caller names them, because the caller is what knows which documents
3
+ // it mounted.
4
+ //
5
+ // The app's OWN CSS is no longer among them, `As of 2026-09-06`: a surface stylesheet is served as
6
+ // a content-hashed file (`style-bundle.ts`) and a same-origin `<link>` is admitted by the `'self'`
7
+ // already in `@ultimat3/http`'s `style-src`. This function used to hash `stylesFor(surface)` for
8
+ // all four surfaces — 157 kB of CSS hashed at boot to admit a block that is not emitted any more,
9
+ // which is a rule describing a document nobody serves. So a production boot (`serve.ts`, which
10
+ // passes no extras) now extends `style-src` with nothing at all.
5
11
 
6
12
  import { cspHashSource } from '@ultimat3/http';
7
- import { SURFACES } from '@ultimat3/render';
8
- import { stylesFor } from '@ultimat3/render/server';
9
13
 
10
14
  /**
11
- * Call AFTER `loadApp`. One hash per distinct body: `stylesFor` is what `dev-render.ts` puts in
12
- * the tag, per surface, so hashing the same call is the only way the two cannot drift. `extra`
13
- * carries the documents this package does not render — `/_x`'s shell — because the caller is what
14
- * knows which of them it mounted.
15
+ * Call with the inline bodies THIS process emits. `x dev` passes the `/_x` shell's stylesheet and
16
+ * the screenshot harness's frame style — documents this package renders itself, which no app
17
+ * surface can see.
15
18
  */
16
- export function inlineStyleSources(extra: readonly string[] = []): readonly string[] {
17
- const bodies = [...SURFACES.map((surface) => stylesFor(surface)), ...extra];
19
+ export function inlineStyleSources(bodies: readonly string[] = []): readonly string[] {
18
20
  return [...new Set(bodies.filter((body) => body.length > 0).map(cspHashSource))].sort();
19
21
  }
@@ -0,0 +1,56 @@
1
+ // Serving the surface stylesheets. `x dev` and the container mount the same route over the same
2
+ // table, for `island-routes.ts`' reason: the URL is minted by one resolver and baked into the
3
+ // document, so a dev-only path would be a page that paints in `x dev` and renders naked in the
4
+ // image.
5
+
6
+ import type { Route, UltimateRequest } from '@ultimat3/http';
7
+ import { applyCacheHeaders, json } from '@ultimat3/http';
8
+ import type { StyleBundle } from './style-bundle';
9
+ import { STYLE_BASE_PATH } from './style-bundle';
10
+
11
+ /**
12
+ * A getter, not the bundle: `x dev` re-registers island CSS on every watcher tick, and a table
13
+ * captured when the route was mounted would serve the stylesheet as it was at boot for the rest of
14
+ * the session.
15
+ */
16
+ export type StyleSource = () => StyleBundle;
17
+
18
+ /**
19
+ * The URL is content-addressed, so the bytes behind it never change and the answer is
20
+ * `public, max-age=31536000, immutable` — the same headers an island chunk earns, and the whole
21
+ * reason this is a file rather than 157 kB of `<style>` inside a `no-store` document.
22
+ *
23
+ * A miss can only be a document older than this process's registry, which is a fact worth stating
24
+ * rather than a bare 404 whose meaning an agent has to guess.
25
+ */
26
+ export function styleRoutes(source: StyleSource): readonly Route[] {
27
+ return [
28
+ {
29
+ method: 'GET',
30
+ path: `${STYLE_BASE_PATH}/*file`,
31
+ meta: { name: 'assets.style', auth: 'public', tags: ['assets'] },
32
+ handler: (request: UltimateRequest): Response => {
33
+ const chunk = source().chunkAt(request.pathname);
34
+ if (chunk === undefined) {
35
+ return json(
36
+ {
37
+ ok: false,
38
+ error: {
39
+ code: 'X_ROUTE_NOT_FOUND',
40
+ cause: `no surface stylesheet is registered at ${request.pathname} — the document that asked for it was rendered against an older build`,
41
+ // No `x` citation, for `island-routes.ts`' reason: this route is mounted in
42
+ // exactly two places (`cmd-dev.ts`, `serve.ts`) and neither reads `.x/static`.
43
+ fix: 'reload the page — this process serves only the stylesheet its own modules registered, and the document holding this URL came from an earlier build',
44
+ },
45
+ },
46
+ { status: 404 },
47
+ );
48
+ }
49
+ return applyCacheHeaders(
50
+ new Response(chunk.css, { headers: { 'content-type': 'text/css; charset=utf-8' } }),
51
+ { mode: 'immutable' },
52
+ );
53
+ },
54
+ },
55
+ ];
56
+ }
@@ -9,6 +9,7 @@ import type { RouteDescriptor } from '@ultimat3/render';
9
9
  import type { IslandBundle } from './island-bundle';
10
10
  import { msg } from './messages';
11
11
  import type { PwaArtifacts } from './pwa-artifacts';
12
+ import type { StyleBundle } from './style-bundle';
12
13
 
13
14
  /** Root scope, so `/sw.js` and nothing under a directory — `assertScope` refuses the rest. */
14
15
  export const SERVICE_WORKER_PATH = '/sw.js';
@@ -43,6 +44,12 @@ export interface ServiceWorkerInput {
43
44
  readonly buildId: string;
44
45
  readonly routes: readonly RouteDescriptor[];
45
46
  readonly islands: IslandBundle;
47
+ /**
48
+ * The surface stylesheets every document links. Precached beside the island chunks and for the
49
+ * same reason: they are content-addressed and served `immutable`, and a document that reaches
50
+ * the offline fallback with no CSS is a page the visitor cannot read.
51
+ */
52
+ readonly styles: StyleBundle;
46
53
  }
47
54
 
48
55
  /**
@@ -71,14 +78,15 @@ const pwaRoutes = (routes: readonly RouteDescriptor[]): readonly PwaRoute[] =>
71
78
  });
72
79
 
73
80
  /**
74
- * Every island chunk, precached. They are content-addressed and served `immutable`, so the
75
- * revision IS the URL's hash and a byte-identical chunk across deploys is never re-downloaded.
81
+ * Every island chunk and every surface stylesheet, precached. They are content-addressed and
82
+ * served `immutable`, so the revision IS the URL's hash and a byte-identical asset across deploys
83
+ * is never re-downloaded.
76
84
  *
77
85
  * Sorted by url, because `buildPrecacheManifest` sorts its own entries but the ASSET list is what
78
86
  * decides which of two equal urls wins, and `sw.js` must be byte-identical for identical input.
79
87
  */
80
- const islandAssets = (islands: IslandBundle): readonly PrecacheAsset[] =>
81
- [...islands.chunks]
88
+ const staticAssets = (islands: IslandBundle, styles: StyleBundle): readonly PrecacheAsset[] =>
89
+ [...islands.chunks, ...styles.chunks]
82
90
  .map((chunk) => ({ url: chunk.url, revision: chunk.url, bytes: chunk.bytes }))
83
91
  .sort((a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0));
84
92
 
@@ -133,7 +141,7 @@ export function serviceWorkerArtifacts(
133
141
  neverCache: pwa.offline.neverCache,
134
142
  },
135
143
  capabilities: { backgroundSync: pwa.backgroundSync, push: pwa.push },
136
- assets: islandAssets(input.islands),
144
+ assets: staticAssets(input.islands, input.styles),
137
145
  },
138
146
  input.buildId,
139
147
  );
@@ -39,8 +39,14 @@ const formIslandSource = (
39
39
 
40
40
  import { Button, Form, Input, setSolidRuntime, UiProvider } from '@ultimat3/ui';
41
41
  import type { JSX } from 'solid-js';
42
- import * as solidRuntime from 'solid-js';
43
- import { createSignal } from 'solid-js';
42
+ import {
43
+ createContext,
44
+ createEffect,
45
+ createMemo,
46
+ createSignal,
47
+ onCleanup,
48
+ useContext,
49
+ } from 'solid-js';
44
50
  import { render } from 'solid-js/web';
45
51
  import styles from './ui.module.scss';
46
52
 
@@ -123,11 +129,15 @@ function ${feature.pascal}FormBody(props: ${feature.pascal}FormProps): JSX.Eleme
123
129
  * registers. Delete the line and the first \`<UiProvider>\` render throws X_UI_RUNTIME_MISSING —
124
130
  * loud on purpose, because a DOM render that lost its runtime is a theme toggle that does nothing.
125
131
  *
132
+ * Six NAMED imports, never \`import * as solidRuntime\`: a namespace object handed to a function
133
+ * keeps every export of solid-js alive, and the bundler cannot shake what it cannot see unused —
134
+ * measured at 14.8 kB minified per island chunk (5.6 kB gzipped) for the namespace form.
135
+ *
126
136
  * The shell is cleared first: Solid's \`render\` APPENDS when the container already has children,
127
137
  * so without it the server's markup stays on screen above a second, live copy of the same thing.
128
138
  */
129
139
  export function mount(el: HTMLElement, props: ${feature.pascal}FormProps): void {
130
- setSolidRuntime(solidRuntime);
140
+ setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
131
141
  el.textContent = '';
132
142
  render(
133
143
  () => (
@@ -307,6 +307,12 @@ const bunfig = (): string => `[test]
307
307
  root = "."
308
308
  # Frozen clock, seeded RNG, sealed network — nondeterminism in a test is a bug.
309
309
  preload = ["@ultimat3/testing/preload"]
310
+ # An island is mounted from a BUILT chunk that \`mountIsland\` writes to a temp .mjs, so
311
+ # \`bun test --coverage\` reports that file: two minified lines, under a name no source has. Bun
312
+ # does not remap a pre-built module through its sourcemap (measured on 1.4.0, and the build does
313
+ # emit one), so the row can only ever be noise — the island's own .island.tsx is not what it
314
+ # describes. Ignoring it removes the phantom; it hides no line any test was covering.
315
+ coveragePathIgnorePatterns = ["**/*.mjs"]
310
316
  `;
311
317
 
312
318
  const scssTypes =
@@ -34,6 +34,7 @@ import type { Finding } from './output';
34
34
  import { findingFrom } from './output';
35
35
  import { checkMigrationDrift } from './schema-drift';
36
36
  import { scanSiteMeta } from './seo-meta';
37
+ import { readStaticReport } from './static-report';
37
38
  import { floorProblemFindings, readVerifyFloor } from './verify-floor';
38
39
  import type { VerifyStep } from './verify-step';
39
40
  import { fromExec, fromFindings, hostFindings } from './verify-step';
@@ -211,6 +212,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
211
212
  // to a hard `X_PRERENDER_FAILED` on `examples/dummy`), and it would be a second builder
212
213
  // beside `apps/web/prerender.ts`, which is where an app reads `SITE_ORIGIN`.
213
214
  const stats = await readBuildStats(ctx.root);
215
+ // The report beside the stats, for the one thing `checkBudgets` reads off it: a route the
216
+ // build rendered and could not weigh because its island was handed props over the cap is
217
+ // reported under `X_ISLAND_PROPS_INVALID` — the build's own sentence, naming the prop and
218
+ // its bytes — and not as an `X_BUDGET_UNMEASURED` whose fix is to go and read this file.
219
+ const report = await readStaticReport(ctx.root);
214
220
  // The load's own findings, FIRST and never dropped. A module that would not import registers
215
221
  // no route, so its budget is missing from the manifest and every route it declared reads as
216
222
  // `X_BUDGET_UNMEASURED` — the symptom, pointing the reader at `x build` for a file that will
@@ -224,7 +230,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
224
230
  // JavaScript does this route's document boot? A live read with no island is a route
225
231
  // whose answer is "none", which no suite can fail on — the page renders, at 200.
226
232
  ...(await liveRouteFindings(ctx.root)),
227
- ...checkBudgets(manifest, stats),
233
+ ...checkBudgets(manifest, stats, report?.unmeasured),
228
234
  ]);
229
235
  },
230
236
  },
@@ -36,13 +36,26 @@ type TypedTest = Exclude<TestType, 'unit'>;
36
36
 
37
37
  const TYPED_SUFFIXES = '{contract,live,job,e2e,eval}';
38
38
 
39
- const SUMMARIES: Readonly<Record<TypedTest, string>> = {
40
- contract: 'action/query schemas, policy denials, emitted OpenAPI and MCP shapes',
41
- live: 'live-query snapshots, incremental patches, reconnect deltas',
42
- job: 'step replay, idempotency dedupe, retry/backoff, outbox atomicity',
43
- e2e: 'the built output, incl. offline and SW update',
44
- eval: 'LLM output scored against thresholds',
45
- };
39
+ /**
40
+ * `Object.create(null)`, not a `{}` literal, because `stepFor` reads it with a COMPUTED key
41
+ * (`SUMMARIES[type]`). On a normal object literal every `Object.prototype` member reads back as
42
+ * present, so a table read that way answers a function instead of `undefined` for a key nobody
43
+ * declared — the defect `scripts/proto-index.ts` exists to keep out, thirteen instances across
44
+ * four sweeps. `type` is a closed union here and cannot be `'constructor'` today, which is
45
+ * exactly the argument every one of those thirteen had before it stopped being true.
46
+ *
47
+ * Same repair, and the same reason, as `packages/i18n/src/catalog.ts`.
48
+ */
49
+ const SUMMARIES: Readonly<Record<TypedTest, string>> = Object.assign(
50
+ Object.create(null) as Record<TypedTest, string>,
51
+ {
52
+ contract: 'action/query schemas, policy denials, emitted OpenAPI and MCP shapes',
53
+ live: 'live-query snapshots, incremental patches, reconnect deltas',
54
+ job: 'step replay, idempotency dedupe, retry/backoff, outbox atomicity',
55
+ e2e: 'the built output, incl. offline and SW update',
56
+ eval: 'LLM output scored against thresholds',
57
+ },
58
+ );
46
59
 
47
60
  /**
48
61
  * Every rule that decides a file's type, MOST SPECIFIC FIRST: the first entry a path matches owns
@@ -0,0 +1,22 @@
1
+ // How a process binds its sockets, and what it admits about itself. A LEAF: it imports nothing,
2
+ // so every role can read it without pulling `dev-roles` — which is what made this its own file.
3
+ // `dev-sync` needs the default and `dev-roles` already imports `dev-sync`, so reading it from
4
+ // there would be a runtime import cycle in the framework's own boot path.
5
+
6
+ export interface WebBinding {
7
+ readonly dev: boolean;
8
+ /**
9
+ * The interface every socket this process opens binds to — the web role, the metrics endpoint
10
+ * and the `sync` node alike. ONE value, because they are one decision: a process that serves
11
+ * its app on loopback and its live-query patch stream on `0.0.0.0` has not bound to loopback,
12
+ * it has just moved which port the exposure is on.
13
+ */
14
+ readonly hostname: string;
15
+ }
16
+
17
+ /**
18
+ * Loopback and dev-mode. What `x dev` means, and what a container must override — a process bound
19
+ * to `localhost` inside a container is unreachable from the port mapping, the load balancer and
20
+ * every PaaS health probe, which is the same failure in four costumes.
21
+ */
22
+ export const DEV_BINDING: WebBinding = { dev: true, hostname: 'localhost' };