@ultimat3/cli 19.1.3 → 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/CLAUDE.md +18 -3
- package/package.json +29 -29
- package/src/app-openapi.ts +13 -5
- package/src/app-permissions.ts +0 -0
- package/src/browser-launcher.ts +53 -4
- package/src/budgets.ts +43 -1
- package/src/cmd-dev.ts +27 -7
- package/src/cmd-shot.ts +3 -1
- package/src/dev-render.ts +28 -7
- package/src/dev-roles.ts +9 -8
- package/src/dev-sync.ts +7 -1
- package/src/dev-watch.ts +53 -0
- package/src/duplicate-packages.ts +278 -0
- package/src/error-codes.ts +6 -0
- package/src/i18n-registration.ts +34 -5
- package/src/index.ts +2 -0
- package/src/island-bundle.ts +121 -9
- package/src/island-harness.ts +11 -4
- package/src/mcp-errors.ts +2 -0
- package/src/mcp-host.ts +3 -0
- package/src/prerender.ts +29 -3
- package/src/serve.ts +12 -1
- package/src/shot-browser.ts +23 -4
- package/src/static-report.ts +21 -1
- package/src/style-bundle.ts +124 -0
- package/src/style-csp.ts +14 -12
- package/src/style-routes.ts +56 -0
- package/src/sw-artifacts.ts +13 -5
- package/src/templates/resource-form-island.ts +13 -3
- package/src/templates/scaffold-repo.ts +6 -0
- package/src/verify-checks.ts +7 -1
- package/src/verify-tests.ts +20 -7
- package/src/web-binding.ts +22 -0
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({
|
|
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
|
-
|
|
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({
|
|
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),
|
package/src/shot-browser.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
102
|
+
return { executablePath };
|
|
84
103
|
}
|
package/src/static-report.ts
CHANGED
|
@@ -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) &&
|
|
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`
|
|
2
|
-
// that admit it.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
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(
|
|
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
|
+
}
|
package/src/sw-artifacts.ts
CHANGED
|
@@ -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
|
|
75
|
-
* revision IS the URL's hash and a byte-identical
|
|
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
|
|
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:
|
|
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
|
|
43
|
-
|
|
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(
|
|
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 =
|
package/src/verify-checks.ts
CHANGED
|
@@ -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
|
},
|
package/src/verify-tests.ts
CHANGED
|
@@ -36,13 +36,26 @@ type TypedTest = Exclude<TestType, 'unit'>;
|
|
|
36
36
|
|
|
37
37
|
const TYPED_SUFFIXES = '{contract,live,job,e2e,eval}';
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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' };
|