@ultimat3/cli 16.0.0 → 18.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.
@@ -0,0 +1,74 @@
1
+ // Single responsibility: the one source image every generated icon derives from, where the matrix
2
+ // is served, and the renderer that turns one into the other. Its own module so the two things that
3
+ // need it — `dev-assets.ts`, which SERVES the matrix, and `pwa-artifacts.ts`, which NAMES it in the
4
+ // web manifest — can share it without importing each other.
5
+
6
+ // why: Bun exposes no path-join primitive, and `ICON_SOURCE` is app-root-relative, so resolving it
7
+ // against the root is string work no `Bun.file` overload does.
8
+ import { join } from 'node:path';
9
+ import type { IconPlan } from '@ultimat3/pwa';
10
+ import { BuiltinImagePipeline, PwaIconMissingError, planIcons } from '@ultimat3/pwa';
11
+
12
+ /**
13
+ * The one source image every generated icon derives from. `x new` scaffolds it, `x doctor` checks
14
+ * it and this file reads it — one constant, because a second spelling is an app that passes the
15
+ * diagnostic and still serves no icons. PNG, not SVG: core's pipeline decodes PNG and JPEG only.
16
+ */
17
+ export const ICON_SOURCE = 'apps/web/site/icon.png';
18
+
19
+ /** Where `planIcons` writes, and therefore the paths the generated web manifest names. */
20
+ export const ICON_BASE_PATH = '/icons';
21
+
22
+ /**
23
+ * The matrix's whole plan, off one constant pair. One call, so the icons `/icons/*` serves and the
24
+ * icons `manifest.webmanifest` names can never be two different lists.
25
+ */
26
+ export const iconPlan = (): IconPlan =>
27
+ planIcons({ sourceIcon: ICON_SOURCE, outDir: ICON_BASE_PATH });
28
+
29
+ /**
30
+ * Whether the app committed the one file the whole matrix derives from. Read where the answer
31
+ * changes what is EMITTED — a manifest naming twelve icons an app has no source for is twelve 404s
32
+ * in an install prompt, which is the promise-nothing-keeps shape this module's callers exist to
33
+ * close. `x doctor` owns the diagnostic and reports the same condition with `X_PWA_ICON_MISSING`.
34
+ */
35
+ export const hasSourceIcon = (root: string): Promise<boolean> =>
36
+ Bun.file(join(root, ICON_SOURCE)).exists();
37
+
38
+ /**
39
+ * Rendered once per process, not per request: the fourteen matrix entries are pure functions of
40
+ * one source file, and re-encoding a 512px PNG on every hit would be work no caller can observe.
41
+ */
42
+ export function iconRenderer(root: string): (plan: IconPlan, path: string) => Promise<Uint8Array> {
43
+ const pipeline = new BuiltinImagePipeline();
44
+ const rendered = new Map<string, Promise<Uint8Array>>();
45
+ const sourceBytes = async (): Promise<Uint8Array> => {
46
+ const file = Bun.file(join(root, ICON_SOURCE));
47
+ if (!(await file.exists())) {
48
+ throw new PwaIconMissingError(
49
+ `${ICON_SOURCE} does not exist, so every icon the web manifest declares is unbacked and ` +
50
+ 'the app is not installable',
51
+ // The same edit `x doctor` reports for the same condition, in `@ultimat3/pwa`'s own words.
52
+ // `x new` was here and takes an app name, so it could never run inside the broken app.
53
+ `add a 1024x1024 square PNG at ${ICON_SOURCE}`,
54
+ );
55
+ }
56
+ return file.bytes();
57
+ };
58
+ return async (plan, path) => {
59
+ const entry = plan.entries.find((candidate) => candidate.outputPath === path);
60
+ if (entry === undefined) {
61
+ throw new PwaIconMissingError(
62
+ `${path} is not in the icon matrix, so no transform describes it`,
63
+ `request one of ${plan.entries.map((one) => one.outputPath).join(', ')}`,
64
+ );
65
+ }
66
+ const existing = rendered.get(path);
67
+ if (existing !== undefined) return existing;
68
+ const bytes = sourceBytes().then((source) => pipeline.resize(source, entry.transform));
69
+ rendered.set(path, bytes);
70
+ // A failed render must not be remembered — the next request comes after the source was added.
71
+ bytes.catch(() => rendered.delete(path));
72
+ return bytes;
73
+ };
74
+ }
package/src/index.ts CHANGED
@@ -72,6 +72,7 @@ export { testCommand } from './cmd-test';
72
72
  export { runVerify, VERIFY_STEPS, verifyCommand, verifyStepNames } from './cmd-verify';
73
73
  export type { CliCommand, CommandContext } from './command';
74
74
  export { failed, ok } from './command';
75
+ export { acceptCreatedTables, createdTables } from './db-accept-created';
75
76
  export type { BranchRow, BranchSubcommand } from './db-branch';
76
77
  export {
77
78
  BRANCH_SUBCOMMANDS,
@@ -83,11 +84,11 @@ export {
83
84
  } from './db-branch';
84
85
  export type { GeneratedFiles, GenerateMigrationOptions, GenerateOutcome } from './db-generate';
85
86
  export { generateAppMigration, migrationSql } from './db-generate';
87
+ export type { SubscribingQuery } from './db-subscribes';
88
+ export { QuerySubscribesUnknownError, replicaIdentityTables } from './db-subscribes';
86
89
  export type { AssetRoutesOptions } from './dev-assets';
87
90
  export {
88
91
  assetRoutes,
89
- ICON_BASE_PATH,
90
- ICON_SOURCE,
91
92
  MEDIA_BASE_PATH,
92
93
  } from './dev-assets';
93
94
  export type { DevDashboardInput, DevStatus } from './dev-dashboard';
@@ -234,6 +235,13 @@ export {
234
235
  } from './framework-schema';
235
236
  export type { Guard } from './guards';
236
237
  export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
238
+ export {
239
+ hasSourceIcon,
240
+ ICON_BASE_PATH,
241
+ ICON_SOURCE,
242
+ iconPlan,
243
+ iconRenderer,
244
+ } from './icon-assets';
237
245
  // The island bundler, and only its entry point. An island is the one module Ultimate ships to a
238
246
  // browser, so an app has to be able to build one to TEST one — `mountIsland` from
239
247
  // `@ultimat3/testing` takes this function as its `build` parameter (issue #260). `discoverIslands`,
@@ -274,6 +282,13 @@ export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
274
282
  export { flagBool, flagList, flagString, GLOBAL_FLAGS, nearest, parseArgs } from './parse';
275
283
  export type { PrerenderedPage, PrerenderOptions, PrerenderReport } from './prerender';
276
284
  export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
285
+ export type { PwaArtifacts } from './pwa-artifacts';
286
+ export {
287
+ loadPwaArtifacts,
288
+ pwaManifestRoute,
289
+ WEB_MANIFEST_PATH,
290
+ writePwaIcons,
291
+ } from './pwa-artifacts';
277
292
  export { COMMANDS, cliVersion, commandFor, SPECS } from './registry';
278
293
  export type { SchemaDifference, SchemaDirection, SchemaPart } from './schema-diff';
279
294
  export { diffDeclaredSchema } from './schema-diff';
@@ -323,8 +338,8 @@ export type { TestCounts } from './test-counts';
323
338
  export { countsOf } from './test-counts';
324
339
  export type { TestFile } from './test-select';
325
340
  export { belongsToType, discoverTests, sampleFiles } from './test-select';
326
- export type { ReproduceOptions, RunShardsOptions, Shard } from './test-shards';
327
- export { planShards, reproduceFor, runShards, shardArgs } from './test-shards';
341
+ export type { ReproduceOptions, RunShardsOptions } from './test-shards';
342
+ export { filesIn, reproduceFor, runShards, testArgs } from './test-shards';
328
343
  export { availableCpus, defaultWorkers, WORKER_CEILING } from './test-workers';
329
344
  export type {
330
345
  CodeFixSite,
@@ -144,4 +144,11 @@ export const readinessProbe = (selector: string): string =>
144
144
  // Children OR text: a component that renders one text node has painted, and one that mounted
145
145
  // and rendered nothing is the silence a non-zero box would otherwise read as success.
146
146
  'filled:box?(box.children.length>0||(box.textContent||"").trim().length>0):false,' +
147
- 'box:{x:Math.round(r.x),y:Math.round(r.y),width:Math.round(r.width),height:Math.round(r.height)}};})()';
147
+ 'box:{x:Math.round(r.x),y:Math.round(r.y),width:Math.round(r.width),height:Math.round(r.height)},' +
148
+ // The box is VIEWPORT coordinates — what `getBoundingClientRect()` answers — and a capture clip
149
+ // is PAGE coordinates. They agree only while the page is at the origin, which is the one case a
150
+ // harness happens to be in and is not a rule anything enforces: a state whose component sits
151
+ // below the fold scrolls, and a clip taken from the raw rect then crops the wrong band with
152
+ // nothing to report it. The offset is returned rather than added here so `box` keeps meaning
153
+ // exactly what the verdict already publishes.
154
+ 'scroll:{x:Math.round(window.scrollX||0),y:Math.round(window.scrollY||0)}};})()';
@@ -5,7 +5,8 @@
5
5
 
6
6
  // why: no Bun native joins a path; `Bun.write` and `Bun.file` both take one already joined.
7
7
  import { join } from 'node:path';
8
- import type { ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
8
+ import { finiteCount } from '@ultimat3/core';
9
+ import type { CaptureClip, ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
9
10
  import { systemScrapeClock } from '@ultimat3/scraping';
10
11
  import type { IslandShotTarget, IslandStatesManifest, IslandViewport } from '@ultimat3/testing';
11
12
  import { islandShotTargets, islandStatesFile } from '@ultimat3/testing';
@@ -138,6 +139,21 @@ export function photographFault(
138
139
  return undefined;
139
140
  }
140
141
 
142
+ /**
143
+ * The capture rectangle for a readiness answer, in PAGE coordinates.
144
+ *
145
+ * `seen` is non-null and its box has area by the time this is reached — `photographFault` refuses
146
+ * both above, and it refuses them BEFORE the shutter for exactly this reason: a zero-area clip is
147
+ * `X_SCRAPE_CAPTURE_CLIP_EMPTY` from the port, which is a worse report of the same fault than
148
+ * "rendered nothing". The `?? 0` pair is the parser's floor and not a second opinion.
149
+ */
150
+ const clipFor = (seen: IslandReadiness | null): CaptureClip => ({
151
+ x: (seen?.box.x ?? 0) + (seen?.scroll.x ?? 0),
152
+ y: (seen?.box.y ?? 0) + (seen?.scroll.y ?? 0),
153
+ width: seen?.box.width ?? 0,
154
+ height: seen?.box.height ?? 0,
155
+ });
156
+
141
157
  const hostFix = (target: IslandShotTarget): string =>
142
158
  `in ${islandStatesFile(target.island)} set island to a path that exports mount(el, props)`;
143
159
 
@@ -157,6 +173,7 @@ async function captureOne(
157
173
  options: IslandShotRun,
158
174
  server: ShotServer,
159
175
  target: IslandShotTarget,
176
+ floor: number,
160
177
  ): Promise<IslandStateShot> {
161
178
  const url = new URL(`${ISLAND_HARNESS_PATH}${target.query}`, server.url).toString();
162
179
  let session: ScrapeSession | undefined;
@@ -169,6 +186,17 @@ async function captureOne(
169
186
  timeoutMs: options.timeoutMs,
170
187
  });
171
188
  const page = session.page;
189
+ // BEFORE the navigation, so the first paint already has it: `prefers-color-scheme` is a live
190
+ // media query, and the theme a component resolves on mount is the one it will keep.
191
+ //
192
+ // This is the INPUT and the harness's `data-theme` attribute is the OUTCOME, and both are set
193
+ // deliberately. The attribute is right for a component that READS a theme it does not own; the
194
+ // preference is the only thing that reaches one that RESOLVES its own. `examples/dummy`'s
195
+ // settings island is the second kind — its state's `theme` prop is `'system'`, so on mount it
196
+ // DELETES the attribute the harness set, both pictures fall through to `:root`, and the two
197
+ // came back byte-identical with the same md5 (issue #338). Re-setting the attribute after
198
+ // readiness is not the repair: it photographs a state the component would never reach.
199
+ await page.colorScheme(target.theme);
172
200
  await page.goto(url, { timeout: options.timeoutMs });
173
201
  const expression = readinessProbe(target.target ?? '[data-x-island]');
174
202
  const probe = (): Promise<IslandReadiness | null> =>
@@ -200,10 +228,16 @@ async function captureOne(
200
228
  ...fault,
201
229
  });
202
230
  }
203
- // Never `fullPage`: the frame is the state's own declared viewport, and a full-page capture
204
- // would grow with whatever the component scrolled.
205
- const bytes = await page.screenshot({ fullPage: false });
206
- const floor = options.minBytes ?? MIN_SHOT_BYTES;
231
+ // The COMPONENT, not the viewport it happens to sit in — the crop this feature was designed
232
+ // around, and which nothing passed until 2026-08-26 (issue #338). The rectangle is the
233
+ // readiness probe's own box, which is the crop target the manifest declared, translated from
234
+ // the DOM's viewport coordinates into the page coordinates a capture clip is in.
235
+ //
236
+ // The clip ALONE. `fullPage: false` beside it is accepted — `assertCaptureFraming` refuses only
237
+ // `=== true`, and `cdp-target.ts` sends `{ clip }` and nothing else either way, so all four
238
+ // pictures really were written with the pair — but it is a field that says nothing: the two
239
+ // are exclusive, and spelling out the default of the one you did not ask for reads as a choice.
240
+ const bytes = await page.screenshot({ clip: clipFor(seen) });
207
241
  if (bytes.byteLength < floor) {
208
242
  throw new IslandUnphotographableError({
209
243
  island: target.island,
@@ -257,6 +291,11 @@ export async function runIslandShot(options: IslandShotRun): Promise<IslandArtif
257
291
  const targets = islandShotTargets(options.manifest).filter(
258
292
  (target) => chosen === null || chosen.has(target.state),
259
293
  );
294
+ // Before the boot, and before a browser: `bytes.byteLength < NaN` is false for every picture, so
295
+ // an unchecked floor does not lower the backstop — it removes it, and "produced nothing and
296
+ // exited 0" is the one outcome a reader cannot tell from success. 0 stays legal and is what
297
+ // `island-shot.test.ts` passes: the fake driver answers an 8-byte PNG signature.
298
+ const floor = finiteCount('runIslandShot', 'minBytes', options.minBytes ?? MIN_SHOT_BYTES);
260
299
  const server = await options.boot();
261
300
  const shots: IslandStateShot[] = [];
262
301
  const failures: unknown[] = [];
@@ -266,7 +305,7 @@ export async function runIslandShot(options: IslandShotRun): Promise<IslandArtif
266
305
  // the app CAN produce plus a named reason for each one it cannot, and the missing-shot gate
267
306
  // below is what turns those reasons into a non-zero exit.
268
307
  try {
269
- shots.push(await captureOne(options, server, target));
308
+ shots.push(await captureOne(options, server, target, floor));
270
309
  } catch (error) {
271
310
  failures.push(error);
272
311
  }
@@ -27,10 +27,13 @@ export const ISLAND_SHOT_MESSAGE_KEYS = [
27
27
  * this tool cannot support — and both are properties of the port rather than of a run, which is
28
28
  * why they are a constant and not a per-run list.
29
29
  *
30
- * The crop one is the honest limit of the shipped browser port: `CaptureRequest` is `fullPage`
31
- * alone (`packages/scraping/src/page.ts`), so a picture is the VIEWPORT and the framing knob is the
32
- * state's own `viewport`, not a clip rectangle. The locale one is the reach of a page-side clock
33
- * patch: `date.toLocaleString()` resolves the zone inside the engine and never through the patched
30
+ * The crop one is what a rectangle still cannot see: the picture IS the component's own box now
31
+ * (`clipFor`, `island-shot.ts`), so what a reader loses is the surroundings — a component that
32
+ * overflows its box, or one whose fault is the space around it, is outside the frame. It said the
33
+ * opposite until 2026-08-26 — "the browser port takes no clip rectangle" — which stopped being
34
+ * true when the port gained `CaptureClip`, and a blind spot that names a capability the tool has
35
+ * is the same lie as one that hides a gap. The locale one is the reach of a page-side clock patch:
36
+ * `date.toLocaleString()` resolves the zone inside the engine and never through the patched
34
37
  * `Intl.DateTimeFormat`.
35
38
  */
36
39
  export const ISLAND_BLIND_SPOTS = [
@@ -54,7 +57,15 @@ export interface IslandReadiness {
54
57
  readonly mounted: boolean;
55
58
  readonly failed: string | null;
56
59
  readonly filled: boolean;
60
+ /** The crop target's rectangle in VIEWPORT coordinates, which is what the DOM answers. */
57
61
  readonly box: IslandBox;
62
+ /**
63
+ * The page's scroll offset at the moment the box was measured. A capture clip is in PAGE
64
+ * coordinates, so this is what turns one into the other — and it is a separate field rather than
65
+ * an addition inside the probe because `box` is published in the verdict and means the DOM's own
66
+ * answer there.
67
+ */
68
+ readonly scroll: { readonly x: number; readonly y: number };
58
69
  }
59
70
 
60
71
  const readinessSchema: StandardSchemaV1<unknown, IslandReadiness> = t.object({
@@ -66,6 +77,7 @@ const readinessSchema: StandardSchemaV1<unknown, IslandReadiness> = t.object({
66
77
  failed: t.nullable(t.string),
67
78
  filled: t.boolean,
68
79
  box: t.object({ x: t.number, y: t.number, width: t.number, height: t.number }),
80
+ scroll: t.object({ x: t.number, y: t.number }),
69
81
  }) as unknown as StandardSchemaV1<unknown, IslandReadiness>;
70
82
 
71
83
  /**
package/src/mcp-errors.ts CHANGED
@@ -168,6 +168,12 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
168
168
  // reproduces the finding, and the finding names the file and the header line to add.
169
169
  X_MIGRATION_UNGENERATABLE:
170
170
  'x verify --only drift --json # then add the `-- ungeneratable: <n>` header line the finding names',
171
+ // The edit comes first and the command re-runs it, exactly as the two schema codes above do:
172
+ // the cause names the query and the name that matched nothing, and `subscribes:` is a field in
173
+ // that query's own file. Regenerating is safe here — this refusal happens BEFORE anything is
174
+ // written, so there is no half-generated migration to undo.
175
+ X_QUERY_SUBSCRIBES_UNKNOWN:
176
+ 'x db gen "retry after fixing subscribes" --json # first edit subscribes: on the query the cause names',
171
177
  X_DB_MIGRATE_FAILED: 'x doctor --json # cause carries the Postgres error verbatim',
172
178
  X_DB_BRANCH_FAILED: 'x db branch ls --json',
173
179
  X_DB_STUDIO_FAILED: 'x doctor --json',
package/src/messages.ts CHANGED
@@ -212,7 +212,7 @@ const CATALOG = {
212
212
  'cli.shot.island.picture': ' pictures {path}',
213
213
  'cli.shot.island.verdict': ' verdict {path}',
214
214
  'cli.shot.island.blind.crop':
215
- 'the picture is the viewport, not a crop — the browser port takes no clip rectangle, so a state sizes its own frame with viewport',
215
+ 'the picture is the crop target and nothing around it — a component that overflows its own box, or whose fault is the space beside it, is outside the frame',
216
216
  'cli.shot.island.blind.locale':
217
217
  'toLocaleString() on a Date resolves its zone inside the engine — only an explicit timeZone is pinned by this harness',
218
218
  'cli.ci.failed':
@@ -3,6 +3,7 @@
3
3
  // `docker/helm`'s HPAs read a number instead of `<unknown>`.
4
4
 
5
5
  import {
6
+ finiteCount,
6
7
  logger,
7
8
  METRICS_CONTENT_TYPE,
8
9
  METRICS_PATH,
@@ -78,7 +79,11 @@ export interface MetricsEndpoint {
78
79
  * signal at the moment of load is worse than no autoscaler.
79
80
  */
80
81
  export function startMetricsEndpoint(options: MetricsEndpointOptions = {}): MetricsEndpoint {
81
- const port = options.port ?? DEFAULT_METRICS_PORT;
82
+ // Screened here rather than left to `Bun.serve`, which refuses a `NaN` with a bare `RangeError`
83
+ // — no code, no `fix:` — at exactly the boot path the refusal below exists to stop reporting
84
+ // that way. Floor 0, because 0 asks the kernel for a free port and `dev-roles.ts` passes it for
85
+ // an ephemeral boot; the ceiling stays Bun's, which names the range it refuses.
86
+ const port = finiteCount('startMetricsEndpoint', 'port', options.port ?? DEFAULT_METRICS_PORT);
82
87
  // `startRoles` opens this FIRST, before any role, so `Bun.serve`'s own bare `Error` was what a
83
88
  // second `x dev` on one machine reported: no code, no fix, at the boot path this package owns.
84
89
  // The return type is inferred, keeping `Bun.serve`'s own shape stated once.
package/src/prerender.ts CHANGED
@@ -17,6 +17,7 @@ import { errorPageDocument, STATIC_ERROR_PAGE } from './error-pages';
17
17
  import { FAVICON_PATH, faviconBytes } from './favicon';
18
18
  import type { IslandBundle } from './island-bundle';
19
19
  import { buildIslands, writeIslands } from './island-bundle';
20
+ import { loadPwaArtifacts, WEB_MANIFEST_PATH, writePwaIcons } from './pwa-artifacts';
20
21
  import type { SkippedRoute, UnmeasuredRoute } from './static-report';
21
22
  import { skippedRoute, skipReasonFor, writeStaticReport } from './static-report';
22
23
 
@@ -147,6 +148,20 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
147
148
  join(options.out, STATIC_ERROR_PAGE),
148
149
  await errorPageDocument(options.root, NOT_FOUND_STATUS),
149
150
  );
151
+ // And the file every document above is about to name. A static export is served with no process
152
+ // behind it, so `<link rel="manifest">` resolves to a 404 unless the bytes are in the artifact —
153
+ // an installable app that is installable only under `x dev` is the dev/prod split this whole
154
+ // wiring exists to close. `undefined` when the app is not installable, and then no document
155
+ // names it either.
156
+ const pwa = await loadPwaArtifacts(options.root);
157
+ if (pwa !== undefined) {
158
+ await Bun.write(join(options.out, WEB_MANIFEST_PATH.slice(1)), pwa.body);
159
+ // And the icons that manifest NAMES. A static host runs no `assetRoutes()`, so every
160
+ // `/icons/*` entry would be a 404 in the install prompt — the manifest half of the same
161
+ // promise `favicon.ico` above keeps. Nothing when the app committed no source icon, which is
162
+ // also when the manifest names no icon.
163
+ await writePwaIcons(options.root, options.out);
164
+ }
150
165
 
151
166
  // Every render below goes through `routeDocument`, which is the function a REQUEST reaches — and
152
167
  // a request arrives inside `runWithContext`, installed by the HTTP pipeline (`dev-render.ts`).
@@ -161,6 +176,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
161
176
  runWithContext(ctx, () =>
162
177
  routeDocument(entry, data, {
163
178
  resolveIsland: (file: string) => islands.resolverFor(file),
179
+ ...(pwa === undefined ? {} : { pwaHead: pwa.head }),
164
180
  }),
165
181
  );
166
182
 
@@ -0,0 +1,188 @@
1
+ // The web manifest an installable app promises, and that no build had ever produced.
2
+ //
3
+ // `pwa.enabled` was a switch with no reader anywhere in the tree (issue #362, and
4
+ // `scripts/lib/config-reader-pins.ts` pinned it as a `jobs.driver` candidate). `@ultimat3/pwa` has
5
+ // shipped `generateWebManifest`, `renderThemeColorMeta`, `planIcons` and `appleTouchLinks` since it
6
+ // existed and NOTHING called them, so every Ultimate app served a `<head>` with no
7
+ // `<link rel="manifest">`, no `theme-color` and no apple-touch icon — and no browser has ever
8
+ // offered to install one, however the config was written.
9
+ //
10
+ // WHY HERE. `dev-assets.ts`'s reason exactly: three packages declare what an installable app is and
11
+ // none of them can read a config file off disk. This one composes tier 0's `pwa` block with tier
12
+ // 4's generator and hands both served surfaces and the static export the same two strings.
13
+ //
14
+ // WHAT IT DOES NOT DO: emit a service worker. `offline`, `backgroundSync` and `push` still have no
15
+ // build behind them — `wiki/PWA-And-Offline.md` says so — and a bad `sw.js` is sticky in a way a
16
+ // manifest is not, so the worker lands behind a real browser check rather than beside this.
17
+
18
+ // why: Bun exposes no synchronous file-existence primitive, and this read is the same one
19
+ // `app-auth.ts` and `dev-cache.ts` each make before importing an app's config — a root with no
20
+ // `app.config.ts` is an ordinary answer here (a scratch root, `x build` outside an app).
21
+ import { existsSync } from 'node:fs';
22
+ // why: Bun exposes no path-join primitive, and `APP_CONFIG_FILE` is app-root-relative — the same
23
+ // necessity `favicon.ts` and `dev-assets.ts` each record for their own root-relative constant.
24
+ import { join } from 'node:path';
25
+ import type { PwaColors } from '@ultimat3/core';
26
+ import type { CacheHint, Route, UltimateRequest } from '@ultimat3/http';
27
+ import { applyCacheHeaders } from '@ultimat3/http';
28
+ import {
29
+ appleTouchLinks,
30
+ generateWebManifest,
31
+ renderThemeColorMeta,
32
+ serializeWebManifest,
33
+ } from '@ultimat3/pwa';
34
+ import { escapeAttribute } from '@ultimat3/seo';
35
+ import { APP_CONFIG_EXPORT } from './app-auth';
36
+ import { APP_CONFIG_FILE } from './app-root';
37
+ import { hasSourceIcon, iconPlan, iconRenderer } from './icon-assets';
38
+
39
+ /** What a browser fetches from `<link rel="manifest">`. The spec's own extension, not `.json`. */
40
+ export const WEB_MANIFEST_PATH = '/manifest.webmanifest';
41
+
42
+ /**
43
+ * Short, never immutable. The path carries no content hash, so an app that changed its install
44
+ * title must be able to publish it — an hour is `favicon.ts`'s number, for the same asset class.
45
+ */
46
+ const MANIFEST_CACHE: CacheHint = { mode: 'public', maxAgeSeconds: 3600 };
47
+
48
+ /** The two strings every surface needs: the file's bytes, and what `<head>` must carry to name it. */
49
+ export interface PwaArtifacts {
50
+ /** `manifest.webmanifest`, serialized. */
51
+ readonly body: string;
52
+ /**
53
+ * `<link rel="manifest">`, both `theme-color` metas, and every apple-touch icon link. One string
54
+ * because a document either carries all of it or none: a manifest link with no theme colour
55
+ * installs an app whose status bar flashes white on every launch, and an apple-touch link with
56
+ * no manifest is an iOS icon for an app iOS will not add.
57
+ */
58
+ readonly head: string;
59
+ }
60
+
61
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
62
+ typeof value === 'object' && value !== null;
63
+
64
+ const text = (value: unknown): string | undefined =>
65
+ typeof value === 'string' && value.trim() !== '' ? value : undefined;
66
+
67
+ /**
68
+ * `colors` read structurally, for `loadSignInPath`'s reason: `defineConfig` returns a plain object
69
+ * and a config resolved through an older core simply has no such key. `validate()` already refused
70
+ * a blank one an `await import` above this line, so anything this rejects is a hand-written config
71
+ * object — and the honest answer for one is no manifest at all, never a colour we invented.
72
+ */
73
+ function colorsOf(value: unknown): PwaColors | undefined {
74
+ if (!isRecord(value)) return undefined;
75
+ const light = isRecord(value['light']) ? value['light'] : undefined;
76
+ const dark = isRecord(value['dark']) ? value['dark'] : undefined;
77
+ if (light === undefined || dark === undefined) return undefined;
78
+ const [lt, lb, dt, db] = [
79
+ text(light['themeColor']),
80
+ text(light['backgroundColor']),
81
+ text(dark['themeColor']),
82
+ text(dark['backgroundColor']),
83
+ ];
84
+ if (lt === undefined || lb === undefined || dt === undefined || db === undefined)
85
+ return undefined;
86
+ return {
87
+ light: { themeColor: lt, backgroundColor: lb },
88
+ dark: { themeColor: dt, backgroundColor: db },
89
+ };
90
+ }
91
+
92
+ /** The `pwa` block, as much of it as this file needs, or `undefined` when the app declares none. */
93
+ interface InstallableApp {
94
+ readonly name: string;
95
+ readonly colors: PwaColors;
96
+ }
97
+
98
+ async function loadInstallable(root: string): Promise<InstallableApp | undefined> {
99
+ const configPath = join(root, APP_CONFIG_FILE);
100
+ if (!existsSync(configPath)) return undefined;
101
+ const module = (await import(configPath)) as Record<string, unknown>;
102
+ const config = module[APP_CONFIG_EXPORT];
103
+ if (!isRecord(config)) return undefined;
104
+ const pwa = config['pwa'];
105
+ // `pwa.enabled`, read. Not a truthiness test: `enabled` is the key this whole file exists to
106
+ // give a reader, and `=== true` is what makes a hand-written `enabled: 'yes'` produce no
107
+ // manifest rather than one nobody asked for.
108
+ if (!isRecord(pwa) || pwa['enabled'] !== true) return undefined;
109
+ const name = text(pwa['name']);
110
+ const colors = colorsOf(pwa['colors']);
111
+ if (name === undefined || colors === undefined) return undefined;
112
+ return { name, colors };
113
+ }
114
+
115
+ /**
116
+ * Resolved ONCE at boot, like `loadSignInPath` and `loadCacheTiers` and unlike `faviconBytes`:
117
+ * `await import` caches the module, so re-reading per request would answer the same object at a
118
+ * per-request cost, and the head string has to be available synchronously while a document renders.
119
+ */
120
+ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | undefined> {
121
+ const app = await loadInstallable(root);
122
+ if (app === undefined) return undefined;
123
+ // The icons the manifest promises are exactly the ones `/icons/*` serves and `x build` writes —
124
+ // ONE plan, so no surface can name a size another will not produce.
125
+ //
126
+ // AND ONLY WHEN THE APP HAS A SOURCE FOR THEM. `planIcons` answers the same fourteen entries
127
+ // whether `apps/web/site/icon.png` exists or not, so a manifest built off it unconditionally
128
+ // promises twelve icons and three apple-touch links that are twelve 404s in an install prompt —
129
+ // the promise-nothing-keeps shape this whole module exists to close, one level down.
130
+ // `examples/dummy` is exactly that app: it declares `pwa.enabled: true` and commits no icon.
131
+ // A missing source is NOT reported here — `x doctor` already refuses it by name with
132
+ // `X_PWA_ICON_MISSING`, and a second reporter of one condition is the duplication this package's
133
+ // own rule forbids. Read at boot, like the rest of this function: adding the file takes effect
134
+ // on the next start, because the manifest is generated once and served as bytes.
135
+ const icons = (await hasSourceIcon(root)) ? iconPlan() : undefined;
136
+ const result = generateWebManifest({
137
+ name: app.name,
138
+ tokens: app.colors,
139
+ icons: icons?.manifestIcons ?? [],
140
+ });
141
+ return {
142
+ body: serializeWebManifest(result.manifest),
143
+ head:
144
+ `<link rel="manifest" href="${escapeAttribute(WEB_MANIFEST_PATH)}">` +
145
+ renderThemeColorMeta(result.themeColorMeta) +
146
+ (icons === undefined ? '' : appleTouchLinks(icons)),
147
+ };
148
+ }
149
+
150
+ /**
151
+ * The icon bytes a STATIC export has to carry, written under `out`. Answers the paths it wrote.
152
+ *
153
+ * A static host runs no `assetRoutes()`, so every `/icons/*` entry the manifest names is a 404
154
+ * unless the bytes are in the artifact — the same rule `prerenderSite` already applies to
155
+ * `favicon.ico` and `404.html`, one asset class further along. Nothing when the app has no source
156
+ * icon, which is also when the manifest names none.
157
+ */
158
+ export async function writePwaIcons(root: string, out: string): Promise<readonly string[]> {
159
+ if (!(await hasSourceIcon(root))) return [];
160
+ const plan = iconPlan();
161
+ const render = iconRenderer(root);
162
+ const written: string[] = [];
163
+ for (const entry of plan.entries) {
164
+ // `outputPath` is `/icons/<file>`; `out` is the export root, so the leading slash goes.
165
+ await Bun.write(join(out, entry.outputPath.slice(1)), await render(plan, entry.outputPath));
166
+ written.push(entry.outputPath);
167
+ }
168
+ return written;
169
+ }
170
+
171
+ /**
172
+ * Mounted through `assetRoutes`, so `x dev` and the container serve it from one place — a surface
173
+ * that answers in dev and not in the image is the failure that file's own header names.
174
+ * Public: a browser fetches a manifest before anyone has signed in, and an installable app that
175
+ * needs a session to describe itself is not installable.
176
+ */
177
+ export const pwaManifestRoute = (artifacts: PwaArtifacts): Route => ({
178
+ method: 'GET',
179
+ path: WEB_MANIFEST_PATH,
180
+ meta: { name: 'assets.manifest', auth: 'public', cache: MANIFEST_CACHE, tags: ['assets'] },
181
+ handler: async (_request: UltimateRequest): Promise<Response> =>
182
+ applyCacheHeaders(
183
+ new Response(artifacts.body, {
184
+ headers: { 'content-type': 'application/manifest+json; charset=utf-8' },
185
+ }),
186
+ MANIFEST_CACHE,
187
+ ),
188
+ });
package/src/serve.ts CHANGED
@@ -25,6 +25,7 @@ import { apiRoutes } from './api-routes';
25
25
  import { loadSignInPath } from './app-auth';
26
26
  import { loadApp } from './app-load';
27
27
  import { appManifest } from './app-manifest';
28
+ import { acceptCreatedTables } from './db-accept-created';
28
29
  import { assetRoutes } from './dev-assets';
29
30
  import { startQueue } from './dev-queue';
30
31
  import { appRoutes } from './dev-render';
@@ -43,6 +44,7 @@ import { islandRoutes } from './island-routes';
43
44
  import { DEFAULT_METRICS_PORT } from './metrics-endpoint';
44
45
  import { readMigrations } from './migrations';
45
46
  import { startOtlpExport } from './otlp-export';
47
+ import { loadPwaArtifacts } from './pwa-artifacts';
46
48
  import type { RuntimeOverrides } from './runtime-overrides';
47
49
 
48
50
  export const DEFAULT_PORT = 3000;
@@ -200,7 +202,11 @@ export async function runMigrations(options: ServeOptions): Promise<MigratedApp>
200
202
  available: migrations.length,
201
203
  appVersion: report.appVersion,
202
204
  });
203
- const drift = await checkDrift({ migrations });
205
+ // Every migration above has just been applied, so a `create table` in one of them is proof
206
+ // the app owns that relation — and a snapshot records only what ENTITIES declare, so without
207
+ // this a hand-written table is `unexpected-table` on this deploy and on every deploy after it
208
+ // (issue #345). Only that one difference, only for a name a migration's SQL creates.
209
+ const drift = acceptCreatedTables(await checkDrift({ migrations }), migrations);
204
210
  // Logged with the first difference, not just a count: a release phase's log is the only place
205
211
  // an operator sees this, and "3 differences" names nothing to act on.
206
212
  if (!drift.ok) {
@@ -293,18 +299,24 @@ async function bootRoles(boot: {
293
299
  // does from the same source — the alternative is a second bundler invocation in the image build
294
300
  // whose output nothing compares against the one the dev loop proved.
295
301
  const islands = await buildIslands(options.root);
302
+ // The same two strings `x dev` resolves, from the same reader: a `<link rel="manifest">` served
303
+ // on a laptop and absent in the image is exactly the dev/prod difference this file exists to
304
+ // prevent, and it is the one an operator cannot see without installing the app.
305
+ const pwa = await loadPwaArtifacts(options.root);
296
306
  const routes: readonly Route[] = [
297
307
  ...apiRoutes(),
298
308
  ...assetRoutes({
299
309
  root: options.root,
300
310
  storage: runtime.storage,
301
311
  ...(options.runtime?.images === undefined ? {} : { images: options.runtime.images }),
312
+ ...(pwa === undefined ? {} : { pwa }),
302
313
  }),
303
314
  ...storageRoutes({ storage: runtime.storage }),
304
315
  ...islandRoutes(() => islands),
305
316
  ...appRoutes({
306
317
  buildId,
307
318
  resolveIsland: (file) => islands.resolverFor(file),
319
+ ...(pwa === undefined ? {} : { pwaHead: pwa.head }),
308
320
  // Only when a store was supplied. `createIsrController` defaults to a per-process memory
309
321
  // store, so twelve replicas hold twelve of them and a purge tag regenerates one twelfth of
310
322
  // the fleet while the other eleven keep serving the page it just invalidated.
@@ -5,7 +5,7 @@
5
5
  // by wiring, not by design.
6
6
 
7
7
  import type { Actor, Clock } from '@ultimat3/core';
8
- import { systemClock } from '@ultimat3/core';
8
+ import { finiteCount, systemClock } from '@ultimat3/core';
9
9
  import type { HttpConfig } from '@ultimat3/http';
10
10
  import {
11
11
  configuredAuthenticator,
@@ -102,7 +102,13 @@ export function syncAuthenticator(
102
102
  // this per connection otherwise.
103
103
  const config = upgradeConfig(buildId);
104
104
  const clock = options.clock ?? systemClock;
105
- const ttlMs = options.ttlMs ?? SYNC_GRANT_TTL_MS;
105
+ // A credential lifetime, screened where it is declared. `expiresAt` is `now + ttlMs`, and
106
+ // `GrantBook.expired()` asks `expiresAt <= now` — false for every `now` when the sum is `NaN`,
107
+ // so the grant never reaches a sweep and the socket keeps a revoked actor's authority for as
108
+ // long as the tab is open. That is the hole this whole file was written to close, and a `??`
109
+ // does not close it: `NaN` is not nullish. At least 1ms — a window that has already shut when
110
+ // the grant is minted is not a window.
111
+ const ttlMs = finiteCount('syncAuthenticator', 'ttlMs', options.ttlMs ?? SYNC_GRANT_TTL_MS, 1);
106
112
 
107
113
  const resolve = async (credential: Credential): Promise<SyncGrant | null> => {
108
114
  const request = new Request(credential.url, {