@ultimat3/cli 20.2.1 → 22.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 +84 -1582
- package/README.md +40 -2
- package/package.json +31 -31
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-env.ts +2 -2
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +56 -13
- package/src/build-errors.ts +54 -0
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +117 -42
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +78 -122
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +119 -0
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +33 -28
- package/src/error-pages.ts +19 -7
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +22 -29
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -334
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +38 -11
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +98 -0
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +2 -2
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/live-routes.ts +82 -42
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +30 -26
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/page-sync.ts +54 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +23 -11
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-render.ts → runtime-render.ts} +119 -20
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -338
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +76 -30
- package/src/templates/resource.ts +10 -4
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +25 -145
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +41 -0
- package/src/verify-run.ts +115 -50
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +22 -30
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/worker-bundle.ts +192 -0
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -100
- package/src/cdp-connection.ts +0 -211
- package/src/cdp-e2e-page.ts +0 -209
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -139
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -97
- package/src/e2e-errors.ts +0 -103
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -150
- package/src/e2e-selection.ts +0 -182
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
package/src/island-bundle.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
// byte-deterministic), plus the resolver that turns a page's `src` specifier into the URL its
|
|
4
4
|
// `data-x-entry` carries. One entry point per island is axiom 6 made mechanical — the page's graph
|
|
5
5
|
// never reaches an island, so a `site/` document stays at 0kb whatever the island imports.
|
|
6
|
+
//
|
|
7
|
+
// No page-client bootstrap is prepended (plan 101, decided 2026-09-22): the handle is one
|
|
8
|
+
// `globalThis` object created lazily by the first transport call or realtime hook. Measured before
|
|
9
|
+
// that decision on `examples/dummy`, a core-importing wrapper cost contact-sales 875 → 8,827 B.
|
|
10
|
+
// What IS prepended is the realtime install, and only where the island's graph reaches
|
|
11
|
+
// `@ultimat3/realtime` (`island-realtime.ts`). Measured on a fixture, As of 2026-09-22: a Solid
|
|
12
|
+
// island reading `useConnection` 64,964 → 65,067 B (+103 B); an island not reaching realtime +0.
|
|
6
13
|
|
|
7
14
|
// Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
|
|
8
15
|
// POSIX by construction), `join`/`basename` the filesystem side.
|
|
@@ -11,6 +18,7 @@ import { frameworkVersion, renderThrowable } from '@ultimat3/core';
|
|
|
11
18
|
import { ISLAND_EXTENSION, IslandInvalidError, islandModuleId } from '@ultimat3/render';
|
|
12
19
|
import { contentHash } from '@ultimat3/render/server';
|
|
13
20
|
import { IslandBuildFailedError } from './errors';
|
|
21
|
+
import { islandRealtimePlugin, REALTIME_ISLAND_ENTRY, reachesRealtime } from './island-realtime';
|
|
14
22
|
import { solidDedupePlugin } from './island-solid-dedupe';
|
|
15
23
|
import { islandStylesPlugin } from './island-styles';
|
|
16
24
|
import { hasPathSegment } from './path-segments';
|
|
@@ -75,10 +83,18 @@ export async function discoverIslands(root: string): Promise<readonly string[]>
|
|
|
75
83
|
async function buildOne(root: string, file: string): Promise<IslandChunk> {
|
|
76
84
|
// `Bun.build` REJECTS on a failed bundle, it does not answer `success: false` — so the catch is
|
|
77
85
|
// the real path here and the `success` test below is the belt for a future default.
|
|
86
|
+
// Only an island whose own graph reaches `@ultimat3/realtime` is wrapped (`island-realtime.ts`);
|
|
87
|
+
// every other one is built from its own file, byte for byte what it was.
|
|
88
|
+
// Inside the refusal too: the realtime probe PARSES the island's graph, and a file that will not
|
|
89
|
+
// parse rejected with a raw `AggregateError: Failed to scan imports` — out of the web boot, with
|
|
90
|
+
// no code, no file and no fix — before `Bun.build` ever ran to raise the one below.
|
|
91
|
+
const realtime = await reachesRealtime(root, file).catch((error: unknown) => {
|
|
92
|
+
throw new IslandBuildFailedError({ file, logs: describeBuildError(error) });
|
|
93
|
+
});
|
|
78
94
|
let built: Awaited<ReturnType<typeof Bun.build>>;
|
|
79
95
|
try {
|
|
80
96
|
built = await Bun.build({
|
|
81
|
-
entrypoints: [join(root, file)],
|
|
97
|
+
entrypoints: [realtime ? REALTIME_ISLAND_ENTRY : join(root, file)],
|
|
82
98
|
target: 'browser',
|
|
83
99
|
format: 'esm',
|
|
84
100
|
splitting: false,
|
|
@@ -96,7 +112,12 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
|
|
|
96
112
|
// The dedupe goes FIRST: it answers `solid-js` specifiers before either plugin loads a file,
|
|
97
113
|
// so the `solid-js/web` helpers the JSX transform writes into a symlinked package resolve
|
|
98
114
|
// to the app's one copy. See `island-solid-dedupe.ts` for the measurement.
|
|
99
|
-
plugins: [
|
|
115
|
+
plugins: [
|
|
116
|
+
...(realtime ? [islandRealtimePlugin(root, file)] : []),
|
|
117
|
+
solidDedupePlugin(root),
|
|
118
|
+
solidJsxPlugin,
|
|
119
|
+
islandStylesPlugin,
|
|
120
|
+
],
|
|
100
121
|
// The third one, and it is a `define` rather than the plugin this used to be: Bun selects
|
|
101
122
|
// the `development`/`production` export condition from the BUILD PROCESS's own `NODE_ENV`,
|
|
102
123
|
// and a defined `process.env.NODE_ENV` overrides it. Measured on 1.4.0, `solid-js` plus
|
|
@@ -138,9 +159,8 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
|
|
|
138
159
|
// The FIRST bytes this process emitted for these inputs, so a URL served `immutable` answers
|
|
139
160
|
// one byte string for as long as the process lives. Without it `x dev` re-mints the chunk on
|
|
140
161
|
// every watcher tick and a browser holding the previous one under `max-age=31536000` has two
|
|
141
|
-
// different files at one address.
|
|
142
|
-
|
|
143
|
-
bytes: new TextEncoder().encode(code).byteLength,
|
|
162
|
+
// different files at one address. `bytes` is measured on THAT code, never on this build's.
|
|
163
|
+
...stableChunk(file, hash, code),
|
|
144
164
|
};
|
|
145
165
|
}
|
|
146
166
|
|
|
@@ -153,7 +173,7 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
|
|
|
153
173
|
*/
|
|
154
174
|
const DEBUG_ID_COMMENT = '\n//# debugId=';
|
|
155
175
|
|
|
156
|
-
function stripDebugId(code: string): string {
|
|
176
|
+
export function stripDebugId(code: string): string {
|
|
157
177
|
const at = code.lastIndexOf(DEBUG_ID_COMMENT);
|
|
158
178
|
return at === -1 ? code : code.slice(0, at);
|
|
159
179
|
}
|
|
@@ -185,7 +205,7 @@ function stripDebugId(code: string): string {
|
|
|
185
205
|
* measured at 193,590 bytes against 131,649, +47% raw and +20% gzipped, on every island of every
|
|
186
206
|
* app. Delete this the day `Bun.build` is deterministic.
|
|
187
207
|
*/
|
|
188
|
-
function graphHash(file: string, map: string): string {
|
|
208
|
+
export function graphHash(file: string, map: string): string {
|
|
189
209
|
const parsed: unknown = JSON.parse(map);
|
|
190
210
|
const contents = sourcesContentOf(parsed);
|
|
191
211
|
if (contents === undefined) {
|
|
@@ -236,11 +256,18 @@ export function clearIslandChunkCache(): void {
|
|
|
236
256
|
* value called `hash` is a digest an attacker may be probing. This one is a build input's
|
|
237
257
|
* identity — the same reason `pr-threads.ts` calls a review state `wanted`.
|
|
238
258
|
*/
|
|
239
|
-
function
|
|
259
|
+
export function stableChunk(
|
|
260
|
+
file: string,
|
|
261
|
+
graph: string,
|
|
262
|
+
code: string,
|
|
263
|
+
): { readonly code: string; readonly bytes: number } {
|
|
240
264
|
const hit = emitted.get(file);
|
|
241
|
-
|
|
242
|
-
emitted.set(file, { graph, code });
|
|
243
|
-
|
|
265
|
+
const served = hit !== undefined && hit.graph === graph ? hit.code : code;
|
|
266
|
+
if (served === code) emitted.set(file, { graph, code });
|
|
267
|
+
// Measured on the code that is SERVED. It was measured on this build's output, which under a
|
|
268
|
+
// minifier that renames differently between builds is a second size for one URL in one process
|
|
269
|
+
// — and a budget weighed on bytes no browser receives.
|
|
270
|
+
return { code: served, bytes: new TextEncoder().encode(served).byteLength };
|
|
244
271
|
}
|
|
245
272
|
|
|
246
273
|
/**
|
package/src/island-capture.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// why: no Bun native joins a path; `Bun.write` takes one already joined.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { finiteCount } from '@ultimat3/core';
|
|
8
|
-
import type { CaptureClip, ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
|
|
9
|
-
import { systemScrapeClock } from '@ultimat3/scraping';
|
|
10
8
|
import type { IslandShotTarget, IslandViewport } from '@ultimat3/testing';
|
|
11
9
|
import { islandStatesFile } from '@ultimat3/testing';
|
|
10
|
+
import type { CaptureClip, ShotDriver, ShotSession } from './browser-launcher-port';
|
|
11
|
+
import { systemShotClock } from './cdp-shot-clock';
|
|
12
12
|
import { ISLAND_HARNESS_PATH } from './island-harness';
|
|
13
13
|
import { readinessProbe } from './island-harness-script';
|
|
14
14
|
import { IslandRequestUnstubbedError, IslandUnphotographableError } from './island-shot-errors';
|
|
@@ -43,7 +43,7 @@ export const ISLAND_CROP_MARGIN_PX = 8;
|
|
|
43
43
|
* takes the viewport as a LAUNCH option (`LocalBrowserOptions.options`) and a state declares its
|
|
44
44
|
* own — so "photograph this state at 480x320" is a different browser, not a different call.
|
|
45
45
|
*/
|
|
46
|
-
export type IslandBrowser = (viewport: IslandViewport) => Promise<
|
|
46
|
+
export type IslandBrowser = (viewport: IslandViewport) => Promise<ShotDriver>;
|
|
47
47
|
|
|
48
48
|
interface Refusal {
|
|
49
49
|
readonly reason: string;
|
|
@@ -181,13 +181,13 @@ export async function captureIslandState(
|
|
|
181
181
|
floor: number,
|
|
182
182
|
): Promise<IslandStateShot> {
|
|
183
183
|
const url = new URL(`${ISLAND_HARNESS_PATH}${target.query}`, server.url).toString();
|
|
184
|
-
let session:
|
|
184
|
+
let session: ShotSession | undefined;
|
|
185
185
|
try {
|
|
186
186
|
const driver = await options.driver(target.viewport);
|
|
187
187
|
session = await driver.open({
|
|
188
188
|
name: 'x shot --island',
|
|
189
189
|
rules: { allowHosts: allowHostsFrom(server.url, options.extraHosts) },
|
|
190
|
-
clock:
|
|
190
|
+
clock: systemShotClock,
|
|
191
191
|
timeoutMs: options.timeoutMs,
|
|
192
192
|
});
|
|
193
193
|
const page = session.page;
|
|
@@ -237,7 +237,7 @@ export async function captureIslandState(
|
|
|
237
237
|
// around, and which nothing passed until 2026-08-26 (issue #338).
|
|
238
238
|
//
|
|
239
239
|
// The clip ALONE. `fullPage: false` beside it is accepted — `assertCaptureFraming` refuses only
|
|
240
|
-
// `=== true`, and `cdp-
|
|
240
|
+
// `=== true`, and `cdp-shot-page.ts` sends `{ clip }` and nothing else either way — but it is a
|
|
241
241
|
// field that says nothing: the two are exclusive, and spelling out the default of the one you
|
|
242
242
|
// did not ask for reads as a choice.
|
|
243
243
|
// `??` screens null and undefined and NOTHING else, so `cropMarginPx: NaN` reached `clipFor`,
|
|
@@ -147,12 +147,16 @@ Intl.DateTimeFormat=ShotDTF;
|
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
149
|
* Ready is quiet, not idle. Fonts first — a picture taken mid-swap photographs the fallback face —
|
|
150
|
-
* then `QUIET_FRAMES` consecutive frames in which nothing started and nothing settled
|
|
150
|
+
* then `QUIET_FRAMES` consecutive frames in which nothing started and nothing settled, counted only
|
|
151
|
+
* once the island has MOUNTED or FAILED (#474): an `idle` island mounts in idle time, after frames
|
|
152
|
+
* that were quiet only because nothing had started yet. A page with no island host is not waited on.
|
|
151
153
|
*/
|
|
152
154
|
const readyScript = (): string => `
|
|
153
155
|
var last=-1;var still=0;
|
|
156
|
+
function answered(){var h=document.querySelector("[data-x-island]");
|
|
157
|
+
return !h||h.hasAttribute("data-x-mounted")||h.hasAttribute("data-x-failed")}
|
|
154
158
|
function tick(){var seen=W.activity;
|
|
155
|
-
if(seen===last)still+=1;else{still=0;last=seen}
|
|
159
|
+
if(!answered()){still=0;last=seen}else if(seen===last)still+=1;else{still=0;last=seen}
|
|
156
160
|
if(still>=${QUIET_FRAMES}){W.ready=true;return}
|
|
157
161
|
requestAnimationFrame(tick)}
|
|
158
162
|
(document.fonts?document.fonts.ready:Promise.resolve()).then(function(){requestAnimationFrame(tick)});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Realtime installed for the author, on exactly the islands that use it: an island whose own import
|
|
2
|
+
// graph reaches `@ultimat3/realtime` is built from a virtual entry that first calls
|
|
3
|
+
// `installRealtime({ signal: createSignal })` with THIS bundle's solid-js, then re-exports the
|
|
4
|
+
// island whole. Every other island is built from its own file and pays nothing (plan 101 slice 14).
|
|
5
|
+
|
|
6
|
+
// why: Bun ships no path API; the entry is joined to the root and resolved from its directory.
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import type { BunPlugin } from 'bun';
|
|
9
|
+
import { firstInGraph } from './live-routes';
|
|
10
|
+
|
|
11
|
+
/** What `Bun.build` is handed for a realtime island; resolved by `islandRealtimePlugin`. */
|
|
12
|
+
export const REALTIME_ISLAND_ENTRY = 'ultimate:island-entry';
|
|
13
|
+
|
|
14
|
+
const REALTIME = '@ultimat3/realtime';
|
|
15
|
+
const NAMESPACE = 'ultimate-island';
|
|
16
|
+
const INSTALL = 'ultimate:island-realtime';
|
|
17
|
+
const MODULE = 'ultimate:island-module';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether the island's own graph value-imports realtime. Relative specifiers only, which is the
|
|
21
|
+
* one blind spot: a PACKAGE importing realtime for the island is not seen, and a hook it calls
|
|
22
|
+
* then throws `X_REALTIME_UNINSTALLED` by name — loud, never silent.
|
|
23
|
+
*/
|
|
24
|
+
export async function reachesRealtime(root: string, file: string): Promise<boolean> {
|
|
25
|
+
const found = await firstInGraph(root, file, (source, path) => {
|
|
26
|
+
// The transpiler, not a regex: it erases `import type` and reads a re-export as an import.
|
|
27
|
+
const scanned = new Bun.Transpiler({ loader: path.endsWith('x') ? 'tsx' : 'ts' }).scanImports(
|
|
28
|
+
source,
|
|
29
|
+
);
|
|
30
|
+
return scanned.some((entry) => entry.path === REALTIME) ? true : undefined;
|
|
31
|
+
});
|
|
32
|
+
// Recorded with the answer, so the document renderer can ask it of a page's islands without a
|
|
33
|
+
// second graph walk per request. Every build re-asks, so an edit that drops realtime drops it.
|
|
34
|
+
if (found === true) realtimeIslands.add(file);
|
|
35
|
+
else realtimeIslands.delete(file);
|
|
36
|
+
return found === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** App-root-relative island files whose graph reaches realtime, as the last build answered. */
|
|
40
|
+
const realtimeIslands = new Set<string>();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The islands (app-root-relative POSIX paths) the last build found reaching realtime. A page needs
|
|
44
|
+
* realtime's page boot only if one of ITS islands does — `settings` paid 34.9 kB of boot script
|
|
45
|
+
* for an island that never touched a record.
|
|
46
|
+
*/
|
|
47
|
+
export function realtimeIslandFiles(): ReadonlySet<string> {
|
|
48
|
+
return realtimeIslands;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `export *` and never a named list: the hydration runtime reads `mount` off the module, and the
|
|
53
|
+
* wrapper must not decide which of an island's names survive. The install is imported FIRST, so it
|
|
54
|
+
* has run before the island's module body — and every hook the island calls — does.
|
|
55
|
+
*/
|
|
56
|
+
const ENTRY_SOURCE = `import '${INSTALL}';\nexport * from '${MODULE}';\n`;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The install names `@ultimat3/realtime` BARE, and `islandRealtimePlugin` resolves it from the
|
|
60
|
+
* ISLAND's own directory — the resolution the island itself gets — so the bundle holds one copy
|
|
61
|
+
* and the signal lands where the hooks read it. Never the resolved absolute path spliced in: this
|
|
62
|
+
* source is part of the chunk's `sourcesContent`, which is what `graphHash` names the URL from, so
|
|
63
|
+
* a path in it gave the same island a different URL in every checkout and every image build.
|
|
64
|
+
* `solid-js` goes through `island-solid-dedupe.ts` like every other import in the graph.
|
|
65
|
+
*/
|
|
66
|
+
const INSTALL_SOURCE =
|
|
67
|
+
`import { installRealtime } from '${REALTIME}';\n` +
|
|
68
|
+
`import { createSignal } from 'solid-js';\n` +
|
|
69
|
+
`installRealtime({ signal: createSignal });\n`;
|
|
70
|
+
|
|
71
|
+
export function islandRealtimePlugin(root: string, file: string): BunPlugin {
|
|
72
|
+
const island = join(root, file);
|
|
73
|
+
return {
|
|
74
|
+
name: 'ultimate-island-realtime',
|
|
75
|
+
setup(build) {
|
|
76
|
+
build.onResolve({ filter: /^ultimate:island-entry$/ }, () => ({
|
|
77
|
+
path: 'entry',
|
|
78
|
+
namespace: NAMESPACE,
|
|
79
|
+
}));
|
|
80
|
+
build.onResolve({ filter: /^ultimate:island-realtime$/ }, () => ({
|
|
81
|
+
path: 'install',
|
|
82
|
+
namespace: NAMESPACE,
|
|
83
|
+
}));
|
|
84
|
+
build.onResolve({ filter: /^ultimate:island-module$/ }, () => ({ path: island }));
|
|
85
|
+
// Every bare `@ultimat3/realtime` in this build, the virtual install's included — a virtual
|
|
86
|
+
// module has no directory to resolve from. The island's directory for all of them, which is
|
|
87
|
+
// what the island itself would get, and one copy in the bundle is the requirement anyway:
|
|
88
|
+
// the signal the install sets is only read by hooks from the same module instance.
|
|
89
|
+
build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({
|
|
90
|
+
path: Bun.resolveSync(REALTIME, dirname(island)),
|
|
91
|
+
}));
|
|
92
|
+
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, (args) => ({
|
|
93
|
+
contents: args.path === 'entry' ? ENTRY_SOURCE : INSTALL_SOURCE,
|
|
94
|
+
loader: 'js',
|
|
95
|
+
}));
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/island-shot.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// and the index are written, and only then does the missing-picture gate turn the reasons into a
|
|
9
9
|
// non-zero exit. One island that will not mount must not cost a reader the other nineteen.
|
|
10
10
|
|
|
11
|
+
// why: Bun has no file delete that tolerates an absent file.
|
|
12
|
+
import { rm } from 'node:fs/promises';
|
|
11
13
|
// why: no Bun native joins a path; `Bun.write` and `Bun.file` both take one already joined.
|
|
12
14
|
import { join } from 'node:path';
|
|
13
15
|
import { finiteCount } from '@ultimat3/core';
|
|
@@ -95,6 +97,9 @@ export async function runIslandSweep(options: IslandSweepRun): Promise<IslandSwe
|
|
|
95
97
|
// for each one it cannot, and the missing-shot gate below is what turns those reasons into
|
|
96
98
|
// a non-zero exit.
|
|
97
99
|
try {
|
|
100
|
+
// Deleted FIRST: the missing-shot gate reads the disk, and a picture an earlier run left
|
|
101
|
+
// there made a capture that failed today read as taken.
|
|
102
|
+
await rm(join(options.outDir, target.file), { force: true });
|
|
98
103
|
shots.push(await captureIslandState(options, server, target, floor));
|
|
99
104
|
} catch (error) {
|
|
100
105
|
failures.push(error);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The island chunks a container serves, built ONCE by `x build --target docker` into
|
|
2
|
+
// `.x/islands/` and read back at boot. Without it every role's boot ran `Bun.build` — Babel, the
|
|
3
|
+
// JSX transform, a minifier — for every island, in the image, on every start, and a chunk whose
|
|
4
|
+
// URL is a hash of its sources was rebuilt from sources that could not have changed.
|
|
5
|
+
//
|
|
6
|
+
// Load-and-VERIFY, never load-and-trust: a store written by another framework or Bun version, one
|
|
7
|
+
// that names a different set of islands than the app has, or a chunk whose bytes do not hash to
|
|
8
|
+
// what the index recorded is refused, and the boot builds instead — with the reason logged.
|
|
9
|
+
|
|
10
|
+
import { join } from 'node:path'; // why: Bun ships no path-join primitive.
|
|
11
|
+
import { frameworkVersion, logger } from '@ultimat3/core';
|
|
12
|
+
import { contentHash } from '@ultimat3/render/server';
|
|
13
|
+
import type { IslandBundle, IslandChunk } from './island-bundle';
|
|
14
|
+
import { buildIslands, discoverIslands, islandBundle } from './island-bundle';
|
|
15
|
+
|
|
16
|
+
/** App-root-relative: `COPY . .` carries it into the image with the rest of `.x/`. */
|
|
17
|
+
export const ISLAND_STORE_DIR = '.x/islands';
|
|
18
|
+
const INDEX = 'index.json';
|
|
19
|
+
|
|
20
|
+
interface StoredChunk {
|
|
21
|
+
readonly file: string;
|
|
22
|
+
readonly moduleId: string;
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly identity: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface StoreIndex {
|
|
28
|
+
readonly framework: string;
|
|
29
|
+
readonly bun: string;
|
|
30
|
+
readonly chunks: readonly StoredChunk[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const chunkFile = (url: string): string => url.slice(url.lastIndexOf('/') + 1);
|
|
34
|
+
|
|
35
|
+
/** Writes every chunk and the index that verifies them. Answers the files written, app-relative. */
|
|
36
|
+
export async function writeIslandStore(
|
|
37
|
+
root: string,
|
|
38
|
+
bundle: IslandBundle,
|
|
39
|
+
): Promise<readonly string[]> {
|
|
40
|
+
const dir = join(root, ISLAND_STORE_DIR);
|
|
41
|
+
const chunks: StoredChunk[] = [];
|
|
42
|
+
const written: string[] = [];
|
|
43
|
+
for (const chunk of bundle.chunks) {
|
|
44
|
+
await Bun.write(join(dir, chunkFile(chunk.url)), chunk.code);
|
|
45
|
+
written.push(`${ISLAND_STORE_DIR}/${chunkFile(chunk.url)}`);
|
|
46
|
+
chunks.push({
|
|
47
|
+
file: chunk.file,
|
|
48
|
+
moduleId: chunk.moduleId,
|
|
49
|
+
url: chunk.url,
|
|
50
|
+
identity: contentHash(chunk.code),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const index: StoreIndex = { framework: frameworkVersion(), bun: Bun.version, chunks };
|
|
54
|
+
await Bun.write(join(dir, INDEX), `${JSON.stringify(index, null, 2)}\n`);
|
|
55
|
+
return [...written, `${ISLAND_STORE_DIR}/${INDEX}`];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isString = (value: unknown): value is string => typeof value === 'string';
|
|
59
|
+
|
|
60
|
+
/** The index, narrowed field by field — a file on disk is input, never a trusted shape. */
|
|
61
|
+
function parseIndex(value: unknown): StoreIndex | undefined {
|
|
62
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
63
|
+
const record = value as Record<string, unknown>;
|
|
64
|
+
const chunks = record['chunks'];
|
|
65
|
+
if (!isString(record['framework']) || !isString(record['bun']) || !Array.isArray(chunks)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const parsed: StoredChunk[] = [];
|
|
69
|
+
for (const entry of chunks) {
|
|
70
|
+
if (typeof entry !== 'object' || entry === null) return undefined;
|
|
71
|
+
const chunk = entry as Record<string, unknown>;
|
|
72
|
+
const { file, moduleId, url, identity } = chunk;
|
|
73
|
+
if (!isString(file) || !isString(moduleId) || !isString(url) || !isString(identity))
|
|
74
|
+
return undefined;
|
|
75
|
+
parsed.push({ file, moduleId, url, identity });
|
|
76
|
+
}
|
|
77
|
+
return { framework: record['framework'], bun: record['bun'], chunks: parsed };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A verified store, or the one sentence saying why it cannot be served. */
|
|
81
|
+
export type StoreRead =
|
|
82
|
+
| { readonly bundle: IslandBundle; readonly stale?: undefined }
|
|
83
|
+
| { readonly bundle?: undefined; readonly stale: string };
|
|
84
|
+
|
|
85
|
+
export async function readIslandStore(root: string): Promise<StoreRead> {
|
|
86
|
+
const dir = join(root, ISLAND_STORE_DIR);
|
|
87
|
+
const file = Bun.file(join(dir, INDEX));
|
|
88
|
+
if (!(await file.exists())) return { stale: `no ${ISLAND_STORE_DIR}/${INDEX}` };
|
|
89
|
+
const index = parseIndex(await file.json().catch(() => undefined));
|
|
90
|
+
if (index === undefined) return { stale: `${ISLAND_STORE_DIR}/${INDEX} does not parse` };
|
|
91
|
+
if (index.framework !== frameworkVersion() || index.bun !== Bun.version) {
|
|
92
|
+
return {
|
|
93
|
+
stale: `built by framework ${index.framework} on Bun ${index.bun}, serving ${frameworkVersion()} on Bun ${Bun.version}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const stored = index.chunks.map((chunk) => chunk.file).sort();
|
|
97
|
+
const present = [...(await discoverIslands(root))];
|
|
98
|
+
if (stored.join('\n') !== present.join('\n')) {
|
|
99
|
+
return { stale: 'the stored islands are not the islands this app has' };
|
|
100
|
+
}
|
|
101
|
+
const chunks: IslandChunk[] = [];
|
|
102
|
+
for (const entry of index.chunks) {
|
|
103
|
+
const bytes = Bun.file(join(dir, chunkFile(entry.url)));
|
|
104
|
+
const code = (await bytes.exists()) ? await bytes.text() : undefined;
|
|
105
|
+
if (code === undefined || contentHash(code) !== entry.identity) {
|
|
106
|
+
return { stale: `${entry.url} is missing or does not match its recorded hash` };
|
|
107
|
+
}
|
|
108
|
+
chunks.push({
|
|
109
|
+
file: entry.file,
|
|
110
|
+
moduleId: entry.moduleId,
|
|
111
|
+
url: entry.url,
|
|
112
|
+
code,
|
|
113
|
+
bytes: new TextEncoder().encode(code).byteLength,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return { bundle: islandBundle(chunks) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The container's islands: the verified store, or a build when there is none to trust. A build is
|
|
121
|
+
* correct and only slower, so a stale store is a warning with its reason, never a refusal.
|
|
122
|
+
*/
|
|
123
|
+
export async function loadOrBuildIslands(root: string): Promise<IslandBundle> {
|
|
124
|
+
const read = await readIslandStore(root);
|
|
125
|
+
if (read.bundle !== undefined) return read.bundle;
|
|
126
|
+
logger.warn('islands built at boot', {
|
|
127
|
+
reason: read.stale,
|
|
128
|
+
fix: 'x build --target docker',
|
|
129
|
+
});
|
|
130
|
+
return buildIslands(root);
|
|
131
|
+
}
|
package/src/island-verdict.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { StandardSchemaV1 } from '@ultimat3/schema';
|
|
7
7
|
import { t, validate } from '@ultimat3/schema';
|
|
8
|
-
import type { ConsoleLine, PageError } from '@ultimat3/scraping';
|
|
9
8
|
import type { IslandShotTarget } from '@ultimat3/testing';
|
|
9
|
+
import type { ConsoleLine, PageError } from './browser-launcher-port';
|
|
10
10
|
import { msg } from './messages';
|
|
11
11
|
import type { JsonValue } from './output';
|
|
12
12
|
|
|
@@ -57,7 +57,7 @@ export interface IslandReadiness {
|
|
|
57
57
|
/**
|
|
58
58
|
* `"WS <url>"` / `"SSE <url>"` for every socket a component constructed — recorded, never
|
|
59
59
|
* gating. The harness's stand-in is inert (constructs, never opens, `close()` is a no-op), so
|
|
60
|
-
* a component
|
|
60
|
+
* a component whose page socket dials `/_x/sync` (`@ultimat3/realtime`) does not fail the state it
|
|
61
61
|
* is mounted in; this is the fact a picture cannot carry about that.
|
|
62
62
|
*/
|
|
63
63
|
readonly sockets: readonly string[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The `manifest` step's job half: a job or a task still carrying the positional name `job()` or
|
|
2
|
+
// `task()` minted (`anonymous-job-2`) was never handed to `defineApi`. That name is what the queue
|
|
3
|
+
// row, `x.manifest.json`, `x jobs show` and every dead-letter trace carry, and it moves whenever the
|
|
4
|
+
// import order does — so a retry scheduled under one deploy's name lands on another deploy's job.
|
|
5
|
+
|
|
6
|
+
import { registeredJobs, registeredTasks } from '@ultimat3/jobs';
|
|
7
|
+
import { loadApp } from './app-load';
|
|
8
|
+
import type { AppLoader } from './app-permissions';
|
|
9
|
+
import type { Finding } from './output';
|
|
10
|
+
|
|
11
|
+
/** What `job()` and `task()` name a declaration nobody named (`packages/jobs/src/job.ts`). */
|
|
12
|
+
const POSITIONAL = /^anonymous-(job|task)-\d+$/;
|
|
13
|
+
|
|
14
|
+
/** Every registered job and task still under its positional name. */
|
|
15
|
+
export function positionalNames(
|
|
16
|
+
names: readonly string[] = [...registeredJobs(), ...registeredTasks()].map((one) => one.name),
|
|
17
|
+
): readonly string[] {
|
|
18
|
+
return names.filter((name) => POSITIONAL.test(name)).sort();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function unregisteredJobFinding(name: string): Finding {
|
|
22
|
+
const kind = name.startsWith('anonymous-task-') ? 'tasks' : 'jobs';
|
|
23
|
+
return {
|
|
24
|
+
code: 'X_JOB_UNREGISTERED',
|
|
25
|
+
cause: `${name} is a ${kind === 'tasks' ? 'task' : 'job'} no defineApi() call names, so it carries the positional name ${kind === 'tasks' ? 'task()' : 'job()'} minted — on the queue row, in the manifest and in every dead-letter trace, and it moves when the import order does`,
|
|
26
|
+
fix: `add import * as <module> from its file and <module> to ${kind}: [...] in apps/web/api/index.ts, then x manifest`,
|
|
27
|
+
at: 'apps/web/api/index.ts',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The step's answer. Loads the app the way every other gate step does — the registries are only
|
|
33
|
+
* whole after the scan — and says nothing when the load failed: a short registry is the load's
|
|
34
|
+
* finding, reported by the step that loaded it.
|
|
35
|
+
*/
|
|
36
|
+
export async function unregisteredJobFindings(
|
|
37
|
+
root: string,
|
|
38
|
+
load: AppLoader = loadApp,
|
|
39
|
+
): Promise<readonly Finding[]> {
|
|
40
|
+
if ((await load(root)).findings.length > 0) return [];
|
|
41
|
+
return positionalNames().map(unregisteredJobFinding);
|
|
42
|
+
}
|
package/src/jobs-driver.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import type { JobDriver } from '@ultimat3/jobs';
|
|
6
6
|
import { jobDriver } from '@ultimat3/jobs';
|
|
7
7
|
import type { CommandContext } from './command';
|
|
8
|
-
import { startQueue } from './dev-queue';
|
|
9
|
-
import { resolveServices } from './dev-services';
|
|
10
8
|
import type { CommandResult } from './output';
|
|
9
|
+
import { resolveServices } from './runtime-bindings';
|
|
10
|
+
import { startQueue } from './runtime-queue';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* `x jobs` needs the app's real driver. Reuse an already-running one first — inside `x dev` or
|