@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/shot-settle.ts
CHANGED
|
@@ -32,9 +32,9 @@ export interface SettleOptions {
|
|
|
32
32
|
/**
|
|
33
33
|
* Read the probe until every booted island has settled, or the window runs out.
|
|
34
34
|
*
|
|
35
|
-
* `
|
|
36
|
-
* and `failed` land after it — so a single read at that instant
|
|
37
|
-
* that hydrates perfectly and the verdict was taken one tick before the outcome existed. Polling
|
|
35
|
+
* The default window, `IDLE_HYDRATE_TIMEOUT_MS`, is the deadline at which the hydration runtime
|
|
36
|
+
* CALLS `import()` — `mounted` and `failed` land after it — so a single read at that instant
|
|
37
|
+
* reports `mounted: 0` for a page that hydrates perfectly and the verdict was taken one tick before the outcome existed. Polling
|
|
38
38
|
* is the only shape that ends EARLY on a fast page and still bounds a slow one.
|
|
39
39
|
*
|
|
40
40
|
* A `null` answer never overwrites a real count: `null` means "not counted", and a probe that
|
package/src/shot-theme.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (issue #489). A requested theme is therefore stored as the visitor's CHOICE, on the page's origin,
|
|
6
6
|
// before the boot reads it: the picture is then what a visitor who chose that theme sees.
|
|
7
7
|
|
|
8
|
-
import { THEME_STORAGE_KEY } from '@ultimat3/
|
|
8
|
+
import { THEME_STORAGE_KEY } from '@ultimat3/core';
|
|
9
9
|
import type { ShotColorScheme } from './browser-launcher-port';
|
|
10
10
|
import { BadFlagError } from './errors';
|
|
11
11
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// `x shot --viewport 390x844,1440x900`: the flag read into sizes, and one route photographed once
|
|
2
|
+
// per size into `<out>/<w>x<h>/` over ONE dev server. Every responsive picture used to need a
|
|
3
|
+
// hand-written browser script beside the tool that exists to make that unnecessary (#444).
|
|
4
|
+
|
|
5
|
+
// why: no Bun native joins a path.
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { renderCauseValue } from '@ultimat3/core';
|
|
8
|
+
import type { ShotRun } from './cmd-shot';
|
|
9
|
+
import { BadFlagError } from './errors';
|
|
10
|
+
import { msg } from './messages';
|
|
11
|
+
import type { CommandResult } from './output';
|
|
12
|
+
import type { ShotServer } from './shot-server';
|
|
13
|
+
import type { ShotArtifacts } from './shot-verdict';
|
|
14
|
+
import { shotLines, shotSummary, verdictJson } from './shot-verdict';
|
|
15
|
+
|
|
16
|
+
/** CSS pixels a page is laid out in. */
|
|
17
|
+
export interface ShotViewport {
|
|
18
|
+
readonly width: number;
|
|
19
|
+
readonly height: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The largest side a size may have: Chrome's maximum texture dimension, past which a capture is
|
|
24
|
+
* tiles the compositor never painted. One bound for both sides, so the refusal states one number.
|
|
25
|
+
*/
|
|
26
|
+
export const VIEWPORT_MAX = 16_384;
|
|
27
|
+
|
|
28
|
+
/** Digits only: `0x10`, `1e3` and `390.5` are not a size anybody typed meaning a size. */
|
|
29
|
+
const SIZE = /^(\d+)x(\d+)$/i;
|
|
30
|
+
|
|
31
|
+
const FIX = 'x shot / --viewport 390x844,1440x900 --json';
|
|
32
|
+
|
|
33
|
+
const refuse = (reason: string): never => {
|
|
34
|
+
throw new BadFlagError({ flag: 'viewport', command: 'shot', reason, fix: FIX });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An island state declares its own `viewport` in its `*.island.states.ts`, and the harness opens one
|
|
39
|
+
* browser per declared size — so a size asked for beside `--island`/`--all-islands` is a second
|
|
40
|
+
* answer to a question the states already answer.
|
|
41
|
+
*/
|
|
42
|
+
export function refuseViewportWithComponent(): never {
|
|
43
|
+
throw new BadFlagError({
|
|
44
|
+
flag: 'viewport',
|
|
45
|
+
command: 'shot',
|
|
46
|
+
reason: 'sizes a route capture; an island is photographed at the viewport each state declares',
|
|
47
|
+
fix: FIX,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A size is its own directory name, so two sizes can never write one picture. */
|
|
52
|
+
export const viewportDir = (viewport: ShotViewport): string =>
|
|
53
|
+
`${String(viewport.width)}x${String(viewport.height)}`;
|
|
54
|
+
|
|
55
|
+
function readSize(entry: string): ShotViewport {
|
|
56
|
+
const match = SIZE.exec(entry);
|
|
57
|
+
if (match === null) {
|
|
58
|
+
return refuse(`${renderCauseValue(entry)} is not <width>x<height>, e.g. 390x844`);
|
|
59
|
+
}
|
|
60
|
+
const width = Number(match[1]);
|
|
61
|
+
const height = Number(match[2]);
|
|
62
|
+
if (width < 1 || height < 1 || width > VIEWPORT_MAX || height > VIEWPORT_MAX) {
|
|
63
|
+
return refuse(`${renderCauseValue(entry)} is outside 1..${String(VIEWPORT_MAX)} on a side`);
|
|
64
|
+
}
|
|
65
|
+
return { width, height };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The flag, every entry judged before any is returned — a run that photographed the good half and
|
|
70
|
+
* refused afterwards would leave a reader with half a set and a failure. Absent is `undefined`:
|
|
71
|
+
* no override, the capture's own default size. Refused, never skipped: an empty entry and the same
|
|
72
|
+
* size twice are both a list the reader did not mean, and the second would overwrite the first.
|
|
73
|
+
*/
|
|
74
|
+
export function parseViewports(raw: string | undefined): readonly ShotViewport[] | undefined {
|
|
75
|
+
if (raw === undefined) return undefined;
|
|
76
|
+
const sizes = raw.split(',').map((entry) => readSize(entry.trim()));
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
for (const size of sizes) {
|
|
79
|
+
const dir = viewportDir(size);
|
|
80
|
+
if (seen.has(dir)) return refuse(`${dir} is listed twice`);
|
|
81
|
+
seen.add(dir);
|
|
82
|
+
}
|
|
83
|
+
return sizes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ViewportRun {
|
|
87
|
+
readonly viewports: readonly ShotViewport[];
|
|
88
|
+
/** Absolute; each size lands in `<outDir>/<w>x<h>/`. */
|
|
89
|
+
readonly outDir: string;
|
|
90
|
+
readonly boot: () => Promise<ShotServer>;
|
|
91
|
+
/** `runShot`, handed in so this module never imports the command that imports it. */
|
|
92
|
+
readonly shoot: (run: ShotRun) => Promise<ShotArtifacts>;
|
|
93
|
+
/** Everything a size shares: the route, the driver, the waits, the locale, the theme. */
|
|
94
|
+
readonly base: Omit<ShotRun, 'outDir' | 'boot' | 'viewport'>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One server for every size, `runShotMatrix`'s rule: each capture gets a boot that never stops it,
|
|
99
|
+
* and the run stops it once after the last picture. Each size is an ordinary `runShot`, so its
|
|
100
|
+
* `verdict.json` means exactly what a single shot's does; `ok` only when every one is.
|
|
101
|
+
*/
|
|
102
|
+
export async function runShotViewports(run: ViewportRun): Promise<CommandResult> {
|
|
103
|
+
// `parseViewports` never answers an empty list; a hand-built one is refused before the boot.
|
|
104
|
+
if (run.viewports.length === 0) refuse('lists no size');
|
|
105
|
+
const server = await run.boot();
|
|
106
|
+
const shared: ShotServer = { ...server, stop: () => Promise.resolve() };
|
|
107
|
+
const shots: { readonly viewport: ShotViewport; readonly artifacts: ShotArtifacts }[] = [];
|
|
108
|
+
try {
|
|
109
|
+
for (const viewport of run.viewports) {
|
|
110
|
+
const artifacts = await run.shoot({
|
|
111
|
+
...run.base,
|
|
112
|
+
viewport,
|
|
113
|
+
outDir: join(run.outDir, viewportDir(viewport)),
|
|
114
|
+
boot: () => Promise.resolve(shared),
|
|
115
|
+
});
|
|
116
|
+
shots.push({ viewport, artifacts });
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
await server.stop().catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
// The summary is the first failure's, else the first picture's — labelled by its size, so a
|
|
122
|
+
// reader of one line knows which of the sizes it speaks for.
|
|
123
|
+
const worst = shots.find((shot) => !shot.artifacts.verdict.ok) ?? shots[0];
|
|
124
|
+
return {
|
|
125
|
+
ok: shots.every((shot) => shot.artifacts.verdict.ok),
|
|
126
|
+
command: 'shot',
|
|
127
|
+
summary:
|
|
128
|
+
worst === undefined
|
|
129
|
+
? refuse('lists no size')
|
|
130
|
+
: msg('cli.shot.viewport.summary', {
|
|
131
|
+
size: viewportDir(worst.viewport),
|
|
132
|
+
summary: shotSummary(worst.artifacts.verdict),
|
|
133
|
+
}),
|
|
134
|
+
lines: shots.flatMap((shot) => [
|
|
135
|
+
msg('cli.shot.viewport.line', {
|
|
136
|
+
size: viewportDir(shot.viewport),
|
|
137
|
+
summary: shotSummary(shot.artifacts.verdict),
|
|
138
|
+
}),
|
|
139
|
+
...shotLines(shot.artifacts).map((line) => ` ${line}`),
|
|
140
|
+
]),
|
|
141
|
+
data: {
|
|
142
|
+
outDir: run.outDir,
|
|
143
|
+
shots: shots.map((shot) => ({
|
|
144
|
+
width: shot.viewport.width,
|
|
145
|
+
height: shot.viewport.height,
|
|
146
|
+
image: shot.artifacts.image,
|
|
147
|
+
verdictFile: shot.artifacts.verdictFile,
|
|
148
|
+
verdict: verdictJson(shot.artifacts.verdict),
|
|
149
|
+
})),
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/signal-shred.ts
CHANGED
|
@@ -4,24 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
export type Reraise = (signal: NodeJS.Signals) => void;
|
|
6
6
|
|
|
7
|
+
/** Ctrl-C, a supervisor's stop, and a terminal closing under the editor. */
|
|
8
|
+
const SHRED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const;
|
|
9
|
+
|
|
7
10
|
const reraiseDefault: Reraise = (signal) => {
|
|
8
11
|
process.kill(process.pid, signal);
|
|
9
12
|
};
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
|
-
* Shred on SIGINT/SIGTERM, then re-raise the same signal
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* Shred on SIGINT/SIGTERM/SIGHUP, then re-raise the same signal with the listeners gone, so it
|
|
16
|
+
* meets the default action and terminates. Returns the undo for the normal path, which shreds in
|
|
17
|
+
* its own `finally`.
|
|
18
|
+
*
|
|
19
|
+
* The listeners stay installed UNTIL the shred is done, and a repeat meanwhile is ignored. With
|
|
20
|
+
* `once` the listener was gone the moment the first signal was dispatched, so a second one — a
|
|
21
|
+
* Ctrl-C the terminal delivers to the whole group and a wrapper forwards again — met the default
|
|
22
|
+
* action: a kernel-level kill mid-`rmSync`, the plaintext left in $TMPDIR (30 of 40 runs).
|
|
15
23
|
*/
|
|
16
24
|
export function shredOnSignal(shred: () => void, reraise: Reraise = reraiseDefault): () => void {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
process.once('SIGINT', handler);
|
|
22
|
-
process.once('SIGTERM', handler);
|
|
23
|
-
return () => {
|
|
24
|
-
process.off('SIGINT', handler);
|
|
25
|
-
process.off('SIGTERM', handler);
|
|
25
|
+
let shredding = false;
|
|
26
|
+
const undo = (): void => {
|
|
27
|
+
for (const signal of SHRED_SIGNALS) process.off(signal, handler);
|
|
26
28
|
};
|
|
29
|
+
function handler(signal: NodeJS.Signals): void {
|
|
30
|
+
if (shredding) return;
|
|
31
|
+
shredding = true;
|
|
32
|
+
try {
|
|
33
|
+
shred();
|
|
34
|
+
} finally {
|
|
35
|
+
undo();
|
|
36
|
+
reraise(signal);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const signal of SHRED_SIGNALS) process.on(signal, handler);
|
|
40
|
+
return undo;
|
|
27
41
|
}
|
package/src/site-asset-routes.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { isUltimateError } from '@ultimat3/core';
|
|
7
7
|
import type { CacheHint, Route, UltimateRequest } from '@ultimat3/http';
|
|
8
|
-
import { applyCacheHeaders,
|
|
8
|
+
import { applyCacheHeaders, jsonResponse } from '@ultimat3/http';
|
|
9
9
|
import type { SiteAsset, SiteAssetTable } from './site-assets';
|
|
10
10
|
import { parseHashedAssetUrl, SITE_ASSET_BASE_PATH } from './site-assets';
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ import { parseHashedAssetUrl, SITE_ASSET_BASE_PATH } from './site-assets';
|
|
|
13
13
|
export const SITE_ASSET_CACHE: CacheHint = { mode: 'immutable' };
|
|
14
14
|
|
|
15
15
|
const notFound = (code: string, cause: string, fix: string): Response =>
|
|
16
|
-
|
|
16
|
+
jsonResponse({ ok: false, error: { code, cause, fix } }, { status: 404 });
|
|
17
17
|
|
|
18
18
|
/** One `bytes=` range, resolved against the file's size; `null` when it cannot be satisfied. */
|
|
19
19
|
export function byteRange(
|
package/src/site-seo.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Environment, SeoSitemapConfig } from '@ultimat3/core';
|
|
6
6
|
import { UltimateError } from '@ultimat3/core';
|
|
7
|
-
import {
|
|
7
|
+
import { localizedPath, unlocalizedPath } from '@ultimat3/i18n';
|
|
8
|
+
import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
|
|
8
9
|
import type { RouteEntry } from '@ultimat3/render';
|
|
9
10
|
import { routeEntries } from '@ultimat3/render';
|
|
10
11
|
import { enumeratePrerender, fillPath } from '@ultimat3/render/server';
|
|
@@ -150,10 +151,11 @@ export async function siteSeo(options: SiteSeoOptions): Promise<SiteSeo> {
|
|
|
150
151
|
// Every routed locale, with `xhtml:link` alternates: one `<url>` per page per locale, each
|
|
151
152
|
// naming the whole cluster and `x-default`. A single-locale app passes none and gets the plain
|
|
152
153
|
// urlset it always had — an alternates cluster of one is noise.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const settings = options.sitemap ?? NO_SITEMAP_SETTINGS;
|
|
154
|
+
// The app's declared set — the one the manifest, the worker and the prerender read — never the
|
|
155
|
+
// ambient locale config, which a build process has not configured.
|
|
156
156
|
const root = options.root ?? process.cwd();
|
|
157
|
+
const { locales, defaultLocale } = await appLocaleSet(root);
|
|
158
|
+
const settings = options.sitemap ?? NO_SITEMAP_SETTINGS;
|
|
157
159
|
const stamp: Stamp = (file) => {
|
|
158
160
|
const lastmod = lastmodOf(file, settings.lastmod, root);
|
|
159
161
|
return lastmod === undefined ? {} : { lastmod };
|
package/src/source-files.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// means a finding one of them can never see.
|
|
4
4
|
|
|
5
5
|
import { hasPathSegment } from './path-segments';
|
|
6
|
+
import { toPosix } from './posix-path';
|
|
6
7
|
|
|
7
8
|
export const SOURCE_GLOBS = [
|
|
8
9
|
'packages/*/src/**/*.{ts,tsx}',
|
|
@@ -38,7 +39,17 @@ export const isGenerated = (path: string): boolean => path.endsWith('.d.ts');
|
|
|
38
39
|
export const isTest = (path: string): boolean => /\.test\.tsx?$/.test(path);
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
+
* Glob answers as the POSIX paths every consumer keys by, in code-unit order of that spelling.
|
|
43
|
+
* The glob answers in the host's separator: on Windows `guards\untranslated-string.ts` missed
|
|
44
|
+
* `i18n`'s `startsWith('guards/')`, so the example `t('…')` in its fix line became a key every
|
|
45
|
+
* fresh scaffold was missing — and `^packages\/` rules matched nothing at all.
|
|
46
|
+
*/
|
|
47
|
+
export function sortedPosix(paths: readonly string[]): string[] {
|
|
48
|
+
return paths.map(toPosix).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every source file under `root`, repo-relative, POSIX and deduplicated across the globs — in a SORTED
|
|
42
53
|
* order per pattern, `As of 2026-09-05`. `Bun.Glob.scan` yields in the filesystem's `readdir`
|
|
43
54
|
* order, which ext4 hashes: the first offender a scan names was `index.ts` on one machine and
|
|
44
55
|
* `tools.ts` on a GitHub runner, so a finding's `cause` depended on which disk held the checkout
|
|
@@ -52,8 +63,7 @@ export async function* eachSourceFile(root: string): AsyncGenerator<string> {
|
|
|
52
63
|
for await (const path of new Bun.Glob(pattern).scan({ cwd: root, absolute: false })) {
|
|
53
64
|
if (!isVendored(path)) matches.push(path);
|
|
54
65
|
}
|
|
55
|
-
|
|
56
|
-
for (const path of matches) {
|
|
66
|
+
for (const path of sortedPosix(matches)) {
|
|
57
67
|
if (seen.has(path)) continue;
|
|
58
68
|
seen.add(path);
|
|
59
69
|
yield path;
|
package/src/static-report.ts
CHANGED
|
@@ -71,6 +71,11 @@ export type UnmeasuredRoute = {
|
|
|
71
71
|
readonly code?: string;
|
|
72
72
|
readonly cause?: string;
|
|
73
73
|
readonly fix?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Who the measurement render ran as, labelled (`measure-scope.ts`'s `measurementActorLabel`) —
|
|
76
|
+
* so a refusal OF that actor names it, and the edit that changes it (#675). Never its facts.
|
|
77
|
+
*/
|
|
78
|
+
readonly actor?: string;
|
|
74
79
|
/**
|
|
75
80
|
* `false` when no build can weigh the route by construction — a `render: 'ssr'` page with params,
|
|
76
81
|
* which may not declare `prerender()`, so the build holds no value to render it at. Not a defect
|
package/src/style-routes.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// image.
|
|
5
5
|
|
|
6
6
|
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
7
|
-
import { applyCacheHeaders,
|
|
7
|
+
import { applyCacheHeaders, jsonResponse, NO_STORE } from '@ultimat3/http';
|
|
8
8
|
import type { StyleBundle } from './style-bundle';
|
|
9
9
|
import { STYLE_BASE_PATH } from './style-bundle';
|
|
10
10
|
|
|
@@ -32,7 +32,7 @@ export function styleRoutes(source: StyleSource): readonly Route[] {
|
|
|
32
32
|
handler: (request: UltimateRequest): Response => {
|
|
33
33
|
const chunk = source().chunkAt(request.pathname);
|
|
34
34
|
if (chunk === undefined) {
|
|
35
|
-
const missing =
|
|
35
|
+
const missing = jsonResponse(
|
|
36
36
|
{
|
|
37
37
|
ok: false,
|
|
38
38
|
error: {
|
package/src/sw-artifacts.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// until #390. Beside `pwa-artifacts.ts` and not inside it: that file needs a root and a config
|
|
4
4
|
// file, this one needs a booted app and a finished build.
|
|
5
5
|
|
|
6
|
-
import { localeConfig, routedLocales } from '@ultimat3/i18n';
|
|
7
6
|
import type { PrecacheManifest } from '@ultimat3/pwa';
|
|
8
7
|
import { generateServiceWorker } from '@ultimat3/pwa';
|
|
9
8
|
import type { RouteDescriptor } from '@ultimat3/render';
|
|
@@ -12,7 +11,7 @@ import { ISLAND_BASE_PATH } from './island-bundle';
|
|
|
12
11
|
import { msg } from './messages';
|
|
13
12
|
import type { PwaArtifacts } from './pwa-artifacts';
|
|
14
13
|
import type { StyleBundle } from './style-bundle';
|
|
15
|
-
import type { RenderedDocument
|
|
14
|
+
import type { RenderedDocument } from './sw-precache-plan';
|
|
16
15
|
import { localePrefixes, precacheAssets, pwaRoutes } from './sw-precache-plan';
|
|
17
16
|
|
|
18
17
|
export type { RenderedDocument, RoutedLocales } from './sw-precache-plan';
|
|
@@ -73,11 +72,6 @@ export interface ServiceWorkerInput {
|
|
|
73
72
|
* offline reload that cannot load the boot restores no record and shows the old count.
|
|
74
73
|
*/
|
|
75
74
|
readonly scripts?: readonly { readonly url: string; readonly bytes: number }[];
|
|
76
|
-
/**
|
|
77
|
-
* The routed locales, default first. Absent, `@ultimat3/i18n`'s own — the answer the server routes
|
|
78
|
-
* with, set once the app's catalogs are loaded, which every caller has done by now.
|
|
79
|
-
*/
|
|
80
|
-
readonly locales?: RoutedLocales;
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
/**
|
|
@@ -157,7 +151,9 @@ export function serviceWorkerArtifacts(
|
|
|
157
151
|
// Absent when this pass did not render the fallback (no route serves it — `x doctor` reports
|
|
158
152
|
// that as `X_PWA_NO_OFFLINE_FALLBACK`), and then `@ultimat3/pwa` falls back to the build id.
|
|
159
153
|
const fallbackDocument = documents.get(fallback);
|
|
160
|
-
|
|
154
|
+
// The set the manifests were built from (`PwaArtifacts.locales`), never the ambient locale
|
|
155
|
+
// config: one answer, so a locale an installed manifest names is always one the worker prefixes.
|
|
156
|
+
const locales = pwa.locales;
|
|
161
157
|
const output = generateServiceWorker(
|
|
162
158
|
pwaRoutes(input.routes, documents, locales),
|
|
163
159
|
{
|
|
@@ -187,6 +183,10 @@ export function serviceWorkerArtifacts(
|
|
|
187
183
|
styles: input.styles,
|
|
188
184
|
scripts: input.scripts ?? [],
|
|
189
185
|
fallback,
|
|
186
|
+
placeholders: {
|
|
187
|
+
urls: [pwa.offline.image, pwa.offline.font].filter((url): url is string => url !== null),
|
|
188
|
+
buildId: input.buildId,
|
|
189
|
+
},
|
|
190
190
|
}),
|
|
191
191
|
},
|
|
192
192
|
input.buildId,
|
package/src/sw-precache-plan.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PrecacheAsset, PwaRoute } from '@ultimat3/pwa';
|
|
|
7
7
|
import { strategyFor } from '@ultimat3/pwa';
|
|
8
8
|
import type { RouteDescriptor } from '@ultimat3/render';
|
|
9
9
|
import type { IslandBundle } from './island-bundle';
|
|
10
|
+
import { parseHashedAssetUrl } from './site-assets';
|
|
10
11
|
import type { StyleBundle } from './style-bundle';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -94,6 +95,12 @@ export interface PrecacheAssetInput {
|
|
|
94
95
|
readonly scripts: readonly { readonly url: string; readonly bytes: number }[];
|
|
95
96
|
/** The offline document's path: its islands ride the precache whatever its own route says. */
|
|
96
97
|
readonly fallback: string;
|
|
98
|
+
/**
|
|
99
|
+
* `pwa.offline.image` / `pwa.offline.font`: the worker answers a failed image or font request
|
|
100
|
+
* from the PRECACHE only, so a placeholder nothing precached is a branch that matches nothing.
|
|
101
|
+
* `buildId` is the revision of one whose bytes this build cannot hash — the offline document's.
|
|
102
|
+
*/
|
|
103
|
+
readonly placeholders?: { readonly urls: readonly string[]; readonly buildId: string };
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
@@ -140,12 +147,35 @@ export function precacheAssets(input: PrecacheAssetInput): readonly PrecacheAsse
|
|
|
140
147
|
for (const chunk of input.islands.chunks) {
|
|
141
148
|
if (wanted.has(chunk.url)) for (const url of chunk.imports) wanted.add(url);
|
|
142
149
|
}
|
|
143
|
-
|
|
150
|
+
const chunks: PrecacheAsset[] = [
|
|
144
151
|
...input.islands.chunks.filter((chunk) => wanted.has(chunk.url)),
|
|
145
152
|
...input.islands.shared.filter((chunk) => wanted.has(chunk.url)),
|
|
146
153
|
...input.styles.chunks,
|
|
147
154
|
...input.scripts,
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
].map((chunk) => ({ url: chunk.url, revision: chunk.url, bytes: chunk.bytes }));
|
|
156
|
+
return [...chunks, ...placeholderAssets(input, new Set(chunks.map((chunk) => chunk.url)))].sort(
|
|
157
|
+
(a, b) => (a.url < b.url ? -1 : a.url > b.url ? 1 : 0),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The placeholders on the offline document's terms (`buildPrecacheManifest`'s `fallback` entry):
|
|
163
|
+
* never refused here — this pass cannot see the site assets, and the worker installs entry by
|
|
164
|
+
* entry, so a URL that 404s costs that entry and the branch falls through to the 503 — and
|
|
165
|
+
* stamped with the build id unless the URL is content-addressed, which is its own revision. Bytes
|
|
166
|
+
* unknown count 0, as the offline document's do. A chunk already listed keeps its own entry.
|
|
167
|
+
*/
|
|
168
|
+
function placeholderAssets(
|
|
169
|
+
input: PrecacheAssetInput,
|
|
170
|
+
listed: ReadonlySet<string>,
|
|
171
|
+
): PrecacheAsset[] {
|
|
172
|
+
const out = new Map<string, PrecacheAsset>();
|
|
173
|
+
const placeholders = input.placeholders;
|
|
174
|
+
if (placeholders === undefined) return [];
|
|
175
|
+
for (const url of placeholders.urls) {
|
|
176
|
+
if (listed.has(url) || out.has(url)) continue;
|
|
177
|
+
const hashed = parseHashedAssetUrl(url) !== undefined;
|
|
178
|
+
out.set(url, { url, revision: hashed ? url : placeholders.buildId, bytes: 0 });
|
|
179
|
+
}
|
|
180
|
+
return [...out.values()];
|
|
151
181
|
}
|
|
@@ -9,8 +9,8 @@ import { finiteCount, systemClock } from '@ultimat3/core';
|
|
|
9
9
|
import type { HttpConfig } from '@ultimat3/http';
|
|
10
10
|
import {
|
|
11
11
|
configuredAuthenticator,
|
|
12
|
-
createRequestContext,
|
|
13
12
|
defineHttpConfig,
|
|
13
|
+
requestContext,
|
|
14
14
|
UltimateRequest,
|
|
15
15
|
} from '@ultimat3/http';
|
|
16
16
|
import type { SyncAuthenticator, SyncGrant } from '@ultimat3/realtime/server';
|
|
@@ -84,7 +84,7 @@ export interface SyncAuthenticatorOptions {
|
|
|
84
84
|
/**
|
|
85
85
|
* What the sync node is given when the app configured an authenticator, and `undefined` when it
|
|
86
86
|
* did not — which keeps `x dev` anonymous and makes the node log that it is, exactly as
|
|
87
|
-
* `
|
|
87
|
+
* `syncNode` documents. A stub that answered `{ actor: anonymous }` would look configured.
|
|
88
88
|
*
|
|
89
89
|
* The grant carries an `expiresAt` and a `refresh`, and both are the app's own resolver asked
|
|
90
90
|
* again: `configureAuthenticator` says who is dialling, and the only honest way to learn that it
|
|
@@ -115,7 +115,7 @@ export function syncAuthenticator(
|
|
|
115
115
|
method: credential.method,
|
|
116
116
|
headers: credential.headers,
|
|
117
117
|
});
|
|
118
|
-
const ctx =
|
|
118
|
+
const ctx = requestContext({
|
|
119
119
|
url: new URL(credential.url),
|
|
120
120
|
method: credential.method,
|
|
121
121
|
role: 'sync',
|
package/src/sync-url.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { ConfigInvalidError } from '@ultimat3/core';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* The sync node's own path (`
|
|
8
|
+
* The sync node's own path (`syncNode`'s default in `@ultimat3/realtime`). Same origin by
|
|
9
9
|
* default because every rung already serves it there: `x dev` and a combined-role container mount
|
|
10
10
|
* the node on the web port, and `docker/helm`'s ingress routes `/_x/sync` to the `sync` service.
|
|
11
11
|
*/
|
package/src/templates/action.ts
CHANGED
|
@@ -154,7 +154,7 @@ const input = { id, orgId${isMutator ? ", title: 'a title'" : ''} };
|
|
|
154
154
|
|
|
155
155
|
// Named here because every projection needs a stable name and this file does not boot the app.
|
|
156
156
|
// At boot \`registerActions(await import('./actions'))\` stamps the same name onto the same
|
|
157
|
-
// object, so
|
|
157
|
+
// object, so each projection of it works there with nothing to remember.
|
|
158
158
|
const target = ${name.camel}.named('${name.camel}');
|
|
159
159
|
${
|
|
160
160
|
outsider
|
|
@@ -234,7 +234,7 @@ ${
|
|
|
234
234
|
? `
|
|
235
235
|
unitTest('the handler answers the row it found', async () => {
|
|
236
236
|
const draft = { orgId, title: 'kept', price: { minor: 0, currency: 'USD' } };
|
|
237
|
-
const row = await runWithContext(
|
|
237
|
+
const row = await runWithContext(ctxOf({ actor: writer }), () => repo.insert(draft));
|
|
238
238
|
const answer = await target.as(writer, ${call});
|
|
239
239
|
expect(answer).toEqual(${found});
|
|
240
240
|
});
|
|
@@ -263,7 +263,7 @@ const actionUnitTest = (name: NameSet, feature: NameSet, shape: HandlerShape): s
|
|
|
263
263
|
return `// ${name.camel}: its declared shape, the input it refuses, and what its handler answers — in
|
|
264
264
|
// process, so all three belong to the \`unit\` step. The contract projections are next door.
|
|
265
265
|
${sortedImports([
|
|
266
|
-
...(stores ? ["import {
|
|
266
|
+
...(stores ? ["import { ctxOf, runWithContext } from '@ultimat3/core';"] : []),
|
|
267
267
|
"import { testActor } from '@ultimat3/policy';",
|
|
268
268
|
`import { ${testing} } from '@ultimat3/testing';`,
|
|
269
269
|
...(stores ? [`import { driver } from '${shape.dbModule}';`] : []),
|
|
@@ -276,7 +276,7 @@ const input = { id, orgId${shape.isMutator ? ", title: 'a title'" : ''} };
|
|
|
276
276
|
${shape.isMutator ? `\n// The client store's table: the entity's own, so the twin and the server row share one key.\nconst TABLE = '${feature.table}';\n` : ''}
|
|
277
277
|
// Named here because every projection needs a stable name and this file does not boot the app.
|
|
278
278
|
// At boot \`registerActions(await import('./actions'))\` stamps the same name onto the same
|
|
279
|
-
// object, so
|
|
279
|
+
// object, so each projection of it works there with nothing to remember.
|
|
280
280
|
const target = ${name.camel}.named('${name.camel}');
|
|
281
281
|
|
|
282
282
|
// Holds the grant in the org the input names: past the policy, so what answers is the handler.
|
|
@@ -107,14 +107,14 @@ const backfillTest = (
|
|
|
107
107
|
): string => `// ${name.camel}'s durable identity: one live run per name, retried under the same key. The work
|
|
108
108
|
// itself — the pass over real rows and the projection it applies — is the unit suite's, next door.
|
|
109
109
|
|
|
110
|
-
import {
|
|
110
|
+
import { memoryJobDriver, resetJobDriver, setJobDriver } from '@ultimat3/jobs';
|
|
111
111
|
import { afterAll, beforeAll, expect, jobTest } from '@ultimat3/testing';
|
|
112
112
|
import { ${name.camel} } from './${name.kebab}';
|
|
113
113
|
|
|
114
114
|
// The driver is process-global, so it is installed and released around this file rather than
|
|
115
115
|
// left behind for whichever test happens to run next.
|
|
116
116
|
beforeAll(() => {
|
|
117
|
-
setJobDriver(
|
|
117
|
+
setJobDriver(memoryJobDriver());
|
|
118
118
|
});
|
|
119
119
|
afterAll(resetJobDriver);
|
|
120
120
|
|
|
@@ -162,7 +162,7 @@ const backfillUnitTest = (
|
|
|
162
162
|
// driver. A sweep rewrites rows no user asked it to, so what is worth failing on is that it reaches
|
|
163
163
|
// every tenant's rows, writes the projection and nothing else, and that a replay changes nothing.
|
|
164
164
|
${sortedImports([
|
|
165
|
-
"import {
|
|
165
|
+
"import { ctxOf, runWithContext } from '@ultimat3/core';",
|
|
166
166
|
"import { testActor } from '@ultimat3/policy';",
|
|
167
167
|
"import { afterEach, expect, unitTest } from '@ultimat3/testing';",
|
|
168
168
|
`import { db, driver } from '${dbModule}';`,
|
|
@@ -175,7 +175,7 @@ const orgB = '00000000-0000-4000-8000-000000000009';
|
|
|
175
175
|
|
|
176
176
|
/** What a request is to the handle: an actor, whose org every read and write runs under. */
|
|
177
177
|
const inOrg = <T>(orgId: string, run: () => Promise<T>): Promise<T> =>
|
|
178
|
-
runWithContext(
|
|
178
|
+
runWithContext(ctxOf({ actor: testActor('member', { orgId }).actor }), run);
|
|
179
179
|
|
|
180
180
|
const store = (orgId: string, title: string): Promise<${feature.pascal}> => {
|
|
181
181
|
const draft = { orgId, title, price: { minor: 1000, currency: 'USD' } };
|
package/src/templates/entity.ts
CHANGED
|
@@ -156,7 +156,7 @@ const repoTest = (
|
|
|
156
156
|
): string => `// The ${name.kebab} repo against the in-memory driver: the contract Postgres serves, with no
|
|
157
157
|
// database. What it pins is what the repo cannot show by being read — whose rows a call reaches.
|
|
158
158
|
${sortedImports([
|
|
159
|
-
"import {
|
|
159
|
+
"import { ctxOf, frozenClock, runWithContext } from '@ultimat3/core';",
|
|
160
160
|
"import { testActor } from '@ultimat3/policy';",
|
|
161
161
|
"import { afterEach, expect, unitTest } from '@ultimat3/testing';",
|
|
162
162
|
`import { driver } from '${dbModule}';`,
|
|
@@ -171,7 +171,7 @@ const clock = frozenClock('2026-01-01T00:00:00.000Z');
|
|
|
171
171
|
|
|
172
172
|
/** What a request is to the handle: an actor, whose org every read and write runs under. */
|
|
173
173
|
const inOrg = <T>(org: string, run: () => Promise<T>): Promise<T> =>
|
|
174
|
-
runWithContext(
|
|
174
|
+
runWithContext(ctxOf({ actor: testActor('member', { orgId: org }).actor, clock }), run);
|
|
175
175
|
|
|
176
176
|
const draft = (title: string) => ({ orgId, title, price: { minor: 1200, currency: 'USD' } });
|
|
177
177
|
|