@ultimat3/cli 24.0.0 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +15 -8
- package/README.md +30 -5
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +3 -1
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +3 -3
- package/src/app-env.ts +21 -14
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +2 -2
- package/src/app-mcp.ts +25 -27
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +2 -8
- package/src/app-permissions-site.ts +31 -7
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +115 -121
- package/src/cdp-shot-driver.ts +17 -0
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth.ts +2 -2
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +89 -9
- package/src/cmd-db-backfill.ts +1 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +5 -2
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -357
- package/src/cmd-doctor.ts +16 -66
- package/src/cmd-env.ts +29 -14
- package/src/cmd-generate.ts +12 -2
- package/src/cmd-i18n.ts +11 -8
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +18 -137
- package/src/cmd-mcp.ts +25 -7
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +9 -0
- package/src/cmd-routes.ts +3 -3
- package/src/cmd-shot-matrix.ts +21 -4
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +35 -39
- package/src/cmd-test.ts +1 -1
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +19 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +1 -1
- package/src/dev-dashboard.ts +5 -1
- package/src/dev-lock.ts +96 -24
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +14 -7
- package/src/doctor-auth.ts +4 -4
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +2 -2
- package/src/drift.ts +29 -6
- package/src/error-codes.ts +6 -0
- package/src/error-page-csp.ts +14 -15
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-kinds.ts +3 -17
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +50 -1
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +21 -8
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +54 -64
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +32 -16
- package/src/local-cli-handoff.ts +90 -0
- package/src/mcp-errors.ts +8 -1
- package/src/mcp-host.ts +6 -5
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +27 -16
- package/src/metrics-endpoint.ts +3 -2
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +18 -0
- package/src/page-speculation.ts +14 -11
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +63 -98
- package/src/pwa-artifacts.ts +8 -1
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/retry-memo.ts +1 -1
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +110 -31
- package/src/role-start.ts +64 -85
- package/src/role-sync.ts +65 -6
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +15 -2
- package/src/runtime-bindings.ts +23 -3
- package/src/runtime-cache.ts +8 -8
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +2 -2
- package/src/runtime-jobs.ts +78 -18
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +2 -2
- package/src/runtime-overrides.ts +3 -3
- package/src/runtime-purge.ts +1 -1
- package/src/runtime-queue.ts +12 -12
- package/src/runtime-realtime.ts +5 -3
- package/src/runtime-render.ts +77 -82
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +33 -13
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +3 -3
- package/src/schema-diff.ts +1 -1
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +2 -2
- package/src/serve-boot.ts +22 -11
- package/src/serve-drain.ts +3 -1
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +19 -8
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -15
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-seo.ts +6 -4
- package/src/source-files.ts +13 -3
- package/src/static-report.ts +5 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +1 -1
- package/src/templates/action.ts +4 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +23 -8
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +2 -1
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container.ts +12 -6
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-templates.ts +74 -18
- package/src/templates/scaffold-helm.ts +31 -4
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +18 -15
- package/src/templates/scaffold-shell.ts +17 -2
- package/src/test-only-shipped.ts +193 -0
- package/src/test-select.ts +2 -1
- package/src/test-shards.ts +1 -4
- package/src/theme-boot.ts +2 -2
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/verify-checks.ts +12 -2
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +87 -10
- package/src/verify-merge.ts +48 -16
- package/src/verify-part-step.ts +10 -2
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +3 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +0 -1
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/jobs-drain.ts +0 -230
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
package/src/cmd-shot.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { mkdirSync } from 'node:fs';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { renderFixShellArg } from '@ultimat3/core';
|
|
11
|
+
import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
11
12
|
import { IDLE_HYDRATE_TIMEOUT_MS } from '@ultimat3/render';
|
|
12
13
|
import { requireAppRoot } from './app-root';
|
|
13
14
|
import { appBrowser } from './browser-launcher';
|
|
@@ -23,7 +24,13 @@ import {
|
|
|
23
24
|
refuseSweepWithRoute,
|
|
24
25
|
refuseSweepWithState,
|
|
25
26
|
} from './cmd-shot-island';
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
MATRIX_DIR,
|
|
29
|
+
matrixRoutes,
|
|
30
|
+
planShotMatrix,
|
|
31
|
+
refuseMatrixWithComponent,
|
|
32
|
+
runShotMatrix,
|
|
33
|
+
} from './cmd-shot-matrix';
|
|
27
34
|
import { shotSpec } from './cmd-shot-spec';
|
|
28
35
|
import type { CliCommand, CommandContext } from './command';
|
|
29
36
|
import { BadFlagError, MissingPositionalError } from './errors';
|
|
@@ -33,12 +40,7 @@ import type { ParsedArgs } from './parse';
|
|
|
33
40
|
import { flagBool, flagString } from './parse';
|
|
34
41
|
import { shotBrowserChoice } from './shot-browser';
|
|
35
42
|
import { readCookieFlag, type ShotCookiePair, shotCookies } from './shot-cookie';
|
|
36
|
-
import {
|
|
37
|
-
acceptLanguageHeaders,
|
|
38
|
-
loadShotLocales,
|
|
39
|
-
localizedShotPath,
|
|
40
|
-
readLocaleFlag,
|
|
41
|
-
} from './shot-locale';
|
|
43
|
+
import { acceptLanguageHeaders, localizedShotPath, readLocaleFlag } from './shot-locale';
|
|
42
44
|
import type { BootDevServer, ShotServer } from './shot-server';
|
|
43
45
|
import { allowHostsFrom, devServerFor, SHOT_DIR } from './shot-server';
|
|
44
46
|
import { SETTLE_POLL_MS, settleIslands } from './shot-settle';
|
|
@@ -53,21 +55,12 @@ import {
|
|
|
53
55
|
shotSummary,
|
|
54
56
|
verdictJson,
|
|
55
57
|
} from './shot-verdict';
|
|
58
|
+
import { parseViewports, refuseViewportWithComponent, runShotViewports } from './shot-viewport';
|
|
56
59
|
|
|
57
60
|
/** Kernel-picked by default: :3000 is usually another project's dev server, not a free port. */
|
|
58
61
|
const DEFAULT_PORT = 0;
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
* How long the page is left alone after `load` before it is photographed: exactly the
|
|
62
|
-
* `requestIdleCallback` deadline `@ultimat3/render`'s hydration runtime gives an `idle` island —
|
|
63
|
-
* shoot sooner and the verdict reports `booted: 0` for a page that hydrates perfectly. READ from
|
|
64
|
-
* that runtime rather than restated, because two copies of one number that must agree is the drift
|
|
65
|
-
* axiom 2 refuses: the settle window is not "2 seconds", it is "the deadline the runtime uses".
|
|
66
|
-
*/
|
|
67
|
-
export const DEFAULT_SETTLE_MS = IDLE_HYDRATE_TIMEOUT_MS;
|
|
68
|
-
|
|
69
|
-
// Re-exported, not re-declared: `cmd-shot.test.ts` and the island path both name them, and a
|
|
70
|
-
// second declaration of a path or an allow-list rule is a second answer.
|
|
63
|
+
// Re-exported, not re-declared: a second declaration of a path or a host rule is a second answer.
|
|
71
64
|
export type { BootDevServer, ShotServer };
|
|
72
65
|
export { allowHostsFrom, devServerFor, SHOT_DIR };
|
|
73
66
|
|
|
@@ -372,7 +365,8 @@ export const shotCommand: CliCommand = {
|
|
|
372
365
|
const sweep = flagBool(ctx.args, 'all-islands');
|
|
373
366
|
const matrix = flagBool(ctx.args, 'matrix');
|
|
374
367
|
const cookies = readCookieFlag(flagString(ctx.args, 'cookie'));
|
|
375
|
-
const
|
|
368
|
+
const viewports = parseViewports(flagString(ctx.args, 'viewport'));
|
|
369
|
+
const app = await appLocaleSet(root);
|
|
376
370
|
const locale = readLocaleFlag(flagString(ctx.args, 'locale'), app);
|
|
377
371
|
// Every ambiguous pair refused BY NAME, before a value is read: a reader who typed two
|
|
378
372
|
// subjects has a belief about which one runs, and half of them would be wrong.
|
|
@@ -385,14 +379,9 @@ export const shotCommand: CliCommand = {
|
|
|
385
379
|
}
|
|
386
380
|
const component = sweep || (island !== undefined && island !== '');
|
|
387
381
|
// The matrix photographs ROUTES; a component beside it is two subjects.
|
|
388
|
-
if (matrix && component)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
command: 'shot',
|
|
392
|
-
reason: 'photographs every site route; --island and --all-islands photograph components',
|
|
393
|
-
fix: 'x shot --matrix --json',
|
|
394
|
-
});
|
|
395
|
-
}
|
|
382
|
+
if (matrix && component) refuseMatrixWithComponent();
|
|
383
|
+
// An island state declares its own size, so a size asked for beside one is a second answer.
|
|
384
|
+
if (component && viewports !== undefined) refuseViewportWithComponent();
|
|
396
385
|
// An island is photographed in BOTH themes by the harness, which owns its `data-theme` and
|
|
397
386
|
// carries no boot script — so a theme asked for beside one is a request nothing could honour.
|
|
398
387
|
if (component && locale !== undefined) {
|
|
@@ -414,7 +403,12 @@ export const shotCommand: CliCommand = {
|
|
|
414
403
|
// `--matrix` alone is every site route; a route beside it narrows the matrix to that one.
|
|
415
404
|
const route = component || (matrix && positional === undefined) ? '' : readRoute(positional);
|
|
416
405
|
const port = intFlag(ctx.args, 'port', PORT_RANGE.min, DEFAULT_PORT, PORT_RANGE.max);
|
|
417
|
-
|
|
406
|
+
// How long the page is left alone after `load` before it is photographed: exactly the
|
|
407
|
+
// `requestIdleCallback` deadline `@ultimat3/render`'s hydration runtime gives an `idle` island —
|
|
408
|
+
// shoot sooner and the verdict reports `booted: 0` for a page that hydrates perfectly. The
|
|
409
|
+
// runtime's own constant, under its own name: the settle window is not "2 seconds", it is "the
|
|
410
|
+
// deadline the runtime uses", and a second name for it is a second import path.
|
|
411
|
+
const settleMs = intFlag(ctx.args, 'settle', 0, IDLE_HYDRATE_TIMEOUT_MS);
|
|
418
412
|
const timeoutMs = intFlag(ctx.args, 'timeout', 1, DEFAULT_PAGE_TIMEOUT_MS);
|
|
419
413
|
// Read only when given: its absence means "any 2xx", which no single default number can say.
|
|
420
414
|
const expectStatus =
|
|
@@ -473,22 +467,24 @@ export const shotCommand: CliCommand = {
|
|
|
473
467
|
locales: locale === undefined ? app.locales : [locale],
|
|
474
468
|
defaultLocale: app.defaultLocale,
|
|
475
469
|
...(theme === undefined ? {} : { themes: [theme] }),
|
|
470
|
+
...(viewports === undefined ? {} : { viewports }),
|
|
476
471
|
});
|
|
477
472
|
const outDir = shotOutDir(ctx.cwd, out, join(root, MATRIX_DIR));
|
|
478
473
|
return runShotMatrix({ cells, outDir, boot, shoot: runShot, base });
|
|
479
474
|
}
|
|
480
475
|
const shown = locale ?? app.defaultLocale;
|
|
481
476
|
const path = localizedShotPath(route, shown, app.defaultLocale);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
})
|
|
492
|
-
|
|
477
|
+
const single = {
|
|
478
|
+
...base,
|
|
479
|
+
route: path,
|
|
480
|
+
// Pinned whether or not `--locale` was given: the default locale, never the box's own.
|
|
481
|
+
acceptLanguage: shown,
|
|
482
|
+
...(theme === undefined ? {} : { colorScheme: theme }),
|
|
483
|
+
};
|
|
484
|
+
const outDir = shotOutDir(ctx.cwd, out, join(root, SHOT_DIR, shotSlug(path)));
|
|
485
|
+
if (viewports !== undefined) {
|
|
486
|
+
return runShotViewports({ viewports, outDir, boot, shoot: runShot, base: single });
|
|
487
|
+
}
|
|
488
|
+
return shotResult(await runShot({ ...single, outDir, boot }));
|
|
493
489
|
},
|
|
494
490
|
};
|
package/src/cmd-test.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// there, because it is a fact about a git diff and not about a path: what the diff touches is
|
|
6
6
|
// `affected.ts`, and this file only maps that answer onto the paths discovery yields.
|
|
7
7
|
|
|
8
|
+
import { TEST_TYPES } from '@ultimat3/testing';
|
|
8
9
|
import type { AffectedScope } from './affected';
|
|
9
10
|
import { affectedScope, affectedScopeJson, DEFAULT_BASE, inScope } from './affected';
|
|
10
11
|
import { testSpec } from './cmd-test-spec';
|
|
@@ -37,7 +38,6 @@ import {
|
|
|
37
38
|
} from './test-workers';
|
|
38
39
|
import { readVerifyFloor } from './verify-floor';
|
|
39
40
|
import type { TestType } from './verify-tests';
|
|
40
|
-
import { TEST_TYPES } from './verify-tests';
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* `--workers`. `Number.parseInt` alone accepted `4abc` and `4.9` as four, while `cmd-verify.ts`'s
|
package/src/coverage-lcov.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/** One `SF:` record, as written: its own totals and every `DA:` line. */
|
|
6
6
|
export interface LcovRecord {
|
|
7
|
-
/** The `SF:` path
|
|
7
|
+
/** The `SF:` path with `/` separators — Bun writes it relative to the directory the run started in. */
|
|
8
8
|
readonly file: string;
|
|
9
9
|
/** `LF` / `LH` / `FNF` / `FNH`. Bun's lcov carries no `FN:` lines, so functions are counts only. */
|
|
10
10
|
readonly linesFound: number;
|
|
@@ -25,7 +25,9 @@ export function parseLcov(text: string): readonly LcovRecord[] {
|
|
|
25
25
|
let lines = new Map<number, number>();
|
|
26
26
|
for (const line of text.split('\n')) {
|
|
27
27
|
if (line.startsWith('SF:')) {
|
|
28
|
-
|
|
28
|
+
// `/` on every OS: Bun on Windows writes `apps\web\…`, and the app's source walk keys its
|
|
29
|
+
// files with `/`, so no record matched and the floor read 0% of a fully tested app.
|
|
30
|
+
file = line.slice(3).replaceAll('\\', '/');
|
|
29
31
|
lf = 0;
|
|
30
32
|
lh = 0;
|
|
31
33
|
fnf = 0;
|
package/src/db-backfill.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// which names were asked for, whether `--write` was passed, and what `x_migrations` says.
|
|
11
11
|
|
|
12
12
|
import type { Environment } from '@ultimat3/core';
|
|
13
|
-
import {
|
|
13
|
+
import { ctxOf } from '@ultimat3/core';
|
|
14
14
|
import { db, isLedgerMissing, readLedger } from '@ultimat3/db';
|
|
15
15
|
import type {
|
|
16
16
|
BackfillDeclaration,
|
|
@@ -218,7 +218,7 @@ async function remainingFor(declaration: BackfillDeclaration): Promise<number |
|
|
|
218
218
|
const count = handle === undefined ? undefined : backfillOrigin(handle)?.count;
|
|
219
219
|
if (count === undefined) return null;
|
|
220
220
|
try {
|
|
221
|
-
return await count({ ctx:
|
|
221
|
+
return await count({ ctx: ctxOf({ role: 'migrate' }) });
|
|
222
222
|
} catch {
|
|
223
223
|
return null;
|
|
224
224
|
}
|
package/src/db-replay-engine.ts
CHANGED
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
// why: Bun ships no path joiner.
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import type { DbClient, LinkedExtensions, Migration } from '@ultimat3/db';
|
|
9
|
-
import {
|
|
10
|
-
createPgliteClient,
|
|
11
|
-
createPostgresClient,
|
|
12
|
-
DbError,
|
|
13
|
-
linkPgliteExtensions,
|
|
14
|
-
raw,
|
|
15
|
-
} from '@ultimat3/db';
|
|
9
|
+
import { DbError, linkPgliteExtensions, pgliteClient, postgresClient, raw } from '@ultimat3/db';
|
|
16
10
|
import { migrationExtensions } from './migration-extensions';
|
|
17
11
|
|
|
18
12
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
@@ -73,14 +67,14 @@ async function withPostgresScratch<T>(
|
|
|
73
67
|
work: (client: DbClient) => Promise<T>,
|
|
74
68
|
): Promise<T> {
|
|
75
69
|
const database = scratchName();
|
|
76
|
-
const admin =
|
|
70
|
+
const admin = postgresClient({ url: adminUrl });
|
|
77
71
|
try {
|
|
78
72
|
// `template0`: the server's `template1` may carry objects an operator put there, and they
|
|
79
73
|
// would be dumped as the app's.
|
|
80
74
|
await admin.execute(raw(`create database ${database} template template0`));
|
|
81
75
|
const target = new URL(adminUrl);
|
|
82
76
|
target.pathname = `/${database}`;
|
|
83
|
-
const client =
|
|
77
|
+
const client = postgresClient({ url: target.toString() });
|
|
84
78
|
try {
|
|
85
79
|
return await work(client);
|
|
86
80
|
} finally {
|
|
@@ -103,7 +97,7 @@ export async function withScratchDatabase<T>(
|
|
|
103
97
|
work: (client: DbClient) => Promise<T>,
|
|
104
98
|
): Promise<T> {
|
|
105
99
|
if (engine.kind === 'postgres') return withPostgresScratch(engine.adminUrl, work);
|
|
106
|
-
const client =
|
|
100
|
+
const client = pgliteClient({
|
|
107
101
|
extensions: engine.extensions,
|
|
108
102
|
...(stateDir === undefined ? {} : { snapshotDir: join(stateDir, 'cache') }),
|
|
109
103
|
});
|
package/src/db-subscribes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Single responsibility: which tables `x db gen` must grant `REPLICA IDENTITY FULL` — the
|
|
2
|
-
//
|
|
3
|
-
// refusal for a declared name no entity's table matches. Only this tier holds
|
|
1
|
+
// Single responsibility: which tables `x db gen` must grant `REPLICA IDENTITY FULL` — the `records`
|
|
2
|
+
// tables of every channel with params, and since #518 never a live query's `subscribes:` — and the
|
|
3
|
+
// refusal for a declared `subscribes:` name no entity's table matches. Only this tier holds both.
|
|
4
4
|
|
|
5
5
|
import { UltimateError } from '@ultimat3/core';
|
|
6
6
|
import { paramsChannelTables } from '@ultimat3/realtime/server';
|
|
@@ -29,11 +29,10 @@ function offer(tables: ReadonlySet<string>): string {
|
|
|
29
29
|
/**
|
|
30
30
|
* A live read declares a relation this app has no entity for.
|
|
31
31
|
*
|
|
32
|
-
* Refused rather than dropped
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* while its author read the declaration as granted, which is the failure #357 exists to end.
|
|
32
|
+
* Refused rather than dropped: `@ultimat3/query` has no table catalog, so its own `subscribes:`
|
|
33
|
+
* assertions cannot see a name no table has — and a typo there is a declaration its author reads
|
|
34
|
+
* as true. Until #518 the list also chose which tables got `REPLICA IDENTITY FULL`, so the typo
|
|
35
|
+
* granted it to nothing (#357); the grant is gone, the declaration's honesty still matters.
|
|
37
36
|
*
|
|
38
37
|
* The name goes in the `cause` and never into a command: it is a string from the app's own source,
|
|
39
38
|
* and the remedy is an edit to a field rather than anything to paste at a shell.
|
|
@@ -47,8 +46,8 @@ export class QuerySubscribesUnknownError extends UltimateError {
|
|
|
47
46
|
'declares a table with that name',
|
|
48
47
|
fix:
|
|
49
48
|
`edit subscribes: on the query "${input.query}" to name a table this app declares ` +
|
|
50
|
-
`(${offer(input.tables)}), or drop the name
|
|
51
|
-
'
|
|
49
|
+
`(${offer(input.tables)}), or drop the name, then x db gen "fix subscribes" --json — a ` +
|
|
50
|
+
'name no table has can never match what the read selects from (X_QUERY_SUBSCRIBES_DRIFT)',
|
|
52
51
|
meta: { query: input.query, table: input.table },
|
|
53
52
|
});
|
|
54
53
|
}
|
|
@@ -57,13 +56,19 @@ export class QuerySubscribesUnknownError extends UltimateError {
|
|
|
57
56
|
/**
|
|
58
57
|
* The tables to hand `GenerateOptions.replicaIdentityFull`, deduped and sorted.
|
|
59
58
|
*
|
|
59
|
+
* A live query's `subscribes:` is VALIDATED here and never granted (#518, owner decision O-518):
|
|
60
|
+
* every entity has a primary key — `entity()` refuses one without — and a live query over a keyed
|
|
61
|
+
* table is correct on DEFAULT identity, because the shared window holds the whole row the
|
|
62
|
+
* in/out/delete decision needs (`realtime/src/pg-identity-window.live.test.ts`, on real WAL). FULL
|
|
63
|
+
* there only made every UPDATE and DELETE log the whole old row. A table already on FULL keeps it:
|
|
64
|
+
* `replica-identity.ts` never reverts, and the drift step reads a recorded FULL as agreement.
|
|
65
|
+
*
|
|
60
66
|
* Sorted so the same app generates the same bytes whatever order its modules registered in — the
|
|
61
67
|
* rule `@ultimat3/db`'s own `pending()` states one layer down, kept here too because a caller that
|
|
62
68
|
* ordered by registration would put a diff in a file for nothing.
|
|
63
69
|
*
|
|
64
|
-
* Every name is checked against `tables` BEFORE
|
|
65
|
-
*
|
|
66
|
-
* table and silently absent for the other.
|
|
70
|
+
* Every `subscribes:` name is checked against `tables` BEFORE anything is returned, so a refused
|
|
71
|
+
* declaration never leaves a migration half-written behind it.
|
|
67
72
|
*
|
|
68
73
|
* `channelTables` defaults to what the declared channels need (`paramsChannelTables()`): a channel
|
|
69
74
|
* with params routes a DELETE to the topic the OLD row names, and under the default identity that
|
|
@@ -76,14 +81,12 @@ export function replicaIdentityTables(
|
|
|
76
81
|
tables: ReadonlySet<string>,
|
|
77
82
|
channelTables: readonly string[] = paramsChannelTables(),
|
|
78
83
|
): readonly string[] {
|
|
79
|
-
const wanted = new Set<string>(channelTables.filter((table) => tables.has(table)));
|
|
80
84
|
for (const query of queries) {
|
|
81
85
|
for (const table of query.subscribes ?? []) {
|
|
82
86
|
if (!tables.has(table)) {
|
|
83
87
|
throw new QuerySubscribesUnknownError({ query: query.name, table, tables });
|
|
84
88
|
}
|
|
85
|
-
wanted.add(table);
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
|
-
return [...
|
|
91
|
+
return [...new Set(channelTables.filter((table) => tables.has(table)))].sort();
|
|
89
92
|
}
|
package/src/dev-boot.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// `x dev`'s boot: every role in one Bun process, embedded Postgres/events/storage started for real,
|
|
2
|
+
// the app's own modules loaded into the framework's registries, the route table they hold served
|
|
3
|
+
// over HTTP with `/_x` beside it, and the watcher that rebuilds on save (`dev-rebuild.ts`). The
|
|
4
|
+
// command that prints it and holds the process is `cmd-dev.ts`.
|
|
5
|
+
|
|
6
|
+
import { devShellStyle } from '@ultimat3/admin/dev';
|
|
7
|
+
import type { Role } from '@ultimat3/core';
|
|
8
|
+
import { configureTelemetry, noopExporter } from '@ultimat3/core';
|
|
9
|
+
import { setStatementObserver } from '@ultimat3/db';
|
|
10
|
+
import type { OverlayNotice, RequestContext } from '@ultimat3/http';
|
|
11
|
+
import { asCtx } from '@ultimat3/http';
|
|
12
|
+
import { loadSignInPath } from './app-auth';
|
|
13
|
+
import { appManifest } from './app-manifest';
|
|
14
|
+
import type { StalePin } from './app-reload-graph';
|
|
15
|
+
import { enableReloadTracking } from './app-reload-graph';
|
|
16
|
+
import { loadAppRuntime } from './app-runtime';
|
|
17
|
+
import type { DevDashboardInput, DevStatus } from './dev-dashboard';
|
|
18
|
+
import { devPanels } from './dev-dashboard';
|
|
19
|
+
import { declareDevEnvironment, needsDevEnvironmentDeclaration } from './dev-environment';
|
|
20
|
+
import { createStatementLedger } from './dev-n-plus-one';
|
|
21
|
+
import type { DevState } from './dev-rebuild';
|
|
22
|
+
import { devRebuilder } from './dev-rebuild';
|
|
23
|
+
import { devRouteTable } from './dev-route-table';
|
|
24
|
+
import { createTraceRecorder } from './dev-traces';
|
|
25
|
+
import { watchTree } from './dev-watch-tree';
|
|
26
|
+
import { buildIslands } from './island-bundle';
|
|
27
|
+
import { FRAME_STYLE } from './island-harness';
|
|
28
|
+
import type { Finding } from './output';
|
|
29
|
+
import type { RunningRoles } from './role-start';
|
|
30
|
+
import { DEV_ROLES, startRoles } from './role-start';
|
|
31
|
+
import type { DevServices } from './runtime-bindings';
|
|
32
|
+
import { resolveServices, withRealtimeEvents } from './runtime-bindings';
|
|
33
|
+
import { attachedIsr } from './runtime-isr';
|
|
34
|
+
import type { RuntimeOverrides } from './runtime-overrides';
|
|
35
|
+
import { replicaOverrides } from './runtime-replica';
|
|
36
|
+
import type { RunningServices } from './runtime-services';
|
|
37
|
+
import { releaseOrThrow, startServices } from './runtime-services';
|
|
38
|
+
import { metricsPortFor, releaseBoot } from './serve';
|
|
39
|
+
import { loopFacts, loopFinding, loopNotice } from './statement-loop';
|
|
40
|
+
|
|
41
|
+
export interface DevServer {
|
|
42
|
+
readonly url: string;
|
|
43
|
+
readonly services: DevServices;
|
|
44
|
+
readonly roles: readonly Role[];
|
|
45
|
+
/**
|
|
46
|
+
* `BUILD_ID` when stamped — the id every response carries. Otherwise the manifest as it stands
|
|
47
|
+
* now, so a reload that registers a new route moves it.
|
|
48
|
+
*/
|
|
49
|
+
readonly buildId: string;
|
|
50
|
+
/**
|
|
51
|
+
* Modules that would not import, primitives that would not register, reloads that would not
|
|
52
|
+
* build — and the statement loops this process has counted so far, which is what puts an N+1 in
|
|
53
|
+
* `x dev`'s own output and in `--json` without a channel of its own.
|
|
54
|
+
*/
|
|
55
|
+
readonly findings: readonly Finding[];
|
|
56
|
+
readonly running: RunningRoles;
|
|
57
|
+
readonly runtime: RunningServices;
|
|
58
|
+
/** Panel keys `/_x` mounted, in tab order. Reported so `--json` names what is reachable. */
|
|
59
|
+
readonly panels: readonly string[];
|
|
60
|
+
/** `POST <path>` of the app's default MCP endpoint, or `null` when nothing was mounted. */
|
|
61
|
+
readonly mcp: string | null;
|
|
62
|
+
/** `POST <path>` of every mounted MCP endpoint, default first — `[]` when none was. */
|
|
63
|
+
readonly mcpPaths: readonly string[];
|
|
64
|
+
stop(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface StartDevOptions {
|
|
68
|
+
readonly root: string;
|
|
69
|
+
readonly port: number;
|
|
70
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
71
|
+
readonly roles?: readonly Role[];
|
|
72
|
+
readonly onReload?: (file: string, durationMs: number) => void;
|
|
73
|
+
/**
|
|
74
|
+
* A save reached a module that defines a primitive, so this process cannot serve it
|
|
75
|
+
* (`takeStalePins`). Given, it is called INSTEAD of `onReload` — `x dev`'s child drains and exits
|
|
76
|
+
* for its supervisor to boot a fresh one. Absent, the save is `X_DEV_RESTART_REQUIRED` on `/_x`.
|
|
77
|
+
*/
|
|
78
|
+
readonly onRestart?: (pins: readonly StalePin[]) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The environment `devDashboard` refuses to mount in. Spread conditionally rather than passed as
|
|
83
|
+
* `undefined`: `exactOptionalPropertyTypes` makes "absent" and "explicitly undefined" different
|
|
84
|
+
* answers, and only the absent one lets the dashboard read the process environment itself.
|
|
85
|
+
*/
|
|
86
|
+
const envOf = (env: StartDevOptions['env']): { env?: string } => {
|
|
87
|
+
const value = env['NODE_ENV'] ?? env['X_ENV'];
|
|
88
|
+
return value === undefined ? {} : { env: value };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Boot order is the production order: services first, then the app's modules (importing them IS
|
|
93
|
+
* the registration), then the roles that serve what those modules registered. A module that
|
|
94
|
+
* fails to import becomes a finding rather than a dead process — the point of the dev loop is to
|
|
95
|
+
* still be reachable while something is broken.
|
|
96
|
+
*/
|
|
97
|
+
export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
98
|
+
// EVERY boot: a scratch server (`x shot`, `ui.shot`) boots here too, and without this a
|
|
99
|
+
// fail-closed dev actor installs nothing — the picture is of a 401. Idempotent.
|
|
100
|
+
declareDevEnvironment(options.env);
|
|
101
|
+
// Before the first scan: the watcher's rescans evict a saved module AND everything importing it,
|
|
102
|
+
// and that needs the import graph recorded as each module is first loaded (`app-reload-graph.ts`).
|
|
103
|
+
enableReloadTracking(true);
|
|
104
|
+
// The declaration lands on `process.env`; the env this boot passes on must carry it too, or every
|
|
105
|
+
// service resolved from `options.env` still reads no `ULTIMATE_ENV`.
|
|
106
|
+
const env = needsDevEnvironmentDeclaration(options.env)
|
|
107
|
+
? { ...options.env, ULTIMATE_ENV: 'development' }
|
|
108
|
+
: options.env;
|
|
109
|
+
const resolved = resolveServices(options.root, env);
|
|
110
|
+
// The app's `apps/<app>/runtime.ts`, read ONCE and handed to `startServices` exactly as
|
|
111
|
+
// `serve.ts`'s `withAppRuntime` does — so the disks (`storage`), the queue, the mail driver and
|
|
112
|
+
// every other override a deployment declares are the ones development runs too. Until 22.4 this
|
|
113
|
+
// boot passed nothing here and built its own embedded disk, so an app's `/_storage` routes and its
|
|
114
|
+
// actions wrote to two different places in `x dev` and to one in production.
|
|
115
|
+
const appRuntime = await loadAppRuntime(options.root);
|
|
116
|
+
const runtime: RunningServices = await startServices(resolved, env, appRuntime);
|
|
117
|
+
// The events binding the boot line reports is the bus the runtime chose, read off it.
|
|
118
|
+
const services = withRealtimeEvents(resolved, env, runtime.realtime);
|
|
119
|
+
// `serve.ts`'s `releaseBoot` shape: everything acquired from here on is released, newest first,
|
|
120
|
+
// if the boot throws — a failed `x dev` left PGlite holding `.x/pgdata` for the retry to meet.
|
|
121
|
+
const acquired: (() => void | Promise<void>)[] = [() => runtime.stop()];
|
|
122
|
+
try {
|
|
123
|
+
return await bootDev(options, services, runtime, acquired, appRuntime);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
await releaseBoot(acquired);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function bootDev(
|
|
131
|
+
options: StartDevOptions,
|
|
132
|
+
services: ReturnType<typeof resolveServices>,
|
|
133
|
+
runtime: RunningServices,
|
|
134
|
+
acquired: (() => void | Promise<void>)[],
|
|
135
|
+
appRuntime: RuntimeOverrides | undefined,
|
|
136
|
+
): Promise<DevServer> {
|
|
137
|
+
// Installed before the app loads, so a span opened during registration is already recorded.
|
|
138
|
+
// Tracing is always on in the framework and free until an exporter is configured; `x dev` is
|
|
139
|
+
// what configures one, which is the whole reason `/_x/timeline` has anything to draw.
|
|
140
|
+
const traces = createTraceRecorder();
|
|
141
|
+
configureTelemetry({ exporter: traces.exporter });
|
|
142
|
+
acquired.push(() => {
|
|
143
|
+
configureTelemetry({ exporter: noopExporter });
|
|
144
|
+
traces.reset();
|
|
145
|
+
});
|
|
146
|
+
// Installed at the same moment and for the same reason: an observer is the single switch that
|
|
147
|
+
// turns statement instrumentation on at all (`@ultimat3/db`'s `observe.ts`), so the timeline's
|
|
148
|
+
// SQL rows and the repeat counts arrive together rather than through two toggles. `serve.ts`
|
|
149
|
+
// installs neither — a production process pays the one `undefined` branch the seam costs
|
|
150
|
+
// uninstalled, and nothing more (axiom 6).
|
|
151
|
+
const statements = createStatementLedger();
|
|
152
|
+
setStatementObserver(statements.observer);
|
|
153
|
+
acquired.push(() => {
|
|
154
|
+
setStatementObserver(undefined);
|
|
155
|
+
statements.reset();
|
|
156
|
+
});
|
|
157
|
+
// ONE load at boot, the same call the rebuild below makes: the manifest and the findings are
|
|
158
|
+
// two projections of one scan. Until 2026-09-07 this was `loadApp` for the findings and then
|
|
159
|
+
// `appManifest` — which loads again — for the manifest, so a save landing between the two put
|
|
160
|
+
// `/_x`'s findings and its manifest on different registration states.
|
|
161
|
+
const app = await appManifest(options.root);
|
|
162
|
+
const state: DevState = {
|
|
163
|
+
manifest: app.manifest,
|
|
164
|
+
reloads: 0,
|
|
165
|
+
reloadFinding: undefined,
|
|
166
|
+
appFindings: app.findings,
|
|
167
|
+
islands: await buildIslands(options.root),
|
|
168
|
+
};
|
|
169
|
+
// The manifest's build id is a content hash of every fact below it, so a dev document's
|
|
170
|
+
// `x-ultimate-build` header names the exact shape the client was served against. Pinned at
|
|
171
|
+
// boot on purpose: the header is handed to the HTTP config and the render modes once, and a
|
|
172
|
+
// reload cannot re-pin it — `state.manifest.buildId` is what `/_x` and `--json` report, so a
|
|
173
|
+
// divergence between the two is visible rather than silent, and a restart closes it.
|
|
174
|
+
// `BUILD_ID` wins when set — `serve.ts`'s rule, so an e2e `deploy.newBuild()` can restart `x dev`
|
|
175
|
+
// as a new build with the same sources.
|
|
176
|
+
// Stamped, it is ALSO what `server.buildId` answers below: the process serves no other build.
|
|
177
|
+
const rawStamp = options.env['BUILD_ID'];
|
|
178
|
+
const stamped = rawStamp !== undefined && rawStamp !== '' ? rawStamp : undefined;
|
|
179
|
+
const buildId = stamped ?? state.manifest.buildId;
|
|
180
|
+
|
|
181
|
+
let server: DevServer;
|
|
182
|
+
// Read at request time, never captured at boot: `/_x/services` must report the reload counter
|
|
183
|
+
// and the findings as they are now, not as they were when the route table was built.
|
|
184
|
+
const dashboard: DevDashboardInput = {
|
|
185
|
+
root: options.root,
|
|
186
|
+
runtime,
|
|
187
|
+
status: (): DevStatus => ({
|
|
188
|
+
url: server.url,
|
|
189
|
+
services: server.services,
|
|
190
|
+
roles: server.roles,
|
|
191
|
+
findings: server.findings,
|
|
192
|
+
reloads: state.reloads,
|
|
193
|
+
}),
|
|
194
|
+
traces,
|
|
195
|
+
statements,
|
|
196
|
+
...envOf(options.env),
|
|
197
|
+
};
|
|
198
|
+
const panels = devPanels(dashboard).map((panel) => panel.key);
|
|
199
|
+
|
|
200
|
+
// `x dev`'s own, so a reload can empty it (a save otherwise kept a page's first render for its
|
|
201
|
+
// ttl); attached, so a tag bust reaches it too. Released with the roles.
|
|
202
|
+
const { isr, release: releaseIsr } = attachedIsr({ buildId });
|
|
203
|
+
acquired.push(releaseIsr);
|
|
204
|
+
const { routes, theme, speculation, inlineStyles, mcpPath, mcpPaths } = await devRouteTable({
|
|
205
|
+
isr,
|
|
206
|
+
root: options.root,
|
|
207
|
+
env: options.env,
|
|
208
|
+
buildId,
|
|
209
|
+
storage: runtime.storage,
|
|
210
|
+
dashboard,
|
|
211
|
+
islands: () => state.islands,
|
|
212
|
+
realtime: runtime.realtime,
|
|
213
|
+
rateLimitStore: appRuntime?.rateLimitStore ?? runtime.rateLimitStore,
|
|
214
|
+
appRoutes: appRuntime?.routes,
|
|
215
|
+
images: appRuntime?.images,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// The app's `apps/<app>/runtime.ts`, composed exactly as `runRole` composes a caller's
|
|
219
|
+
// `runtime`: the replica scope in front, the app's own middleware behind it. Before this the
|
|
220
|
+
// first argument was `undefined` here and an app's middleware reached no development process.
|
|
221
|
+
const replicaOverride = replicaOverrides(appRuntime, services.db, options.env);
|
|
222
|
+
const running = await startRoles({
|
|
223
|
+
roles: options.roles ?? DEV_ROLES,
|
|
224
|
+
port: options.port,
|
|
225
|
+
// `serve.ts`'s expression, called rather than restated: `METRICS_PORT` was read in the
|
|
226
|
+
// container and ignored here, so the scrape port an operator moved was the one port `x dev`
|
|
227
|
+
// could not move — and the second `x dev` on a box died binding the hardcoded 9090.
|
|
228
|
+
metricsPort: metricsPortFor(options.env, options.port),
|
|
229
|
+
buildId,
|
|
230
|
+
runtime,
|
|
231
|
+
routes,
|
|
232
|
+
env: options.env,
|
|
233
|
+
// Read from `app.config.ts` rather than threaded through `DevOptions`: `x dev` and `serve.ts`
|
|
234
|
+
// must not be able to disagree about where the app's sign-in page is.
|
|
235
|
+
signInPath: await loadSignInPath(options.root),
|
|
236
|
+
// The same seam `serve.ts` passes: the app's own error page is a FILE under this root.
|
|
237
|
+
root: options.root,
|
|
238
|
+
// The `/_x` shell, the harness's frame, the app's own error pages and its brand — the inline
|
|
239
|
+
// bodies this process serves; the app's surfaces are content-hashed files `'self'` admits.
|
|
240
|
+
inlineStyles: [await devShellStyle(), FRAME_STYLE, ...inlineStyles],
|
|
241
|
+
inlineScripts: [theme.cspSource, ...(speculation === undefined ? [] : [speculation.cspSource])],
|
|
242
|
+
// The overlay renders this request's own loops under the error it is already showing.
|
|
243
|
+
// `serve.ts` boots through the same `startRoles` and passes nothing (axiom 6).
|
|
244
|
+
devNotices: (ctx: RequestContext): readonly OverlayNotice[] =>
|
|
245
|
+
statements.repeatsFor(asCtx(ctx)).map(loopFacts).map(loopNotice),
|
|
246
|
+
// The read-replica scope, opened per request. Absent for every app that names no
|
|
247
|
+
// `DATABASE_REPLICA_URL` — which is every embedded boot by construction, since PGlite has no
|
|
248
|
+
// standby — so this key does not exist on a homework app's boot at all.
|
|
249
|
+
...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
|
|
250
|
+
});
|
|
251
|
+
acquired.push(() => running.stop());
|
|
252
|
+
|
|
253
|
+
const rebuild = devRebuilder(options, state, isr);
|
|
254
|
+
// Watched one directory at a time, so an ignored one costs no descriptor at all — `dev-watch.ts`
|
|
255
|
+
// decides which, from the app's own `.gitignore`. A recursive watch on the root registered one
|
|
256
|
+
// inotify descriptor per directory in the tree, `.git/`, `node_modules/` and the `.x/` this
|
|
257
|
+
// process writes to included, and filtered the events afterwards.
|
|
258
|
+
const watcher = watchTree({ root: options.root, onChange: rebuild });
|
|
259
|
+
|
|
260
|
+
server = {
|
|
261
|
+
url: running.url ?? `http://localhost:${options.port}`,
|
|
262
|
+
services,
|
|
263
|
+
roles: running.roles,
|
|
264
|
+
mcp: mcpPath,
|
|
265
|
+
mcpPaths,
|
|
266
|
+
get buildId(): string {
|
|
267
|
+
return stamped ?? state.manifest.buildId;
|
|
268
|
+
},
|
|
269
|
+
// A getter, not a snapshot: `/_x` and `--json` must show the reload that just failed and the
|
|
270
|
+
// loop the last request tripped, not the findings as they were when the route table was built.
|
|
271
|
+
// The loops come last and carry their request id, so a boot report reads as a boot report and
|
|
272
|
+
// a diagnostic that arrived a minute later reads as one too.
|
|
273
|
+
get findings(): readonly Finding[] {
|
|
274
|
+
const loops = statements.repeats().map(loopFacts).map(loopFinding);
|
|
275
|
+
return state.reloadFinding === undefined
|
|
276
|
+
? [...state.appFindings, ...loops]
|
|
277
|
+
: [...state.appFindings, state.reloadFinding, ...loops];
|
|
278
|
+
},
|
|
279
|
+
running,
|
|
280
|
+
runtime,
|
|
281
|
+
panels,
|
|
282
|
+
// The boot's own unwind list IS the stop list — one list, so the two cannot drift — plus the
|
|
283
|
+
// watcher. Every step runs and the first failure is rethrown (`releaseOrThrow`): one refused
|
|
284
|
+
// role stop used to leave PGlite's lock held for the next `x dev`. Newest first: the watcher,
|
|
285
|
+
// the roles, ISR, the statement ledger and the exporter (after the roles, so an in-flight
|
|
286
|
+
// request still has both), then the services.
|
|
287
|
+
stop: () => releaseOrThrow([...acquired, () => watcher.close()]),
|
|
288
|
+
};
|
|
289
|
+
return server;
|
|
290
|
+
}
|