@ultimat3/cli 22.15.0 → 24.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 +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -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 +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- 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/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- 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/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- 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 +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -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/naming.ts +28 -0
- 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 +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- 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 +144 -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-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- 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 +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- 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 +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- 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
- package/src/verify-errors.ts +0 -49
package/src/verify-tests.ts
CHANGED
|
@@ -14,14 +14,19 @@ import type { TestType } from '@ultimat3/testing';
|
|
|
14
14
|
import { TEST_TYPES } from '@ultimat3/testing';
|
|
15
15
|
import { checkEvalBaselines, checkEvalCoverage, checkEvalRecording } from './app-evals';
|
|
16
16
|
import { APP_CONFIG_FILE } from './app-root';
|
|
17
|
+
import { judgeCoverage } from './coverage-floor';
|
|
18
|
+
import { msg } from './messages';
|
|
17
19
|
import { countsOf } from './test-counts';
|
|
18
20
|
import { testEnvOverrides } from './test-dotenv';
|
|
21
|
+
import { IGNORED_TEST_PATTERNS } from './test-ignores';
|
|
19
22
|
import type { TestFile } from './test-select';
|
|
20
23
|
import { discoverTests } from './test-select';
|
|
21
24
|
import { machineLease } from './test-slots';
|
|
22
25
|
import type { WorkerPlan } from './test-workers';
|
|
23
26
|
import { availableCpus, SERIAL_TYPES, totalMemory, workerPlan } from './test-workers';
|
|
27
|
+
import { runCovered } from './verify-coverage-run';
|
|
24
28
|
import { withE2eApp } from './verify-e2e';
|
|
29
|
+
import { readVerifyFloor } from './verify-floor';
|
|
25
30
|
import { shardFiles } from './verify-shard';
|
|
26
31
|
import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
|
|
27
32
|
import { fromExec, fromFindings } from './verify-step';
|
|
@@ -120,7 +125,9 @@ const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
|
|
|
120
125
|
* Build output, and nested projects that carry their own `x verify`. `examples/**` and
|
|
121
126
|
* `dummy/**` are the second kind: each holds a whole app gated by its own run of this same step
|
|
122
127
|
* list, so collecting them here would report one app failure on two different gates. The patterns
|
|
123
|
-
* are relative to the run's root, so this excludes nothing when the app itself is the root.
|
|
128
|
+
* are relative to the run's root, so this excludes nothing when the app itself is the root. The
|
|
129
|
+
* policy — which directories, and which are anchored at the root — is `test-ignores.ts`'s, the same
|
|
130
|
+
* one `discoverTests` filters the parallel steps by; this is its glob projection.
|
|
124
131
|
*
|
|
125
132
|
* `dummy/**` was added after the framework gate started running a demo app's tests and disagreeing
|
|
126
133
|
* with `bun run test` — which already excluded it — for no reason a reader could see.
|
|
@@ -129,7 +136,7 @@ const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
|
|
|
129
136
|
* those apps and blocks on a per-app ratchet. Until 2026-08 that was only true of `examples/**`,
|
|
130
137
|
* and `dummy/social-media-clone` was excluded by this list and gated by nothing.
|
|
131
138
|
*/
|
|
132
|
-
const NEVER_A_TEST =
|
|
139
|
+
const NEVER_A_TEST = IGNORED_TEST_PATTERNS;
|
|
133
140
|
|
|
134
141
|
const ignoreFlags = (patterns: readonly string[]): readonly string[] =>
|
|
135
142
|
patterns.map((pattern) => `--path-ignore-patterns=${pattern}`);
|
|
@@ -240,6 +247,21 @@ const runType = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome>
|
|
|
240
247
|
ctx.workers === undefined ? workerPlan(availableCpus(), totalMemory(), env) : undefined;
|
|
241
248
|
const workers = ctx.workers ?? (plan as WorkerPlan).workers;
|
|
242
249
|
const lease = plan === undefined ? undefined : machineLease(plan.workers, env);
|
|
250
|
+
const widthReason =
|
|
251
|
+
plan === undefined
|
|
252
|
+
? `${String(Math.min(workers, files.length))} workers (--workers)`
|
|
253
|
+
: files.length < plan.workers
|
|
254
|
+
? `${String(files.length)} of ${plan.reason}`
|
|
255
|
+
: plan.reason;
|
|
256
|
+
if (type === 'unit' && isApp(ctx.root)) {
|
|
257
|
+
return coveredUnit(ctx, {
|
|
258
|
+
files: files.map((file) => file.path),
|
|
259
|
+
workers,
|
|
260
|
+
widthReason,
|
|
261
|
+
shard,
|
|
262
|
+
lease,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
243
265
|
const outcome = await runParallel({
|
|
244
266
|
root: ctx.root,
|
|
245
267
|
runner: ctx.runner,
|
|
@@ -247,18 +269,71 @@ const runType = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome>
|
|
|
247
269
|
workers,
|
|
248
270
|
type,
|
|
249
271
|
isolate: ctx.isolate === true,
|
|
250
|
-
widthReason
|
|
251
|
-
plan === undefined
|
|
252
|
-
? `${String(Math.min(workers, files.length))} workers (--workers)`
|
|
253
|
-
: files.length < plan.workers
|
|
254
|
-
? `${String(files.length)} of ${plan.reason}`
|
|
255
|
-
: plan.reason,
|
|
272
|
+
widthReason,
|
|
256
273
|
...(lease === undefined ? {} : { lease }),
|
|
257
274
|
...(ctx.env === undefined ? {} : { env: ctx.env }),
|
|
258
275
|
});
|
|
259
276
|
return shard === undefined ? outcome : { ...outcome, shard };
|
|
260
277
|
};
|
|
261
278
|
|
|
279
|
+
/**
|
|
280
|
+
* An APP's unit step: the suite, and what it covered of the app's own source held to the floor in
|
|
281
|
+
* `x.verify.json` (`coverage-floor.ts`). Only the unit suite counts — it is the one every run
|
|
282
|
+
* executes, and an opt-in suite cannot hold a floor a default run must meet. Only an app: the
|
|
283
|
+
* framework's packages are held to the same bar one process each, by `scripts/coverage-gate.ts`,
|
|
284
|
+
* because one run over a monorepo reads every package diluted by the ones it imports.
|
|
285
|
+
*
|
|
286
|
+
* A red suite is not judged — a slice that died wrote no lcov, and a coverage finding beside
|
|
287
|
+
* `X_TEST_FAILED` would send the reader to the wrong fix. A `--shard` slice is not judged either:
|
|
288
|
+
* it hands its facts to `x verify merge`, which folds every shard and judges once.
|
|
289
|
+
*/
|
|
290
|
+
const coveredUnit = async (
|
|
291
|
+
ctx: VerifyContext,
|
|
292
|
+
run: {
|
|
293
|
+
readonly files: readonly string[];
|
|
294
|
+
readonly workers: number;
|
|
295
|
+
readonly widthReason: string;
|
|
296
|
+
readonly shard: ReturnType<typeof shardFiles> | undefined;
|
|
297
|
+
readonly lease: ReturnType<typeof machineLease> | undefined;
|
|
298
|
+
},
|
|
299
|
+
): Promise<StepOutcome> => {
|
|
300
|
+
const { coverage, ...outcome } = await runCovered({
|
|
301
|
+
root: ctx.root,
|
|
302
|
+
runner: ctx.runner,
|
|
303
|
+
files: run.files,
|
|
304
|
+
workers: run.workers,
|
|
305
|
+
isolate: ctx.isolate === true,
|
|
306
|
+
widthReason: run.widthReason,
|
|
307
|
+
...(run.lease === undefined ? {} : { lease: run.lease }),
|
|
308
|
+
...(ctx.env === undefined ? {} : { env: ctx.env }),
|
|
309
|
+
});
|
|
310
|
+
if (!outcome.ok) return run.shard === undefined ? outcome : { ...outcome, shard: run.shard };
|
|
311
|
+
if (run.shard !== undefined) {
|
|
312
|
+
return {
|
|
313
|
+
...outcome,
|
|
314
|
+
shard: run.shard,
|
|
315
|
+
coverage,
|
|
316
|
+
output: msg('cli.verify.coverageDeferred'),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const verdict = await judgeCoverage(
|
|
320
|
+
ctx.root,
|
|
321
|
+
coverage,
|
|
322
|
+
(await readVerifyFloor(ctx.root))?.coverage,
|
|
323
|
+
);
|
|
324
|
+
return {
|
|
325
|
+
...outcome,
|
|
326
|
+
ok: verdict.findings.length === 0,
|
|
327
|
+
findings: verdict.findings,
|
|
328
|
+
output: verdict.output,
|
|
329
|
+
measured: {
|
|
330
|
+
lines: verdict.measure.lines,
|
|
331
|
+
funcs: verdict.measure.funcs,
|
|
332
|
+
files: verdict.measure.files,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
262
337
|
const isApp = (root: string): boolean => existsSync(join(root, APP_CONFIG_FILE));
|
|
263
338
|
|
|
264
339
|
/**
|
package/src/workspace-checks.ts
CHANGED
|
@@ -10,10 +10,17 @@
|
|
|
10
10
|
// public subject. `isReExportManifest` is the whole carve-out — one statement of logic in such a
|
|
11
11
|
// file re-arms the ceiling on the same save, which is what keeps it from being a hole.
|
|
12
12
|
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { ERROR_DOCS_URL, renderCauseValue } from '@ultimat3/core';
|
|
16
16
|
import type { Finding } from './output';
|
|
17
|
+
import {
|
|
18
|
+
entryFilesOf,
|
|
19
|
+
FIXTURE_SUFFIX,
|
|
20
|
+
fixtureReachFinding,
|
|
21
|
+
fixturesReachedFrom,
|
|
22
|
+
mayReachFixture,
|
|
23
|
+
} from './publish-closure';
|
|
17
24
|
import { isReExportManifest } from './reexport-manifest';
|
|
18
25
|
import { eachSourceFile, isGenerated } from './source-files';
|
|
19
26
|
import { checkRootReferences } from './tsconfig-references';
|
|
@@ -177,6 +184,16 @@ export function checkLockstep(
|
|
|
177
184
|
*/
|
|
178
185
|
export const TEST_EXCLUSION = '!src/**/*.test.ts';
|
|
179
186
|
|
|
187
|
+
/**
|
|
188
|
+
* The same rule for the other suffix test code is written under. A `-fixture.ts` is a harness a
|
|
189
|
+
* test imports — a fake driver, a recorded page, a scaffolded tree — and `src` sweeps it into the
|
|
190
|
+
* tarball exactly as it sweeps a `*.test.ts`: six packages shipped 18 of them, 2026-10.
|
|
191
|
+
*/
|
|
192
|
+
export const FIXTURE_EXCLUSION = '!src/**/*-fixture.ts';
|
|
193
|
+
|
|
194
|
+
/** Every exclusion a published package's `files` carries, in the order it is written. */
|
|
195
|
+
export const TEST_CODE_EXCLUSIONS: readonly string[] = [TEST_EXCLUSION, FIXTURE_EXCLUSION];
|
|
196
|
+
|
|
180
197
|
export const missingPublishedFileFinding = (dir: string, entry: string): Finding => ({
|
|
181
198
|
code: 'X_PACKAGE_SHAPE',
|
|
182
199
|
cause: `packages/${dir}/package.json ships "${entry}" in "files", but packages/${dir}/${entry} does not exist`,
|
|
@@ -185,10 +202,10 @@ export const missingPublishedFileFinding = (dir: string, entry: string): Finding
|
|
|
185
202
|
at: `packages/${dir}/package.json`,
|
|
186
203
|
});
|
|
187
204
|
|
|
188
|
-
export const publishesTestsFinding = (dir: string): Finding => ({
|
|
205
|
+
export const publishesTestsFinding = (dir: string, exclusion: string): Finding => ({
|
|
189
206
|
code: 'X_PACKAGE_SHAPE',
|
|
190
|
-
cause: `packages/${dir}/package.json does not exclude ${
|
|
191
|
-
fix: `add "${
|
|
207
|
+
cause: `packages/${dir}/package.json does not exclude ${exclusion} from "files"`,
|
|
208
|
+
fix: `add "${exclusion}" to "files" in packages/${dir}/package.json, after "src"`,
|
|
192
209
|
docs: ERROR_DOCS_URL,
|
|
193
210
|
at: `packages/${dir}/package.json`,
|
|
194
211
|
});
|
|
@@ -222,7 +239,7 @@ export const buildArtifactsFinding = (dir: string, count: number): Finding => ({
|
|
|
222
239
|
export const noFilesAllowlistFinding = (dir: string): Finding => ({
|
|
223
240
|
code: 'X_PACKAGE_SHAPE',
|
|
224
241
|
cause: `packages/${dir}/package.json publishes with no "files" allowlist, so the tarball carries whatever is in the directory`,
|
|
225
|
-
fix: `add "files": ["src", "${
|
|
242
|
+
fix: `add "files": ["src", ${TEST_CODE_EXCLUSIONS.map((entry) => `"${entry}"`).join(', ')}, "README.md", "LICENSE"] to packages/${dir}/package.json`,
|
|
226
243
|
docs: ERROR_DOCS_URL,
|
|
227
244
|
at: `packages/${dir}/package.json`,
|
|
228
245
|
});
|
|
@@ -239,7 +256,7 @@ export const noFilesAllowlistFinding = (dir: string): Finding => ({
|
|
|
239
256
|
* The second half is `src` sweeping in every `*.test.ts` beside it. Tests are the framework's own,
|
|
240
257
|
* they run against a preloaded frozen clock and a sealed network, and a consumer's test runner
|
|
241
258
|
* collecting them is a failure nobody asked for — 393 files, over half of `@ultimat3/cli`'s
|
|
242
|
-
* tarball.
|
|
259
|
+
* tarball. A `*-fixture.ts` is the same code under its other suffix (`TEST_CODE_EXCLUSIONS`).
|
|
243
260
|
*
|
|
244
261
|
* Private packages are exempt: a generated app's `packages/*` never reach a registry, carry no
|
|
245
262
|
* `files` and need no license of their own.
|
|
@@ -258,8 +275,10 @@ export function checkPublishShape(root: string, manifests: readonly ManifestFact
|
|
|
258
275
|
if (existsSync(join(root, 'packages', manifest.dir, entry))) continue;
|
|
259
276
|
findings.push(missingPublishedFileFinding(manifest.dir, entry));
|
|
260
277
|
}
|
|
261
|
-
|
|
262
|
-
|
|
278
|
+
for (const exclusion of TEST_CODE_EXCLUSIONS) {
|
|
279
|
+
if (!manifest.files.includes(exclusion)) {
|
|
280
|
+
findings.push(publishesTestsFinding(manifest.dir, exclusion));
|
|
281
|
+
}
|
|
263
282
|
}
|
|
264
283
|
}
|
|
265
284
|
return findings;
|
|
@@ -312,6 +331,28 @@ export interface PackageShapeOptions {
|
|
|
312
331
|
readonly release?: string;
|
|
313
332
|
}
|
|
314
333
|
|
|
334
|
+
/**
|
|
335
|
+
* The fixtures a published package's entry points reach. A package with no fixture costs one glob
|
|
336
|
+
* — 19 of 31 — and one with some is read once: `mayReachFixture` answers from the text, and only a
|
|
337
|
+
* package it cannot clear pays for the import walk.
|
|
338
|
+
*/
|
|
339
|
+
async function fixtureReach(root: string, dir: string, manifest: unknown): Promise<Finding[]> {
|
|
340
|
+
const base = join(root, 'packages', dir);
|
|
341
|
+
const fixtures = new Bun.Glob(`src/**/*${FIXTURE_SUFFIX}`).scan({ cwd: base, absolute: false });
|
|
342
|
+
if ((await fixtures.next()).done === true) return [];
|
|
343
|
+
const entries = entryFilesOf(manifest);
|
|
344
|
+
// What the tarball carries of `src`, plus the fixtures themselves: a test's imports are not a
|
|
345
|
+
// consumer's, so no `*.test.ts` is read.
|
|
346
|
+
const sources = new Map<string, string>();
|
|
347
|
+
for await (const path of new Bun.Glob('src/**/*.{ts,tsx}').scan({ cwd: base, absolute: false })) {
|
|
348
|
+
if (!/\.test\.tsx?$/.test(path)) sources.set(path, readFileSync(join(base, path), 'utf8'));
|
|
349
|
+
}
|
|
350
|
+
if (!mayReachFixture(sources, entries)) return [];
|
|
351
|
+
return fixturesReachedFrom({ read: (path) => sources.get(path) }, entries).map((fixture) =>
|
|
352
|
+
fixtureReachFinding(dir, fixture, entries),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
315
356
|
/** Every package ships the same contract files; a missing one is a build error, not a chore. */
|
|
316
357
|
export async function checkPackageShape(
|
|
317
358
|
root: string,
|
|
@@ -348,6 +389,7 @@ export async function checkPackageShape(
|
|
|
348
389
|
findings.push(badVersionFinding(dir, version));
|
|
349
390
|
continue;
|
|
350
391
|
}
|
|
392
|
+
if (record.private !== true) findings.push(...(await fixtureReach(root, dir, manifest)));
|
|
351
393
|
facts.push({
|
|
352
394
|
dir,
|
|
353
395
|
name: typeof record.name === 'string' ? record.name : `@ultimat3/${dir}`,
|
|
@@ -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/build-errors.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// The CLI's build and bundle refusals: an entry an app does not have, an island that will not
|
|
2
|
-
// compile, a framework browser script that will not bundle. Split from `errors.ts` at its 500-line
|
|
3
|
-
// ceiling; `errors.ts` re-exports every name, so no import site moves.
|
|
4
|
-
|
|
5
|
-
import { UltimateError } from '@ultimat3/core';
|
|
6
|
-
import { quoteArg } from './shell-quote';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A build target names an entry file the app does not have. `x build` refuses before it spawns the
|
|
10
|
-
* builder: `bun build`'s own "module not found" says nothing about which file an Ultimate app is
|
|
11
|
-
* supposed to own, and `docker build`'s says nothing about which target wanted it.
|
|
12
|
-
*/
|
|
13
|
-
export class BuildEntryMissingError extends UltimateError {
|
|
14
|
-
constructor(input: { target: string; entry: string }) {
|
|
15
|
-
super({
|
|
16
|
-
code: 'X_BUILD_ENTRY_MISSING',
|
|
17
|
-
cause: `x build --target ${input.target} builds from ${input.entry}, and the app does not have it`,
|
|
18
|
-
fix: `x new scratch-app --dry-run --json # its file list carries ${input.entry}; copy that file into this app`,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* A client entry would not compile. `X_BUILD_FAILED`, not a code of its own: an island is a bundle
|
|
25
|
-
* entry point like any other, and the target's own logs are what says which line. The fix builds
|
|
26
|
-
* exactly that one file, so the next message an author reads is the compiler's and not the CLI's.
|
|
27
|
-
*/
|
|
28
|
-
export class IslandBuildFailedError extends UltimateError {
|
|
29
|
-
constructor(input: { file: string; logs: string }) {
|
|
30
|
-
super({
|
|
31
|
-
code: 'X_BUILD_FAILED',
|
|
32
|
-
cause: `${input.file} is an island entry point and would not bundle: ${input.logs}`,
|
|
33
|
-
fix: `bun build --target browser ${input.file}`,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Which framework script a page ships: the sync worker, the page boot, or the client router. */
|
|
39
|
-
export type FrameworkScriptKind = 'sync worker' | 'page boot' | 'client router';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* One of the page's framework scripts would not bundle. Same code as an island: "a browser entry
|
|
43
|
-
* the framework builds did not build" is one condition, and the entry here is framework code, not
|
|
44
|
-
* the app's — the cause names WHICH script, so the reader knows what the page is now missing.
|
|
45
|
-
*/
|
|
46
|
-
export class FrameworkScriptBuildFailedError extends UltimateError {
|
|
47
|
-
constructor(input: { what: FrameworkScriptKind; entry: string; logs: string }) {
|
|
48
|
-
super({
|
|
49
|
-
code: 'X_BUILD_FAILED',
|
|
50
|
-
cause: `the ${input.what} (${input.entry}) would not bundle for the browser: ${input.logs}`,
|
|
51
|
-
fix: `bun build --target browser --format iife ${quoteArg(input.entry)}`,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
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
|
-
}
|