@ultimat3/cli 22.14.0 → 23.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 +25 -8
- package/README.md +28 -5
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/api-registration.ts +36 -11
- package/src/app-boundaries.ts +6 -20
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +14 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +35 -3
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +56 -9
- package/src/cmd-db.ts +60 -11
- package/src/cmd-dev.ts +2 -2
- package/src/cmd-doctor.ts +35 -11
- package/src/cmd-generate.ts +118 -17
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +120 -3
- package/src/cmd-new.ts +15 -1
- package/src/cmd-routes.ts +15 -1
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +48 -12
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-verify.ts +10 -2
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/dev-dashboard.ts +2 -2
- package/src/dev-route-table.ts +30 -6
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/document-options.ts +64 -0
- package/src/error-codes.ts +32 -0
- package/src/framework-schema.ts +15 -0
- package/src/generate-files.ts +76 -10
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +19 -0
- package/src/generate-write.ts +15 -27
- package/src/generated-imports.ts +59 -0
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +11 -2
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +14 -7
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +3 -0
- package/src/json-merge.ts +46 -0
- package/src/mcp-errors.ts +29 -1
- package/src/messages.ts +33 -0
- package/src/metrics-endpoint.ts +71 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +18 -9
- package/src/page-speculation.ts +124 -0
- package/src/permission-grants.ts +41 -9
- package/src/prerender.ts +9 -5
- package/src/publish-closure.ts +101 -0
- package/src/role-load.ts +143 -0
- package/src/role-start.ts +18 -1
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-queue.ts +8 -1
- package/src/runtime-render.ts +36 -60
- package/src/runtime-services.ts +2 -1
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-dump-drift.ts +64 -0
- package/src/serve-boot.ts +85 -147
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +164 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/source-edit.ts +49 -0
- package/src/static-document.ts +108 -0
- package/src/tasks-facts.ts +4 -1
- package/src/templates/action.ts +144 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +38 -29
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +171 -51
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container.ts +16 -0
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +142 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +52 -2
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +36 -70
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +18 -2
- package/src/transport-calls.ts +105 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/verify-checks.ts +26 -7
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +256 -0
- package/src/verify-errors.ts +15 -6
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +117 -24
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +93 -35
- package/src/verify-shard.ts +20 -9
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +78 -6
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// One workspace declaring another in its `package.json`, as a TEXT edit. A generator that makes
|
|
2
|
+
// `packages/db` import the web app owes the manifest line `package-shape` would otherwise demand
|
|
3
|
+
// (`X_WORKSPACE_DEP_UNDECLARED`), and re-serialising the whole file would reformat arrays and
|
|
4
|
+
// spacing the author — or their formatter — chose.
|
|
5
|
+
|
|
6
|
+
import { containedPath } from './generate-write';
|
|
7
|
+
|
|
8
|
+
const MANIFEST = 'package.json';
|
|
9
|
+
|
|
10
|
+
/** The fields a manifest edit reads. Anything else in the file is carried through untouched. */
|
|
11
|
+
interface Manifest {
|
|
12
|
+
readonly name?: unknown;
|
|
13
|
+
readonly version?: unknown;
|
|
14
|
+
readonly dependencies?: unknown;
|
|
15
|
+
readonly devDependencies?: unknown;
|
|
16
|
+
readonly peerDependencies?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseManifest(text: string): Manifest | undefined {
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(text);
|
|
22
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
23
|
+
? (parsed as Manifest)
|
|
24
|
+
: undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
// A manifest that does not parse is `bun install`'s finding, and `package-shape`'s.
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const stringRecord = (value: unknown): Readonly<Record<string, string>> =>
|
|
32
|
+
typeof value === 'object' && value !== null
|
|
33
|
+
? Object.fromEntries(
|
|
34
|
+
Object.entries(value).filter(
|
|
35
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
: {};
|
|
39
|
+
|
|
40
|
+
const declares = (manifest: Manifest, name: string): boolean =>
|
|
41
|
+
[manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((block) =>
|
|
42
|
+
Object.hasOwn(stringRecord(block), name),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `manifest` with `name` declared under `dependencies`, or `undefined` when it already is — or
|
|
47
|
+
* when the text is not a manifest this can edit safely. The block is rewritten from its parsed
|
|
48
|
+
* entries, sorted the way `bun add` sorts them; every byte outside it is the author's.
|
|
49
|
+
*/
|
|
50
|
+
export function withDependency(
|
|
51
|
+
manifest: string,
|
|
52
|
+
name: string,
|
|
53
|
+
version: string,
|
|
54
|
+
): string | undefined {
|
|
55
|
+
const parsed = parseManifest(manifest);
|
|
56
|
+
if (parsed === undefined || declares(parsed, name)) return undefined;
|
|
57
|
+
const block = /^([ \t]*)"dependencies":\s*\{/m.exec(manifest);
|
|
58
|
+
if (block === null) {
|
|
59
|
+
const close = manifest.lastIndexOf('}');
|
|
60
|
+
const body = manifest.slice(0, close).trimEnd();
|
|
61
|
+
// `{}` has no property to put a comma after; nothing a scaffold writes is that.
|
|
62
|
+
if (close === -1 || body.endsWith('{')) return undefined;
|
|
63
|
+
return `${body},\n "dependencies": {\n "${name}": "${version}"\n }\n}\n`;
|
|
64
|
+
}
|
|
65
|
+
const indent = block[1] ?? '';
|
|
66
|
+
const open = block.index + block[0].length;
|
|
67
|
+
const close = manifest.indexOf('}', open);
|
|
68
|
+
if (close === -1) return undefined;
|
|
69
|
+
const entries = Object.entries({ ...stringRecord(parsed.dependencies), [name]: version })
|
|
70
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
71
|
+
.map(([key, value]) => `${indent} ${JSON.stringify(key)}: ${JSON.stringify(value)}`);
|
|
72
|
+
return `${manifest.slice(0, open)}\n${entries.join(',\n')}\n${indent}${manifest.slice(close)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A workspace's `name` and `version`, or `undefined` when its manifest supplies no name. */
|
|
76
|
+
export async function readWorkspace(
|
|
77
|
+
root: string,
|
|
78
|
+
dir: string,
|
|
79
|
+
): Promise<{ readonly name: string; readonly version: string } | undefined> {
|
|
80
|
+
const file = Bun.file(containedPath(root, `${dir}/${MANIFEST}`));
|
|
81
|
+
if (!(await file.exists())) return undefined;
|
|
82
|
+
const parsed = parseManifest(await file.text());
|
|
83
|
+
if (parsed === undefined || typeof parsed.name !== 'string' || parsed.name === '') {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
name: parsed.name,
|
|
88
|
+
version: typeof parsed.version === 'string' ? parsed.version : '0.0.0',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One file a caller is about to write: its app-root-relative path and its whole new text. */
|
|
93
|
+
export interface PlannedEdit {
|
|
94
|
+
readonly path: string;
|
|
95
|
+
readonly contents: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The edit that declares the workspace at `toDir` in `fromDir`'s manifest, NOT yet written — a
|
|
100
|
+
* registrar plans every file it touches and writes all of them or none. `undefined` when the edge
|
|
101
|
+
* is already declared or either manifest is unreadable; the `package-shape` step then names the
|
|
102
|
+
* line, which is the same edit by hand.
|
|
103
|
+
*/
|
|
104
|
+
export async function planWorkspaceDependency(
|
|
105
|
+
root: string,
|
|
106
|
+
fromDir: string,
|
|
107
|
+
toDir: string,
|
|
108
|
+
): Promise<PlannedEdit | undefined> {
|
|
109
|
+
const target = await readWorkspace(root, toDir);
|
|
110
|
+
const path = `${fromDir}/${MANIFEST}`;
|
|
111
|
+
const file = Bun.file(containedPath(root, path));
|
|
112
|
+
if (target === undefined || !(await file.exists())) return undefined;
|
|
113
|
+
const contents = withDependency(await file.text(), target.name, target.version);
|
|
114
|
+
return contents === undefined ? undefined : { path, contents };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Declares it now. Answers the path it rewrote, or `undefined` when there was nothing to write. */
|
|
118
|
+
export async function declareWorkspaceDependency(
|
|
119
|
+
root: string,
|
|
120
|
+
fromDir: string,
|
|
121
|
+
toDir: string,
|
|
122
|
+
): Promise<string | undefined> {
|
|
123
|
+
const edit = await planWorkspaceDependency(root, fromDir, toDir);
|
|
124
|
+
if (edit === undefined) return undefined;
|
|
125
|
+
await Bun.write(containedPath(root, edit.path), edit.contents);
|
|
126
|
+
return edit.path;
|
|
127
|
+
}
|
package/src/cmd-dev-fixture.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// The app `x dev` boots in its tests, and the one place it is declared: every assertion that
|
|
2
|
-
// needs a booted app lives in `cmd-dev.test.ts`, because a process has ONE lifecycle — the second
|
|
3
|
-
// in-process boot is refused (X_LIFECYCLE_DRAINED) — and the coverage gate runs a package in one
|
|
4
|
-
// process. So the fixture is a module of its own, and that file keeps its line budget for tests.
|
|
5
|
-
//
|
|
6
|
-
// What the app declares, and what each declaration is here to prove:
|
|
7
|
-
// app.config.ts the root marker a real `x dev` cannot start without; `ai.mcp` by default
|
|
8
|
-
// apps/web/mcp.ts the app's own MCP endpoint, mounted by the web role
|
|
9
|
-
// apps/web/runtime.ts the app's middleware and disks, reaching a development process
|
|
10
|
-
// .git/HEAD the fixture is its own repository — see the entry below
|
|
11
|
-
// apps/web/app/hello/* an SSR page with a component, edited on disk while `x dev` runs
|
|
12
|
-
// apps/web/app/notes/* a memory-backed entity and a live query, fed by the in-process bridge
|
|
13
|
-
// apps/web/app/posts/* an action, a policy and a query, mounted as HTTP routes
|
|
14
|
-
// apps/web/site/pricing/* a static page with its own stylesheet, under the CSP `x dev` sends
|
|
15
|
-
import { resetRegistry as resetActions } from '@ultimat3/action';
|
|
16
|
-
import { clearRegistry as clearEntities } from '@ultimat3/entity';
|
|
17
|
-
import { resetJobs, resetTasks } from '@ultimat3/jobs';
|
|
18
|
-
import { clearPermissions, clearRoles } from '@ultimat3/policy';
|
|
19
|
-
import { resetRegistry as resetQueries } from '@ultimat3/query';
|
|
20
|
-
import type { Frame } from '@ultimat3/realtime';
|
|
21
|
-
import { decode } from '@ultimat3/realtime';
|
|
22
|
-
import type { WsLike } from '@ultimat3/realtime/server';
|
|
23
|
-
import { clearRoutes } from '@ultimat3/render';
|
|
24
|
-
import { resetAppLoad } from './app-load';
|
|
25
|
-
|
|
26
|
-
export const DEV_FIXTURE_FILES: Readonly<Record<string, string>> = {
|
|
27
|
-
'package.json': JSON.stringify({ name: 'dev-fixture', version: '1.4.0' }),
|
|
28
|
-
|
|
29
|
-
// Its own repository, so the ignore walk stops HERE. The framework's root `.gitignore` lists
|
|
30
|
-
// `packages/cli/.dev-fixture/`, and `devIgnore` honours every ancestor up to a `.git` — without
|
|
31
|
-
// this marker the watcher admitted the fixture root and nothing under it, so no save in this
|
|
32
|
-
// tree ever reached `rebuild`, and the reload path was booted by every run and exercised by none.
|
|
33
|
-
'.git/HEAD': 'ref: refs/heads/main\n',
|
|
34
|
-
|
|
35
|
-
// The root marker a real `x dev` cannot start without, and where `ai.mcp` is declared — by
|
|
36
|
-
// default `{ expose: true, path: '/mcp' }`, which is what the MCP mount reads.
|
|
37
|
-
'app.config.ts': `import { defineConfig } from '@ultimat3/core';
|
|
38
|
-
export const config = defineConfig({ name: 'dev-fixture' });
|
|
39
|
-
`,
|
|
40
|
-
|
|
41
|
-
// The app's own MCP endpoint, in the contract `app-mcp.ts` reads. `resolveToken` answering
|
|
42
|
-
// `null` rejects every bearer, which is enough to prove the ROUTE is mounted (401, not 404).
|
|
43
|
-
'apps/web/mcp.ts': `import { defineAppMcp } from '@ultimat3/mcp';
|
|
44
|
-
export const mcp = defineAppMcp({ include: 'exposed', resolveToken: () => null });
|
|
45
|
-
`,
|
|
46
|
-
|
|
47
|
-
// The app's own middleware, in the contract `app-runtime.ts` reads: a header on every response
|
|
48
|
-
// is the cheapest proof that the chain `x dev` composed is the app's and not only the replica's.
|
|
49
|
-
// Its disks too: `x dev` built its own embedded disk and ignored this one until 22.4.
|
|
50
|
-
'apps/web/runtime.ts': `import { defineStorage, localDriver } from '@ultimat3/storage';
|
|
51
|
-
const stamp = async (request, ctx, next) => {
|
|
52
|
-
const response = await next(request, ctx);
|
|
53
|
-
const headers = new Headers(response.headers);
|
|
54
|
-
headers.set('x-dev-runtime', 'app');
|
|
55
|
-
return new Response(response.body, { status: response.status, headers });
|
|
56
|
-
};
|
|
57
|
-
const storage = defineStorage({
|
|
58
|
-
disks: { appdisk: localDriver({ root: new URL('../../.app-disk', import.meta.url).pathname }) },
|
|
59
|
-
default: 'appdisk',
|
|
60
|
-
});
|
|
61
|
-
export const runtime = { middleware: [stamp], storage };
|
|
62
|
-
`,
|
|
63
|
-
|
|
64
|
-
// A memory-backed entity, so the fixture needs no migration: the row observer sits on
|
|
65
|
-
// `database()`'s repo wrapper, the same seam a Postgres-backed repo writes through.
|
|
66
|
-
'apps/web/app/notes/entity.ts': `import { database, entity, memoryDriver, text, uuid } from '@ultimat3/entity';
|
|
67
|
-
export const notes = entity('notes', { columns: { id: uuid().primaryKey(), title: text() } });
|
|
68
|
-
export const db = database({ notes }, { driver: memoryDriver() });
|
|
69
|
-
`,
|
|
70
|
-
'apps/web/app/notes/live.ts': `import { allow } from '@ultimat3/policy';
|
|
71
|
-
import { from, query, t } from '@ultimat3/query';
|
|
72
|
-
import { db } from './entity';
|
|
73
|
-
export const liveNotes = query({
|
|
74
|
-
input: t.object({}),
|
|
75
|
-
policy: allow('public'),
|
|
76
|
-
live: true,
|
|
77
|
-
subscribes: ['notes'],
|
|
78
|
-
sql: () =>
|
|
79
|
-
from<{ id: string; title: string }>('notes', () => db.notes.where({}).all())
|
|
80
|
-
.orderBy('id')
|
|
81
|
-
.limit(50),
|
|
82
|
-
});
|
|
83
|
-
`,
|
|
84
|
-
|
|
85
|
-
'apps/web/app/posts/policy.ts': `import { allow, can, definePermissions, defineRoles } from '@ultimat3/policy';
|
|
86
|
-
export const permissions = definePermissions(['post:publish'] as const);
|
|
87
|
-
export const roles = defineRoles({
|
|
88
|
-
author: { grants: ['post:publish'] },
|
|
89
|
-
reader: { grants: [] },
|
|
90
|
-
});
|
|
91
|
-
export const canPostWrite = can('post:publish');
|
|
92
|
-
export const anyone = allow();
|
|
93
|
-
`,
|
|
94
|
-
|
|
95
|
-
'apps/web/app/posts/actions.ts': `import { action, t } from '@ultimat3/action';
|
|
96
|
-
import { anyone, canPostWrite } from './policy';
|
|
97
|
-
|
|
98
|
-
export const publishPost = action({
|
|
99
|
-
input: t.object({ id: t.uuid }),
|
|
100
|
-
output: t.object({ id: t.uuid }),
|
|
101
|
-
policy: canPostWrite,
|
|
102
|
-
async handle({ input }) {
|
|
103
|
-
return { id: input.id };
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
export const echoPost = action({
|
|
108
|
-
input: t.object({ word: t.string }),
|
|
109
|
-
output: t.object({ word: t.string }),
|
|
110
|
-
policy: anyone,
|
|
111
|
-
async handle({ input }) {
|
|
112
|
-
return { word: input.word };
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
`,
|
|
116
|
-
|
|
117
|
-
// A page with a body, on the surface an author edits all day. What the reload test rewrites: the
|
|
118
|
-
// module must be imported again for the new body to be served, which `import()` alone never does.
|
|
119
|
-
'apps/web/app/hello/page.tsx': `import { defineRoute } from '@ultimat3/render';
|
|
120
|
-
|
|
121
|
-
export const config = defineRoute({
|
|
122
|
-
render: 'ssr',
|
|
123
|
-
hydrate: 'visible',
|
|
124
|
-
offline: 'runtime',
|
|
125
|
-
budget: { js: '60kb' },
|
|
126
|
-
meta: () => ({ title: 'Hello', description: 'A page that is edited while x dev runs' }),
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
export function Page() {
|
|
130
|
-
return <p>generation one</p>;
|
|
131
|
-
}
|
|
132
|
-
`,
|
|
133
|
-
|
|
134
|
-
// An ISR page whose body is a COMPONENT in another file — the shape every real page has. What the
|
|
135
|
-
// component-reload test rewrites: the page module's own bytes never change, so only evicting what
|
|
136
|
-
// imports the component re-evaluates it, and only clearing the ISR store stops the first render
|
|
137
|
-
// from answering for the rest of the process (notificado.co, 2026-09-27).
|
|
138
|
-
'apps/web/site/news/headline.tsx': `export function Headline() {
|
|
139
|
-
return <h1>headline one</h1>;
|
|
140
|
-
}
|
|
141
|
-
`,
|
|
142
|
-
|
|
143
|
-
'apps/web/site/news/page.tsx': `import { defineRoute } from '@ultimat3/render';
|
|
144
|
-
import { Headline } from './headline';
|
|
145
|
-
|
|
146
|
-
export const config = defineRoute({
|
|
147
|
-
render: 'isr',
|
|
148
|
-
revalidate: { ttl: '1h' },
|
|
149
|
-
offline: 'runtime',
|
|
150
|
-
hydrate: 'never',
|
|
151
|
-
budget: { js: '0kb' },
|
|
152
|
-
meta: () => ({ title: 'News', description: 'A page whose component is edited while x dev runs' }),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
export function Page() {
|
|
156
|
-
return <main><Headline /></main>;
|
|
157
|
-
}
|
|
158
|
-
`,
|
|
159
|
-
|
|
160
|
-
// A stylesheet the page imports, because that import is what registers it — and the document's
|
|
161
|
-
// inline `<style>` is what the CSP has to name. Without one this file served no styled page and
|
|
162
|
-
// could not have caught the policy that blanked every deployed app.
|
|
163
|
-
'apps/web/site/pricing/page.module.scss': `.price { color: #123456; }
|
|
164
|
-
`,
|
|
165
|
-
|
|
166
|
-
'apps/web/site/pricing/page.tsx': `import { defineRoute } from '@ultimat3/render';
|
|
167
|
-
import './page.module.scss';
|
|
168
|
-
|
|
169
|
-
export const config = defineRoute({
|
|
170
|
-
render: 'static',
|
|
171
|
-
offline: 'precache',
|
|
172
|
-
hydrate: 'never',
|
|
173
|
-
budget: { js: '0kb' },
|
|
174
|
-
meta: () => ({ title: 'Pricing', description: 'What it costs' }),
|
|
175
|
-
});
|
|
176
|
-
`,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
export const resetRegistries = (): void => {
|
|
180
|
-
resetActions();
|
|
181
|
-
resetQueries();
|
|
182
|
-
clearEntities();
|
|
183
|
-
clearRoutes();
|
|
184
|
-
resetJobs();
|
|
185
|
-
resetTasks();
|
|
186
|
-
clearPermissions();
|
|
187
|
-
clearRoles();
|
|
188
|
-
resetAppLoad();
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/** A `WsLike` that keeps every frame the node sends, decoded — what a live assertion reads. */
|
|
192
|
-
export class FakeWs implements WsLike {
|
|
193
|
-
readonly frames: Frame[] = [];
|
|
194
|
-
send(data: string): number {
|
|
195
|
-
this.frames.push(decode(data));
|
|
196
|
-
return data.length;
|
|
197
|
-
}
|
|
198
|
-
close(): void {}
|
|
199
|
-
subscribe(): void {}
|
|
200
|
-
unsubscribe(): void {}
|
|
201
|
-
getBufferedAmount(): number {
|
|
202
|
-
return 0;
|
|
203
|
-
}
|
|
204
|
-
}
|
package/src/dev-live-fixture.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// The `x dev` live tests' process discipline, in one place: spawn the supervisor, read its two
|
|
2
|
-
// streams, and — on every path, a failing one included — reap every process it started and prove
|
|
3
|
-
// none is left. Two children of `cmd-dev-restart.live.test.ts` ran for six hours with ppid 1 and a
|
|
4
|
-
// deleted cwd because the test's `finally` SIGKILLed the supervisor, which cannot forward a signal
|
|
5
|
-
// it never gets, and nothing then looked for what it had started (2026-09-29).
|
|
6
|
-
|
|
7
|
-
import { readdirSync, readlinkSync } from 'node:fs'; // why: /proc is read synchronously; Bun has no readlink.
|
|
8
|
-
|
|
9
|
-
/** One pump per stream — reading a stream twice closes its reader under the first `for await`. */
|
|
10
|
-
export function pump(stream: ReadableStream<Uint8Array>): { seen: () => string } {
|
|
11
|
-
const decoder = new TextDecoder();
|
|
12
|
-
let seen = '';
|
|
13
|
-
void (async () => {
|
|
14
|
-
for await (const chunk of stream) seen += decoder.decode(chunk, { stream: true });
|
|
15
|
-
})();
|
|
16
|
-
return { seen: () => seen };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Poll the buffer until `marker` shows up. The caller's own timeout is the deadline. */
|
|
20
|
-
export async function waitFor(output: { seen: () => string }, marker: string): Promise<string> {
|
|
21
|
-
for (;;) {
|
|
22
|
-
const seen = output.seen();
|
|
23
|
-
if (seen.includes(marker)) return seen;
|
|
24
|
-
await Bun.sleep(25);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Whether `pid` is still a process — signal 0 delivers nothing and only asks. */
|
|
29
|
-
export function alive(pid: number): boolean {
|
|
30
|
-
try {
|
|
31
|
-
process.kill(pid, 0);
|
|
32
|
-
return true;
|
|
33
|
-
} catch {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Every descendant of `pid`, depth first — asked while `pid` lives, or its children are init's. */
|
|
39
|
-
export function descendantsOf(pid: number): readonly number[] {
|
|
40
|
-
const out = Bun.spawnSync(['pgrep', '-P', String(pid)]).stdout.toString();
|
|
41
|
-
const children = out
|
|
42
|
-
.split('\n')
|
|
43
|
-
.map((line) => Number(line.trim()))
|
|
44
|
-
.filter((child) => Number.isInteger(child) && child > 0);
|
|
45
|
-
return children.flatMap((child) => [child, ...descendantsOf(child)]);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Every process whose cwd is `dir` — a deleted one included, which Linux spells `<dir> (deleted)`.
|
|
50
|
-
* The one question that finds an orphan after its parent is gone, when no process tree reaches it.
|
|
51
|
-
* Empty where there is no `/proc` (macOS): the descendant walk is then the whole answer.
|
|
52
|
-
*/
|
|
53
|
-
export function processesIn(dir: string): readonly number[] {
|
|
54
|
-
let pids: readonly string[];
|
|
55
|
-
try {
|
|
56
|
-
pids = readdirSync('/proc').filter((name) => /^\d+$/.test(name));
|
|
57
|
-
} catch {
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
60
|
-
const found: number[] = [];
|
|
61
|
-
for (const pid of pids) {
|
|
62
|
-
try {
|
|
63
|
-
const cwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
64
|
-
if (cwd === dir || cwd === `${dir} (deleted)`) found.push(Number(pid));
|
|
65
|
-
} catch {
|
|
66
|
-
// Gone between the listing and the read, or not ours to read.
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return found;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Whether anything still answers on `port` at the loopback address `x dev` binds. */
|
|
73
|
-
export async function listening(port: number): Promise<boolean> {
|
|
74
|
-
try {
|
|
75
|
-
const socket = await Bun.connect({
|
|
76
|
-
hostname: '127.0.0.1',
|
|
77
|
-
port,
|
|
78
|
-
socket: { data: () => undefined },
|
|
79
|
-
});
|
|
80
|
-
socket.end();
|
|
81
|
-
return true;
|
|
82
|
-
} catch {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* SIGKILL `pids`, then wait until each is gone. A test's last resort, run in `finally` — by then
|
|
89
|
-
* every graceful stop the test meant to prove has had its chance. `performance.now()`, never
|
|
90
|
-
* `Date.now()`: the test preload freezes the wall clock, and a deadline read off it never arrives.
|
|
91
|
-
*/
|
|
92
|
-
export async function reap(pids: readonly number[], timeoutMs = 10_000): Promise<void> {
|
|
93
|
-
for (const pid of pids) {
|
|
94
|
-
try {
|
|
95
|
-
process.kill(pid, 'SIGKILL');
|
|
96
|
-
} catch {
|
|
97
|
-
// Already gone.
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const until = performance.now() + timeoutMs;
|
|
101
|
-
while (pids.some(alive) && performance.now() < until) await Bun.sleep(25);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** What was left: the pids still in `dir` and whether `port` still answers. Both empty is clean. */
|
|
105
|
-
export async function leftovers(
|
|
106
|
-
dir: string,
|
|
107
|
-
port: number | undefined,
|
|
108
|
-
): Promise<{ readonly pids: readonly number[]; readonly port: boolean }> {
|
|
109
|
-
return {
|
|
110
|
-
pids: processesIn(dir),
|
|
111
|
-
port: port === undefined ? false : await listening(port),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The net under a TIMED-OUT test: bun abandons its body without running `finally`, so an
|
|
117
|
-
* `afterEach` reaps whatever still runs in the fixture directory — the only handle left on an
|
|
118
|
-
* orphan whose parent is gone.
|
|
119
|
-
*/
|
|
120
|
-
export async function reapIn(dir: string): Promise<void> {
|
|
121
|
-
await reap(processesIn(dir));
|
|
122
|
-
}
|
package/src/policy-fixture.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// The one policy declaration set both `x policy` test files run against. Shared rather than
|
|
2
|
-
// copied, for the reason `thrown-by.ts` gives: every count in both files' assertions is read off
|
|
3
|
-
// THIS set, so a second copy drifts and each file keeps passing while they stop agreeing.
|
|
4
|
-
|
|
5
|
-
import { action, registerActions, t } from '@ultimat3/action';
|
|
6
|
-
import { and, can, definePermissions, defineRoles } from '@ultimat3/policy';
|
|
7
|
-
import { from, query, registerQuery } from '@ultimat3/query';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The row the fixture's two reads return, over an empty source: `x policy` describes a query and
|
|
11
|
-
* evaluates its policy, it never executes one, so the rows are the one fact that may be missing.
|
|
12
|
-
*/
|
|
13
|
-
interface PostRow {
|
|
14
|
-
readonly id: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Four permissions, three roles, two actions and two queries — each fact earning its place.
|
|
19
|
-
*
|
|
20
|
-
* `archivePost` is guarded by a COMPOSITE, and that is the point of it: its display capability is
|
|
21
|
-
* the label `and(post:publish, post:read)`, which is not any permission, so a report that matched
|
|
22
|
-
* on the label counted this action as enforcing nothing and printed both of its permissions as
|
|
23
|
-
* dead grants. It enforces two, and the facts say so.
|
|
24
|
-
*
|
|
25
|
-
* `post:delete` is the control: declared, granted to `admin`, enforced by nothing at all. It is
|
|
26
|
-
* what a genuinely dead grant looks like, and it is what keeps `unenforced` a claim worth making
|
|
27
|
-
* now that a composite no longer lands in it by accident.
|
|
28
|
-
*
|
|
29
|
-
* `post:publish` is enforced by two actions AND a query, so the aggregation across declarations
|
|
30
|
-
* has something to aggregate.
|
|
31
|
-
*
|
|
32
|
-
* Registers only; the registries are process-global, so the caller clears them first.
|
|
33
|
-
*/
|
|
34
|
-
export function registerPolicyFixture(): void {
|
|
35
|
-
definePermissions(['post:publish', 'post:read', 'post:delete', 'feed:read'] as const);
|
|
36
|
-
defineRoles({
|
|
37
|
-
admin: { grants: ['post:publish', 'post:read', 'post:delete', 'feed:read'] },
|
|
38
|
-
editor: { grants: ['post:publish', 'post:read'] },
|
|
39
|
-
reader: { grants: ['post:read', 'feed:read'] },
|
|
40
|
-
});
|
|
41
|
-
registerActions({
|
|
42
|
-
publishPost: action({
|
|
43
|
-
input: t.object({}),
|
|
44
|
-
output: t.object({}),
|
|
45
|
-
policy: can('post:publish'),
|
|
46
|
-
async handle() {
|
|
47
|
-
return {};
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
archivePost: action({
|
|
51
|
-
input: t.object({}),
|
|
52
|
-
output: t.object({}),
|
|
53
|
-
policy: and(
|
|
54
|
-
can('post:publish'),
|
|
55
|
-
can('post:read', ({ actor }) => actor?.id === 'admin'),
|
|
56
|
-
),
|
|
57
|
-
async handle() {
|
|
58
|
-
return {};
|
|
59
|
-
},
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
62
|
-
registerQuery(
|
|
63
|
-
'postFeed',
|
|
64
|
-
query({
|
|
65
|
-
input: t.object({}),
|
|
66
|
-
policy: can('feed:read'),
|
|
67
|
-
sql: () => from<PostRow>('posts', []).orderBy('id').limit(10),
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
registerQuery(
|
|
71
|
-
'publishedPosts',
|
|
72
|
-
query({
|
|
73
|
-
input: t.object({}),
|
|
74
|
-
policy: can('post:publish'),
|
|
75
|
-
sql: () => from<PostRow>('posts', []).orderBy('id').limit(10),
|
|
76
|
-
}),
|
|
77
|
-
);
|
|
78
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// The browser island `wiki/Realtime.md` promises: one live hook and nothing else. It is a real
|
|
2
|
-
// module rather than a string a test writes to a temp path, because module resolution is the thing
|
|
3
|
-
// under test — `@ultimat3/realtime`'s client entry must reach neither the bus nor the WAL decoder.
|
|
4
|
-
// Bundled AND imported by `realtime-browser-barrel.test.ts` — the import is what gives it an lcov
|
|
5
|
-
// record, since `Bun.build()` reads this file without evaluating it.
|
|
6
|
-
|
|
7
|
-
import { useQuery } from '@ultimat3/realtime';
|
|
8
|
-
|
|
9
|
-
export const probeUseQuery = useQuery;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// The one `RunningServices` every `role-start` test file boots roles against, and the one reset
|
|
2
|
-
// between them. Shared rather than copied, for the reason `policy-fixture.ts` gives: three files
|
|
3
|
-
// start the same roles, and a second copy of the runtime drifts while each file keeps passing.
|
|
4
|
-
//
|
|
5
|
-
// Its own module rather than a `.test.ts` neighbours import, because `tsconfig.json` excludes
|
|
6
|
-
// `*.test.ts` — a fixture written there is one `tsc` never reads.
|
|
7
|
-
|
|
8
|
-
import { noopPurgeDriver } from '@ultimat3/cache';
|
|
9
|
-
import { type RealtimeConfig, resetLifecycle } from '@ultimat3/core';
|
|
10
|
-
import { resetHttpConfig } from '@ultimat3/http';
|
|
11
|
-
import {
|
|
12
|
-
createMemoryDriver,
|
|
13
|
-
createMemoryEventBus,
|
|
14
|
-
createMemoryOutboxStore,
|
|
15
|
-
resetJobs,
|
|
16
|
-
resetJobsFacade,
|
|
17
|
-
resetTasks,
|
|
18
|
-
} from '@ultimat3/jobs';
|
|
19
|
-
import { createMemoryDriver as createMemoryMailDriver } from '@ultimat3/mail';
|
|
20
|
-
import { DEFAULT_PRESENCE_TTL_MS, InProcessTransport } from '@ultimat3/realtime/server';
|
|
21
|
-
import { defineStorage, localDriver } from '@ultimat3/storage';
|
|
22
|
-
import { resolveServices } from './runtime-bindings';
|
|
23
|
-
import type { RunningServices } from './runtime-services';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Every service a role touches, embedded but real — no PGlite boot for a role-wiring test.
|
|
27
|
-
*
|
|
28
|
-
* `root` is a parameter and not a constant: each test file owns its own directory and deletes it,
|
|
29
|
-
* so two files sharing one on-disk storage root cannot leave the other's fixture half-removed.
|
|
30
|
-
*/
|
|
31
|
-
/** Enabled, in-process: the section every role test before 22.0.0 was implicitly booting. */
|
|
32
|
-
const REALTIME_ON: RealtimeConfig = { enabled: true, transport: 'memory', urlEnv: undefined };
|
|
33
|
-
|
|
34
|
-
export function fixtureRuntime(root: string, realtime = REALTIME_ON): RunningServices {
|
|
35
|
-
const services = resolveServices(root, {});
|
|
36
|
-
const transport = new InProcessTransport();
|
|
37
|
-
return {
|
|
38
|
-
services,
|
|
39
|
-
db: { async ping() {}, async close() {} } as unknown as RunningServices['db'],
|
|
40
|
-
jobs: createMemoryDriver(),
|
|
41
|
-
// A real store, not a stub: the `worker` role starts the outbox relay against it, and a relay
|
|
42
|
-
// whose `claim()` rejects on the first 200ms tick is an unhandled rejection in whichever test
|
|
43
|
-
// happens to still be running.
|
|
44
|
-
outbox: createMemoryOutboxStore(),
|
|
45
|
-
events: createMemoryEventBus(),
|
|
46
|
-
transport,
|
|
47
|
-
realtime,
|
|
48
|
-
transportDetail: 'in-process fanout',
|
|
49
|
-
// The sync role reads this to build its `PresenceRegistry`; the default is what a boot with no
|
|
50
|
-
// `NATS_URL` resolves to, so the fixture is the real number rather than a rounder one.
|
|
51
|
-
presenceTtlMs: DEFAULT_PRESENCE_TTL_MS,
|
|
52
|
-
storage: defineStorage({ disks: { local: localDriver({ root: `${root}/storage` }) } }),
|
|
53
|
-
mail: createMemoryMailDriver(),
|
|
54
|
-
mailDetail: 'embedded',
|
|
55
|
-
purge: noopPurgeDriver(),
|
|
56
|
-
purgeDetail: 'none',
|
|
57
|
-
stop: async () => transport.close(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The process-global state one started role leaves behind. `resetLifecycle` is the load-bearing
|
|
63
|
-
* one: core's lifecycle is process-wide and a stopped server leaves it drained, so without it the
|
|
64
|
-
* SECOND web role in a file answers every request `X_DRAINING` — a suite that only passes when its
|
|
65
|
-
* tests are run one at a time. `@ultimat3/http`'s own server suite resets it for the same reason.
|
|
66
|
-
*/
|
|
67
|
-
export function resetDevRolesState(): void {
|
|
68
|
-
resetJobs();
|
|
69
|
-
resetJobsFacade();
|
|
70
|
-
resetTasks();
|
|
71
|
-
resetLifecycle();
|
|
72
|
-
// `configureHttp()` is a process-global registration made at MODULE scope, so one file that
|
|
73
|
-
// loads an app leaves that app's CORS origins, body limit and buckets standing for every later
|
|
74
|
-
// file in the same `bun test` process — and a scaffolded app now ships such a module
|
|
75
|
-
// (`apps/web/app/http.ts`). Nothing is broken today; this is the same rule the four resets above
|
|
76
|
-
// already follow, applied to the fifth global before it is the one that costs an afternoon.
|
|
77
|
-
resetHttpConfig();
|
|
78
|
-
}
|