@ultimat3/cli 8.0.0 → 10.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 +9 -3
- package/package.json +26 -25
- package/src/affected.ts +0 -3
- package/src/app-boundaries.ts +4 -5
- package/src/app-env.ts +7 -2
- package/src/app-load.ts +7 -0
- package/src/browser-launcher.ts +0 -2
- package/src/budgets.ts +4 -3
- package/src/cmd-build.ts +2 -2
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-deploy.ts +20 -6
- package/src/cmd-docs.ts +2 -1
- package/src/cmd-doctor.ts +3 -5
- package/src/cmd-env.ts +2 -2
- package/src/cmd-fix.ts +2 -4
- package/src/cmd-new.ts +2 -2
- package/src/cmd-shot.ts +22 -2
- package/src/db-finding.ts +2 -2
- package/src/db-seed.ts +0 -3
- package/src/dev-assets.ts +4 -7
- package/src/dev-cache.ts +140 -36
- package/src/dev-lock.ts +8 -7
- package/src/dev-purge.ts +120 -0
- package/src/dev-queue.ts +31 -6
- package/src/dev-render.ts +11 -14
- package/src/dev-runtime.ts +62 -15
- package/src/dev-storage.ts +8 -2
- package/src/dev-sync.ts +37 -2
- package/src/document-styles.ts +4 -2
- package/src/drift.ts +3 -2
- package/src/error-codes.ts +9 -2
- package/src/error-contract.ts +5 -5
- package/src/errors.ts +1 -29
- package/src/flag-reads.ts +2 -2
- package/src/generate-write.ts +3 -2
- package/src/guards.ts +4 -4
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +34 -10
- package/src/island-routes.ts +7 -1
- package/src/island-styles.ts +1 -1
- package/src/mcp-errors.ts +2 -2
- package/src/metrics-endpoint.ts +0 -2
- package/src/output.ts +2 -2
- package/src/prerender.ts +34 -20
- package/src/runtime-overrides.ts +1 -1
- package/src/serve.ts +1 -1
- package/src/solid-loader.ts +1 -1
- package/src/static-report.ts +41 -3
- package/src/style-csp.ts +2 -1
- package/src/templates/scaffold-container.ts +12 -0
- package/src/templates/scaffold-docs.ts +11 -4
- package/src/templates/scaffold-domain-package.ts +3 -1
- package/src/templates/scaffold-repo.ts +19 -9
- package/src/templates/slice-foundation.ts +3 -5
- package/src/test-shards.ts +2 -2
- package/src/tsconfig-references.ts +2 -2
- package/src/verify-checks.ts +3 -2
- package/src/verify-floor.ts +8 -6
- package/src/verify-run.ts +2 -2
- package/src/verify-step.ts +2 -1
- package/src/verify-test-run.ts +2 -2
- package/src/workspace-checks.ts +10 -12
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +7 -1
package/src/island-bundle.ts
CHANGED
|
@@ -6,12 +6,9 @@
|
|
|
6
6
|
// Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
|
|
7
7
|
// POSIX by construction), `join`/`basename` the filesystem side.
|
|
8
8
|
import { basename, join, posix, relative, sep } from 'node:path';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
IslandInvalidError,
|
|
13
|
-
islandModuleId,
|
|
14
|
-
} from '@ultimat3/render';
|
|
9
|
+
import { renderThrowable } from '@ultimat3/core';
|
|
10
|
+
import { ISLAND_EXTENSION, IslandInvalidError, islandModuleId } from '@ultimat3/render';
|
|
11
|
+
import { contentHash } from '@ultimat3/render/server';
|
|
15
12
|
import { IslandBuildFailedError } from './errors';
|
|
16
13
|
import { solidProductionPlugin } from './island-solid-production';
|
|
17
14
|
import { islandStylesPlugin } from './island-styles';
|
|
@@ -122,11 +119,38 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
|
|
|
122
119
|
* import or syntax error, and flattening them is what puts the line number in the cause instead of
|
|
123
120
|
* the word "Bundle failed".
|
|
124
121
|
*/
|
|
125
|
-
function describeBuildError(error: unknown): string {
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
export function describeBuildError(error: unknown): string {
|
|
123
|
+
// `renderThrowable`, never `instanceof` + `.message` + `String()`. All three run on a value this
|
|
124
|
+
// process did not build — a `Proxy` traps `getPrototypeOf`, a `message` getter can raise, and
|
|
125
|
+
// `String()` throws outright on a Symbol — and what comes back is carried in
|
|
126
|
+
// `IslandBuildFailedError.logs`, which `errors.ts` interpolates straight into a `cause:`. That
|
|
127
|
+
// is a cross-file hop neither `scripts/catch-render.ts` nor `scripts/error-render.ts` can
|
|
128
|
+
// follow: a throw here loses the whole refusal and replaces it with a TypeError about reporting.
|
|
129
|
+
//
|
|
130
|
+
// The AggregateError branch stays, and it is the reason this function exists: `Bun.build` packs
|
|
131
|
+
// one entry per unresolved import or syntax error into `errors`, and flattening them is what
|
|
132
|
+
// puts a line number in the cause instead of the words "Bundle failed". `stringField` decides
|
|
133
|
+
// whether the value really is that shape, because `instanceof` is a question a Proxy answers.
|
|
134
|
+
const aggregate = aggregatedErrors(error);
|
|
135
|
+
if (aggregate !== undefined && aggregate.length > 0) {
|
|
136
|
+
return aggregate.map((one: unknown) => renderThrowable(one)).join('; ');
|
|
137
|
+
}
|
|
138
|
+
return renderThrowable(error);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `value.errors`, read the way `@ultimat3/core`'s `stringField` reads a string field: narrowed
|
|
143
|
+
* first, dereferenced inside a `try`, `undefined` for anything else. `instanceof AggregateError`
|
|
144
|
+
* is a question a `Proxy` answers with its own `getPrototypeOf` trap, so it is not a check.
|
|
145
|
+
*/
|
|
146
|
+
function aggregatedErrors(value: unknown): readonly unknown[] | undefined {
|
|
147
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
148
|
+
try {
|
|
149
|
+
const held: unknown = (value as Record<string, unknown>)['errors'];
|
|
150
|
+
return Array.isArray(held) ? held : undefined;
|
|
151
|
+
} catch {
|
|
152
|
+
return undefined;
|
|
128
153
|
}
|
|
129
|
-
return error instanceof Error ? error.message : String(error);
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
export interface BuildIslandsOptions {
|
package/src/island-routes.ts
CHANGED
|
@@ -34,7 +34,13 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
|
|
|
34
34
|
error: {
|
|
35
35
|
code: 'X_ROUTE_NOT_FOUND',
|
|
36
36
|
cause: `no island chunk is built at ${request.pathname} — the document that asked for it was rendered against an older build`,
|
|
37
|
-
|
|
37
|
+
// No `x` citation, deliberately — `dev-lock.ts`'s shape. This route is mounted
|
|
38
|
+
// in exactly two places (`cmd-dev.ts`, `serve.ts`) and NEITHER reads `.x/static`:
|
|
39
|
+
// in `x dev` the chunks are rebuilt on the watcher tick, and in the container they
|
|
40
|
+
// were built at boot. `x build --target static` — which is what this said — writes
|
|
41
|
+
// an export directory neither process serves from, so running it changed nothing
|
|
42
|
+
// for the only two readers this line has ever had.
|
|
43
|
+
fix: 'reload the page — this process serves only the chunks it built, and the document holding this URL came from an earlier build',
|
|
38
44
|
},
|
|
39
45
|
},
|
|
40
46
|
{ status: 404 },
|
package/src/island-styles.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// element renders unclassed, and `Bun.build` reports `success: true` with no log.
|
|
5
5
|
|
|
6
6
|
import { renderThrowable, UltimateError } from '@ultimat3/core';
|
|
7
|
-
import { loadStylesheet } from '@ultimat3/render';
|
|
7
|
+
import { loadStylesheet } from '@ultimat3/render/server';
|
|
8
8
|
import type { BunPlugin } from 'bun';
|
|
9
9
|
import { IslandBuildFailedError } from './errors';
|
|
10
10
|
|
package/src/mcp-errors.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describeErrorCode, hasErrorCode, listErrorCodes } from '@ultimat3/core';
|
|
6
6
|
import type { ErrorExplanation } from '@ultimat3/mcp';
|
|
7
7
|
import type { CliErrorCode } from './error-codes';
|
|
8
|
-
import { CLI_ERROR_CODES
|
|
8
|
+
import { CLI_ERROR_CODES } from './error-codes';
|
|
9
9
|
import { codeFixes, codeFixScan } from './error-fixes';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -214,7 +214,7 @@ export function explainErrorCode(code: string): ErrorExplanation | undefined {
|
|
|
214
214
|
code,
|
|
215
215
|
cause: described.title,
|
|
216
216
|
fix: cli ? CLI_FIXES[code] : projectedFix(code),
|
|
217
|
-
docs:
|
|
217
|
+
docs: described.docs,
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
|
package/src/metrics-endpoint.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
stringField,
|
|
12
12
|
UltimateError,
|
|
13
13
|
} from '@ultimat3/core';
|
|
14
|
-
import { docsFor } from './error-codes';
|
|
15
14
|
import { neighbouringPort } from './flag-number';
|
|
16
15
|
|
|
17
16
|
/**
|
|
@@ -44,7 +43,6 @@ export class MetricsPortInUseError extends UltimateError {
|
|
|
44
43
|
code: 'X_PORT_IN_USE',
|
|
45
44
|
cause: `the metrics port ${input.port} is already bound, so no role could open its scrape listener`,
|
|
46
45
|
fix: `METRICS_PORT=${neighbouringPort(input.port)} x dev --json`,
|
|
47
|
-
docs: docsFor('X_PORT_IN_USE'),
|
|
48
46
|
});
|
|
49
47
|
}
|
|
50
48
|
}
|
package/src/output.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and the JSON renderer are projections of it, so `--json` can never drift from the terminal
|
|
3
3
|
// output (axiom 4). The human renderer owns the canonical 3-line error format.
|
|
4
4
|
|
|
5
|
-
import { renderThrowable, singleLine, stringField } from '@ultimat3/core';
|
|
5
|
+
import { ERROR_DOCS_URL, renderThrowable, singleLine, stringField } from '@ultimat3/core';
|
|
6
6
|
import { msg } from './messages';
|
|
7
7
|
|
|
8
8
|
export interface Finding {
|
|
@@ -107,7 +107,7 @@ export function findingFrom(value: unknown): Finding {
|
|
|
107
107
|
code: 'X_CLI_UNEXPECTED',
|
|
108
108
|
cause: renderThrowable(value),
|
|
109
109
|
fix: 'x doctor --json',
|
|
110
|
-
docs:
|
|
110
|
+
docs: ERROR_DOCS_URL,
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
package/src/prerender.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
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
|
+
import { createContext, renderThrowable, runWithContext } from '@ultimat3/core';
|
|
8
8
|
import type { RouteEntry } from '@ultimat3/render';
|
|
9
|
-
import {
|
|
9
|
+
import { routeEntries } from '@ultimat3/render';
|
|
10
|
+
import { renderStatic } from '@ultimat3/render/server';
|
|
10
11
|
import { loadApp } from './app-load';
|
|
11
12
|
import { appManifest } from './app-manifest';
|
|
12
13
|
import type { RouteStats } from './budgets';
|
|
@@ -14,9 +15,13 @@ import { measureDocumentJs, writeBuildStats } from './budgets';
|
|
|
14
15
|
import { routeDocument } from './dev-render';
|
|
15
16
|
import type { IslandBundle } from './island-bundle';
|
|
16
17
|
import { buildIslands, writeIslands } from './island-bundle';
|
|
17
|
-
import type { SkippedRoute } from './static-report';
|
|
18
|
+
import type { SkippedRoute, UnmeasuredRoute } from './static-report';
|
|
18
19
|
import { skippedRoute, skipReasonFor, writeStaticReport } from './static-report';
|
|
19
20
|
|
|
21
|
+
// Re-exported, never re-declared: `static-report.ts` owns the shape because the report on disk
|
|
22
|
+
// carries it, and this file already imports that module.
|
|
23
|
+
export type { UnmeasuredRoute };
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* `static` only. `isr` revalidates and `ssr`/`stream` need a process, so writing any of them to
|
|
22
27
|
* disk would publish a page whose staleness nothing can correct — and the route already declared
|
|
@@ -45,12 +50,6 @@ export interface PrerenderedPage {
|
|
|
45
50
|
readonly bytes: number;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
/** A route that declared a budget and could not be rendered here, and what stopped it. */
|
|
49
|
-
export interface UnmeasuredRoute {
|
|
50
|
-
readonly path: string;
|
|
51
|
-
readonly reason: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
53
|
export interface PrerenderReport {
|
|
55
54
|
readonly out: string;
|
|
56
55
|
readonly buildId: string;
|
|
@@ -129,6 +128,22 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
129
128
|
const islands = await buildIslands(options.root);
|
|
130
129
|
await writeIslands(islands, options.out);
|
|
131
130
|
|
|
131
|
+
// Every render below goes through `routeDocument`, which is the function a REQUEST reaches — and
|
|
132
|
+
// a request arrives inside `runWithContext`, installed by the HTTP pipeline (`dev-render.ts`).
|
|
133
|
+
// Called bare, any route whose component, `load` or `meta` reads `useContext()` threw
|
|
134
|
+
// `X_NO_CONTEXT`: measured against `examples/dummy`, `/posts/new` and `/settings` were filed
|
|
135
|
+
// unmeasured for that reason alone, and a `render: 'static'` route reading it failed the whole
|
|
136
|
+
// build. One context for the build, `role: 'web'` because that is the role serving these
|
|
137
|
+
// documents, and this build's own id so a component reading `ctx.buildId` stamps the artifact
|
|
138
|
+
// with the id the report and the stats carry.
|
|
139
|
+
const ctx = createContext({ role: 'web', buildId });
|
|
140
|
+
const document = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
|
|
141
|
+
runWithContext(ctx, () =>
|
|
142
|
+
routeDocument(entry, data, {
|
|
143
|
+
resolveIsland: (file: string) => islands.resolverFor(file),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
132
147
|
for (const entry of routeEntries()) {
|
|
133
148
|
const facts = { surface: entry.surface, render: entry.config.render, route: entry.path };
|
|
134
149
|
const reason = skipReasonFor(facts);
|
|
@@ -140,11 +155,10 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
140
155
|
// routes it never used to touch would be a worse regression than the gap it closes. A route
|
|
141
156
|
// that will not render here is reported, gets no stats entry, and stays `X_BUDGET_UNMEASURED`.
|
|
142
157
|
try {
|
|
143
|
-
const html = await
|
|
144
|
-
entry,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
158
|
+
const html = await document(entry, {
|
|
159
|
+
url: new URL(entry.path, origin).href,
|
|
160
|
+
params: {},
|
|
161
|
+
});
|
|
148
162
|
const measured = await measureDocumentJs(html, options.out);
|
|
149
163
|
const chain = heaviestSource(islands, measured.entries);
|
|
150
164
|
routes.push({
|
|
@@ -161,12 +175,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
161
175
|
}
|
|
162
176
|
const artifacts = await renderStatic(
|
|
163
177
|
entry,
|
|
164
|
-
({ path, params }) =>
|
|
165
|
-
routeDocument(
|
|
166
|
-
entry,
|
|
167
|
-
{ url: new URL(path, origin).href, params },
|
|
168
|
-
{ resolveIsland: (file: string) => islands.resolverFor(file) },
|
|
169
|
-
),
|
|
178
|
+
({ path, params }) => document(entry, { url: new URL(path, origin).href, params }),
|
|
170
179
|
{ buildId },
|
|
171
180
|
);
|
|
172
181
|
// `enumeratePrerender` answers `[]` for a dynamic route with no `prerender()`, so a
|
|
@@ -208,6 +217,11 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
|
|
|
208
217
|
buildId,
|
|
209
218
|
emitted: pages.map((page) => ({ route: page.route, path: page.path, file: page.file })),
|
|
210
219
|
skipped,
|
|
220
|
+
// The list `X_BUDGET_UNMEASURED`'s `fix:` cites by name. It rode home on the in-process
|
|
221
|
+
// `PrerenderReport` and nowhere else, and `cmd-build.ts` discards a successful subprocess's
|
|
222
|
+
// stdout — so the one command the finding tells an author to run printed no `unmeasured` key
|
|
223
|
+
// and no reason. Written into the report is what makes the instruction true.
|
|
224
|
+
unmeasured,
|
|
211
225
|
});
|
|
212
226
|
return {
|
|
213
227
|
out: options.out,
|
package/src/runtime-overrides.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Middleware, RateLimitStore } from '@ultimat3/http';
|
|
|
9
9
|
import type { JobDriver } from '@ultimat3/jobs';
|
|
10
10
|
import type { MailDriver } from '@ultimat3/mail';
|
|
11
11
|
import type { SyncAuthenticator, Transport } from '@ultimat3/realtime/server';
|
|
12
|
-
import type { IsrStore } from '@ultimat3/render';
|
|
12
|
+
import type { IsrStore } from '@ultimat3/render/server';
|
|
13
13
|
import type { ImageTransformDriver } from '@ultimat3/seo';
|
|
14
14
|
import type { Storage } from '@ultimat3/storage';
|
|
15
15
|
|
package/src/serve.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
migrate,
|
|
21
21
|
} from '@ultimat3/db';
|
|
22
22
|
import type { Route } from '@ultimat3/http';
|
|
23
|
-
import { createIsrController } from '@ultimat3/render';
|
|
23
|
+
import { createIsrController } from '@ultimat3/render/server';
|
|
24
24
|
import { apiRoutes } from './api-routes';
|
|
25
25
|
import { loadSignInPath } from './app-auth';
|
|
26
26
|
import { loadApp } from './app-load';
|
package/src/solid-loader.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `tsc` compiles this file inside ITS program, where a `.d.ts` sitting in this directory is
|
|
8
8
|
// not included and its `declare module` never applies. `tsc -b` proves it in this repo.
|
|
9
9
|
import { transformAsync } from '@babel/core';
|
|
10
|
-
import { contentHash } from '@ultimat3/render';
|
|
10
|
+
import { contentHash } from '@ultimat3/render/server';
|
|
11
11
|
import solidPreset from 'babel-preset-solid';
|
|
12
12
|
import type { BunPlugin } from 'bun';
|
|
13
13
|
import { IslandBuildFailedError } from './errors';
|
package/src/static-report.ts
CHANGED
|
@@ -48,6 +48,19 @@ export type SkippedRoute = RouteFacts & {
|
|
|
48
48
|
readonly why: string;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A route that declared a budget and could not be rendered here, and what stopped it.
|
|
53
|
+
*
|
|
54
|
+
* Declared HERE and not in `prerender.ts`, though `prerenderSite` is what fills it: this file is
|
|
55
|
+
* the report's shape and `prerender.ts` imports it, so the other direction is a cycle. `prerender`
|
|
56
|
+
* re-exports the name for its own callers.
|
|
57
|
+
*/
|
|
58
|
+
export type UnmeasuredRoute = {
|
|
59
|
+
/** The DECLARED path, as `X_BUDGET_UNMEASURED`'s `at:` spells it, so the two rows join. */
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly reason: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
51
64
|
/** One HTML file in the artifact, and the declared route that produced it. */
|
|
52
65
|
export type EmittedPage = {
|
|
53
66
|
readonly route: string;
|
|
@@ -62,6 +75,15 @@ export type StaticReport = {
|
|
|
62
75
|
readonly buildId: string;
|
|
63
76
|
readonly emitted: readonly EmittedPage[];
|
|
64
77
|
readonly skipped: readonly SkippedRoute[];
|
|
78
|
+
/**
|
|
79
|
+
* Every budgeted route this build could not weigh, with the reason. `X_BUDGET_UNMEASURED`'s own
|
|
80
|
+
* `fix:` cites this list by name — `x build --target static --json # its "unmeasured" list says
|
|
81
|
+
* why` — and until it was written here the list existed only on the in-process `PrerenderReport`
|
|
82
|
+
* and reached no `x` command's output at all: `cmd-build.ts` discards a successful subprocess's
|
|
83
|
+
* stdout. A `fix:` naming a key nothing prints is axiom 4 inverted at the step meant to unblock
|
|
84
|
+
* the reader.
|
|
85
|
+
*/
|
|
86
|
+
readonly unmeasured: readonly UnmeasuredRoute[];
|
|
65
87
|
};
|
|
66
88
|
|
|
67
89
|
/**
|
|
@@ -139,6 +161,9 @@ const isSkipped = (value: unknown): value is SkippedRoute =>
|
|
|
139
161
|
typeof value['why'] === 'string' &&
|
|
140
162
|
inDomain(SKIP_REASONS, value['reason']);
|
|
141
163
|
|
|
164
|
+
const isUnmeasured = (value: unknown): value is UnmeasuredRoute =>
|
|
165
|
+
isRecord(value) && typeof value['path'] === 'string' && typeof value['reason'] === 'string';
|
|
166
|
+
|
|
142
167
|
const isEmitted = (value: unknown): value is EmittedPage =>
|
|
143
168
|
isRecord(value) &&
|
|
144
169
|
typeof value['route'] === 'string' &&
|
|
@@ -152,13 +177,20 @@ const isEmitted = (value: unknown): value is EmittedPage =>
|
|
|
152
177
|
*/
|
|
153
178
|
export function parseStaticReport(value: unknown): StaticReport | undefined {
|
|
154
179
|
if (!isRecord(value)) return undefined;
|
|
155
|
-
const { target, out, buildId, emitted, skipped } = value;
|
|
180
|
+
const { target, out, buildId, emitted, skipped, unmeasured } = value;
|
|
156
181
|
if (target !== 'static' || typeof out !== 'string' || typeof buildId !== 'string') {
|
|
157
182
|
return undefined;
|
|
158
183
|
}
|
|
159
184
|
if (!Array.isArray(emitted) || !emitted.every(isEmitted)) return undefined;
|
|
160
185
|
if (!Array.isArray(skipped) || !skipped.every(isSkipped)) return undefined;
|
|
161
|
-
|
|
186
|
+
// OPTIONAL on the way in, total on the way out. `.x/` survives a checkout of an older commit,
|
|
187
|
+
// and a report written before this field existed has to read as "nothing unmeasured" — refusing
|
|
188
|
+
// it would answer `X_BUDGET_UNMEASURED` for every route on a build that had weighed them all.
|
|
189
|
+
// Present and malformed is still no report, exactly as a malformed skip row is.
|
|
190
|
+
if (unmeasured !== undefined && (!Array.isArray(unmeasured) || !unmeasured.every(isUnmeasured))) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return { target, out, buildId, emitted, skipped, unmeasured: unmeasured ?? [] };
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
export async function writeStaticReport(root: string, report: StaticReport): Promise<string> {
|
|
@@ -198,7 +230,9 @@ export async function removeStaticReport(root: string): Promise<void> {
|
|
|
198
230
|
export function staticReportData(report: StaticReport | undefined): Record<string, JsonValue> {
|
|
199
231
|
// Never `{ ...report }`: `out` is an absolute build path and `buildId` is this run's, neither of
|
|
200
232
|
// which the inventory is about — `data` already carries `artifact` and the build's own id.
|
|
201
|
-
return report === undefined
|
|
233
|
+
return report === undefined
|
|
234
|
+
? {}
|
|
235
|
+
: { emitted: report.emitted, skipped: report.skipped, unmeasured: report.unmeasured };
|
|
202
236
|
}
|
|
203
237
|
|
|
204
238
|
/**
|
|
@@ -209,6 +243,10 @@ export function renderStaticReport(report: StaticReport): readonly string[] {
|
|
|
209
243
|
const rows: readonly (readonly string[])[] = [
|
|
210
244
|
...report.emitted.map((page) => ['emitted', page.route, page.file]),
|
|
211
245
|
...report.skipped.map((route) => ['skipped', route.route, route.why]),
|
|
246
|
+
// The same three columns, so the terminal and `--json` carry the same three lists. A route
|
|
247
|
+
// whose budget could not be weighed is invisible in `emitted` and, when it also rendered, in
|
|
248
|
+
// `skipped` too — and it is the row `X_BUDGET_UNMEASURED` sends its reader here to read.
|
|
249
|
+
...report.unmeasured.map((route) => ['unmeasured', route.path, route.reason]),
|
|
212
250
|
];
|
|
213
251
|
const widths = [0, 1].map((index) =>
|
|
214
252
|
Math.max(...rows.map((row) => (row[index] ?? '').length), 0),
|
package/src/style-csp.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// stylesheet the document no longer carries — and the CSP would block the framework's own CSS.
|
|
5
5
|
|
|
6
6
|
import { cspHashSource } from '@ultimat3/http';
|
|
7
|
-
import { SURFACES
|
|
7
|
+
import { SURFACES } from '@ultimat3/render';
|
|
8
|
+
import { stylesFor } from '@ultimat3/render/server';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Call AFTER `loadApp`. One hash per distinct body: `stylesFor` is what `dev-render.ts` puts in
|
|
@@ -197,6 +197,18 @@ services:
|
|
|
197
197
|
deploy: { replicas: 1 } # scales on concurrent websockets, no sticky sessions — pinned by the port
|
|
198
198
|
# The sync role binds PORT + 1. PORT is unset here, so it is 3000 and this listens on 3001.
|
|
199
199
|
ports: ['3001:3001']
|
|
200
|
+
# ...which is why the image's own HEALTHCHECK cannot be inherited here. It fetches $PORT —
|
|
201
|
+
# 3000 — and this role never binds it, so the container reports \`unhealthy\` from
|
|
202
|
+
# \`start_period\` onward and never recovers, and anything gated on \`sync: service_healthy\`
|
|
203
|
+
# would never start. Literal 3001 rather than an expression, for the same reason \`ports:\`
|
|
204
|
+
# above is literal: PORT is unset in this file, and two ways of saying one number drift.
|
|
205
|
+
# \`docker/helm\` states the same rule as \`PORT = .port - 1\`.
|
|
206
|
+
healthcheck:
|
|
207
|
+
test: ['CMD', 'bun', '--eval', "fetch('http://127.0.0.1:3001/readyz').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
208
|
+
interval: 10s
|
|
209
|
+
timeout: 3s
|
|
210
|
+
start_period: 30s
|
|
211
|
+
retries: 3
|
|
200
212
|
|
|
201
213
|
worker:
|
|
202
214
|
<<: *image
|
|
@@ -58,7 +58,7 @@ file in it is deletable.
|
|
|
58
58
|
|
|
59
59
|
const readme = (app: NameSet): string => `# ${app.pascal}
|
|
60
60
|
|
|
61
|
-
Built with [Ultimate](https://ultimate
|
|
61
|
+
Built with [Ultimate](https://github.com/developerz-ai/ultimate). Bun-only, Postgres, SolidJS.
|
|
62
62
|
|
|
63
63
|
## 🚀 Start
|
|
64
64
|
|
|
@@ -137,27 +137,34 @@ exec bunx x verify "$@"
|
|
|
137
137
|
const composeDev = (
|
|
138
138
|
app: NameSet,
|
|
139
139
|
): string => `# Optional: x dev needs none of this. Use it when you want the real Postgres/NATS/MinIO locally.
|
|
140
|
+
#
|
|
141
|
+
# Every published port binds 127.0.0.1, not 0.0.0.0. This stack ships its credentials in the file,
|
|
142
|
+
# as a dev stack reasonably does — so the short form \`'5432:5432'\` would put an authenticated
|
|
143
|
+
# database and an open object store on every interface this machine has, including the café wifi.
|
|
144
|
+
# Docker publishes a port by writing DNAT rules, so a host firewall does not stop it. To reach this
|
|
145
|
+
# stack from another machine, put a tunnel in front of it (\`ssh -L\`) rather than widening the bind;
|
|
146
|
+
# production topology is docker-compose.prod.yml, and it is a different file for a reason.
|
|
140
147
|
services:
|
|
141
148
|
db:
|
|
142
149
|
image: postgres:17-alpine
|
|
143
150
|
environment:
|
|
144
151
|
POSTGRES_PASSWORD: ${app.kebab}
|
|
145
152
|
POSTGRES_DB: ${app.kebab}
|
|
146
|
-
ports: ['5432:5432']
|
|
153
|
+
ports: ['127.0.0.1:5432:5432']
|
|
147
154
|
healthcheck:
|
|
148
155
|
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
149
156
|
interval: 5s
|
|
150
157
|
nats:
|
|
151
158
|
image: nats:2-alpine
|
|
152
159
|
command: ['-js']
|
|
153
|
-
ports: ['4222:4222']
|
|
160
|
+
ports: ['127.0.0.1:4222:4222']
|
|
154
161
|
s3:
|
|
155
162
|
image: minio/minio
|
|
156
163
|
command: ['server', '/data']
|
|
157
164
|
environment:
|
|
158
165
|
MINIO_ROOT_USER: ${app.kebab}
|
|
159
166
|
MINIO_ROOT_PASSWORD: ${app.kebab}-dev
|
|
160
|
-
ports: ['9000:9000']
|
|
167
|
+
ports: ['127.0.0.1:9000:9000']
|
|
161
168
|
`;
|
|
162
169
|
|
|
163
170
|
/** Docs, shims and container files for a new app, in the order a reader meets them. */
|
|
@@ -31,6 +31,9 @@ export type Role = (typeof ROLES)[number];
|
|
|
31
31
|
* Never a bare Error: an agent reading the failure needs the code, the cause and the exact command
|
|
32
32
|
* that resolves it. Adding across currencies is a bug in the caller's data, not a runtime hiccup,
|
|
33
33
|
* so the fix names the conversion that has to happen first.
|
|
34
|
+
*
|
|
35
|
+
* No \`docs:\`: \`UltimateError\` resolves the link from the code's registered descriptor, so it has
|
|
36
|
+
* one home. A per-code URL written here is a page that does not exist.
|
|
34
37
|
*/
|
|
35
38
|
export class ${app.pascal}CurrencyMismatchError extends UltimateError {
|
|
36
39
|
constructor(input: { readonly left: string; readonly right: string }) {
|
|
@@ -38,7 +41,6 @@ export class ${app.pascal}CurrencyMismatchError extends UltimateError {
|
|
|
38
41
|
code: '${currencyCode(app)}',
|
|
39
42
|
cause: \`cannot add \${input.left} to \${input.right}: Money is only additive inside one currency\`,
|
|
40
43
|
fix: \`bun test packages/domain/src/index.test.ts # add() is defined only inside one currency — convert \${input.right} to \${input.left} before calling it\`,
|
|
41
|
-
docs: 'https://ultimate.dev/errors/${currencyCode(app)}',
|
|
42
44
|
});
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -123,13 +123,21 @@ const envDeclaration = (): string => `${envSchemaSource()}
|
|
|
123
123
|
export const env = defineEnv(envSchema);`;
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* No `installPrompt`, no `afterSignInPath`, no `modelEnv`. All
|
|
127
|
-
* `defineConfig`, defaulted by it, and read by NO file; all
|
|
128
|
-
* `packages/core/src/config.ts` as of 2026-08-22,
|
|
129
|
-
* the marker this comment used to cite. So scaffolding
|
|
130
|
-
* is `TS2353` in the generated app's first `x verify
|
|
131
|
-
*
|
|
132
|
-
*
|
|
126
|
+
* No `installPrompt`, no `afterSignInPath`, no `modelEnv`, and no `realtime.tier`. All four were
|
|
127
|
+
* declared by `defineConfig`, defaulted by it, and read by NO file; all four are DELETED from
|
|
128
|
+
* `packages/core/src/config.ts` — the first three as of 2026-08-22, `tier` on 2026-08-23 — whose
|
|
129
|
+
* header now records the removals rather than the marker this comment used to cite. So scaffolding
|
|
130
|
+
* one is no longer a switch with no wire: it is `TS2353` in the generated app's first `x verify`,
|
|
131
|
+
* which is CI's `scaffold-smoke` job. The note belongs HERE rather than in the emitted file: an app
|
|
132
|
+
* author has no use for a comment about keys their config does not name.
|
|
133
|
+
*
|
|
134
|
+
* `tier` is the one that got through, and it says what the list was worth. It accepted
|
|
135
|
+
* `'channels' | 'live-queries' | 'local-first'`, so `'local-first'` read as a durable client store
|
|
136
|
+
* that does not exist (`createOpfsLocalStore` throws `X_NOT_IMPLEMENTED`) — silent, and shaped like
|
|
137
|
+
* a capability. Which realtime tier an app is on is decided by what it DECLARES: a `channel()`
|
|
138
|
+
* topic, a `live: true` query, a local store. `scaffold-config.test.ts` no longer holds a list of
|
|
139
|
+
* dead names to remember; it resolves every key path this literal writes against what `defineConfig`
|
|
140
|
+
* really returns, so the fourteenth deletion fails there with no edit here.
|
|
133
141
|
*/
|
|
134
142
|
const appConfig = (
|
|
135
143
|
app: NameSet,
|
|
@@ -149,10 +157,12 @@ export const config = defineConfig({
|
|
|
149
157
|
defaultCurrency: 'USD',
|
|
150
158
|
// Env KEYS, never the value: the same image deploys to every environment. The database is
|
|
151
159
|
// configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
|
|
152
|
-
cache:
|
|
160
|
+
// The tiers ARE the cache selection: add 'redis' to build the shared rung, which reads
|
|
161
|
+
// \`REDIS_URL\` and refuses the boot when it is unset.
|
|
162
|
+
cache: { tiers: ['request-memo', 'lru'] },
|
|
153
163
|
jobs: { queues: ['${app.kebab}-default'], concurrency: 4 },
|
|
154
164
|
// In-process transport by default; set urlEnv and transport: 'nats' to scale past one node.
|
|
155
|
-
realtime: { enabled: true,
|
|
165
|
+
realtime: { enabled: true, transport: 'memory' },
|
|
156
166
|
pwa: { enabled: true, offline: 'runtime' },
|
|
157
167
|
ai: { mcp: { expose: true, path: '/mcp' } },
|
|
158
168
|
});
|
|
@@ -19,10 +19,7 @@ import { policyFiles } from './policy';
|
|
|
19
19
|
*/
|
|
20
20
|
export type SliceModule = 'entity' | 'policy' | 'errors';
|
|
21
21
|
|
|
22
|
-
/** The feature's own code, derived once.
|
|
23
|
-
* `.../X_NOT_FOUND` beside a `code:` of `X_INVOICE_NOT_FOUND`, so following the link from a real
|
|
24
|
-
* failure landed on a different code's page — the same interpolation `error-codes.ts`'s `docsFor`
|
|
25
|
-
* already does for every framework code. */
|
|
22
|
+
/** The feature's own code, derived once. */
|
|
26
23
|
const notFoundCode = (feature: NameSet): string =>
|
|
27
24
|
`X_${feature.kebab.toUpperCase().split('-').join('_')}_NOT_FOUND`;
|
|
28
25
|
|
|
@@ -41,13 +38,14 @@ const errorsSource = (feature: NameSet): string => {
|
|
|
41
38
|
|
|
42
39
|
import { UltimateError } from '@ultimat3/core';
|
|
43
40
|
|
|
41
|
+
// No \`docs:\`. \`UltimateError\` resolves it from the code's registered descriptor, so the link has
|
|
42
|
+
// one home; a per-code URL written here is a page that does not exist.
|
|
44
43
|
export class ${feature.pascal}NotFoundError extends UltimateError {
|
|
45
44
|
constructor(input: { id: string }) {
|
|
46
45
|
super({
|
|
47
46
|
code: '${errorCode}',
|
|
48
47
|
cause: \`no ${feature.kebab} with id \${input.id}\`,
|
|
49
48
|
fix: 'x queries list --json, then pass an id the ${feature.kebab} read returns',
|
|
50
|
-
docs: 'https://ultimate.dev/errors/${errorCode}',
|
|
51
49
|
});
|
|
52
50
|
}
|
|
53
51
|
}
|
package/src/test-shards.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// cmd-test.ts because a printed reproduction is only true if it carries every input to the split —
|
|
4
4
|
// that rule is this file's, and argv parsing is that one's.
|
|
5
5
|
|
|
6
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
7
|
import type { AffectedSelection } from './affected';
|
|
7
|
-
import { docsFor } from './error-codes';
|
|
8
8
|
import type { Runner } from './exec';
|
|
9
9
|
import { execOutput } from './exec';
|
|
10
10
|
import { msg } from './messages';
|
|
@@ -137,7 +137,7 @@ const failureOf = (shard: Shard, code: number, plan: ReproduceOptions): Finding
|
|
|
137
137
|
code: 'X_TEST_SHARD_FAILED',
|
|
138
138
|
cause: `shard ${shard.index} of ${plan.workers} exited ${code} (${shard.files.length} file(s))`,
|
|
139
139
|
fix: reproduceFor(shard, plan),
|
|
140
|
-
docs:
|
|
140
|
+
docs: ERROR_DOCS_URL,
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
export async function runShards(options: RunShardsOptions): Promise<CommandResult> {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// root that has no `tsconfig.json` at all, so an `existsSync` ahead of it was a second question
|
|
8
8
|
// with one answer.
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
11
11
|
import type { Finding } from './output';
|
|
12
12
|
import { maskLiterals, stripComments } from './ts-scan';
|
|
13
13
|
|
|
@@ -82,7 +82,7 @@ export const unreferencedFinding = (dir: string): Finding => ({
|
|
|
82
82
|
code: 'X_PACKAGE_UNREFERENCED',
|
|
83
83
|
cause: `packages/${dir} is a published workspace and ${ROOT_TSCONFIG} has no reference to it, so tsc -b never builds it`,
|
|
84
84
|
fix: `add { "path": "./packages/${dir}" } to "references" in ${ROOT_TSCONFIG}, then run bunx tsc -b --pretty false`,
|
|
85
|
-
docs:
|
|
85
|
+
docs: ERROR_DOCS_URL,
|
|
86
86
|
at: ROOT_TSCONFIG,
|
|
87
87
|
});
|
|
88
88
|
|
package/src/verify-checks.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import type { Manifest } from '@ultimat3/manifest';
|
|
8
9
|
import {
|
|
9
10
|
AGENTS_MD_FILENAME,
|
|
@@ -330,7 +331,7 @@ async function specFindings(root: string, manifest: Manifest): Promise<readonly
|
|
|
330
331
|
code: 'X_MANIFEST_STALE',
|
|
331
332
|
cause: `${OPENAPI_FILE} does not match the actions the code registers`,
|
|
332
333
|
fix: 'x manifest',
|
|
333
|
-
docs:
|
|
334
|
+
docs: ERROR_DOCS_URL,
|
|
334
335
|
at: OPENAPI_FILE,
|
|
335
336
|
},
|
|
336
337
|
];
|
|
@@ -344,6 +345,6 @@ const seoFinding = (issue: MetaIssue): Finding => ({
|
|
|
344
345
|
code: issue.code,
|
|
345
346
|
cause: issue.cause,
|
|
346
347
|
fix: issue.fix,
|
|
347
|
-
docs:
|
|
348
|
+
docs: ERROR_DOCS_URL,
|
|
348
349
|
at: issue.file,
|
|
349
350
|
});
|