@ultimat3/cli 23.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 +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- 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 +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- 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 +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- 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 +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- 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-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- 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 +2 -6
- package/src/island-bundle.ts +57 -79
- 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 +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- 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 +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- 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-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- 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 +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- 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 -33
- 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-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -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 +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -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/naming.ts +28 -0
- 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 +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- 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-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- 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-client.ts +4 -2
- 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-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- 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 +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- 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/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
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,25 +38,22 @@ 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
|
-
* `--workers
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* `--workers`. `Number.parseInt` alone accepted `4abc` and `4.9` as four, while `cmd-verify.ts`'s
|
|
44
|
+
* own comment claimed `x test --workers` refused the same values `x verify` does — so the two
|
|
45
|
+
* commands disagreed about the same flag. One reader now answers for both.
|
|
46
46
|
*/
|
|
47
|
-
const
|
|
47
|
+
const readWorkers = (args: ParsedArgs): number | undefined =>
|
|
48
48
|
readIntFlag(args, {
|
|
49
|
-
name,
|
|
49
|
+
name: 'workers',
|
|
50
50
|
command: 'test',
|
|
51
|
-
min,
|
|
51
|
+
min: 1,
|
|
52
52
|
// The ceiling the summary already claimed and the reader never enforced: `--workers 5000` was
|
|
53
53
|
// accepted and the run clamps only to the file count, which is one Bun worker per test FILE,
|
|
54
|
-
// each with the framework module graph and a cloned database.
|
|
55
|
-
// N-way split, so the same bound holds it (the exact upper index is `workers - 1`, refused a
|
|
56
|
-
// line below by the check that knows the real width).
|
|
54
|
+
// each with the framework module graph and a cloned database.
|
|
57
55
|
max: WORKER_CEILING,
|
|
58
|
-
example:
|
|
56
|
+
example: 'x test --workers 1',
|
|
59
57
|
});
|
|
60
58
|
|
|
61
59
|
/**
|
|
@@ -168,29 +166,17 @@ export const testCommand: CliCommand = {
|
|
|
168
166
|
});
|
|
169
167
|
}
|
|
170
168
|
const files = sample === undefined ? selected : sampleFiles(selected, sample);
|
|
171
|
-
const explicit =
|
|
169
|
+
const explicit = readWorkers(ctx.args);
|
|
172
170
|
const plan =
|
|
173
171
|
explicit === undefined ? workerPlan(availableCpus(), totalMemory(), ctx.env) : undefined;
|
|
174
172
|
const requested = explicit ?? (plan as WorkerPlan).workers;
|
|
175
173
|
const floor = await readVerifyFloor(ctx.cwd);
|
|
176
174
|
const isolate = flagBool(ctx.args, 'isolate') || floor?.isolate === true;
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// the
|
|
180
|
-
// — true for `x test live`, false for the bare `x test --workers 8` that selects every type,
|
|
181
|
-
// which ran the same `live` and `e2e` files eight at a time. It stays because a `--worker` run
|
|
182
|
-
// is one process per CI JOB: sharding a serial type across N of them is the cluster-wide race
|
|
183
|
-
// in a second disguise, and refusing the index is where that is caught.
|
|
175
|
+
// A serial type is one worker whatever `--workers` says. `test-passes.ts` makes the same call
|
|
176
|
+
// off the FILES, which is what decides which files really run one at a time; this keeps the
|
|
177
|
+
// width the run reports and reproduces with the one it ran at.
|
|
184
178
|
const ceiling = type !== undefined && SERIAL_TYPES.includes(type) ? 1 : files.length;
|
|
185
179
|
const workers = Math.max(1, Math.min(requested, ceiling));
|
|
186
|
-
const only = readIndex(ctx.args, 'worker', 0);
|
|
187
|
-
if (only !== undefined && only >= workers) {
|
|
188
|
-
throw new BadFlagError({
|
|
189
|
-
flag: 'worker',
|
|
190
|
-
command: 'test',
|
|
191
|
-
reason: `shard ${only} does not exist in a ${workers}-worker split (0..${workers - 1})`,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
180
|
const result = await runShards({
|
|
195
181
|
root: ctx.cwd,
|
|
196
182
|
runner: ctx.runner,
|
|
@@ -201,11 +187,9 @@ export const testCommand: CliCommand = {
|
|
|
201
187
|
// A default-width run leases its workers from the machine pool, so a second `x test` or
|
|
202
188
|
// gate on the same box runs narrower instead of doubling its memory.
|
|
203
189
|
...(plan === undefined ? {} : withLease(machineLease(plan.workers, ctx.env))),
|
|
204
|
-
...(only === undefined ? {} : { only }),
|
|
205
190
|
...(filter === undefined ? {} : { filter }),
|
|
206
191
|
...(type === undefined ? {} : { type }),
|
|
207
|
-
// `kept` is the corpus the
|
|
208
|
-
// `selected`, not `discovered`: with `--affected` the sample was taken from the narrowed
|
|
192
|
+
// `kept` is the corpus the run saw. `selected`, not `discovered`: with `--affected` the sample was taken from the narrowed
|
|
209
193
|
// set, and reporting the whole tree as its total would name a corpus no run ever had.
|
|
210
194
|
...(sample === undefined ? {} : { sample: { kept: files.length, total: selected.length } }),
|
|
211
195
|
// The fourth input to the split. Without it a failing shard's `fix:` re-splits the whole
|
package/src/cmd-verify.ts
CHANGED
|
@@ -20,14 +20,13 @@ import type { ParsedArgs } from './parse';
|
|
|
20
20
|
import { flagBool, flagString } from './parse';
|
|
21
21
|
import { WORKER_CEILING, WORKER_FLOOR } from './test-workers';
|
|
22
22
|
import { VERIFY_STEPS } from './verify-checks';
|
|
23
|
-
import { VerifyMergeInputError, VerifyShardInvalidError } from './verify-errors';
|
|
24
23
|
import { readVerifyFloor } from './verify-floor';
|
|
25
24
|
import type { VerifyPart } from './verify-merge';
|
|
26
|
-
import { coverageRiders, mergeParts, parsePart } from './verify-merge';
|
|
25
|
+
import { coverageRiders, mergeParts, parsePart, VerifyMergeInputError } from './verify-merge';
|
|
27
26
|
import { stepStream } from './verify-progress';
|
|
28
27
|
import { runVerify } from './verify-run';
|
|
29
28
|
import type { ShardSpec, Timings } from './verify-shard';
|
|
30
|
-
import { assertShardable, parseShard, readTimings } from './verify-shard';
|
|
29
|
+
import { assertShardable, parseShard, readTimings, VerifyShardInvalidError } from './verify-shard';
|
|
31
30
|
import type { VerifyStepName } from './verify-step';
|
|
32
31
|
import { VERIFY_STEP_NAMES } from './verify-step';
|
|
33
32
|
import { writeErrorLine } from './write-line';
|
|
@@ -150,8 +149,9 @@ export const readOnlySteps = (args: ParsedArgs): readonly VerifyStepName[] | und
|
|
|
150
149
|
if (unknown.length === 0) {
|
|
151
150
|
return VERIFY_STEP_NAMES.filter((name) => items.includes(name));
|
|
152
151
|
}
|
|
152
|
+
// An empty item corrects to nothing — never to whichever step is nearest to ''.
|
|
153
153
|
const corrected = items.map((item) =>
|
|
154
|
-
isStepName(item) ? item : nearestName(item, VERIFY_STEP_NAMES),
|
|
154
|
+
isStepName(item) ? item : item === '' ? undefined : nearestName(item, VERIFY_STEP_NAMES),
|
|
155
155
|
);
|
|
156
156
|
const fix = corrected.every((item) => item !== undefined)
|
|
157
157
|
? `x verify --only ${renderFixShellArg([...new Set(corrected)].join(','), '<step,step>')} --json`
|
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,8 +1,9 @@
|
|
|
1
|
-
// Single responsibility: which tables `x db gen` must grant `REPLICA IDENTITY FULL
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
+
import { paramsChannelTables } from '@ultimat3/realtime/server';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The two fields this reads, and nothing else. Structurally satisfied by `QueryDescriptor` (whose
|
|
@@ -28,11 +29,10 @@ function offer(tables: ReadonlySet<string>): string {
|
|
|
28
29
|
/**
|
|
29
30
|
* A live read declares a relation this app has no entity for.
|
|
30
31
|
*
|
|
31
|
-
* Refused rather than dropped
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 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.
|
|
36
36
|
*
|
|
37
37
|
* The name goes in the `cause` and never into a command: it is a string from the app's own source,
|
|
38
38
|
* and the remedy is an edit to a field rather than anything to paste at a shell.
|
|
@@ -46,8 +46,8 @@ export class QuerySubscribesUnknownError extends UltimateError {
|
|
|
46
46
|
'declares a table with that name',
|
|
47
47
|
fix:
|
|
48
48
|
`edit subscribes: on the query "${input.query}" to name a table this app declares ` +
|
|
49
|
-
`(${offer(input.tables)}), or drop the name
|
|
50
|
-
'
|
|
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)',
|
|
51
51
|
meta: { query: input.query, table: input.table },
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -56,26 +56,37 @@ export class QuerySubscribesUnknownError extends UltimateError {
|
|
|
56
56
|
/**
|
|
57
57
|
* The tables to hand `GenerateOptions.replicaIdentityFull`, deduped and sorted.
|
|
58
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
|
+
*
|
|
59
66
|
* Sorted so the same app generates the same bytes whatever order its modules registered in — the
|
|
60
67
|
* rule `@ultimat3/db`'s own `pending()` states one layer down, kept here too because a caller that
|
|
61
68
|
* ordered by registration would put a diff in a file for nothing.
|
|
62
69
|
*
|
|
63
|
-
* Every name is checked against `tables` BEFORE
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
72
|
+
*
|
|
73
|
+
* `channelTables` defaults to what the declared channels need (`paramsChannelTables()`): a channel
|
|
74
|
+
* with params routes a DELETE to the topic the OLD row names, and under the default identity that
|
|
75
|
+
* image is the key alone — no topic, no `remove`, and members keep the deleted record. They are
|
|
76
|
+
* derived from entity projections rather than typed by an author, so one outside `tables` is
|
|
77
|
+
* dropped, not refused: there is no declaration to send anyone back to.
|
|
66
78
|
*/
|
|
67
79
|
export function replicaIdentityTables(
|
|
68
80
|
queries: readonly SubscribingQuery[],
|
|
69
81
|
tables: ReadonlySet<string>,
|
|
82
|
+
channelTables: readonly string[] = paramsChannelTables(),
|
|
70
83
|
): readonly string[] {
|
|
71
|
-
const wanted = new Set<string>();
|
|
72
84
|
for (const query of queries) {
|
|
73
85
|
for (const table of query.subscribes ?? []) {
|
|
74
86
|
if (!tables.has(table)) {
|
|
75
87
|
throw new QuerySubscribesUnknownError({ query: query.name, table, tables });
|
|
76
88
|
}
|
|
77
|
-
wanted.add(table);
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
|
-
return [...
|
|
91
|
+
return [...new Set(channelTables.filter((table) => tables.has(table)))].sort();
|
|
81
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
|
+
}
|