@ultimat3/cli 22.11.0 → 22.13.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.
@@ -0,0 +1,158 @@
1
+ // An island output's identity and the one byte string served under it: the source-graph hash
2
+ // that names a URL (`graphHash`), the per-process pin that keeps one URL one byte string
3
+ // (`stableChunk`), and the bundler-diagnostic flattening every island refusal carries. Split from
4
+ // `island-bundle.ts`, which builds; `worker-bundle.ts` names its chunks by the same rules.
5
+
6
+ import { frameworkVersion, renderThrowable } from '@ultimat3/core';
7
+ import { contentHash } from '@ultimat3/render/server';
8
+ import { IslandBuildFailedError } from './errors';
9
+
10
+ /**
11
+ * `sourcemap: 'external'` appends `//# debugId=<hex>` to the chunk. It is a pointer to a map this
12
+ * framework does not serve, so it is removed rather than shipped — and removing it makes the
13
+ * emitted bytes identical to what the same build produced before the map was asked for, which is
14
+ * what keeps `bytes` a budget number and not a build-flag artefact. `slice`, never a `replace` with
15
+ * an empty replacement — `bun run sql-literal-copies` refuses that shape anywhere but `db/sql.ts`.
16
+ */
17
+ const DEBUG_ID_COMMENT = '\n//# debugId=';
18
+
19
+ export function stripDebugId(code: string): string {
20
+ const at = code.lastIndexOf(DEBUG_ID_COMMENT);
21
+ return at === -1 ? code : code.slice(0, at);
22
+ }
23
+
24
+ /**
25
+ * The chunk's identity, computed from what went IN rather than from what came out.
26
+ *
27
+ * `Bun.build` is not byte-deterministic under `minify`. Measured on 1.4.0, one entry point, no
28
+ * source file touched: a 131,589-byte island alternated between two outputs of IDENTICAL length
29
+ * differing only in minified identifier names (`var ca=Object.defineProperty` against
30
+ * `var la=…`) — roughly one build in ten, which is a race in the renamer and not anything a caller
31
+ * can order. Hashing that output made the URL flap: ten distinct `session-console-*.js` names in
32
+ * ten minutes, so a service worker's precache manifest named a chunk that already 404ed and a
33
+ * browser's `immutable` cache never hit on a 131 kB download. Twelve consecutive builds hash
34
+ * identically here.
35
+ *
36
+ * `sourcesContent`, hashed per file and SORTED, so the identity is independent of the order the
37
+ * bundler happened to visit the graph in. The PATHS are deliberately not in it: they are absolute
38
+ * on the build machine and would make a chunk built in a container disagree with the same chunk
39
+ * built on a laptop for no difference a browser could observe. `file` is, so two islands with
40
+ * byte-identical sources under different names stay two chunks; the framework version and the Bun
41
+ * version are, because both decide the emitted bytes while no source file moves — an upgrade must
42
+ * mint a new URL rather than leave a stale chunk pinned in a browser for a year.
43
+ *
44
+ * What this gives up, stated plainly: the URL is source-addressed, not byte-addressed, so two
45
+ * processes building the same sources can serve two byte-strings at one URL. They are the same
46
+ * program under different local identifier names. That is the trade a nondeterministic bundler
47
+ * forces, and the alternative — `minify: { identifiers: false }`, which IS deterministic — was
48
+ * measured at 193,590 bytes against 131,649, +47% raw and +20% gzipped, on every island of every
49
+ * app. Delete this the day `Bun.build` is deterministic.
50
+ */
51
+ export function graphHash(file: string, map: string): string {
52
+ const parsed: unknown = JSON.parse(map);
53
+ const contents = sourcesContentOf(parsed);
54
+ if (contents === undefined) {
55
+ throw new IslandBuildFailedError({
56
+ file,
57
+ logs: 'the bundler emitted a source map with no sourcesContent, so the chunk has no stable identity',
58
+ });
59
+ }
60
+ const graph = contents.map((source) => contentHash(source)).sort();
61
+ return contentHash([file, frameworkVersion(), Bun.version, ...graph].join('\u0000'));
62
+ }
63
+
64
+ /**
65
+ * `sourcesContent`, read the way `aggregatedErrors` below reads `errors`: narrowed first,
66
+ * dereferenced inside a `try`, `undefined` for anything that is not a full list of strings. A
67
+ * partial list is refused rather than padded — a graph with holes in it hashes two different
68
+ * islands the same.
69
+ */
70
+ export function sourcesContentOf(
71
+ value: unknown,
72
+ allowEmpty = false,
73
+ ): readonly string[] | undefined {
74
+ if (typeof value !== 'object' || value === null) return undefined;
75
+ try {
76
+ const held: unknown = (value as Record<string, unknown>)['sourcesContent'];
77
+ // Empty is admitted only for a split build's output, where a chunk of bundler runtime helpers
78
+ // has no source file behind it and `island-link.ts` names it by its code instead.
79
+ if (!Array.isArray(held) || (held.length === 0 && !allowEmpty)) return undefined;
80
+ return held.every((one: unknown) => typeof one === 'string')
81
+ ? (held as readonly string[])
82
+ : undefined;
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The code this process already emitted for these inputs, or the code it just built.
90
+ *
91
+ * Keyed by PATH and validated by the input hash, `transformIslandTsx`'s cache's shape and for its
92
+ * reason: one entry per island bounds the map by the island count, which is the only quantity that
93
+ * should bound it, and an entry whose hash no longer matches is replaced rather than served.
94
+ */
95
+ const emitted = new Map<string, { readonly graph: string; readonly code: string }>();
96
+
97
+ /** Test seam: the table is process-global because the dev server it serves is too. */
98
+ export function clearIslandChunkCache(): void {
99
+ emitted.clear();
100
+ }
101
+
102
+ /**
103
+ * `graph`, never `hash`: `bun run secret-compare` reads the NAME of a comparison's operands, and a
104
+ * value called `hash` is a digest an attacker may be probing. This one is a build input's
105
+ * identity — the same reason `pr-threads.ts` calls a review state `wanted`.
106
+ */
107
+ export function stableChunk(
108
+ file: string,
109
+ graph: string,
110
+ code: string,
111
+ ): { readonly code: string; readonly bytes: number } {
112
+ const hit = emitted.get(file);
113
+ const served = hit !== undefined && hit.graph === graph ? hit.code : code;
114
+ if (served === code) emitted.set(file, { graph, code });
115
+ // Measured on the code that is SERVED. It was measured on this build's output, which under a
116
+ // minifier that renames differently between builds is a second size for one URL in one process
117
+ // — and a budget weighed on bytes no browser receives.
118
+ return { code: served, bytes: new TextEncoder().encode(served).byteLength };
119
+ }
120
+
121
+ /**
122
+ * The bundler's own diagnostics, kept verbatim. An `AggregateError` holds one entry per unresolved
123
+ * import or syntax error, and flattening them is what puts the line number in the cause instead of
124
+ * the word "Bundle failed".
125
+ */
126
+ export function describeBuildError(error: unknown): string {
127
+ // `renderThrowable`, never `instanceof` + `.message` + `String()`. All three run on a value this
128
+ // process did not build — a `Proxy` traps `getPrototypeOf`, a `message` getter can raise, and
129
+ // `String()` throws outright on a Symbol — and what comes back is carried in
130
+ // `IslandBuildFailedError.logs`, which `errors.ts` interpolates straight into a `cause:`. That
131
+ // is a cross-file hop neither `scripts/catch-render.ts` nor `scripts/error-render.ts` can
132
+ // follow: a throw here loses the whole refusal and replaces it with a TypeError about reporting.
133
+ //
134
+ // The AggregateError branch stays, and it is the reason this function exists: `Bun.build` packs
135
+ // one entry per unresolved import or syntax error into `errors`, and flattening them is what
136
+ // puts a line number in the cause instead of the words "Bundle failed". `stringField` decides
137
+ // whether the value really is that shape, because `instanceof` is a question a Proxy answers.
138
+ const aggregate = aggregatedErrors(error);
139
+ if (aggregate !== undefined && aggregate.length > 0) {
140
+ return aggregate.map((one: unknown) => renderThrowable(one)).join('; ');
141
+ }
142
+ return renderThrowable(error);
143
+ }
144
+
145
+ /**
146
+ * `value.errors`, read the way `@ultimat3/core`'s `stringField` reads a string field: narrowed
147
+ * first, dereferenced inside a `try`, `undefined` for anything else. `instanceof AggregateError`
148
+ * is a question a `Proxy` answers with its own `getPrototypeOf` trap, so it is not a check.
149
+ */
150
+ function aggregatedErrors(value: unknown): readonly unknown[] | undefined {
151
+ if (typeof value !== 'object' || value === null) return undefined;
152
+ try {
153
+ const held: unknown = (value as Record<string, unknown>)['errors'];
154
+ return Array.isArray(held) ? held : undefined;
155
+ } catch {
156
+ return undefined;
157
+ }
158
+ }
@@ -0,0 +1,138 @@
1
+ // One split `Bun.build` of every island, turned into the files a browser fetches: an entry per
2
+ // island and a chunk per module two or more islands share, each named by its SOURCE identity and
3
+ // importing the others by that name. `island-bundle.ts` runs the build; this file only links.
4
+ //
5
+ // Why the names are rewritten rather than taken from Bun's `chunk-[hash]`: that hash is of the
6
+ // OUTPUT, and `Bun.build` under `minify` is not byte-deterministic (`graphHash` in
7
+ // `island-bundle.ts` has the measurement). An output-addressed shared chunk flaps its URL, and
8
+ // every entry importing it flaps with it — the precache-404 and cold-cache defect, back.
9
+
10
+ import { posix } from 'node:path'; // why: Bun ships no path API; `basename` of a specifier.
11
+ import { frameworkVersion } from '@ultimat3/core';
12
+ import { contentHash } from '@ultimat3/render/server';
13
+ import { IslandBuildFailedError } from './errors';
14
+
15
+ /** One code output of the build, as `island-bundle.ts` hands it over. */
16
+ export interface BuiltOutput {
17
+ /** Bun's own output path, `./` stripped: `apps/web/app/x.island.js`, `chunk-ab12cd34.js`. */
18
+ readonly path: string;
19
+ /** The island file it is the entry for; `undefined` for a shared chunk. */
20
+ readonly file?: string;
21
+ /** Emitted code, the debug-id comment already removed. */
22
+ readonly code: string;
23
+ /** Its source map's `sourcesContent` — empty for a chunk of bundler runtime helpers only. */
24
+ readonly sources: readonly string[];
25
+ }
26
+
27
+ export interface LinkedFile {
28
+ /** The output this came from, so the caller can pair it with its island. */
29
+ readonly path: string;
30
+ readonly file?: string;
31
+ /** `chunk-<identity>` for a shared chunk; the caller prefixes an entry with its module id. */
32
+ readonly identity: string;
33
+ /** The code with every import of another output rewritten to that output's final name. */
34
+ readonly code: string;
35
+ /** Final basenames of the outputs this one imports, statically or by `import()`, direct only. */
36
+ readonly imports: readonly string[];
37
+ }
38
+
39
+ /** The final basename a shared chunk is served under — beside every entry, under one base path. */
40
+ export const sharedChunkName = (identity: string): string => `chunk-${identity}.js`;
41
+
42
+ /** Static imports and `import()` both: a browser fetches either before the island is whole. */
43
+ const LOADS = new Set(['import-statement', 'dynamic-import']);
44
+
45
+ interface Scanned {
46
+ readonly exports: readonly string[];
47
+ /** Specifiers exactly as written, each naming another output of this build. */
48
+ readonly specifiers: readonly string[];
49
+ }
50
+
51
+ function scan(code: string, byName: ReadonlyMap<string, BuiltOutput>): Scanned {
52
+ const scanned = new Bun.Transpiler({ loader: 'js' }).scan(code);
53
+ const specifiers = scanned.imports
54
+ .filter((one) => LOADS.has(one.kind) && byName.has(posix.basename(one.path)))
55
+ .map((one) => one.path);
56
+ return { exports: [...scanned.exports].sort(), specifiers: [...new Set(specifiers)] };
57
+ }
58
+
59
+ /**
60
+ * Every output's identity, from what went in: its own sources (hashed and sorted, as `graphHash`
61
+ * does), its EXPORT names, and the identities of every output it imports. The exports are in it
62
+ * because they are the link between two files built together — an importer's `import{a as o}`
63
+ * is only correct against a chunk exporting `a`, so a chunk whose interface moved must not keep a
64
+ * URL an older importer is holding. The imports are in it because an entry's bytes NAME its
65
+ * chunks: a moved chunk moves every entry that loads it.
66
+ */
67
+ export function linkOutputs(outputs: readonly BuiltOutput[]): readonly LinkedFile[] {
68
+ const byName = new Map(outputs.map((one) => [posix.basename(one.path), one]));
69
+ const scans = new Map(outputs.map((one) => [one.path, scan(one.code, byName)]));
70
+ const identities = new Map<string, string>();
71
+ const visiting = new Set<string>();
72
+
73
+ const identityOf = (output: BuiltOutput): string => {
74
+ const known = identities.get(output.path);
75
+ if (known !== undefined) return known;
76
+ if (visiting.has(output.path)) throw cycle(output);
77
+ visiting.add(output.path);
78
+ const scanned = scans.get(output.path) ?? { exports: [], specifiers: [] };
79
+ const deps = scanned.specifiers
80
+ .map((spec) => byName.get(posix.basename(spec)))
81
+ .filter((dep): dep is BuiltOutput => dep !== undefined)
82
+ .map(identityOf)
83
+ .sort();
84
+ // A chunk of bundler runtime helpers has no source file behind it — its code IS its source.
85
+ const own = output.sources.length === 0 ? [contentHash(output.code)] : output.sources;
86
+ const identity = contentHash(
87
+ [
88
+ output.file ?? 'shared',
89
+ frameworkVersion(),
90
+ Bun.version,
91
+ ...own.map((source) => contentHash(source)).sort(),
92
+ 'exports',
93
+ ...scanned.exports,
94
+ 'imports',
95
+ ...deps,
96
+ ].join('\u0000'),
97
+ );
98
+ visiting.delete(output.path);
99
+ identities.set(output.path, identity);
100
+ return identity;
101
+ };
102
+
103
+ return outputs.map((output) => {
104
+ const identity = identityOf(output);
105
+ const scanned = scans.get(output.path) ?? { exports: [], specifiers: [] };
106
+ let code = output.code;
107
+ const imports: string[] = [];
108
+ for (const spec of scanned.specifiers) {
109
+ const dep = byName.get(posix.basename(spec));
110
+ if (dep === undefined) continue;
111
+ const name = sharedChunkName(identityOf(dep));
112
+ imports.push(name);
113
+ // The quoted literal, whole: Bun writes every specifier double-quoted, and a chunk name is a
114
+ // hash no program text contains by accident. Relative, so it resolves beside the importer
115
+ // under any base path — `/islands/` served, a temp directory in `mountIsland`.
116
+ code = code.split(JSON.stringify(spec)).join(JSON.stringify(`./${name}`));
117
+ }
118
+ return {
119
+ path: output.path,
120
+ ...(output.file === undefined ? {} : { file: output.file }),
121
+ identity,
122
+ code,
123
+ imports: [...new Set(imports)].sort(),
124
+ };
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Two outputs importing each other: esbuild-family splitting does not emit one, and an identity
130
+ * built from its imports' identities has no answer for it. Refused by name rather than answered
131
+ * with a hash that silently ignores an edge.
132
+ */
133
+ function cycle(output: BuiltOutput): IslandBuildFailedError {
134
+ return new IslandBuildFailedError({
135
+ file: output.file ?? output.path,
136
+ logs: `the island build emitted a chunk import cycle through ${output.path}, so its outputs have no source identity to be named by`,
137
+ });
138
+ }
@@ -8,13 +8,20 @@ import { dirname, join } from 'node:path';
8
8
  import type { BunPlugin } from 'bun';
9
9
  import { firstInGraph } from './live-routes';
10
10
 
11
- /** What `Bun.build` is handed for a realtime island; resolved by `islandRealtimePlugin`. */
12
- export const REALTIME_ISLAND_ENTRY = 'ultimate:island-entry';
11
+ /**
12
+ * What `Bun.build` is handed for a realtime island: this prefix plus the island's app-root-relative
13
+ * path, so ONE build can carry every realtime island's wrapper as its own entry point. Resolved by
14
+ * `islandRealtimePlugin`, and the prefix is what `island-bundle.ts` strips back off Bun's output path.
15
+ */
16
+ export const REALTIME_ISLAND_ENTRY = 'ultimate:island-entry:';
17
+
18
+ /** The entry point `Bun.build` is handed for a realtime island. */
19
+ export const realtimeIslandEntry = (file: string): string => `${REALTIME_ISLAND_ENTRY}${file}`;
13
20
 
14
21
  const REALTIME = '@ultimat3/realtime';
15
22
  const NAMESPACE = 'ultimate-island';
16
23
  const INSTALL = 'ultimate:island-realtime';
17
- const MODULE = 'ultimate:island-module';
24
+ const MODULE = 'ultimate:island-module:';
18
25
 
19
26
  /**
20
27
  * Whether the island's own graph value-imports realtime. Relative specifiers only, which is the
@@ -51,9 +58,11 @@ export function realtimeIslandFiles(): ReadonlySet<string> {
51
58
  /**
52
59
  * `export *` and never a named list: the hydration runtime reads `mount` off the module, and the
53
60
  * wrapper must not decide which of an island's names survive. The install is imported FIRST, so it
54
- * has run before the island's module body — and every hook the island calls — does.
61
+ * has run before the island's module body — and every hook the island calls — does. One install
62
+ * module for every realtime island of the build, so a page with two of them loads it once.
55
63
  */
56
- const ENTRY_SOURCE = `import '${INSTALL}';\nexport * from '${MODULE}';\n`;
64
+ const entrySource = (file: string): string =>
65
+ `import '${INSTALL}';\nexport * from '${MODULE}${file}';\n`;
57
66
 
58
67
  /**
59
68
  * The install names `@ultimat3/realtime` BARE, and `islandRealtimePlugin` resolves it from the
@@ -68,29 +77,37 @@ const INSTALL_SOURCE =
68
77
  `import { createSignal } from 'solid-js';\n` +
69
78
  `installRealtime({ signal: createSignal });\n`;
70
79
 
80
+ /**
81
+ * `file` is the island whose directory every bare `@ultimat3/realtime` resolves from — the first
82
+ * realtime island of the build. One directory for all of them, because one copy in the bundle is
83
+ * the requirement: the signal the install sets is only read by hooks from the same module instance.
84
+ */
71
85
  export function islandRealtimePlugin(root: string, file: string): BunPlugin {
72
86
  const island = join(root, file);
73
87
  return {
74
88
  name: 'ultimate-island-realtime',
75
89
  setup(build) {
76
- build.onResolve({ filter: /^ultimate:island-entry$/ }, () => ({
77
- path: 'entry',
90
+ build.onResolve({ filter: /^ultimate:island-entry:/ }, (args) => ({
91
+ path: `entry:${args.path.slice(REALTIME_ISLAND_ENTRY.length)}`,
78
92
  namespace: NAMESPACE,
79
93
  }));
80
94
  build.onResolve({ filter: /^ultimate:island-realtime$/ }, () => ({
81
95
  path: 'install',
82
96
  namespace: NAMESPACE,
83
97
  }));
84
- build.onResolve({ filter: /^ultimate:island-module$/ }, () => ({ path: island }));
98
+ build.onResolve({ filter: /^ultimate:island-module:/ }, (args) => ({
99
+ path: join(root, args.path.slice(MODULE.length)),
100
+ }));
85
101
  // Every bare `@ultimat3/realtime` in this build, the virtual install's included — a virtual
86
102
  // module has no directory to resolve from. The island's directory for all of them, which is
87
- // what the island itself would get, and one copy in the bundle is the requirement anyway:
88
- // the signal the install sets is only read by hooks from the same module instance.
103
+ // what the island itself would get.
89
104
  build.onResolve({ filter: /^@ultimat3\/realtime$/ }, () => ({
90
105
  path: Bun.resolveSync(REALTIME, dirname(island)),
91
106
  }));
92
107
  build.onLoad({ filter: /.*/, namespace: NAMESPACE }, (args) => ({
93
- contents: args.path === 'entry' ? ENTRY_SOURCE : INSTALL_SOURCE,
108
+ contents: args.path.startsWith('entry:')
109
+ ? entrySource(args.path.slice('entry:'.length))
110
+ : INSTALL_SOURCE,
94
111
  loader: 'js',
95
112
  }));
96
113
  },
@@ -26,7 +26,9 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
26
26
  path: `${ISLAND_BASE_PATH}/*file`,
27
27
  meta: { name: 'assets.island', auth: 'public', tags: ['assets'] },
28
28
  handler: (request: UltimateRequest): Response => {
29
- const chunk = source().chunkAt(request.pathname);
29
+ // An entry or a shared chunk: an entry's bytes import its chunks by a relative name, so both
30
+ // have to answer at the one base path, under the one cache rule.
31
+ const chunk = source().assetAt(request.pathname);
30
32
  if (chunk === undefined) {
31
33
  const missing = json(
32
34
  {
@@ -10,7 +10,7 @@
10
10
  import { join, relative, sep } from 'node:path'; // why: Bun ships no path-join primitive.
11
11
  import { frameworkVersion, logger } from '@ultimat3/core';
12
12
  import { contentHash, loadStylesheet } from '@ultimat3/render/server';
13
- import type { IslandBundle, IslandChunk } from './island-bundle';
13
+ import type { IslandBundle, IslandChunk, SharedChunk } from './island-bundle';
14
14
  import { buildIslands, discoverIslands, islandBundle } from './island-bundle';
15
15
  import { islandStylesheets } from './island-styles';
16
16
 
@@ -23,12 +23,23 @@ interface StoredChunk {
23
23
  readonly moduleId: string;
24
24
  readonly url: string;
25
25
  readonly identity: string;
26
+ /** The shared chunk URLs the entry loads, transitively (`IslandChunk.imports`). */
27
+ readonly imports: readonly string[];
28
+ }
29
+
30
+ /** A shared chunk: no island of its own, verified by the same hash. */
31
+ interface StoredShared {
32
+ readonly url: string;
33
+ readonly identity: string;
34
+ readonly importers: readonly string[];
26
35
  }
27
36
 
28
37
  interface StoreIndex {
29
38
  readonly framework: string;
30
39
  readonly bun: string;
31
40
  readonly chunks: readonly StoredChunk[];
41
+ /** Absent in a store written before islands were split: read as none. */
42
+ readonly shared: readonly StoredShared[];
32
43
  /**
33
44
  * The stylesheets the island build registered, app-relative and sorted. Registered again at boot
34
45
  * from the app's own sources, so a container serving these chunks joins the SAME surface
@@ -56,8 +67,15 @@ export async function writeIslandStore(
56
67
  moduleId: chunk.moduleId,
57
68
  url: chunk.url,
58
69
  identity: contentHash(chunk.code),
70
+ imports: chunk.imports,
59
71
  });
60
72
  }
73
+ const shared: StoredShared[] = [];
74
+ for (const chunk of bundle.shared) {
75
+ await Bun.write(join(dir, chunkFile(chunk.url)), chunk.code);
76
+ written.push(`${ISLAND_STORE_DIR}/${chunkFile(chunk.url)}`);
77
+ shared.push({ url: chunk.url, identity: contentHash(chunk.code), importers: chunk.importers });
78
+ }
61
79
  const stylesheets = islandStylesheets()
62
80
  .map((path) => relative(root, path).split(sep).join('/'))
63
81
  .filter((path) => !path.startsWith('..'))
@@ -66,6 +84,7 @@ export async function writeIslandStore(
66
84
  framework: frameworkVersion(),
67
85
  bun: Bun.version,
68
86
  chunks,
87
+ shared,
69
88
  stylesheets,
70
89
  };
71
90
  await Bun.write(join(dir, INDEX), `${JSON.stringify(index, null, 2)}\n`);
@@ -74,6 +93,9 @@ export async function writeIslandStore(
74
93
 
75
94
  const isString = (value: unknown): value is string => typeof value === 'string';
76
95
 
96
+ const stringsOf = (value: unknown): readonly string[] =>
97
+ Array.isArray(value) ? value.filter(isString) : [];
98
+
77
99
  /** The index, narrowed field by field — a file on disk is input, never a trusted shape. */
78
100
  function parseIndex(value: unknown): StoreIndex | undefined {
79
101
  if (typeof value !== 'object' || value === null) return undefined;
@@ -86,15 +108,28 @@ function parseIndex(value: unknown): StoreIndex | undefined {
86
108
  for (const entry of chunks) {
87
109
  if (typeof entry !== 'object' || entry === null) return undefined;
88
110
  const chunk = entry as Record<string, unknown>;
89
- const { file, moduleId, url, identity } = chunk;
111
+ const { file, moduleId, url, identity, imports } = chunk;
90
112
  if (!isString(file) || !isString(moduleId) || !isString(url) || !isString(identity))
91
113
  return undefined;
92
- parsed.push({ file, moduleId, url, identity });
114
+ parsed.push({ file, moduleId, url, identity, imports: stringsOf(imports) });
115
+ }
116
+ const shared: StoredShared[] = [];
117
+ for (const entry of Array.isArray(record['shared']) ? record['shared'] : []) {
118
+ if (typeof entry !== 'object' || entry === null) return undefined;
119
+ const { url, identity, importers } = entry as Record<string, unknown>;
120
+ if (!isString(url) || !isString(identity)) return undefined;
121
+ shared.push({ url, identity, importers: stringsOf(importers) });
93
122
  }
94
123
  const sheets = record['stylesheets'];
95
124
  // Absent in a 22.3.2 store: read as none, and the boot builds when it matters (see below).
96
125
  const stylesheets = Array.isArray(sheets) ? sheets.filter(isString) : [];
97
- return { framework: record['framework'], bun: record['bun'], chunks: parsed, stylesheets };
126
+ return {
127
+ framework: record['framework'],
128
+ bun: record['bun'],
129
+ chunks: parsed,
130
+ shared,
131
+ stylesheets,
132
+ };
98
133
  }
99
134
 
100
135
  /** A verified store, or the one sentence saying why it cannot be served. */
@@ -118,11 +153,15 @@ export async function readIslandStore(root: string): Promise<StoreRead> {
118
153
  if (stored.join('\n') !== present.join('\n')) {
119
154
  return { stale: 'the stored islands are not the islands this app has' };
120
155
  }
121
- const chunks: IslandChunk[] = [];
122
- for (const entry of index.chunks) {
156
+ const verified = async (entry: { readonly url: string; readonly identity: string }) => {
123
157
  const bytes = Bun.file(join(dir, chunkFile(entry.url)));
124
158
  const code = (await bytes.exists()) ? await bytes.text() : undefined;
125
- if (code === undefined || contentHash(code) !== entry.identity) {
159
+ return code === undefined || contentHash(code) !== entry.identity ? undefined : code;
160
+ };
161
+ const chunks: IslandChunk[] = [];
162
+ for (const entry of index.chunks) {
163
+ const code = await verified(entry);
164
+ if (code === undefined) {
126
165
  return { stale: `${entry.url} is missing or does not match its recorded hash` };
127
166
  }
128
167
  chunks.push({
@@ -131,8 +170,26 @@ export async function readIslandStore(root: string): Promise<StoreRead> {
131
170
  url: entry.url,
132
171
  code,
133
172
  bytes: new TextEncoder().encode(code).byteLength,
173
+ imports: entry.imports,
174
+ });
175
+ }
176
+ const shared: SharedChunk[] = [];
177
+ for (const entry of index.shared) {
178
+ const code = await verified(entry);
179
+ if (code === undefined) {
180
+ return { stale: `${entry.url} is missing or does not match its recorded hash` };
181
+ }
182
+ shared.push({
183
+ url: entry.url,
184
+ code,
185
+ bytes: new TextEncoder().encode(code).byteLength,
186
+ importers: entry.importers,
134
187
  });
135
188
  }
189
+ // An entry importing a chunk the store does not hold would boot to a 404 in every pod.
190
+ const held = new Set(shared.map((chunk) => chunk.url));
191
+ const orphan = chunks.flatMap((chunk) => chunk.imports).find((url) => !held.has(url));
192
+ if (orphan !== undefined) return { stale: `${orphan}, a shared chunk, is not in the store` };
136
193
  // In sorted order, as `island` sheets: `stylesFor` orders those by path, so this is the same
137
194
  // surface stylesheet the build that wrote the store joined.
138
195
  for (const sheet of index.stylesheets) {
@@ -140,7 +197,7 @@ export async function readIslandStore(root: string): Promise<StoreRead> {
140
197
  if (!(await source.exists())) return { stale: `${sheet}, an island stylesheet, is missing` };
141
198
  loadStylesheet(join(root, sheet), await source.text(), 'island');
142
199
  }
143
- return { bundle: islandBundle(chunks) };
200
+ return { bundle: islandBundle(chunks, shared) };
144
201
  }
145
202
 
146
203
  /**
package/src/mcp-errors.ts CHANGED
@@ -253,6 +253,9 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
253
253
  'x verify --only budgets --json # after moving each await in Page into export const load',
254
254
  X_SITEMAP_EXTRA_INVALID:
255
255
  'x routes --json # then list in seo.sitemap.extra only a path a registered app/ route without a policy answers',
256
+ X_DEV_RESTART_REQUIRED:
257
+ 'x dev --json # a fresh process; the supervised x dev restarts itself on such a save',
258
+ X_DEV_ROOT_GONE: 'x dev --json # from the app root, once it exists again',
256
259
  };
257
260
 
258
261
  const isCliCode = (code: string): code is CliErrorCode =>
package/src/messages.ts CHANGED
@@ -76,6 +76,7 @@ const CATALOG = {
76
76
  'cli.dev.mail.external': 'mail=external({driver} via {detail})',
77
77
  'cli.dev.mail.refused': 'mail=refused({detail})',
78
78
  'cli.dev.hmr': 'reloaded {file} in {ms}ms',
79
+ 'cli.dev.restart': 'restarting: {reason} — a module that defines a primitive holds the old code',
79
80
  // A hard kill leaves the lock behind and that is normal, not a fault — worth one line so a
80
81
  // reader knows why the boot paused, and never a finding.
81
82
  'cli.dev.staleLock': 'cleared a stale dev.lock — the previous x dev did not shut down cleanly',
package/src/prerender.ts CHANGED
@@ -141,7 +141,12 @@ function heaviestSource(
141
141
  );
142
142
  if (heaviest === undefined) return undefined;
143
143
  const chunk = bundle.chunkAt(heaviest.url);
144
- return chunk === undefined ? [heaviest.url] : [chunk.file];
144
+ if (chunk !== undefined) return [chunk.file];
145
+ // A shared chunk has no file of its own: the chain is an island on THIS page that imports it.
146
+ const importer = bundle.chunks.find(
147
+ (one) => one.imports.includes(heaviest.url) && entries.some((entry) => entry.url === one.url),
148
+ );
149
+ return importer === undefined ? [heaviest.url] : [importer.file, heaviest.url];
145
150
  }
146
151
 
147
152
  export const DEFAULT_ORIGIN = 'https://localhost';
@@ -135,8 +135,14 @@ export function precacheAssets(input: PrecacheAssetInput): readonly PrecacheAsse
135
135
  }
136
136
  }
137
137
  }
138
+ // A wanted entry brings the shared chunks it imports: offline, an entry whose chunk was never
139
+ // cached is an island that fails to boot on the one page that promised to work without a network.
140
+ for (const chunk of input.islands.chunks) {
141
+ if (wanted.has(chunk.url)) for (const url of chunk.imports) wanted.add(url);
142
+ }
138
143
  return [
139
144
  ...input.islands.chunks.filter((chunk) => wanted.has(chunk.url)),
145
+ ...input.islands.shared.filter((chunk) => wanted.has(chunk.url)),
140
146
  ...input.styles.chunks,
141
147
  ...input.scripts,
142
148
  ]
@@ -9,7 +9,7 @@ import { dirname, join, relative } from 'node:path';
9
9
  import type { Route, UltimateRequest } from '@ultimat3/http';
10
10
  import { applyCacheHeaders, json } from '@ultimat3/http';
11
11
  import { FrameworkScriptBuildFailedError, type FrameworkScriptKind } from './errors';
12
- import { describeBuildError, graphHash, stripDebugId } from './island-bundle';
12
+ import { describeBuildError, graphHash, stripDebugId } from './island-identity';
13
13
 
14
14
  /** Under the dev namespace `/_x` — where the socket it opens (`/_x/sync`) already lives. */
15
15
  export const SYNC_WORKER_BASE_PATH = '/_x/sync-worker';