@ultimat3/cli 23.0.0 → 25.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 +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +57 -79
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -33
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/naming.ts +28 -0
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// can only be one of those per checkout.
|
|
5
5
|
|
|
6
6
|
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
7
|
-
import { html,
|
|
7
|
+
import { html, jsonResponse } from '@ultimat3/http';
|
|
8
8
|
import type { IslandStatesManifest } from '@ultimat3/testing';
|
|
9
9
|
import {
|
|
10
10
|
islandShotFile,
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
islandStatesNames,
|
|
14
14
|
parseIslandAddress,
|
|
15
15
|
} from '@ultimat3/testing';
|
|
16
|
+
import { hostRefusal } from './dev-dashboard-guard';
|
|
16
17
|
import { harnessPage, ISLAND_HARNESS_PATH } from './island-harness';
|
|
17
18
|
import type { IslandSource } from './island-routes';
|
|
19
|
+
import { quoteArg } from './shell-quote';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* A getter, never the manifests: `x dev` rebuilds on the watcher tick, and a set captured when the
|
|
@@ -25,14 +27,20 @@ import type { IslandSource } from './island-routes';
|
|
|
25
27
|
export type IslandStatesSource = () => Promise<readonly IslandStatesManifest[]>;
|
|
26
28
|
|
|
27
29
|
const refused = (cause: string, fix: string): Response =>
|
|
28
|
-
|
|
30
|
+
jsonResponse(
|
|
29
31
|
{ ok: false, error: { code: 'X_SHOT_ISLAND_UNPHOTOGRAPHABLE', cause, fix } },
|
|
30
32
|
{ status: 404 },
|
|
31
33
|
);
|
|
32
34
|
|
|
35
|
+
/** A value opening with `-` reads as a flag however it is quoted, so it becomes the placeholder. */
|
|
36
|
+
const unflagged = (value: string, placeholder: string): string =>
|
|
37
|
+
value.startsWith('-') ? placeholder : value;
|
|
38
|
+
|
|
33
39
|
export interface HarnessRouteInput {
|
|
34
40
|
readonly islands: IslandSource;
|
|
35
41
|
readonly states: IslandStatesSource;
|
|
42
|
+
/** This dev server's URL, read per request — only what a refused Host's `fix:` line names. */
|
|
43
|
+
readonly devUrl: () => string;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
@@ -44,6 +52,10 @@ export interface HarnessRouteInput {
|
|
|
44
52
|
* An island or a state this process does not know is the one case that IS refused, and it can only
|
|
45
53
|
* mean the two processes disagree: `x shot` computed its picture list from the states files on disk
|
|
46
54
|
* and this server was booted against an older set.
|
|
55
|
+
*
|
|
56
|
+
* Before any of that, the Host: this is a `/_x` route, so it answers a loopback Host only, exactly
|
|
57
|
+
* as the dashboard does (`hostRefusal`, `X_DEV_HOST_REFUSED`). Without it a DNS-rebinding page
|
|
58
|
+
* read an island rendered with its declared props — and imported states files — same-origin.
|
|
47
59
|
*/
|
|
48
60
|
export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[] {
|
|
49
61
|
return [
|
|
@@ -52,6 +64,8 @@ export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[]
|
|
|
52
64
|
path: ISLAND_HARNESS_PATH,
|
|
53
65
|
meta: { name: 'dev._x.island', auth: 'public', tags: ['dev'] },
|
|
54
66
|
handler: async (request: UltimateRequest): Promise<Response> => {
|
|
67
|
+
const misdirected = hostRefusal(request, input.devUrl());
|
|
68
|
+
if (misdirected !== undefined) return misdirected;
|
|
55
69
|
const address = parseIslandAddress(request.url.search);
|
|
56
70
|
const all = await input.states();
|
|
57
71
|
const manifest = islandStatesMatching(all, address.island)[0];
|
|
@@ -71,7 +85,9 @@ export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[]
|
|
|
71
85
|
if (declared === undefined || target === undefined) {
|
|
72
86
|
return refused(
|
|
73
87
|
`${manifest.island} declares no state ${wanted === '' ? '(none named)' : wanted} in theme ${address.theme}`,
|
|
74
|
-
|
|
88
|
+
// Every value here is read off a states file on disk: quoted where it enters the line,
|
|
89
|
+
// and one opening with `-` is the placeholder — quoting does not stop a flag (L3).
|
|
90
|
+
`x shot --island ${quoteArg(unflagged(manifest.name, '<island>'))} --state ${quoteArg(unflagged(manifest.states[0]?.id ?? '<id>', '<id>'))} --json`,
|
|
75
91
|
);
|
|
76
92
|
}
|
|
77
93
|
// The chunk is looked up rather than built here: `x dev` already built every island at
|
|
@@ -81,7 +97,7 @@ export function islandHarnessRoutes(input: HarnessRouteInput): readonly Route[]
|
|
|
81
97
|
if (chunk === undefined) {
|
|
82
98
|
return refused(
|
|
83
99
|
`${manifest.island} is declared as an island's states file but this build has no chunk for it`,
|
|
84
|
-
`x g island ${manifest.name} --at ${manifest.island.split('/').slice(0, -1).join('/')}`,
|
|
100
|
+
`x g island ${quoteArg(unflagged(manifest.name, '<island>'))} --at ${quoteArg(unflagged(manifest.island.split('/').slice(0, -1).join('/'), '<dir>'))}`,
|
|
85
101
|
);
|
|
86
102
|
}
|
|
87
103
|
return html(harnessPage({ target, state: declared, entry: chunk.url }));
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// the path that serves a page. `@ultimat3/testing`'s `sealNetwork()` patches THIS process's
|
|
7
7
|
// `globalThis.fetch` and cannot reach the page's realm.
|
|
8
8
|
|
|
9
|
+
import { encodeRecordEnvelope, RECORDS_HEADER } from '@ultimat3/core';
|
|
9
10
|
import type { IslandRouteStub } from '@ultimat3/testing';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -26,6 +27,25 @@ export const HARNESS_GLOBAL = '__xShot';
|
|
|
26
27
|
*/
|
|
27
28
|
const embed = (value: unknown): string => JSON.stringify(value ?? null).replaceAll('<', '\\u003c');
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A stub as the page script answers it: `records` becomes the envelope core's own
|
|
32
|
+
* `encodeRecordEnvelope` builds, behind core's own `RECORDS_HEADER` — the server's two halves,
|
|
33
|
+
* never a second spelling of either in the browser string below (#683).
|
|
34
|
+
*/
|
|
35
|
+
const wireStub = (stub: IslandRouteStub): unknown => {
|
|
36
|
+
const { respond } = stub;
|
|
37
|
+
if (respond.kind !== 'records') return stub;
|
|
38
|
+
return {
|
|
39
|
+
match: stub.match,
|
|
40
|
+
respond: {
|
|
41
|
+
kind: 'json',
|
|
42
|
+
...(respond.status === undefined ? {} : { status: respond.status }),
|
|
43
|
+
body: encodeRecordEnvelope(respond.data, respond.records, respond.removed),
|
|
44
|
+
headers: { [RECORDS_HEADER]: '1' },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
29
49
|
export interface HarnessScriptOptions {
|
|
30
50
|
readonly stubs: readonly IslandRouteStub[];
|
|
31
51
|
/** The frozen instant, with an explicit offset — the manifest's `now`. */
|
|
@@ -44,7 +64,7 @@ export interface HarnessScriptOptions {
|
|
|
44
64
|
* a `pending` fixture, which is a state an author declares on purpose.
|
|
45
65
|
*/
|
|
46
66
|
const sealScript = (stubs: readonly IslandRouteStub[]): string => `
|
|
47
|
-
var W=window.${HARNESS_GLOBAL};var STUBS=${embed(stubs)};
|
|
67
|
+
var W=window.${HARNESS_GLOBAL};var STUBS=${embed(stubs.map(wireStub))};
|
|
48
68
|
function bump(){W.activity+=1}
|
|
49
69
|
function stubFor(method,path){var k=method.toUpperCase()+' '+path;
|
|
50
70
|
for(var i=0;i<STUBS.length;i+=1){if(k.indexOf(STUBS[i].match)===0)return STUBS[i].respond}
|
|
@@ -57,8 +77,9 @@ function answer(respond,k){
|
|
|
57
77
|
if(respond===null)return Promise.reject(refuse(k));
|
|
58
78
|
if(respond.kind==='pending')return new Promise(function(){});
|
|
59
79
|
if(respond.kind==='offline')return Promise.reject(new TypeError('x shot: offline fixture for '+k));
|
|
80
|
+
var h=Object.assign({'content-type':'application/json'},respond.headers||{});
|
|
60
81
|
return Promise.resolve(new Response(JSON.stringify(respond.body===undefined?null:respond.body),
|
|
61
|
-
{status:respond.status||200,headers:
|
|
82
|
+
{status:respond.status||200,headers:h}))}
|
|
62
83
|
window.fetch=function(input,init){
|
|
63
84
|
var url=typeof input==='string'?input:(input&&input.url)||String(input);
|
|
64
85
|
var method=(init&&init.method)||(typeof input==='object'&&input&&input.method)||'GET';
|
package/src/island-identity.ts
CHANGED
|
@@ -24,14 +24,16 @@ export function stripDebugId(code: string): string {
|
|
|
24
24
|
/**
|
|
25
25
|
* The chunk's identity, computed from what went IN rather than from what came out.
|
|
26
26
|
*
|
|
27
|
-
* `Bun.build`
|
|
28
|
-
* source file touched: a 131,589-byte island alternated between two
|
|
29
|
-
* differing only in minified identifier names
|
|
30
|
-
* `var la=…`) — roughly one build in ten,
|
|
31
|
-
* can order. Hashing that output made the URL flap: ten distinct
|
|
32
|
-
* ten minutes, so a service worker's precache manifest named a
|
|
33
|
-
* browser's `immutable` cache never hit on a 131 kB download.
|
|
34
|
-
*
|
|
27
|
+
* `Bun.build` was not byte-deterministic under `minify` before 1.4.1 (oven-sh/bun#40657). Measured
|
|
28
|
+
* on 1.4.0, one entry point, no source file touched: a 131,589-byte island alternated between two
|
|
29
|
+
* outputs of IDENTICAL length differing only in minified identifier names
|
|
30
|
+
* (`var ca=Object.defineProperty` against `var la=…`) — roughly one build in ten, a race in the
|
|
31
|
+
* renamer and not anything a caller can order. Hashing that output made the URL flap: ten distinct
|
|
32
|
+
* `session-console-*.js` names in ten minutes, so a service worker's precache manifest named a
|
|
33
|
+
* chunk that already 404ed and a browser's `immutable` cache never hit on a 131 kB download.
|
|
34
|
+
* Fixed upstream in 1.4.1; on 1.4.2, 192 child-process build pairs of `examples/dummy`'s islands
|
|
35
|
+
* under 12- and 16-way contention emitted one byte string per island (2026-10-05, issue #354), and
|
|
36
|
+
* `island-bytes.test.ts` now demands byte identity outright.
|
|
35
37
|
*
|
|
36
38
|
* `sourcesContent`, hashed per file and SORTED, so the identity is independent of the order the
|
|
37
39
|
* bundler happened to visit the graph in. The PATHS are deliberately not in it: they are absolute
|
|
@@ -41,12 +43,13 @@ export function stripDebugId(code: string): string {
|
|
|
41
43
|
* version are, because both decide the emitted bytes while no source file moves — an upgrade must
|
|
42
44
|
* mint a new URL rather than leave a stale chunk pinned in a browser for a year.
|
|
43
45
|
*
|
|
44
|
-
* What this gives up, stated plainly: the URL is source-addressed, not byte-addressed
|
|
45
|
-
* processes building the same sources
|
|
46
|
-
* program under different local identifier names
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* What this gives up, stated plainly: the URL is source-addressed, not byte-addressed. On a Bun
|
|
47
|
+
* with the renamer race (< 1.4.1) two processes building the same sources served two byte-strings
|
|
48
|
+
* at one URL — the same program under different local identifier names — and the alternative,
|
|
49
|
+
* `minify: { identifiers: false }`, was +47% raw and +20% gzipped. The `engines` floor is now past
|
|
50
|
+
* the fix, so the reason this function exists is gone; it is kept for now because moving every
|
|
51
|
+
* island URL to a content hash re-keys every browser cache and every precache manifest, which is a
|
|
52
|
+
* change of its own to make deliberately (issue #354's follow-up), not a comment edit.
|
|
50
53
|
*/
|
|
51
54
|
export function graphHash(file: string, map: string): string {
|
|
52
55
|
const parsed: unknown = JSON.parse(map);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// One copy of each framework package per island chunk: the app's. `island-solid-dedupe.ts`'s rule,
|
|
2
|
+
// for `@ultimat3/*`. The framework ships in lockstep, so the copy the APP installed is the right
|
|
3
|
+
// one for every importer, and a second copy is a second registry, never a feature.
|
|
4
|
+
//
|
|
5
|
+
// Without it `Bun.build` resolves each import from the importing file, so a package nested under
|
|
6
|
+
// another (`node_modules/@ultimat3/ui/node_modules/@ultimat3/core`) is a second module. Linux hid
|
|
7
|
+
// it: Bun links a `file:` package file by file with symlinks, and a realpath folds every nest into
|
|
8
|
+
// one. Windows links without symlinks, so the scaffold smoke's `/posts` island shipped `core` and
|
|
9
|
+
// `schema` more than once: 82,106 B against 60,649 B on Linux, over a 64kb budget.
|
|
10
|
+
// Reproduced on Linux by replacing those links with copies: 75,023 B, and 59,926 B with this plugin.
|
|
11
|
+
|
|
12
|
+
// why: Bun ships no path API and no existence check; `dirname` walks up the way Node resolution
|
|
13
|
+
// does, `join` builds each candidate, and `existsSync` asks whether one is installed.
|
|
14
|
+
import { existsSync, lstatSync, readdirSync, realpathSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import type { BunPlugin } from 'bun';
|
|
17
|
+
import { conditionTarget } from './island-solid-dedupe';
|
|
18
|
+
|
|
19
|
+
/** `@ultimat3/<name>` and every subpath of it. */
|
|
20
|
+
export const FRAMEWORK_SPECIFIER = /^@ultimat3\/[^/]+(?:\/.*)?$/;
|
|
21
|
+
|
|
22
|
+
/** `@ultimat3/core/page` → `{ name: '@ultimat3/core', subpath: './page' }`. */
|
|
23
|
+
export function splitFrameworkSpecifier(specifier: string): {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly subpath: string;
|
|
26
|
+
} {
|
|
27
|
+
const [scope = '', pkg = '', ...rest] = specifier.split('/');
|
|
28
|
+
return { name: `${scope}/${pkg}`, subpath: rest.length === 0 ? '.' : `./${rest.join('/')}` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AppPackage {
|
|
32
|
+
/** The installed package's directory — every export target is joined onto it. */
|
|
33
|
+
readonly dir: string;
|
|
34
|
+
/** Its `exports` map, verbatim. */
|
|
35
|
+
readonly exports: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The copy the app's own entry would find: `node_modules/<name>` in the app root, then each
|
|
40
|
+
* directory above it. `null` when none is installed there — a package only nested under another
|
|
41
|
+
* is left to Bun, which resolves it from its importer as before.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveAppPackage(root: string, name: string): Promise<AppPackage | null> {
|
|
44
|
+
for (let dir = root; ; dir = dirname(dir)) {
|
|
45
|
+
const manifest = join(dir, 'node_modules', name, 'package.json');
|
|
46
|
+
if (existsSync(manifest)) {
|
|
47
|
+
const parsed = (await Bun.file(manifest).json()) as { readonly exports?: unknown };
|
|
48
|
+
// The REAL directory: Bun keys a module by its realpath, so returning the `node_modules/…`
|
|
49
|
+
// spelling of a symlinked package would bundle it a second time beside every file reached
|
|
50
|
+
// through its real path — the duplication this plugin exists to remove.
|
|
51
|
+
return { dir: realpathSync(dirname(manifest)), exports: parsed.exports };
|
|
52
|
+
}
|
|
53
|
+
if (dirname(dir) === dir) return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Where `subpath` lands in the app's copy, under the browser conditions, or `undefined`. */
|
|
58
|
+
export function appPackagePath(pkg: AppPackage, subpath: string): string | undefined {
|
|
59
|
+
const exports = pkg.exports;
|
|
60
|
+
if (exports === null || typeof exports !== 'object' || !Object.hasOwn(exports, subpath)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const target = conditionTarget((exports as Record<string, unknown>)[subpath]);
|
|
64
|
+
return target === undefined ? undefined : join(pkg.dir, target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Installed beside `solidDedupePlugin`, before any plugin loads a file. One lookup per package. */
|
|
68
|
+
export function frameworkDedupePlugin(root: string): BunPlugin {
|
|
69
|
+
const found = new Map<string, Promise<AppPackage | null>>();
|
|
70
|
+
return {
|
|
71
|
+
name: 'ultimate-framework-dedupe',
|
|
72
|
+
setup(build): void {
|
|
73
|
+
build.onResolve({ filter: FRAMEWORK_SPECIFIER }, async (args) => {
|
|
74
|
+
const { name, subpath } = splitFrameworkSpecifier(args.path);
|
|
75
|
+
let pending = found.get(name);
|
|
76
|
+
if (pending === undefined) {
|
|
77
|
+
pending = resolveAppPackage(root, name);
|
|
78
|
+
found.set(name, pending);
|
|
79
|
+
}
|
|
80
|
+
const pkg = await pending;
|
|
81
|
+
const path = pkg === null ? undefined : appPackagePath(pkg, subpath);
|
|
82
|
+
if (path === undefined) return undefined;
|
|
83
|
+
// Only when Bun's own answer is a SECOND copy, or this file under a second spelling
|
|
84
|
+
// (`sameModule`). A path a plugin returns loses its package's `sideEffects`, so taking over
|
|
85
|
+
// every import shipped ~2.2 KB of untree-shaken core in each island of examples/dummy
|
|
86
|
+
// (21,945 → 24,202 B on /pricing) while folding nothing on Linux.
|
|
87
|
+
return sameModule(path, args.path, args.importer) ? undefined : { path };
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether Bun, left alone, resolves `specifier` from `importer` to `path` itself — as the same
|
|
95
|
+
* STRING, because Bun keys a module by its path string, never by the file. This compared realpaths
|
|
96
|
+
* once, which is right on Linux (Bun's resolver answers a realpath, so the strings agree) and wrong
|
|
97
|
+
* on Windows, where it answers the junction spelling: both sides realpath to one file, Bun kept its
|
|
98
|
+
* own spelling, every nest this plugin folded got the realpath one, and one file shipped as two
|
|
99
|
+
* modules (the windows job's `/posts`, 81,306 B against Linux's 59,849 B). `resolve` is injectable
|
|
100
|
+
* so that spelling can be handed in on a host whose resolver never produces it.
|
|
101
|
+
*/
|
|
102
|
+
export function sameModule(
|
|
103
|
+
path: string,
|
|
104
|
+
specifier: string,
|
|
105
|
+
importer: string,
|
|
106
|
+
resolve: (specifier: string, from: string) => string = Bun.resolveSync,
|
|
107
|
+
): boolean {
|
|
108
|
+
try {
|
|
109
|
+
return resolve(specifier, dirname(importer)) === path;
|
|
110
|
+
} catch {
|
|
111
|
+
// Unresolvable from the importer: the app's copy is the only answer there is.
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether any installed `@ultimat3/*` package carries a NESTED framework package that is a real
|
|
118
|
+
* directory rather than a symlink — a second copy no realpath folds, which is a `file:` install on
|
|
119
|
+
* Windows. Only then is the plugin installed: Bun stops honouring a package's `sideEffects` for
|
|
120
|
+
* every import an `onResolve` hook sees, even one it answers `undefined` to, so a hook on a tree
|
|
121
|
+
* with nothing to fold cost each island of examples/dummy ~2.2 KB (21,945 → 24,202 B on /pricing).
|
|
122
|
+
*/
|
|
123
|
+
export function hasNestedFrameworkCopies(root: string): boolean {
|
|
124
|
+
for (let dir = root; ; dir = dirname(dir)) {
|
|
125
|
+
const scope = join(dir, 'node_modules', '@ultimat3');
|
|
126
|
+
if (existsSync(scope)) {
|
|
127
|
+
for (const pkg of readdirSync(scope)) {
|
|
128
|
+
const nested = join(scope, pkg, 'node_modules', '@ultimat3');
|
|
129
|
+
if (!existsSync(nested)) continue;
|
|
130
|
+
for (const inner of readdirSync(nested)) {
|
|
131
|
+
if (!lstatSync(join(nested, inner)).isSymbolicLink()) return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (dirname(dir) === dir) return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The plugin list entry: the dedupe when a nested copy exists, nothing otherwise. */
|
|
140
|
+
export function frameworkDedupePlugins(
|
|
141
|
+
root: string,
|
|
142
|
+
platform: NodeJS.Platform = process.platform,
|
|
143
|
+
): readonly BunPlugin[] {
|
|
144
|
+
// Windows always: a `file:` install there links packages through junctions Bun does not fold,
|
|
145
|
+
// so the same file is reached under two spellings — the windows job's `/posts` shipped 80.2kb
|
|
146
|
+
// against Linux's 60kb with only the nested-copy check deciding.
|
|
147
|
+
return platform === 'win32' || hasNestedFrameworkCopies(root)
|
|
148
|
+
? [frameworkDedupePlugin(root)]
|
|
149
|
+
: [];
|
|
150
|
+
}
|
package/src/island-realtime.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
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
|
|
3
|
-
// `installRealtime({ signal: createSignal })` with THIS
|
|
4
|
-
// island whole. Every other island
|
|
2
|
+
// graph reaches `@ultimat3/realtime` is built from a virtual entry that first awaits the page
|
|
3
|
+
// runtime (`island-runtime.ts`), then calls `installRealtime({ signal: createSignal })` with THIS
|
|
4
|
+
// bundle's solid-js, then re-exports the island whole. Every other island pays nothing.
|
|
5
5
|
|
|
6
|
-
// why: Bun ships no path API; the entry is joined to the root
|
|
7
|
-
import {
|
|
6
|
+
// why: Bun ships no path API; the entry is joined to the root, the runtime named by its basename.
|
|
7
|
+
import { basename, join } from 'node:path';
|
|
8
8
|
import type { BunPlugin } from 'bun';
|
|
9
|
+
import { RUNTIME_CHUNK_SPECIFIER, type RuntimeChunk } from './island-runtime';
|
|
9
10
|
import { firstInGraph } from './live-routes';
|
|
11
|
+
import { PAGE_BOOT_BASE_PATH } from './worker-bundle';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* What `Bun.build` is handed for a realtime island: this prefix plus the island's app-root-relative
|
|
@@ -22,6 +24,7 @@ const REALTIME = '@ultimat3/realtime';
|
|
|
22
24
|
const NAMESPACE = 'ultimate-island';
|
|
23
25
|
const INSTALL = 'ultimate:island-realtime';
|
|
24
26
|
const MODULE = 'ultimate:island-module:';
|
|
27
|
+
const WAIT = 'ultimate:island-page-wait';
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Whether the island's own graph value-imports realtime. Relative specifiers only, which is the
|
|
@@ -71,19 +74,31 @@ const entrySource = (file: string): string =>
|
|
|
71
74
|
* source is part of the chunk's `sourcesContent`, which is what `graphHash` names the URL from, so
|
|
72
75
|
* a path in it gave the same island a different URL in every checkout and every image build.
|
|
73
76
|
* `solid-js` goes through `island-solid-dedupe.ts` like every other import in the graph.
|
|
77
|
+
*
|
|
78
|
+
* The await comes first: no hook may run before the page runtime is installed, and the module
|
|
79
|
+
* graph is not done — so `mount` is not called — until it is. The page boot carries the runtime
|
|
80
|
+
* on a document rendered for a principal; anywhere else the island loads the runtime chunk, by a
|
|
81
|
+
* path relative to its own URL, so it resolves under `/islands/` and beside it in `mountIsland`.
|
|
82
|
+
* The chunk's URL is in this source, so a new runtime is a new URL for every island loading it.
|
|
83
|
+
* Then the first-paint hold (#506): `mount` waits for the restored records and the open outbox
|
|
84
|
+
* (capped), so a reload's first render already carries a queued write's overlay.
|
|
74
85
|
*/
|
|
75
|
-
const
|
|
76
|
-
`import { installRealtime } from '${REALTIME}';\n` +
|
|
86
|
+
const installSource = (runtimeUrl: string): string =>
|
|
87
|
+
`import { holdFirstPaint, installRealtime } from '${REALTIME}';\n` +
|
|
88
|
+
`import { awaitPageRuntime } from '${WAIT}';\n` +
|
|
77
89
|
`import { createSignal } from 'solid-js';\n` +
|
|
90
|
+
`await awaitPageRuntime({ boot: '${PAGE_BOOT_BASE_PATH}/', load: () => import('./${basename(runtimeUrl)}') });\n` +
|
|
91
|
+
`await holdFirstPaint();\n` +
|
|
78
92
|
`installRealtime({ signal: createSignal });\n`;
|
|
79
93
|
|
|
80
94
|
/**
|
|
81
|
-
* `
|
|
82
|
-
*
|
|
83
|
-
*
|
|
95
|
+
* `runtime` names the realtime every bare `@ultimat3/realtime` of the build resolves to — the copy
|
|
96
|
+
* the runtime chunk was built against, resolved from the first realtime island's directory. One
|
|
97
|
+
* copy for all of them, because one copy in the bundle is the requirement: the signal the install
|
|
98
|
+
* sets is only read by hooks from the same module instance, and the page object it reads is the
|
|
99
|
+
* shape that copy's runtime installs.
|
|
84
100
|
*/
|
|
85
|
-
export function islandRealtimePlugin(root: string,
|
|
86
|
-
const island = join(root, file);
|
|
101
|
+
export function islandRealtimePlugin(root: string, runtime: RuntimeChunk): BunPlugin {
|
|
87
102
|
return {
|
|
88
103
|
name: 'ultimate-island-realtime',
|
|
89
104
|
setup(build) {
|
|
@@ -98,16 +113,21 @@ export function islandRealtimePlugin(root: string, file: string): BunPlugin {
|
|
|
98
113
|
build.onResolve({ filter: /^ultimate:island-module:/ }, (args) => ({
|
|
99
114
|
path: join(root, args.path.slice(MODULE.length)),
|
|
100
115
|
}));
|
|
116
|
+
// The wait is realtime's, from the copy the hooks are bundled from; the runtime chunk is
|
|
117
|
+
// another file of this build's output, fetched by the browser and never bundled in.
|
|
118
|
+
build.onResolve({ filter: /^ultimate:island-page-wait$/ }, () => ({ path: runtime.wait }));
|
|
119
|
+
build.onResolve({ filter: RUNTIME_CHUNK_SPECIFIER }, (args) => ({
|
|
120
|
+
path: args.path,
|
|
121
|
+
external: true,
|
|
122
|
+
}));
|
|
101
123
|
// Every bare `@ultimat3/realtime` in this build, the virtual install's included — a virtual
|
|
102
124
|
// module has no directory to resolve from. The island's directory for all of them, which is
|
|
103
125
|
// what the island itself would get.
|
|
104
|
-
build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({
|
|
105
|
-
path: Bun.resolveSync(REALTIME, dirname(island)),
|
|
106
|
-
}));
|
|
126
|
+
build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({ path: runtime.realtime }));
|
|
107
127
|
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, (args) => ({
|
|
108
128
|
contents: args.path.startsWith('entry:')
|
|
109
129
|
? entrySource(args.path.slice('entry:'.length))
|
|
110
|
-
:
|
|
130
|
+
: installSource(runtime.url),
|
|
111
131
|
loader: 'js',
|
|
112
132
|
}));
|
|
113
133
|
},
|
package/src/island-routes.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// would be a page that boots in `x dev` and 404s in the image.
|
|
4
4
|
|
|
5
5
|
import type { Route, UltimateRequest } from '@ultimat3/http';
|
|
6
|
-
import { applyCacheHeaders,
|
|
6
|
+
import { applyCacheHeaders, jsonResponse, NO_STORE } from '@ultimat3/http';
|
|
7
7
|
import type { IslandBundle } from './island-bundle';
|
|
8
8
|
import { ISLAND_BASE_PATH } from './island-bundle';
|
|
9
9
|
|
|
@@ -30,7 +30,7 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
|
|
|
30
30
|
// have to answer at the one base path, under the one cache rule.
|
|
31
31
|
const chunk = source().assetAt(request.pathname);
|
|
32
32
|
if (chunk === undefined) {
|
|
33
|
-
const missing =
|
|
33
|
+
const missing = jsonResponse(
|
|
34
34
|
{
|
|
35
35
|
ok: false,
|
|
36
36
|
error: {
|