@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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// The page runtime as ONE chunk beside the islands (#505): realtime's `page-runtime.ts` — the store,
|
|
2
|
+
// the socket stack, the query client, core's transport — built once per build, addressed by its
|
|
3
|
+
// source graph like any island, and loaded by a realtime island only on a document the page boot
|
|
4
|
+
// is absent from. Islands carry thin hooks over it; none carries a copy of it.
|
|
5
|
+
|
|
6
|
+
// why: Bun ships no path API; the runtime is resolved beside the realtime the islands bundle.
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import type { BunPlugin } from 'bun';
|
|
9
|
+
import { IslandBuildFailedError } from './errors';
|
|
10
|
+
import type { IslandChunk, SharedChunk } from './island-bundle';
|
|
11
|
+
import { realSpelling } from './island-duplicates';
|
|
12
|
+
import { describeBuildError, graphHash, stableChunk, stripDebugId } from './island-identity';
|
|
13
|
+
import { frameworkDedupePlugins } from './island-package-dedupe';
|
|
14
|
+
import type { SourcePaths } from './island-sources';
|
|
15
|
+
import { sourcesOnDisk } from './island-sources';
|
|
16
|
+
|
|
17
|
+
/** The runtime, as realtime exports it — resolved from the copy every realtime island bundles. */
|
|
18
|
+
const RUNTIME_SPECIFIER = '@ultimat3/realtime/page-runtime';
|
|
19
|
+
|
|
20
|
+
/** What an island awaits before its first hook: realtime's own, from the same copy. */
|
|
21
|
+
const WAIT_SPECIFIER = '@ultimat3/realtime/page-runtime-wait';
|
|
22
|
+
|
|
23
|
+
/** The virtual entry the runtime is built from, and the module it installs from. */
|
|
24
|
+
const ENTRY = 'ultimate:realtime-runtime';
|
|
25
|
+
const MODULE = 'ultimate:realtime-runtime-module';
|
|
26
|
+
const NAMESPACE = 'ultimate-realtime-runtime';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `page-runtime.<identity>.js`. The dot is the point: an island entry is `<moduleId>-<identity>.js`
|
|
30
|
+
* and a module id is `[a-z0-9-]` only (`islandModuleId`), so no island — not even one named
|
|
31
|
+
* `realtime.island.tsx` — can be served at a URL this name could be.
|
|
32
|
+
*/
|
|
33
|
+
const RUNTIME_CHUNK_FILE = /^page-runtime\.[0-9a-z]+\.js$/;
|
|
34
|
+
|
|
35
|
+
/** The relative specifier a realtime island's bootstrap `import()`s the runtime chunk by. */
|
|
36
|
+
export const RUNTIME_CHUNK_SPECIFIER = /^\.\/page-runtime\.[0-9a-z]+\.js$/;
|
|
37
|
+
|
|
38
|
+
/** Whether `url` (any base path) is the page runtime's chunk — the one predicate, never a prefix. */
|
|
39
|
+
export function isRuntimeChunk(url: string): boolean {
|
|
40
|
+
const slash = url.lastIndexOf('/');
|
|
41
|
+
return slash !== -1 && RUNTIME_CHUNK_FILE.test(url.slice(slash + 1));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The page runtime a realtime island loads — built once per `buildIslands` call. */
|
|
45
|
+
export type Runtimes = (island: string) => Promise<RuntimeChunk>;
|
|
46
|
+
|
|
47
|
+
/** One runtime chunk: served beside the islands, never a `data-x-entry` of its own. */
|
|
48
|
+
export interface RuntimeChunk {
|
|
49
|
+
/**
|
|
50
|
+
* The realtime barrel it was built against — the ONE copy every realtime island of the build
|
|
51
|
+
* bundles its hooks from, or the page object the runtime installs is not the shape they read.
|
|
52
|
+
*/
|
|
53
|
+
readonly realtime: string;
|
|
54
|
+
/** What an island's bootstrap awaits the runtime with: `page-runtime-wait.ts`, of that copy. */
|
|
55
|
+
readonly wait: string;
|
|
56
|
+
readonly url: string;
|
|
57
|
+
readonly code: string;
|
|
58
|
+
readonly bytes: number;
|
|
59
|
+
readonly sources: SourcePaths;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the runtime at most once per build: every realtime island of one `buildIslands` call loads
|
|
64
|
+
* the SAME chunk, whether the islands are built together or one by one. The first island's realtime
|
|
65
|
+
* copy is the build's; another copy at the SAME version is the same code and folds onto it (a
|
|
66
|
+
* Windows `file:` install copies a package where a symlink would fold it). Another VERSION is
|
|
67
|
+
* refused: the first runtime to install wins the page, so that copy's islands would call services
|
|
68
|
+
* of a shape they were not built against.
|
|
69
|
+
*/
|
|
70
|
+
export function runtimeBuilder(root: string, basePath: string): Runtimes {
|
|
71
|
+
let first: { readonly copy: RealtimeCopy; readonly chunk: Promise<RuntimeChunk> } | undefined;
|
|
72
|
+
return async (island: string): Promise<RuntimeChunk> => {
|
|
73
|
+
// From the island's own directory: the resolution the island itself gets — by its REAL path,
|
|
74
|
+
// the spelling the framework dedupe gives every other `@ultimat3/*` file. Windows' resolver
|
|
75
|
+
// answers a junction's spelling, and Bun keys a module by its path string, so the barrel's own
|
|
76
|
+
// relative imports would ship again beside the realpath ones (`island-duplicates.ts`).
|
|
77
|
+
const copy = await realtimeCopy(
|
|
78
|
+
island,
|
|
79
|
+
realSpelling(Bun.resolveSync('@ultimat3/realtime', dirname(join(root, island)))),
|
|
80
|
+
);
|
|
81
|
+
if (first === undefined) {
|
|
82
|
+
first = { copy, chunk: buildRuntime(root, island, copy.barrel, basePath) };
|
|
83
|
+
} else if (first.copy.version !== copy.version) {
|
|
84
|
+
throw new IslandBuildFailedError({
|
|
85
|
+
file: island,
|
|
86
|
+
logs: `it resolves @ultimat3/realtime ${copy.version} (${copy.dir}) while another island of this build resolves ${first.copy.version} (${first.copy.dir}): one page runs ONE runtime, and @ultimat3/realtime is released in lockstep, so install one version of it across the apps`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return first.chunk;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface RealtimeCopy {
|
|
94
|
+
/** The barrel the island resolved. */
|
|
95
|
+
readonly barrel: string;
|
|
96
|
+
/** The package's directory, for the refusal. */
|
|
97
|
+
readonly dir: string;
|
|
98
|
+
readonly version: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The installed package the barrel belongs to: the nearest `package.json` above it. */
|
|
102
|
+
async function realtimeCopy(island: string, barrel: string): Promise<RealtimeCopy> {
|
|
103
|
+
for (let dir = dirname(barrel); dir !== dirname(dir); dir = dirname(dir)) {
|
|
104
|
+
const manifest = Bun.file(join(dir, 'package.json'));
|
|
105
|
+
if (!(await manifest.exists())) continue;
|
|
106
|
+
const parsed: unknown = await manifest.json();
|
|
107
|
+
const version =
|
|
108
|
+
typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'version') : undefined;
|
|
109
|
+
return { barrel, dir, version: typeof version === 'string' ? version : 'an unversioned copy' };
|
|
110
|
+
}
|
|
111
|
+
throw new IslandBuildFailedError({
|
|
112
|
+
file: island,
|
|
113
|
+
logs: `the @ultimat3/realtime it resolves (${barrel}) has no package.json above it, so its version cannot be checked against the build's`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A subpath of the realtime at `realtime`, through its `exports` — never a path into its source. */
|
|
118
|
+
function exported(island: string, realtime: string, specifier: string): string {
|
|
119
|
+
try {
|
|
120
|
+
return realSpelling(Bun.resolveSync(specifier, dirname(realtime)));
|
|
121
|
+
} catch {
|
|
122
|
+
throw new IslandBuildFailedError({
|
|
123
|
+
file: island,
|
|
124
|
+
logs: `the realtime it resolves (${realtime}) exports no ${specifier}: @ultimat3/realtime and @ultimat3/cli are released in lockstep, so install the same version of both`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function buildRuntime(
|
|
130
|
+
root: string,
|
|
131
|
+
island: string,
|
|
132
|
+
realtime: string,
|
|
133
|
+
basePath: string,
|
|
134
|
+
): Promise<RuntimeChunk> {
|
|
135
|
+
const file = exported(island, realtime, RUNTIME_SPECIFIER);
|
|
136
|
+
const wait = exported(island, realtime, WAIT_SPECIFIER);
|
|
137
|
+
let built: Awaited<ReturnType<typeof Bun.build>>;
|
|
138
|
+
try {
|
|
139
|
+
built = await Bun.build({
|
|
140
|
+
entrypoints: [ENTRY],
|
|
141
|
+
root,
|
|
142
|
+
target: 'browser',
|
|
143
|
+
format: 'esm',
|
|
144
|
+
minify: true,
|
|
145
|
+
plugins: [runtimeEntryPlugin(file), ...frameworkDedupePlugins(root)],
|
|
146
|
+
// `island-bundle.ts`'s reasons, verbatim: only ever built to be shipped, and the map's
|
|
147
|
+
// `sourcesContent` is the one stable identity a minified bundle has.
|
|
148
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
149
|
+
sourcemap: 'external',
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new IslandBuildFailedError({ file: island, logs: describeBuildError(error) });
|
|
153
|
+
}
|
|
154
|
+
const output = built.outputs.find((artifact) => artifact.kind === 'entry-point');
|
|
155
|
+
const map = built.outputs.find((artifact) => artifact.kind === 'sourcemap');
|
|
156
|
+
if (!built.success || output === undefined || map === undefined) {
|
|
157
|
+
throw new IslandBuildFailedError({
|
|
158
|
+
file: island,
|
|
159
|
+
logs: built.logs.map((log) => String(log)).join('; '),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const text = await map.text();
|
|
163
|
+
const identity = graphHash(ENTRY, text);
|
|
164
|
+
const url = `${basePath}/page-runtime.${identity}.js`;
|
|
165
|
+
return {
|
|
166
|
+
realtime,
|
|
167
|
+
wait,
|
|
168
|
+
url,
|
|
169
|
+
...stableChunk(url, identity, stripDebugId(await output.text())),
|
|
170
|
+
sources: await sourcesOnDisk(root, JSON.parse(text)),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The entry installs the runtime as its whole body; the module is named BARE in that source and
|
|
176
|
+
* resolved here, because the source is part of `sourcesContent` and a machine path in it would give
|
|
177
|
+
* one runtime a different URL in every checkout (the reason `island-realtime.ts` names realtime).
|
|
178
|
+
*/
|
|
179
|
+
function runtimeEntryPlugin(file: string): BunPlugin {
|
|
180
|
+
return {
|
|
181
|
+
name: 'ultimate-realtime-runtime',
|
|
182
|
+
setup(build) {
|
|
183
|
+
build.onResolve({ filter: /^ultimate:realtime-runtime$/ }, () => ({
|
|
184
|
+
path: 'entry',
|
|
185
|
+
namespace: NAMESPACE,
|
|
186
|
+
}));
|
|
187
|
+
build.onResolve({ filter: /^ultimate:realtime-runtime-module$/ }, () => ({ path: file }));
|
|
188
|
+
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, () => ({
|
|
189
|
+
contents: `import { installPageRuntime } from '${MODULE}';\ninstallPageRuntime();\n`,
|
|
190
|
+
loader: 'js',
|
|
191
|
+
}));
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Every realtime entry names the runtime among what it loads: a precache and a budget count it. */
|
|
197
|
+
export function loadingRuntime(
|
|
198
|
+
chunks: readonly IslandChunk[],
|
|
199
|
+
live: readonly string[],
|
|
200
|
+
runtime: RuntimeChunk | undefined,
|
|
201
|
+
): readonly IslandChunk[] {
|
|
202
|
+
if (runtime === undefined) return chunks;
|
|
203
|
+
return chunks.map((chunk) =>
|
|
204
|
+
live.includes(chunk.file)
|
|
205
|
+
? { ...chunk, imports: [...new Set([...chunk.imports, runtime.url])].sort() }
|
|
206
|
+
: chunk,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The runtime as the bundle's shared asset — served, written and precached beside the entries. */
|
|
211
|
+
export function runtimeShared(
|
|
212
|
+
runtime: RuntimeChunk | undefined,
|
|
213
|
+
live: readonly string[],
|
|
214
|
+
): readonly SharedChunk[] {
|
|
215
|
+
if (runtime === undefined) return [];
|
|
216
|
+
const { url, code, bytes, sources } = runtime;
|
|
217
|
+
return [{ url, code, bytes, importers: [...live].sort(), sources }];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* One-island builds as one bundle's two tables: every entry, and each shared asset once by URL with
|
|
222
|
+
* its importers the union — each solo build names the runtime for itself.
|
|
223
|
+
*/
|
|
224
|
+
export function mergeBuilds(
|
|
225
|
+
builds: readonly {
|
|
226
|
+
readonly chunks: readonly IslandChunk[];
|
|
227
|
+
readonly shared: readonly SharedChunk[];
|
|
228
|
+
}[],
|
|
229
|
+
): [readonly IslandChunk[], readonly SharedChunk[]] {
|
|
230
|
+
const byUrl = new Map<string, SharedChunk>();
|
|
231
|
+
for (const one of builds.flatMap((build) => build.shared)) {
|
|
232
|
+
const seen = byUrl.get(one.url);
|
|
233
|
+
byUrl.set(
|
|
234
|
+
one.url,
|
|
235
|
+
seen === undefined
|
|
236
|
+
? one
|
|
237
|
+
: { ...seen, importers: [...new Set([...seen.importers, ...one.importers])].sort() },
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return [builds.flatMap((build) => build.chunks), [...byUrl.values()]];
|
|
241
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The files on disk an island chunk was built from, and whether they still hold those bytes. The
|
|
2
|
+
// prebuilt store (`island-store.ts`) verified only its own chunk bytes, so a source edited after
|
|
3
|
+
// `x build --target prebuilt` still verified and a boot served the chunk built from the old one.
|
|
4
|
+
|
|
5
|
+
// why: Bun ships no path join/resolve/relative primitive.
|
|
6
|
+
import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { contentHash } from '@ultimat3/render/server';
|
|
8
|
+
import { hasPathSegment } from './path-segments';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* App-root-relative POSIX paths of the files an output was built from — its source map's
|
|
12
|
+
* `sources`, outside `node_modules` and inside the app root.
|
|
13
|
+
*/
|
|
14
|
+
export type SourcePaths = readonly string[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A path the store may record and a boot may read: app-root-relative, normalized, inside the
|
|
18
|
+
* root, outside `node_modules`. The writer filters on it and the reader REFUSES on it — the index is
|
|
19
|
+
* a file on disk, and a path read at every boot must never be `../../etc/hostname` or `/dev/zero`.
|
|
20
|
+
*/
|
|
21
|
+
export function isRecordableSource(path: string): boolean {
|
|
22
|
+
return (
|
|
23
|
+
path !== '' &&
|
|
24
|
+
posix.normalize(path) === path &&
|
|
25
|
+
!path.startsWith('..') &&
|
|
26
|
+
!isAbsolute(path) &&
|
|
27
|
+
!path.includes('\\') &&
|
|
28
|
+
!hasPathSegment(path, 'node_modules')
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One recorded source: its path and the content digest of its bytes when the store was written. */
|
|
33
|
+
export interface SourceStamp {
|
|
34
|
+
readonly path: string;
|
|
35
|
+
readonly digest: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A source map's `sources` that are files in the app. A virtual module (the realtime wrapper) has
|
|
40
|
+
* no file; `node_modules` changes only with an install, and hashing it would make every pod boot
|
|
41
|
+
* pay for the dependency tree.
|
|
42
|
+
*/
|
|
43
|
+
export async function sourcesOnDisk(root: string, map: unknown): Promise<SourcePaths> {
|
|
44
|
+
const listed: unknown =
|
|
45
|
+
typeof map === 'object' && map !== null ? (map as Record<string, unknown>)['sources'] : [];
|
|
46
|
+
// Bun 1.4.2 writes each source relative to the BUILDING process's cwd, not to the map or to the
|
|
47
|
+
// build's `root` (measured: a build run from the repo root over a /tmp app names
|
|
48
|
+
// `../../tmp/…/plain.island.tsx`), so that is what they are resolved against.
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const paths = (Array.isArray(listed) ? listed : [])
|
|
51
|
+
.filter((one): one is string => typeof one === 'string')
|
|
52
|
+
.map((one) => relative(root, resolve(cwd, one)).split(sep).join('/'))
|
|
53
|
+
.filter(isRecordableSource);
|
|
54
|
+
const present = await Promise.all(paths.map((one) => Bun.file(join(root, one)).exists()));
|
|
55
|
+
return [...new Set(paths.filter((_, index) => present[index]))].sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Each path with the content digest of its bytes now — `undefined` for a file that is gone. */
|
|
59
|
+
export type SourceDigester = (path: string) => Promise<string | undefined>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A digester that reads each file once, however many chunks name it: a module shared by every
|
|
63
|
+
* island is one read per boot, not one per island.
|
|
64
|
+
*/
|
|
65
|
+
export function sourceDigester(root: string): SourceDigester {
|
|
66
|
+
const seen = new Map<string, Promise<string | undefined>>();
|
|
67
|
+
return (path) => {
|
|
68
|
+
let digested = seen.get(path);
|
|
69
|
+
if (digested === undefined) {
|
|
70
|
+
const file = Bun.file(join(root, path));
|
|
71
|
+
digested = file
|
|
72
|
+
.exists()
|
|
73
|
+
.then(async (exists) => (exists ? contentHash(await file.text()) : undefined));
|
|
74
|
+
seen.set(path, digested);
|
|
75
|
+
}
|
|
76
|
+
return digested;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What the store records for one chunk's sources. */
|
|
81
|
+
export async function stampSources(
|
|
82
|
+
paths: SourcePaths,
|
|
83
|
+
digestOf: SourceDigester,
|
|
84
|
+
): Promise<readonly SourceStamp[]> {
|
|
85
|
+
const stamps: SourceStamp[] = [];
|
|
86
|
+
for (const path of paths) {
|
|
87
|
+
const now = await digestOf(path);
|
|
88
|
+
if (now !== undefined) stamps.push({ path, digest: now });
|
|
89
|
+
}
|
|
90
|
+
return stamps;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The first recorded source that is gone or whose content digest moved, else `undefined`. A plain
|
|
95
|
+
* comparison: a digest of a source file the app commits is no secret, and nothing here is timed.
|
|
96
|
+
*/
|
|
97
|
+
export async function changedSource(
|
|
98
|
+
stamps: readonly SourceStamp[],
|
|
99
|
+
digestOf: SourceDigester,
|
|
100
|
+
): Promise<string | undefined> {
|
|
101
|
+
for (const stamp of stamps) {
|
|
102
|
+
if ((await digestOf(stamp.path)) !== stamp.digest) return stamp.path;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
@@ -53,6 +53,12 @@ export async function discoverIslandStates(root: string): Promise<readonly strin
|
|
|
53
53
|
async function manifestsIn(root: string, file: string): Promise<readonly IslandStatesManifest[]> {
|
|
54
54
|
const absolute = join(root, file);
|
|
55
55
|
assertIslandStatesPure(file, await Bun.file(absolute).text());
|
|
56
|
+
// Evicted first: Bun caches `import()` by path, and `x dev` loads the states on every shot
|
|
57
|
+
// request in one process — so an edited states file was never read again until a restart, and
|
|
58
|
+
// `app-load.ts`'s reload skips this file by name. The check above read the NEW text; this makes
|
|
59
|
+
// the import evaluate that same text. A states file imports no relative module (the purity rule
|
|
60
|
+
// refuses one), so the file itself is the whole of what can be stale.
|
|
61
|
+
delete require.cache[absolute];
|
|
56
62
|
const module: unknown = await import(absolute);
|
|
57
63
|
const exported = typeof module === 'object' && module !== null ? Object.values(module) : [];
|
|
58
64
|
const found = exported.filter(isIslandStatesManifest);
|
package/src/island-store.ts
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
import { join, relative, sep } from 'node:path'; // why: Bun ships no path-join primitive.
|
|
11
11
|
import { frameworkVersion } from '@ultimat3/core';
|
|
12
12
|
import { contentHash, loadStylesheet } from '@ultimat3/render/server';
|
|
13
|
+
import { IslandBuildFailedError } from './errors';
|
|
13
14
|
import type { IslandBundle, IslandChunk, SharedChunk } from './island-bundle';
|
|
14
15
|
import { buildIslands, discoverIslands, islandBundle } from './island-bundle';
|
|
16
|
+
import type { SourceDigester, SourceStamp } from './island-sources';
|
|
17
|
+
import { changedSource, isRecordableSource, sourceDigester, stampSources } from './island-sources';
|
|
15
18
|
import { islandStylesheets } from './island-styles';
|
|
16
19
|
import { PREBUILT_ISLANDS_DIR } from './serve-prebuilt-paths';
|
|
17
20
|
|
|
@@ -26,6 +29,8 @@ interface StoredChunk {
|
|
|
26
29
|
readonly identity: string;
|
|
27
30
|
/** The shared chunk URLs the entry loads, transitively (`IslandChunk.imports`). */
|
|
28
31
|
readonly imports: readonly string[];
|
|
32
|
+
/** The files it was built from, hashed at write — rehashed at read (`island-sources.ts`). */
|
|
33
|
+
readonly sources: readonly SourceStamp[];
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/** A shared chunk: no island of its own, verified by the same hash. */
|
|
@@ -33,6 +38,7 @@ interface StoredShared {
|
|
|
33
38
|
readonly url: string;
|
|
34
39
|
readonly identity: string;
|
|
35
40
|
readonly importers: readonly string[];
|
|
41
|
+
readonly sources: readonly SourceStamp[];
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
interface StoreIndex {
|
|
@@ -52,12 +58,32 @@ interface StoreIndex {
|
|
|
52
58
|
|
|
53
59
|
const chunkFile = (url: string): string => url.slice(url.lastIndexOf('/') + 1);
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Refuses a bundle in which an island's own file is a source of nothing it loads. Its file is
|
|
63
|
+
* always one — in its entry chunk, or in a shared chunk when another island imports it — so its
|
|
64
|
+
* absence means the map's paths stopped resolving into the root (`sourcesOnDisk`), and the store
|
|
65
|
+
* would record nothing to rehash: a boot would serve that chunk after any edit, the K8 bug again.
|
|
66
|
+
*/
|
|
67
|
+
function refuseUntracked(bundle: IslandBundle): void {
|
|
68
|
+
const shared = new Map(bundle.shared.map((one) => [one.url, one.sources ?? []]));
|
|
69
|
+
for (const chunk of bundle.chunks) {
|
|
70
|
+
const loaded = [chunk.sources ?? [], ...chunk.imports.map((url) => shared.get(url) ?? [])];
|
|
71
|
+
if (loaded.some((sources) => sources.includes(chunk.file))) continue;
|
|
72
|
+
throw new IslandBuildFailedError({
|
|
73
|
+
file: chunk.file,
|
|
74
|
+
logs: `neither its chunk nor a shared chunk it loads records ${chunk.file} as a source — the source map's paths, resolved against the build's cwd ${process.cwd()}, no longer land in the app root, so a prebuilt store would serve this chunk after the file is edited`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
/** Writes every chunk and the index that verifies them. Answers the files written, app-relative. */
|
|
56
80
|
export async function writeIslandStore(
|
|
57
81
|
root: string,
|
|
58
82
|
bundle: IslandBundle,
|
|
59
83
|
): Promise<readonly string[]> {
|
|
84
|
+
refuseUntracked(bundle);
|
|
60
85
|
const dir = join(root, ISLAND_STORE_DIR);
|
|
86
|
+
const digestOf = sourceDigester(root);
|
|
61
87
|
const chunks: StoredChunk[] = [];
|
|
62
88
|
const written: string[] = [];
|
|
63
89
|
for (const chunk of bundle.chunks) {
|
|
@@ -69,13 +95,19 @@ export async function writeIslandStore(
|
|
|
69
95
|
url: chunk.url,
|
|
70
96
|
identity: contentHash(chunk.code),
|
|
71
97
|
imports: chunk.imports,
|
|
98
|
+
sources: await stampSources(chunk.sources ?? [], digestOf),
|
|
72
99
|
});
|
|
73
100
|
}
|
|
74
101
|
const shared: StoredShared[] = [];
|
|
75
102
|
for (const chunk of bundle.shared) {
|
|
76
103
|
await Bun.write(join(dir, chunkFile(chunk.url)), chunk.code);
|
|
77
104
|
written.push(`${ISLAND_STORE_DIR}/${chunkFile(chunk.url)}`);
|
|
78
|
-
shared.push({
|
|
105
|
+
shared.push({
|
|
106
|
+
url: chunk.url,
|
|
107
|
+
identity: contentHash(chunk.code),
|
|
108
|
+
importers: chunk.importers,
|
|
109
|
+
sources: await stampSources(chunk.sources ?? [], digestOf),
|
|
110
|
+
});
|
|
79
111
|
}
|
|
80
112
|
const stylesheets = islandStylesheets()
|
|
81
113
|
.map((path) => relative(root, path).split(sep).join('/'))
|
|
@@ -97,8 +129,29 @@ const isString = (value: unknown): value is string => typeof value === 'string';
|
|
|
97
129
|
const stringsOf = (value: unknown): readonly string[] =>
|
|
98
130
|
Array.isArray(value) ? value.filter(isString) : [];
|
|
99
131
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
132
|
+
/**
|
|
133
|
+
* `undefined` when the field is absent or any entry is malformed or names a path outside the app:
|
|
134
|
+
* a store that cannot say what it was built from cannot be checked, which is stale — never
|
|
135
|
+
* "nothing to check".
|
|
136
|
+
*/
|
|
137
|
+
function stampsOf(value: unknown): readonly SourceStamp[] | undefined {
|
|
138
|
+
if (!Array.isArray(value)) return undefined;
|
|
139
|
+
const stamps: SourceStamp[] = [];
|
|
140
|
+
for (const entry of value) {
|
|
141
|
+
if (typeof entry !== 'object' || entry === null) return undefined;
|
|
142
|
+
const { path, digest } = entry as Record<string, unknown>;
|
|
143
|
+
// A path the writer could not have recorded is refused before anything reads it.
|
|
144
|
+
if (!isString(path) || !isString(digest) || !isRecordableSource(path)) return undefined;
|
|
145
|
+
stamps.push({ path, digest });
|
|
146
|
+
}
|
|
147
|
+
return stamps;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The index, narrowed field by field — a file on disk is input, never a trusted shape. `unchecked`
|
|
152
|
+
* names an output that records no sources: a store written before they were recorded.
|
|
153
|
+
*/
|
|
154
|
+
function parseIndex(value: unknown): StoreIndex | { readonly unchecked: string } | undefined {
|
|
102
155
|
if (typeof value !== 'object' || value === null) return undefined;
|
|
103
156
|
const record = value as Record<string, unknown>;
|
|
104
157
|
const chunks = record['chunks'];
|
|
@@ -112,14 +165,18 @@ function parseIndex(value: unknown): StoreIndex | undefined {
|
|
|
112
165
|
const { file, moduleId, url, identity, imports } = chunk;
|
|
113
166
|
if (!isString(file) || !isString(moduleId) || !isString(url) || !isString(identity))
|
|
114
167
|
return undefined;
|
|
115
|
-
|
|
168
|
+
const sources = stampsOf(chunk['sources']);
|
|
169
|
+
if (sources === undefined) return { unchecked: url };
|
|
170
|
+
parsed.push({ file, moduleId, url, identity, imports: stringsOf(imports), sources });
|
|
116
171
|
}
|
|
117
172
|
const shared: StoredShared[] = [];
|
|
118
173
|
for (const entry of Array.isArray(record['shared']) ? record['shared'] : []) {
|
|
119
174
|
if (typeof entry !== 'object' || entry === null) return undefined;
|
|
120
|
-
const { url, identity, importers } = entry as Record<string, unknown>;
|
|
175
|
+
const { url, identity, importers, sources: listed } = entry as Record<string, unknown>;
|
|
121
176
|
if (!isString(url) || !isString(identity)) return undefined;
|
|
122
|
-
|
|
177
|
+
const sources = stampsOf(listed);
|
|
178
|
+
if (sources === undefined) return { unchecked: url };
|
|
179
|
+
shared.push({ url, identity, importers: stringsOf(importers), sources });
|
|
123
180
|
}
|
|
124
181
|
const sheets = record['stylesheets'];
|
|
125
182
|
// Absent in a 22.3.2 store: read as none, and the boot builds when it matters (see below).
|
|
@@ -133,6 +190,18 @@ function parseIndex(value: unknown): StoreIndex | undefined {
|
|
|
133
190
|
};
|
|
134
191
|
}
|
|
135
192
|
|
|
193
|
+
/** The first output with a recorded source that is gone or no longer hashes the same. */
|
|
194
|
+
async function firstEdited(
|
|
195
|
+
outputs: readonly { readonly url: string; readonly sources: readonly SourceStamp[] }[],
|
|
196
|
+
digestOf: SourceDigester,
|
|
197
|
+
): Promise<{ readonly url: string; readonly path: string } | undefined> {
|
|
198
|
+
for (const output of outputs) {
|
|
199
|
+
const path = await changedSource(output.sources, digestOf);
|
|
200
|
+
if (path !== undefined) return { url: output.url, path };
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
136
205
|
/** A verified store, or the one sentence saying why it cannot be served. */
|
|
137
206
|
export type StoreRead =
|
|
138
207
|
| { readonly bundle: IslandBundle; readonly stale?: undefined }
|
|
@@ -144,6 +213,11 @@ export async function readIslandStore(root: string): Promise<StoreRead> {
|
|
|
144
213
|
if (!(await file.exists())) return { stale: `no ${ISLAND_STORE_DIR}/${INDEX}` };
|
|
145
214
|
const index = parseIndex(await file.json().catch(() => undefined));
|
|
146
215
|
if (index === undefined) return { stale: `${ISLAND_STORE_DIR}/${INDEX} does not parse` };
|
|
216
|
+
if ('unchecked' in index) {
|
|
217
|
+
return {
|
|
218
|
+
stale: `${index.unchecked} records no checkable sources, so an edit since it was built would be unseen`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
147
221
|
if (index.framework !== frameworkVersion() || index.bun !== Bun.version) {
|
|
148
222
|
return {
|
|
149
223
|
stale: `built by framework ${index.framework} on Bun ${index.bun}, serving ${frameworkVersion()} on Bun ${Bun.version}`,
|
|
@@ -154,6 +228,16 @@ export async function readIslandStore(root: string): Promise<StoreRead> {
|
|
|
154
228
|
if (stored.join('\n') !== present.join('\n')) {
|
|
155
229
|
return { stale: 'the stored islands are not the islands this app has' };
|
|
156
230
|
}
|
|
231
|
+
// Every recorded source first: a chunk whose bytes verify was still built from whatever the files
|
|
232
|
+
// said THEN. Only the recorded paths are read, each once, so a boot pays for the app's own island
|
|
233
|
+
// sources and nothing else.
|
|
234
|
+
const digestOf = sourceDigester(root);
|
|
235
|
+
const edited = await firstEdited([...index.chunks, ...index.shared], digestOf);
|
|
236
|
+
if (edited !== undefined) {
|
|
237
|
+
return {
|
|
238
|
+
stale: `${edited.path}, a source of ${edited.url}, changed since the store was built`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
157
241
|
const verified = async (entry: { readonly url: string; readonly identity: string }) => {
|
|
158
242
|
const bytes = Bun.file(join(dir, chunkFile(entry.url)));
|
|
159
243
|
const code = (await bytes.exists()) ? await bytes.text() : undefined;
|
package/src/jobs-json.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
StepTrace,
|
|
14
14
|
WorkerRecord,
|
|
15
15
|
} from '@ultimat3/jobs';
|
|
16
|
-
import type { DrainFailure, DrainSkip } from './jobs-drain';
|
|
17
16
|
import type { JsonValue } from './output';
|
|
18
17
|
|
|
19
18
|
function stepTraceToJson(step: StepTrace): JsonValue {
|
|
@@ -181,28 +180,3 @@ export function deadLetterToJson(entry: DeadLetterEntry): JsonValue {
|
|
|
181
180
|
retryCommand: entry.retryCommand,
|
|
182
181
|
};
|
|
183
182
|
}
|
|
184
|
-
|
|
185
|
-
export function drainFailureToJson(failure: DrainFailure): JsonValue {
|
|
186
|
-
return {
|
|
187
|
-
id: failure.id,
|
|
188
|
-
name: failure.name,
|
|
189
|
-
finding: {
|
|
190
|
-
code: failure.finding.code,
|
|
191
|
-
cause: failure.finding.cause,
|
|
192
|
-
fix: failure.finding.fix,
|
|
193
|
-
docs: failure.finding.docs ?? null,
|
|
194
|
-
at: failure.finding.at ?? null,
|
|
195
|
-
},
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** A candidate the drain refused to touch, and why — the half of the outcome `moved` cannot show. */
|
|
200
|
-
export function drainSkipToJson(skip: DrainSkip): JsonValue {
|
|
201
|
-
return {
|
|
202
|
-
id: skip.id,
|
|
203
|
-
name: skip.name,
|
|
204
|
-
queue: skip.queue,
|
|
205
|
-
state: skip.state,
|
|
206
|
-
reason: skip.reason,
|
|
207
|
-
};
|
|
208
|
-
}
|