@ultimat3/cli 1.2.0 → 3.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 +761 -0
- package/README.md +42 -9
- package/package.json +25 -23
- package/src/api-routes.ts +16 -0
- package/src/app-auth.ts +32 -0
- package/src/app-entities.ts +18 -0
- package/src/app-env.ts +103 -0
- package/src/app-load.ts +20 -3
- package/src/bin.ts +4 -3
- package/src/budgets.ts +134 -9
- package/src/cmd-build.ts +69 -21
- package/src/cmd-db-branch.ts +219 -0
- package/src/cmd-db.ts +458 -153
- package/src/cmd-deploy.ts +59 -6
- package/src/cmd-dev.ts +92 -18
- package/src/cmd-docs.ts +167 -0
- package/src/cmd-doctor.ts +74 -10
- package/src/cmd-env.ts +95 -0
- package/src/cmd-errors.ts +33 -13
- package/src/cmd-fix.ts +5 -1
- package/src/cmd-generate.ts +146 -111
- package/src/cmd-help.ts +16 -5
- package/src/cmd-i18n.ts +2 -0
- package/src/cmd-jobs.ts +47 -33
- package/src/cmd-mcp.ts +11 -2
- package/src/cmd-new.ts +14 -8
- package/src/cmd-planned.ts +55 -10
- package/src/cmd-policy.ts +1 -0
- package/src/cmd-registries.ts +3 -0
- package/src/cmd-secrets.ts +368 -0
- package/src/cmd-tasks.ts +1 -0
- package/src/cmd-test.ts +29 -24
- package/src/cmd-verify.ts +197 -25
- package/src/db-backfill.ts +401 -0
- package/src/db-branch.ts +269 -0
- package/src/db-destructive.ts +29 -0
- package/src/db-finding.ts +28 -0
- package/src/db-generate.ts +144 -0
- package/src/db-seed.ts +294 -0
- package/src/db-snapshot.ts +24 -0
- package/src/dev-assets.ts +108 -23
- package/src/dev-cache.ts +122 -0
- package/src/dev-dashboard.ts +19 -4
- package/src/dev-hooks.ts +27 -2
- package/src/dev-n-plus-one.ts +191 -0
- package/src/dev-queue.ts +105 -19
- package/src/dev-render.ts +158 -26
- package/src/dev-roles-fixture.ts +67 -0
- package/src/dev-roles.ts +167 -78
- package/src/dev-runtime.ts +117 -40
- package/src/dev-services.ts +15 -0
- package/src/dev-storage.ts +247 -0
- package/src/dev-sync.ts +107 -0
- package/src/dev-traces.ts +37 -7
- package/src/dispatch.ts +4 -2
- package/src/document-styles.ts +54 -0
- package/src/drift.ts +78 -10
- package/src/error-catalog.ts +8 -18
- package/src/error-codes.ts +192 -0
- package/src/error-contract.ts +29 -7
- package/src/error-fixes.ts +114 -0
- package/src/errors.ts +201 -138
- package/src/exec.ts +42 -8
- package/src/fix-command.ts +268 -0
- package/src/flag-number.ts +67 -0
- package/src/framework-scope.ts +49 -0
- package/src/generate-kinds.ts +97 -0
- package/src/guards.ts +186 -0
- package/src/index.ts +92 -15
- package/src/island-bundle.ts +166 -0
- package/src/island-routes.ts +50 -0
- package/src/jobs-driver.ts +33 -0
- package/src/jobs-json.ts +24 -0
- package/src/jobs-report.ts +17 -4
- package/src/mcp-db-target.ts +52 -27
- package/src/mcp-errors.ts +128 -19
- package/src/mcp-host.ts +44 -25
- package/src/messages.ts +93 -2
- package/src/metrics-endpoint.ts +64 -16
- package/src/migrations.ts +37 -4
- package/src/otlp-export.ts +64 -0
- package/src/output.ts +46 -16
- package/src/parse.ts +41 -3
- package/src/policy-facts.ts +38 -6
- package/src/policy-fixture.ts +14 -7
- package/src/prerender.ts +111 -2
- package/src/registry.ts +21 -3
- package/src/runtime-overrides.ts +66 -0
- package/src/safe-url-label.ts +24 -0
- package/src/scaffold-fixture.ts +10 -0
- package/src/scaffold-typecheck.ts +16 -38
- package/src/serve.ts +185 -13
- package/src/shell-quote.ts +15 -0
- package/src/source-files.ts +4 -0
- package/src/statement-loop.ts +74 -0
- package/src/style-csp.ts +18 -0
- package/src/sync-authenticator.ts +59 -0
- package/src/templates/action.ts +15 -30
- package/src/templates/admin-page.ts +103 -0
- package/src/templates/admin.ts +11 -7
- package/src/templates/backfill.ts +212 -0
- package/src/templates/entity.ts +72 -31
- package/src/templates/guard.ts +143 -0
- package/src/templates/index.ts +12 -1
- package/src/templates/island.ts +67 -0
- package/src/templates/job.ts +53 -13
- package/src/templates/naming.ts +17 -1
- package/src/templates/policy.ts +35 -28
- package/src/templates/query.ts +24 -5
- package/src/templates/resource.ts +19 -11
- package/src/templates/route.ts +90 -15
- package/src/templates/scaffold-app.ts +142 -45
- package/src/templates/scaffold-claude-agents.ts +149 -0
- package/src/templates/scaffold-claude-commands.ts +221 -0
- package/src/templates/scaffold-claude.ts +134 -0
- package/src/templates/scaffold-container.ts +46 -2
- package/src/templates/scaffold-db-package.ts +91 -0
- package/src/templates/scaffold-docs.ts +24 -5
- package/src/templates/scaffold-domain-package.ts +90 -0
- package/src/templates/scaffold-env.ts +87 -0
- package/src/templates/scaffold-i18n.ts +4 -1
- package/src/templates/scaffold-mcp-package.ts +49 -0
- package/src/templates/scaffold-package-shape.ts +25 -4
- package/src/templates/scaffold-repo.ts +116 -257
- package/src/templates/scaffold-roles.ts +68 -0
- package/src/templates/scaffold-ui-package.ts +56 -0
- package/src/templates/slice-foundation.ts +88 -0
- package/src/templates/wrap.ts +95 -0
- package/src/test-counts.ts +35 -0
- package/src/test-select.ts +30 -15
- package/src/test-shards.ts +20 -11
- package/src/test-workers.ts +50 -0
- package/src/ts-scan.ts +284 -15
- package/src/tsconfig-references.ts +103 -0
- package/src/verify-floor.ts +133 -0
- package/src/verify-step.ts +19 -0
- package/src/verify-test-run.ts +72 -0
- package/src/verify-tests.ts +160 -71
- package/src/version-loader.ts +20 -3
- package/src/workspace-checks.ts +87 -16
- package/src/write-line.ts +34 -0
package/src/prerender.ts
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
// only which routes qualify and where the bytes land.
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { renderThrowable } from '@ultimat3/core';
|
|
7
8
|
import type { RouteEntry } from '@ultimat3/render';
|
|
8
9
|
import { renderStatic, routeEntries } from '@ultimat3/render';
|
|
9
10
|
import { loadApp } from './app-load';
|
|
10
11
|
import { appManifest } from './app-manifest';
|
|
12
|
+
import type { RouteStats } from './budgets';
|
|
13
|
+
import { measureDocumentJs, writeBuildStats } from './budgets';
|
|
11
14
|
import { routeDocument } from './dev-render';
|
|
15
|
+
import type { IslandBundle } from './island-bundle';
|
|
16
|
+
import { buildIslands, writeIslands } from './island-bundle';
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* `static` only. `isr` revalidates and `ssr`/`stream`/`spa` need a process, so writing any of them
|
|
@@ -32,16 +37,66 @@ export interface PrerenderedPage {
|
|
|
32
37
|
readonly bytes: number;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/** A route that declared a budget and could not be rendered here, and what stopped it. */
|
|
41
|
+
export interface UnmeasuredRoute {
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly reason: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export interface PrerenderReport {
|
|
36
47
|
readonly out: string;
|
|
37
48
|
readonly buildId: string;
|
|
38
49
|
readonly pages: readonly PrerenderedPage[];
|
|
39
50
|
/** Routes that exist and are not static. Reported, so "only 2 pages" is never a mystery. */
|
|
40
51
|
readonly skipped: readonly string[];
|
|
52
|
+
/**
|
|
53
|
+
* Routes whose budget this build could not weigh, with the reason. `X_BUDGET_UNMEASURED` is what
|
|
54
|
+
* the gate then reports for each; this is the half that says WHY, which a per-route finding read
|
|
55
|
+
* off a stats file cannot know.
|
|
56
|
+
*/
|
|
57
|
+
readonly unmeasured: readonly UnmeasuredRoute[];
|
|
58
|
+
/** Where the measured stats landed, for the `budgets` gate step to read. */
|
|
59
|
+
readonly stats: string;
|
|
60
|
+
/** Client entries emitted, one chunk each. Reported so "which JS shipped?" needs no unzip. */
|
|
61
|
+
readonly islands: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The heaviest thing the document actually boots, named by the source file an author can open.
|
|
66
|
+
* An island chunk is content-addressed, so its URL says nothing on its own — mapping it back
|
|
67
|
+
* through the bundle is what turns `X_BUDGET_EXCEEDED` from a number into an instruction.
|
|
68
|
+
*/
|
|
69
|
+
function heaviestSource(
|
|
70
|
+
bundle: IslandBundle,
|
|
71
|
+
entries: readonly { readonly url: string; readonly bytes: number }[],
|
|
72
|
+
): readonly string[] | undefined {
|
|
73
|
+
const heaviest = entries.reduce<{ url: string; bytes: number } | undefined>(
|
|
74
|
+
(max, entry) => (max === undefined || entry.bytes > max.bytes ? entry : max),
|
|
75
|
+
undefined,
|
|
76
|
+
);
|
|
77
|
+
if (heaviest === undefined) return undefined;
|
|
78
|
+
const chunk = bundle.chunkAt(heaviest.url);
|
|
79
|
+
return chunk === undefined ? [heaviest.url] : [chunk.file];
|
|
41
80
|
}
|
|
42
81
|
|
|
43
82
|
export const DEFAULT_ORIGIN = 'https://localhost';
|
|
44
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Prerendering and measuring are two questions, and conflating them made `X_BUDGET_UNMEASURED`
|
|
86
|
+
* unclosable by any invocation: only `static` was ever rendered, so a `budget:` on an ssr, isr,
|
|
87
|
+
* stream or spa route produced no `build-stats.json` entry however the build was run, and a gate
|
|
88
|
+
* whose finding no command can close is a gate an author learns to ignore.
|
|
89
|
+
*
|
|
90
|
+
* The first question decides what lands on a CDN — `isPrerenderable`, unchanged, because a page
|
|
91
|
+
* whose staleness nothing can correct must not be published. The second asks what a browser
|
|
92
|
+
* executes, and every render mode makes that promise. So a budgeted route is rendered IN MEMORY
|
|
93
|
+
* through the same `routeDocument` a request takes, weighed, and thrown away.
|
|
94
|
+
*/
|
|
95
|
+
const declaresBudget = (entry: RouteEntry): boolean => {
|
|
96
|
+
const budget = entry.config.budget;
|
|
97
|
+
return budget !== undefined && (budget.js !== undefined || budget.lcp !== undefined);
|
|
98
|
+
};
|
|
99
|
+
|
|
45
100
|
export async function prerenderSite(options: PrerenderOptions): Promise<PrerenderReport> {
|
|
46
101
|
// The same load `x dev` and `x manifest` perform: importing the app's modules IS what fills the
|
|
47
102
|
// route registry, so there is no route table to prerender before this runs.
|
|
@@ -50,22 +105,76 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
50
105
|
const origin = options.origin ?? DEFAULT_ORIGIN;
|
|
51
106
|
const pages: PrerenderedPage[] = [];
|
|
52
107
|
const skipped: string[] = [];
|
|
108
|
+
const routes: RouteStats[] = [];
|
|
109
|
+
const unmeasured: UnmeasuredRoute[] = [];
|
|
110
|
+
|
|
111
|
+
// Before the first document: a page's `data-x-entry` is a built chunk's URL, so the chunks have
|
|
112
|
+
// to exist to be named. Written into `out` too — a static export is served with no process
|
|
113
|
+
// behind it, so the artifact carries every byte the browser will ask for.
|
|
114
|
+
const islands = await buildIslands(options.root);
|
|
115
|
+
await writeIslands(islands, options.out);
|
|
53
116
|
|
|
54
117
|
for (const entry of routeEntries()) {
|
|
55
118
|
if (!isPrerenderable(entry)) {
|
|
56
119
|
skipped.push(entry.path);
|
|
120
|
+
if (!declaresBudget(entry)) continue;
|
|
121
|
+
// Non-fatal, and that is deliberate: an ssr page's `load` may want a request, a session or a
|
|
122
|
+
// database this build does not have, and a `x build --target static` that started failing on
|
|
123
|
+
// routes it never used to touch would be a worse regression than the gap it closes. A route
|
|
124
|
+
// that will not render here is reported, gets no stats entry, and stays `X_BUDGET_UNMEASURED`.
|
|
125
|
+
try {
|
|
126
|
+
const html = await routeDocument(
|
|
127
|
+
entry,
|
|
128
|
+
{ url: new URL(entry.path, origin).href, params: {} },
|
|
129
|
+
{ resolveIsland: (file: string) => islands.resolverFor(file) },
|
|
130
|
+
);
|
|
131
|
+
const measured = await measureDocumentJs(html, options.out);
|
|
132
|
+
const chain = heaviestSource(islands, measured.entries);
|
|
133
|
+
routes.push({
|
|
134
|
+
path: entry.path,
|
|
135
|
+
jsBytes: measured.jsBytes,
|
|
136
|
+
...(chain === undefined ? {} : { heaviestChain: chain }),
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// `renderThrowable`, never `String(error)`: this is a caught unknown, and a hostile
|
|
140
|
+
// `toString` here would take the whole build down instead of one route's measurement.
|
|
141
|
+
unmeasured.push({ path: entry.path, reason: renderThrowable(error) });
|
|
142
|
+
}
|
|
57
143
|
continue;
|
|
58
144
|
}
|
|
59
145
|
const artifacts = await renderStatic(
|
|
60
146
|
entry,
|
|
61
|
-
({ path, params }) =>
|
|
147
|
+
({ path, params }) =>
|
|
148
|
+
routeDocument(
|
|
149
|
+
entry,
|
|
150
|
+
{ url: new URL(path, origin).href, params },
|
|
151
|
+
{ resolveIsland: (file: string) => islands.resolverFor(file) },
|
|
152
|
+
),
|
|
62
153
|
{ buildId },
|
|
63
154
|
);
|
|
64
155
|
for (const artifact of artifacts) {
|
|
65
156
|
const file = join(options.out, artifact.outputPath);
|
|
66
157
|
const bytes = await Bun.write(file, artifact.html);
|
|
67
158
|
pages.push({ path: artifact.path, file: artifact.outputPath, hash: artifact.hash, bytes });
|
|
159
|
+
// Measured from the document that was just written, so the `budgets` step compares a
|
|
160
|
+
// declared budget against bytes that exist on disk rather than against a graph's estimate.
|
|
161
|
+
const measured = await measureDocumentJs(artifact.html, options.out);
|
|
162
|
+
const chain = heaviestSource(islands, measured.entries);
|
|
163
|
+
routes.push({
|
|
164
|
+
path: artifact.path,
|
|
165
|
+
jsBytes: measured.jsBytes,
|
|
166
|
+
...(chain === undefined ? {} : { heaviestChain: chain }),
|
|
167
|
+
});
|
|
68
168
|
}
|
|
69
169
|
}
|
|
70
|
-
|
|
170
|
+
const stats = await writeBuildStats(options.root, { routes });
|
|
171
|
+
return {
|
|
172
|
+
out: options.out,
|
|
173
|
+
buildId,
|
|
174
|
+
pages,
|
|
175
|
+
skipped,
|
|
176
|
+
unmeasured,
|
|
177
|
+
stats,
|
|
178
|
+
islands: islands.chunks.map((chunk) => chunk.file),
|
|
179
|
+
};
|
|
71
180
|
}
|
package/src/registry.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { buildCommand } from './cmd-build';
|
|
|
5
5
|
import { dbCommand } from './cmd-db';
|
|
6
6
|
import { deployCommand } from './cmd-deploy';
|
|
7
7
|
import { devCommand } from './cmd-dev';
|
|
8
|
+
import { docsCommand } from './cmd-docs';
|
|
8
9
|
import { doctorCommand } from './cmd-doctor';
|
|
10
|
+
import { envCommand } from './cmd-env';
|
|
9
11
|
import { errorsCommand } from './cmd-errors';
|
|
10
12
|
import { fixCommand } from './cmd-fix';
|
|
11
13
|
import { generateCommand } from './cmd-generate';
|
|
@@ -19,6 +21,7 @@ import { plannedCommands } from './cmd-planned';
|
|
|
19
21
|
import { policyCommand } from './cmd-policy';
|
|
20
22
|
import { actionsCommand, entitiesCommand, queriesCommand } from './cmd-registries';
|
|
21
23
|
import { routesCommand } from './cmd-routes';
|
|
24
|
+
import { secretsCommand } from './cmd-secrets';
|
|
22
25
|
import { tasksCommand } from './cmd-tasks';
|
|
23
26
|
import { testCommand } from './cmd-test';
|
|
24
27
|
import { verifyCommand } from './cmd-verify';
|
|
@@ -26,8 +29,20 @@ import type { CliCommand } from './command';
|
|
|
26
29
|
import type { CommandSpec } from './parse';
|
|
27
30
|
import { loadVersion } from './version-loader';
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
let cliVersionCache: string | undefined;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Single source of truth for the CLI's own version — loaded from its package.json, lazily.
|
|
36
|
+
* `index.ts` re-exports this module, so importing `@ultimat3/cli` for `runRole` alone — what a
|
|
37
|
+
* compiled `apps/web/server.ts` does — must not read a manifest a `--compile` binary does not
|
|
38
|
+
* carry. An eager `const` here reintroduced exactly the failure `frameworkVersion()` was made
|
|
39
|
+
* lazy to fix, one file over: it compiled clean and threw at import on the first boot that
|
|
40
|
+
* actually ran the artifact.
|
|
41
|
+
*/
|
|
42
|
+
export function cliVersion(): string {
|
|
43
|
+
if (cliVersionCache === undefined) cliVersionCache = loadVersion();
|
|
44
|
+
return cliVersionCache;
|
|
45
|
+
}
|
|
31
46
|
|
|
32
47
|
const CORE: readonly CliCommand[] = [
|
|
33
48
|
newCommand,
|
|
@@ -40,6 +55,8 @@ const CORE: readonly CliCommand[] = [
|
|
|
40
55
|
mcpCommand,
|
|
41
56
|
doctorCommand,
|
|
42
57
|
deployCommand,
|
|
58
|
+
envCommand,
|
|
59
|
+
secretsCommand,
|
|
43
60
|
manifestCommand,
|
|
44
61
|
routesCommand,
|
|
45
62
|
actionsCommand,
|
|
@@ -50,6 +67,7 @@ const CORE: readonly CliCommand[] = [
|
|
|
50
67
|
policyCommand,
|
|
51
68
|
i18nCommand,
|
|
52
69
|
errorsCommand,
|
|
70
|
+
docsCommand,
|
|
53
71
|
fixCommand,
|
|
54
72
|
];
|
|
55
73
|
|
|
@@ -62,7 +80,7 @@ export const COMMANDS: readonly CliCommand[] = [
|
|
|
62
80
|
...CORE,
|
|
63
81
|
...plannedCommands(),
|
|
64
82
|
createHelpCommand(() => SPECS),
|
|
65
|
-
createVersionCommand(
|
|
83
|
+
createVersionCommand(cliVersion),
|
|
66
84
|
];
|
|
67
85
|
|
|
68
86
|
export const SPECS: readonly CommandSpec[] = COMMANDS.map((command) => command.spec);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The one place a host hands the framework a driver. `ServeOptions` had `{ root, env, role, port,
|
|
2
|
+
// metricsPort }` and nothing else, so the ONLY way to install a driver was to call an ambient
|
|
3
|
+
// setter from an app module — which `loadApp` imports *after* `startServices` has already captured
|
|
4
|
+
// its own. This type is the seam that removes the need to, and every field is read exactly once,
|
|
5
|
+
// as the first arm of `overrides?.x ?? <the env-selected default>`.
|
|
6
|
+
|
|
7
|
+
import type { PurgeDriver } from '@ultimat3/cache';
|
|
8
|
+
import type { Middleware, RateLimitStore } from '@ultimat3/http';
|
|
9
|
+
import type { JobDriver } from '@ultimat3/jobs';
|
|
10
|
+
import type { MailDriver } from '@ultimat3/mail';
|
|
11
|
+
import type { SyncAuthenticator, Transport } from '@ultimat3/realtime';
|
|
12
|
+
import type { IsrStore } from '@ultimat3/render';
|
|
13
|
+
import type { ImageTransformDriver } from '@ultimat3/seo';
|
|
14
|
+
import type { Storage } from '@ultimat3/storage';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What a deployment may substitute for a boot decision the environment would otherwise make.
|
|
18
|
+
*
|
|
19
|
+
* Every field is OPTIONAL and every field REPLACES the default rather than sitting beside it: the
|
|
20
|
+
* env switch each one used to be is now the `??` arm of one expression, so there is exactly one
|
|
21
|
+
* answer to "which driver is this process running" (axiom 1). A field nothing consumes is not
|
|
22
|
+
* here — the entity `Driver` in particular, because `@ultimat3/entity` exposes no installer for
|
|
23
|
+
* one (`database(entities, { driver })` is the app's own call), and a slot the boot cannot honour
|
|
24
|
+
* is the class of defect this seam exists to end.
|
|
25
|
+
*/
|
|
26
|
+
export interface RuntimeOverrides {
|
|
27
|
+
/**
|
|
28
|
+
* The queue every enqueue AND every claim uses. Installed as the ambient driver too, so
|
|
29
|
+
* `jobDriver()` and the worker cannot disagree — `startRoles` refuses to boot when they do.
|
|
30
|
+
*/
|
|
31
|
+
readonly jobs?: JobDriver;
|
|
32
|
+
/** Replaces the `S3_ENDPOINT`/embedded-disk decision whole: disks, default disk and all. */
|
|
33
|
+
readonly storage?: Storage;
|
|
34
|
+
/** Replaces the `SMTP_URL` / `RESEND_API_KEY` selection. */
|
|
35
|
+
readonly mail?: MailDriver;
|
|
36
|
+
/**
|
|
37
|
+
* Replaces the `NATS_URL` selection. Already connected when it arrives, and NOT closed by
|
|
38
|
+
* `stop()`: whoever built it owns its socket, exactly as `createServer` does not close a
|
|
39
|
+
* `rateLimitStore` it was handed.
|
|
40
|
+
*/
|
|
41
|
+
readonly transport?: Transport;
|
|
42
|
+
/** Replaces the `CLOUDFLARE_*` / `FASTLY_*` selection behind the `cdn` cache tier. */
|
|
43
|
+
readonly purge?: PurgeDriver;
|
|
44
|
+
/**
|
|
45
|
+
* Where the HTTP rate limiter keeps its counters. It also DECIDES `rateLimit.scope`: a store
|
|
46
|
+
* that says `'shared'` is a deployment declaring fleet-wide numbers, and `assertRateLimitScope`
|
|
47
|
+
* holds the two halves together rather than a literal in the boot contradicting the store.
|
|
48
|
+
*/
|
|
49
|
+
readonly rateLimitStore?: RateLimitStore;
|
|
50
|
+
/**
|
|
51
|
+
* Where regenerated ISR pages live. Omitted, `createIsrController` keeps a per-process memory
|
|
52
|
+
* store — twelve replicas then hold twelve of them, and a purge tag reaches one twelfth of the
|
|
53
|
+
* fleet.
|
|
54
|
+
*/
|
|
55
|
+
readonly isrStore?: IsrStore;
|
|
56
|
+
/** Prepended to the pipeline by `createServer`, which `startRoles` never passed one. */
|
|
57
|
+
readonly middleware?: readonly Middleware[];
|
|
58
|
+
/** The `/media/*` transform. Omitted, `builtinImageDriver` — core's PNG/JPEG pipeline. */
|
|
59
|
+
readonly images?: ImageTransformDriver;
|
|
60
|
+
/**
|
|
61
|
+
* Who is dialling the `sync` node. Omitted, the app's own `configureAuthenticator()` is adapted
|
|
62
|
+
* — this field exists because that adapter can only ever answer `{ actor }`, and a real
|
|
63
|
+
* deployment's token has an `expiresAt` and a `refresh`, which is the whole of re-authorization.
|
|
64
|
+
*/
|
|
65
|
+
readonly syncAuthenticate?: SyncAuthenticator;
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Single responsibility: a connection URL rendered so it can be printed, logged and scraped —
|
|
2
|
+
// scheme, host, path, and never the userinfo, query or fragment that carry the password.
|
|
3
|
+
// DATABASE_URL, NATS_URL and S3_ENDPOINT all reach `x dev --json` as raw strings, so the redaction
|
|
4
|
+
// has one implementation rather than one per caller that remembers.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `fallback` is the caller's, not this file's: a string that does not parse as a URL is a fact
|
|
8
|
+
* about that binding, and only the caller knows which binding it was. Never the raw string — an
|
|
9
|
+
* unparseable value is exactly where a hand-written credential ends up.
|
|
10
|
+
*/
|
|
11
|
+
export function safeUrlLabel(url: string, fallback: string): string {
|
|
12
|
+
let label: string;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
label = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
16
|
+
} catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
// A scheme with no `//` is parsed as ONE opaque path — `app:user:pw@host/db` keeps its whole
|
|
20
|
+
// credential in `pathname` and `username` is empty, so dropping the userinfo fields is not
|
|
21
|
+
// enough. Any surviving `@` is treated as userinfo we failed to split, and the fallback wins: a
|
|
22
|
+
// host printed with a `@` in it is a redaction that did not happen.
|
|
23
|
+
return label.includes('@') ? fallback : label;
|
|
24
|
+
}
|
package/src/scaffold-fixture.ts
CHANGED
|
@@ -28,9 +28,19 @@ export const FIXTURE_GENERATORS: readonly GenerateOptions[] = [
|
|
|
28
28
|
{ kind: 'query', name: 'invoice-search', feature: 'invoice' },
|
|
29
29
|
{ kind: 'query', name: 'invoice-feed', feature: 'invoice', live: true },
|
|
30
30
|
{ kind: 'job', name: 'sweep-invoices', feature: 'invoice' },
|
|
31
|
+
{ kind: 'backfill', name: 'reindex-invoices', feature: 'invoice' },
|
|
31
32
|
{ kind: 'task', name: 'nightly-sweep', feature: 'invoice' },
|
|
32
33
|
{ kind: 'route', name: 'pricing', surface: 'site' },
|
|
33
34
|
{ kind: 'route', name: 'billing', surface: 'app' },
|
|
35
|
+
// `--at`, pointed at the `site/` route above: an island's whole reason to exist is a 0kb page
|
|
36
|
+
// that needs one interactive control, so the fixture places it where that is true.
|
|
37
|
+
{ kind: 'island', name: 'currency-picker', at: 'apps/web/site/pricing' },
|
|
38
|
+
// The one screen the admin derives from nothing — and the one generator that must NOT emit a
|
|
39
|
+
// `defineRoute`, so compiling it is how that stays true.
|
|
40
|
+
{ kind: 'admin:page', name: 'reconcile', permission: 'ledger:reconcile' },
|
|
41
|
+
// The app's own convention. It is the only generated file that imports `@ultimat3/cli` for its
|
|
42
|
+
// types, so compiling it is what proves a scaffolded app can actually write one.
|
|
43
|
+
{ kind: 'guard', name: 'migration-safety' },
|
|
34
44
|
];
|
|
35
45
|
|
|
36
46
|
/** The whole scaffolded surface: a new app, then every generator run inside it. */
|
|
@@ -66,50 +66,28 @@ export interface KnownGap {
|
|
|
66
66
|
readonly owner: string;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// `InvariantColumns` is an index-signature type, so `c.title` is `ColumnExpr | undefined` under
|
|
70
|
-
// `noUncheckedIndexedAccess`. Every hand-written entity in `examples/dummy` reproduces it
|
|
71
|
-
// identically: the fix is a column proxy typed from the entity's own columns, in @ultimat3/entity
|
|
72
|
-
// — a different template cannot avoid it without dropping to `satisfies()`, which would silently
|
|
73
|
-
// stop emitting the Postgres CHECK.
|
|
74
|
-
//
|
|
75
|
-
// Measured, not guessed: no open-keyed form avoids the `| undefined` (index signature, `Record`,
|
|
76
|
-
// and a mapped type over `string` or a template-literal pattern all produce it), and typing the
|
|
77
|
-
// proxy from `columns` only reaches `c` when the element of `invariants:` is itself
|
|
78
|
-
// context-sensitive. `invariant(name, build)` is a *call*, which TypeScript checks before
|
|
79
|
-
// `entity()`'s own `C` is fixed, so `K` falls back to `string` and nothing changes. Making it
|
|
80
|
-
// reach means changing the shape of `invariants:` — a documented primitive, so a major.
|
|
81
|
-
const INVARIANT_PROXY =
|
|
82
|
-
'@ultimat3/entity — type the invariant column proxy from the declared columns (needs a major: ' +
|
|
83
|
-
'the `invariants:` element shape has to become context-sensitive)';
|
|
84
|
-
|
|
85
|
-
/** Every entity the fixture generates. Each one declares the same two invariants. */
|
|
86
|
-
const FIXTURE_ENTITIES = [
|
|
87
|
-
'apps/web/app/credit-note/entity.ts',
|
|
88
|
-
'apps/web/app/invoice/entity.ts',
|
|
89
|
-
'apps/web/app/post/entity.ts',
|
|
90
|
-
] as const;
|
|
91
|
-
|
|
92
69
|
/**
|
|
93
70
|
* Diagnostics a template cannot fix, pinned one occurrence at a time. Pinned, never ignored:
|
|
94
71
|
* `unexpectedIn` fails on anything not listed here — including a second copy of a listed
|
|
95
72
|
* diagnostic, because each entry is consumed by exactly one match — and `staleGapsIn` fails when
|
|
96
73
|
* a listed entry stops reproducing, so an entry cannot outlive the bug it describes.
|
|
74
|
+
*
|
|
75
|
+
* Empty, and that is the point: `invariants:` is one callback over the whole list, so `c` is typed
|
|
76
|
+
* from the entity's own columns and the six `TS18048 'c.title' is possibly 'undefined'` pins this
|
|
77
|
+
* list used to carry are gone. A new entry here is a template shipping a diagnostic, which needs
|
|
78
|
+
* an owner who can remove it — not a permanent exemption.
|
|
97
79
|
*/
|
|
98
|
-
export const KNOWN_GAPS: readonly KnownGap[] =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
/** The pins one invocation may spend. Another variant's pins are not its to spend. */
|
|
111
|
-
export const gapsFor = (variant: string): readonly KnownGap[] =>
|
|
112
|
-
KNOWN_GAPS.filter((gap) => gap.variant === variant);
|
|
80
|
+
export const KNOWN_GAPS: readonly KnownGap[] = [];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The pins one invocation may spend. Another variant's pins are not its to spend. `gaps` is a
|
|
84
|
+
* parameter for the same reason it is on `unexpectedIn` and `staleGapsIn`: with `KNOWN_GAPS`
|
|
85
|
+
* legitimately empty, the bookkeeping is only testable against a list a test supplies.
|
|
86
|
+
*/
|
|
87
|
+
export const gapsFor = (
|
|
88
|
+
variant: string,
|
|
89
|
+
gaps: readonly KnownGap[] = KNOWN_GAPS,
|
|
90
|
+
): readonly KnownGap[] => gaps.filter((gap) => gap.variant === variant);
|
|
113
91
|
|
|
114
92
|
const matches = (diagnostic: TypeDiagnostic, gap: KnownGap): boolean =>
|
|
115
93
|
diagnostic.code === gap.code &&
|