@ultimat3/cli 20.2.0 → 21.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 +70 -1
- package/package.json +30 -30
- package/src/app-env.ts +2 -2
- package/src/budgets.ts +45 -12
- package/src/build-errors.ts +54 -0
- package/src/cdp-browser.ts +21 -27
- package/src/cdp-connection.ts +66 -30
- package/src/cdp-e2e-page.ts +84 -113
- package/src/cdp-e2e-session.ts +199 -0
- package/src/cdp-launch.ts +95 -41
- package/src/cdp-offline-script.ts +73 -0
- package/src/cdp-pipe.ts +77 -0
- package/src/cmd-deploy.ts +7 -0
- package/src/cmd-dev.ts +23 -86
- package/src/cmd-shot.ts +30 -4
- package/src/dev-live-feed.ts +2 -0
- package/src/dev-render.ts +119 -20
- package/src/dev-route-table.ts +119 -0
- package/src/dev-services.ts +4 -1
- package/src/dev-sync.ts +5 -3
- package/src/e2e-app.ts +103 -0
- package/src/e2e-browser-handle.ts +55 -0
- package/src/e2e-driver.ts +32 -12
- package/src/e2e-errors.ts +14 -0
- package/src/e2e-page.ts +5 -2
- package/src/e2e-preload.ts +64 -0
- package/src/e2e-probe.ts +23 -0
- package/src/e2e-spawn.ts +169 -0
- package/src/error-codes.ts +7 -0
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +8 -29
- package/src/index.ts +18 -4
- package/src/island-bundle.ts +38 -11
- package/src/island-realtime.ts +91 -0
- package/src/island-solid-dedupe.ts +108 -0
- package/src/island-verdict.ts +1 -1
- package/src/live-routes.ts +82 -42
- package/src/mcp-errors.ts +3 -0
- package/src/page-sync.ts +54 -0
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/serve.ts +9 -0
- package/src/shot-theme.ts +52 -0
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/resource-form-island.ts +30 -21
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-container.ts +18 -3
- package/src/templates/scaffold-dashboard-shared.ts +8 -5
- package/src/templates/scaffold-env.ts +6 -0
- package/src/verify-e2e.ts +38 -0
- package/src/verify-run.ts +105 -50
- package/src/verify-tests.ts +21 -4
- package/src/worker-bundle.ts +192 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The page's framework scripts (plan 101), each built as its own classic-script browser bundle,
|
|
2
|
+
// addressed by a hash of its source graph, served `immutable` by the same route in `x dev` and the
|
|
3
|
+
// container: the ONE sync worker (`@ultimat3/realtime/sync-worker`, slice 11) and the ONE page boot
|
|
4
|
+
// (`@ultimat3/realtime/boot` — the disk restore and the outbox, once per page instead of once per
|
|
5
|
+
// island). A new deploy is a new URL, so an old tab keeps what it started with.
|
|
6
|
+
|
|
7
|
+
// why: Bun ships no path API; the entry is resolved to a file and named in the cause.
|
|
8
|
+
import { dirname, join, relative } from 'node:path';
|
|
9
|
+
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
10
|
+
import { applyCacheHeaders, json } from '@ultimat3/http';
|
|
11
|
+
import { FrameworkScriptBuildFailedError, type FrameworkScriptKind } from './errors';
|
|
12
|
+
import { describeBuildError, graphHash, stripDebugId } from './island-bundle';
|
|
13
|
+
|
|
14
|
+
/** Under the dev namespace `/_x` — where the socket it opens (`/_x/sync`) already lives. */
|
|
15
|
+
export const SYNC_WORKER_BASE_PATH = '/_x/sync-worker';
|
|
16
|
+
|
|
17
|
+
/** What an app resolves: realtime's worker entry, from the APP's install, never the CLI's own. */
|
|
18
|
+
export const SYNC_WORKER_SPECIFIER = '@ultimat3/realtime/sync-worker';
|
|
19
|
+
|
|
20
|
+
/** Under the dev namespace too; one per document that carries a principal scope. */
|
|
21
|
+
export const PAGE_BOOT_BASE_PATH = '/_x/page-boot';
|
|
22
|
+
|
|
23
|
+
/** Realtime's page boot, from the APP's install. */
|
|
24
|
+
export const PAGE_BOOT_SPECIFIER = '@ultimat3/realtime/boot';
|
|
25
|
+
|
|
26
|
+
/** One framework script: the sync worker or the page boot. */
|
|
27
|
+
export interface FrameworkScript {
|
|
28
|
+
/** `<base>/<hash>.js` — what the document names. */
|
|
29
|
+
readonly url: string;
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly bytes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The worker, by the name every caller already uses. */
|
|
35
|
+
export type SyncWorkerChunk = FrameworkScript;
|
|
36
|
+
|
|
37
|
+
export interface BuildSyncWorkerOptions {
|
|
38
|
+
/** An absolute entry path. Absent resolves `SYNC_WORKER_SPECIFIER` from `root`. */
|
|
39
|
+
readonly entry?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `undefined` when the app cannot resolve realtime's worker: an app with no realtime has no
|
|
44
|
+
* socket to share, and the tab-side host falls back to an in-page engine by design (slice 11's
|
|
45
|
+
* transparent fallback) — absence is an answer, not a failure.
|
|
46
|
+
*/
|
|
47
|
+
export function buildSyncWorker(
|
|
48
|
+
root: string,
|
|
49
|
+
options: BuildSyncWorkerOptions = {},
|
|
50
|
+
): Promise<FrameworkScript | undefined> {
|
|
51
|
+
return buildFrameworkScript(
|
|
52
|
+
root,
|
|
53
|
+
'sync worker',
|
|
54
|
+
SYNC_WORKER_SPECIFIER,
|
|
55
|
+
SYNC_WORKER_BASE_PATH,
|
|
56
|
+
options.entry,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The page boot, or `undefined` for an app with no realtime (nothing to restore, no outbox). A
|
|
62
|
+
* document then carries no boot script, and `pageRealtime().booted` answers at once.
|
|
63
|
+
*/
|
|
64
|
+
export function buildPageBoot(
|
|
65
|
+
root: string,
|
|
66
|
+
options: BuildSyncWorkerOptions = {},
|
|
67
|
+
): Promise<FrameworkScript | undefined> {
|
|
68
|
+
return buildFrameworkScript(
|
|
69
|
+
root,
|
|
70
|
+
'page boot',
|
|
71
|
+
PAGE_BOOT_SPECIFIER,
|
|
72
|
+
PAGE_BOOT_BASE_PATH,
|
|
73
|
+
options.entry,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function buildFrameworkScript(
|
|
78
|
+
root: string,
|
|
79
|
+
what: FrameworkScriptKind,
|
|
80
|
+
specifier: string,
|
|
81
|
+
basePath: string,
|
|
82
|
+
explicit: string | undefined,
|
|
83
|
+
): Promise<FrameworkScript | undefined> {
|
|
84
|
+
const entry = explicit ?? resolveEntry(root, specifier);
|
|
85
|
+
if (entry === undefined) return undefined;
|
|
86
|
+
const label = relative(root, entry);
|
|
87
|
+
let built: Awaited<ReturnType<typeof Bun.build>>;
|
|
88
|
+
try {
|
|
89
|
+
built = await Bun.build({
|
|
90
|
+
entrypoints: [entry],
|
|
91
|
+
target: 'browser',
|
|
92
|
+
// A CLASSIC script: `new SharedWorker(url, { name })` with no `type: 'module'` runs it, and a
|
|
93
|
+
// `<script defer>` runs it before any island module that follows it in the document.
|
|
94
|
+
format: 'iife',
|
|
95
|
+
splitting: false,
|
|
96
|
+
minify: true,
|
|
97
|
+
// `island-bundle.ts`'s reasons, verbatim: a chunk is only ever built to be shipped, and the
|
|
98
|
+
// map's `sourcesContent` is the one stable identity a minified bundle has.
|
|
99
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
100
|
+
sourcemap: 'external',
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new FrameworkScriptBuildFailedError({
|
|
104
|
+
what,
|
|
105
|
+
entry: label,
|
|
106
|
+
logs: describeBuildError(error),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const output = built.outputs.find((artifact) => artifact.kind === 'entry-point');
|
|
110
|
+
const map = built.outputs.find((artifact) => artifact.kind === 'sourcemap');
|
|
111
|
+
if (!built.success || output === undefined || map === undefined) {
|
|
112
|
+
throw new FrameworkScriptBuildFailedError({
|
|
113
|
+
what,
|
|
114
|
+
entry: label,
|
|
115
|
+
logs: built.logs.map((log) => String(log)).join('; '),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const code = stripDebugId(await output.text());
|
|
119
|
+
return {
|
|
120
|
+
url: `${basePath}/${graphHash(specifier, await map.text())}.js`,
|
|
121
|
+
code,
|
|
122
|
+
bytes: new TextEncoder().encode(code).byteLength,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The app's realtime, from the root or — in a workspace app, where `@ultimat3/realtime` is a
|
|
128
|
+
* dependency of `apps/<app>` and not of the root (`examples/dummy`) — from the first app that has
|
|
129
|
+
* it: where its islands resolve it, which is the copy the scripts must match. Resolving from the
|
|
130
|
+
* root alone built no worker and no boot for such an app, silently.
|
|
131
|
+
*/
|
|
132
|
+
function resolveEntry(root: string, specifier: string): string | undefined {
|
|
133
|
+
for (const dir of [root, ...appDirs(root)]) {
|
|
134
|
+
try {
|
|
135
|
+
return Bun.resolveSync(specifier, dir);
|
|
136
|
+
} catch {
|
|
137
|
+
// Not installed here, or a realtime that predates the export: try the next app.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function appDirs(root: string): readonly string[] {
|
|
144
|
+
const glob = new Bun.Glob('apps/*/package.json');
|
|
145
|
+
return [...glob.scanSync({ cwd: root, onlyFiles: true })]
|
|
146
|
+
.sort()
|
|
147
|
+
.map((file) => join(root, dirname(file)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The script a build produced, read per request so `x dev` and the container share one route. */
|
|
151
|
+
export type SyncWorkerSource = () => FrameworkScript | undefined;
|
|
152
|
+
|
|
153
|
+
export function syncWorkerRoutes(source: SyncWorkerSource): readonly Route[] {
|
|
154
|
+
return [scriptRoute(SYNC_WORKER_BASE_PATH, 'assets.sync-worker', 'sync worker', source)];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function pageBootRoutes(source: SyncWorkerSource): readonly Route[] {
|
|
158
|
+
return [scriptRoute(PAGE_BOOT_BASE_PATH, 'assets.page-boot', 'page boot', source)];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function scriptRoute(
|
|
162
|
+
basePath: string,
|
|
163
|
+
name: string,
|
|
164
|
+
what: string,
|
|
165
|
+
source: SyncWorkerSource,
|
|
166
|
+
): Route {
|
|
167
|
+
return {
|
|
168
|
+
method: 'GET',
|
|
169
|
+
path: `${basePath}/:file`,
|
|
170
|
+
meta: { name, auth: 'public', tags: ['assets'] },
|
|
171
|
+
handler: (request: UltimateRequest): Response => {
|
|
172
|
+
const script = source();
|
|
173
|
+
if (script === undefined || script.url !== request.pathname) {
|
|
174
|
+
return json(
|
|
175
|
+
{
|
|
176
|
+
ok: false,
|
|
177
|
+
error: {
|
|
178
|
+
code: 'X_ROUTE_NOT_FOUND',
|
|
179
|
+
cause: `no ${what} is built at ${request.pathname} — the document that named it was rendered against another build`,
|
|
180
|
+
fix: `reload the page — this process serves only the ${what} it built`,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{ status: 404 },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return applyCacheHeaders(
|
|
187
|
+
new Response(script.code, { headers: { 'content-type': 'text/javascript' } }),
|
|
188
|
+
{ mode: 'immutable' },
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|