@farm.js/islx 0.0.6 → 0.0.8

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.
@@ -1,4 +1,4 @@
1
- import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-B1GLTLdk.mjs";
1
+ import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-zEYsfFxW.mjs";
2
2
  import { m as HYDRATED_ATTRIBUTE } from "./wire-QEQWBr5s.mjs";
3
3
  // src/client/schedule.ts
4
4
  /**
@@ -24,6 +24,15 @@ function isIslandStrategy(value) {
24
24
  }
25
25
  /** Elements whose click is held and replayed rather than merely observed. */
26
26
  const REPLAYABLE = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
27
+ /**
28
+ * The element a click before hydration is held on: a control, or a same-page link, whose handler
29
+ * the island may need (a table of contents marking its heading). Any other link navigates.
30
+ */
31
+ function heldTarget(target) {
32
+ const link = target.closest("a[href]");
33
+ if (link) return link.getAttribute("href")?.startsWith("#") ? link : null;
34
+ return target.closest(REPLAYABLE);
35
+ }
27
36
  function whenIdle(callback) {
28
37
  const scope = window;
29
38
  if (typeof scope.requestIdleCallback === "function") {
@@ -190,8 +199,8 @@ function scheduleHydration({ container, strategy, signal, hydrate }) {
190
199
  };
191
200
  const onClick = (event) => {
192
201
  const mouse = event;
193
- const target = event.target instanceof Element ? event.target.closest(REPLAYABLE) : null;
194
- if (!target || !container.contains(target) || target.closest("a[href]") || mouse.button !== 0 || mouse.metaKey || mouse.altKey || mouse.ctrlKey || mouse.shiftKey) return;
202
+ const target = event.target instanceof Element ? heldTarget(event.target) : null;
203
+ if (!target || !container.contains(target) || mouse.button !== 0 || mouse.metaKey || mouse.altKey || mouse.ctrlKey || mouse.shiftKey) return;
195
204
  hold(event, target, "click");
196
205
  };
197
206
  const onSubmit = (event) => {
@@ -1,7 +1,7 @@
1
1
  import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-C8to-RQW.mjs";
2
2
  import { M as VALUE_SCRIPT_TYPE, c as CHUNK_SCRIPT_TYPE, k as TREE_SCRIPT_TYPE, p as HTML_ELEMENT } from "./wire-QEQWBr5s.mjs";
3
3
  import { t as createServerReference, u as getValueStore } from "./actions-C-IwUCKU.mjs";
4
- import { a as getIslandServerHook, o as isolateIslands, r as getClientReference } from "./island-DVOr-OUw.mjs";
4
+ import { a as getIslandServerHook, o as isolateIslands, r as getClientReference } from "./island-DXAGP4FN.mjs";
5
5
  import * as React from "react";
6
6
  // src/react/tree.ts
7
7
  /**
@@ -52,7 +52,14 @@ interface IslandRenderState {
52
52
  * Wraps a client component so the server renders it as an island. `src` is the module the browser
53
53
  * imports, `exportName` the component's export there.
54
54
  */
55
- declare function createIsland<P extends object>(Component: React.ComponentType<P>, src: string, exportName: string, strategy?: IslandStrategy): React.ComponentType<P>;
55
+ declare function createIsland<P extends object>(Component: React.ComponentType<P>, src: string, exportName: string, declared?: IslandStrategy,
56
+ /**
57
+ * `inferred`: the compiler inferred `declared` (`"interaction"`). A render that passes the island
58
+ * server output hydrates at load instead: islands inside that output would wait with it.
59
+ */
60
+ options?: {
61
+ inferred?: boolean;
62
+ }): React.ComponentType<P>;
56
63
  /**
57
64
  * Marks a navigation response that carries only the page's payload. The browser rebuilds such a
58
65
  * page in its layout's React root and never shows its HTML, so the server leaves the HTML out.
@@ -1,5 +1,5 @@
1
- import { i as transformActions, r as hasModuleDirective, t as ActionCompileError } from "./actions-Eutk6UzT.mjs";
2
- import { t as ISLAND_STRATEGIES } from "./schedule-CjoybS3b.mjs";
1
+ import { a as transformActions, i as hasModuleDirective, n as ActionCompileError, t as inferStrategy } from "./infer-BIVT5aDz.mjs";
2
+ import { t as ISLAND_STRATEGIES } from "./schedule-Cwn9vQL0.mjs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { walk } from "estree-walker";
@@ -78,10 +78,12 @@ function transformClientModule(options) {
78
78
  throw new ActionCompileError(message, file, code, node.start);
79
79
  };
80
80
  const strategies = readStrategies(program, fail);
81
+ const inference = options.infer && strategies.all === void 0 && strategies.byName.size === 0 ? inferStrategy(file, code, options.infer) : void 0;
82
+ const inferred = (name) => options.strategy?.(name) === void 0 && inference?.strategy === "interaction";
81
83
  const strategyOf = (name) => {
82
84
  const configured = options.strategy?.(name);
83
85
  if (configured !== void 0 && !ISLAND_STRATEGIES.includes(configured)) throw new Error(`islx: strategy for ${name} must be one of ${ISLAND_STRATEGIES.join(", ")}`);
84
- return configured ?? strategies.byName.get(name) ?? strategies.all ?? "load";
86
+ return configured ?? strategies.byName.get(name) ?? strategies.all ?? (inference?.strategy === "interaction" ? "interaction" : "load");
85
87
  };
86
88
  const output = new MagicString(code);
87
89
  const wrapped = [];
@@ -140,7 +142,9 @@ function transformClientModule(options) {
140
142
  const runtime = options.runtime ?? "@farm.js/islx/react";
141
143
  const src = options.reference(relative);
142
144
  const lines = [...wrapped.map(({ local, exportName }, index) => {
143
- return `const $$islx_island_${index} = $$islx_island(${local}, ${src}, ${JSON.stringify(exportName)}, ${JSON.stringify(strategyOf(exportName === "default" ? "default" : exportName))});`;
145
+ const strategy = JSON.stringify(strategyOf(exportName));
146
+ const flags = inferred(exportName) ? ", { inferred: true }" : "";
147
+ return `const $$islx_island_${index} = $$islx_island(${local}, ${src}, ${JSON.stringify(exportName)}, ${strategy}${flags});`;
144
148
  }), `export { ${wrapped.map(({ exportName }, index) => `$$islx_island_${index} as ${exportName}`).join(", ")} };`];
145
149
  const imports = [`import { createIsland as $$islx_island } from ${JSON.stringify(runtime)};`, ...(options.sideEffects ?? []).map((module) => `import ${JSON.stringify(module)};`)];
146
150
  output.prepend(`${imports.join("\n")}\n`);
@@ -154,7 +158,8 @@ function transformClientModule(options) {
154
158
  islands: wrapped.map(({ exportName }) => ({
155
159
  exportName,
156
160
  strategy: strategyOf(exportName)
157
- }))
161
+ })),
162
+ ...inference ? { inference } : {}
158
163
  };
159
164
  }
160
165
  // src/vite.ts
@@ -177,6 +182,8 @@ function transformClientModule(options) {
177
182
  const RUNTIME_ID = "virtual:islx/runtime";
178
183
  /** @internal The browser entry, for a framework whose own entry imports it. */
179
184
  const ENTRY_ID = "virtual:islx/client-entry";
185
+ /** The boot entry: pages start with it, and it loads ENTRY_ID when the page first needs it. */
186
+ const BOOT_ID = "virtual:islx/boot";
180
187
  const LIBRARY_RUNTIME_ID = "virtual:islx/react-for-libraries";
181
188
  const CLIENT_HOOKS_ID = "virtual:islx/hooks-client";
182
189
  const SERVER_HOOKS_ID = "virtual:islx/hooks-server";
@@ -244,6 +251,7 @@ function serializeHooks(hooks, where, parse) {
244
251
  * build until the browser build names them. */
245
252
  const ENTRY_PLACEHOLDER = "__islx_client_entry_url__";
246
253
  const MODULES_PLACEHOLDER = "__islx_client_modules__";
254
+ const BOOT_PLACEHOLDER = "__islx_boot_url__";
247
255
  const SCRIPT = /\.[cm]?[jt]sx?$/;
248
256
  async function createParser() {
249
257
  const withErrors = (result) => Object.assign(result.program, { errors: result.errors ?? [] });
@@ -315,6 +323,7 @@ function islx(options = {}, host = {}) {
315
323
  const discovered = /* @__PURE__ */ new Set();
316
324
  /** The browser entry's URL, once the browser build named it. */
317
325
  let entryUrl;
326
+ let bootUrl;
318
327
  /** The files each client module needs, once the browser build named them. */
319
328
  const moduleFiles = {};
320
329
  /** Whether the browser build ran first (a framework that orders the build). */
@@ -355,7 +364,8 @@ function islx(options = {}, host = {}) {
355
364
  outDir: clientBuild?.outDir ?? "dist/client",
356
365
  rollupOptions: { input: host.frameworkEntry ? input : {
357
366
  ...typeof input === "string" ? { index: input } : Array.isArray(input) ? Object.fromEntries(input.map((entry, index) => [`input${index}`, entry])) : input,
358
- "islx-entry": ENTRY_ID
367
+ "islx-entry": ENTRY_ID,
368
+ ...options.boot === false ? {} : { "islx-boot": BOOT_ID }
359
369
  } }
360
370
  }
361
371
  } }
@@ -368,17 +378,18 @@ function islx(options = {}, host = {}) {
368
378
  },
369
379
  resolveId(id) {
370
380
  if (id === LIBRARY_RUNTIME_ID) return this.resolve("@farm.js/islx/react", path.join(config.root, "package.json"), { skipSelf: true });
371
- if (id === RUNTIME_ID || id === "virtual:islx/client-entry" || id === CLIENT_HOOKS_ID || id === SERVER_HOOKS_ID) return `\0${id}`;
381
+ if (id === RUNTIME_ID || id === "virtual:islx/client-entry" || id === "virtual:islx/boot" || id === CLIENT_HOOKS_ID || id === SERVER_HOOKS_ID) return `\0${id}`;
372
382
  },
373
383
  async load(id) {
374
384
  if (id === `\0${RUNTIME_ID}`) {
375
385
  const entry = host.frameworkEntry ? void 0 : !isBuild() ? `${config.base}@id/__x00__${ENTRY_ID}` : clientBuilt ? entryUrl : ENTRY_PLACEHOLDER;
386
+ const boot = host.frameworkEntry || options.boot === false ? void 0 : !isBuild() ? `${config.base}@id/__x00__${BOOT_ID}` : clientBuilt ? bootUrl : BOOT_PLACEHOLDER;
376
387
  const modules = !isBuild() ? "{}" : clientBuilt ? JSON.stringify(moduleFiles) : `JSON.parse(${JSON.stringify(MODULES_PLACEHOLDER)})`;
377
388
  return [
378
389
  ...host.frameworkRender ? [`import "@farm.js/islx/react/server";`] : [],
379
390
  `import { setBuildDefaults, setServerHooks } from "@farm.js/islx/server";`,
380
391
  `import hooks from ${JSON.stringify(SERVER_HOOKS_ID)};`,
381
- `setBuildDefaults({ entry: ${JSON.stringify(entry)}, key: ${JSON.stringify(buildKey)}, modules: ${modules} });`,
392
+ `setBuildDefaults({ entry: ${JSON.stringify(entry)}, boot: ${JSON.stringify(boot)}, key: ${JSON.stringify(buildKey)}, modules: ${modules} });`,
382
393
  `setServerHooks(hooks);`
383
394
  ].join("\n");
384
395
  }
@@ -389,6 +400,7 @@ function islx(options = {}, host = {}) {
389
400
  if (typeof hooks === "string") return `export { default } from ${JSON.stringify(path.resolve(config.root, hooks))};`;
390
401
  return serializeHooks(hooks, where, parse);
391
402
  }
403
+ if (id === `\0virtual:islx/boot`) return `import { startBoot } from "@farm.js/islx/boot";\nstartBoot();\n`;
392
404
  if (id === `\0virtual:islx/client-entry`) {
393
405
  const load = isBuild() ? `const modules = {\n${(await clientInputs()).map((relative) => ` ${JSON.stringify(relative)}: () => import(${JSON.stringify(path.resolve(config.root, relative))}),`).join("\n")}\n};\nconst load = (src) => modules[src] ? modules[src]() : Promise.reject(new Error("islx: no client module " + src));` : `const root = new URL(${JSON.stringify(`file://${toPosix(config.root)}/`)});\nconst load = (src) => import(/* @vite-ignore */ src.startsWith("../") ? "/@fs" + new URL(src, root).pathname : ${JSON.stringify(config.base)} + src);`;
394
406
  return [
@@ -428,8 +440,10 @@ function islx(options = {}, host = {}) {
428
440
  },
429
441
  sideEffects: [RUNTIME_ID],
430
442
  directive: clientDirective,
431
- ...library ? { runtime: LIBRARY_RUNTIME_ID } : {}
443
+ ...library ? { runtime: LIBRARY_RUNTIME_ID } : {},
444
+ ...options.infer !== false && !library ? { infer: { parse } } : {}
432
445
  });
446
+ for (const read of island?.inference?.files ?? []) if (read !== file) this.addWatchFile(read);
433
447
  if (island) {
434
448
  if (!library) transformActions({
435
449
  code,
@@ -484,9 +498,9 @@ function islx(options = {}, host = {}) {
484
498
  const outDir = path.resolve(config.root, environment.config.build.outDir);
485
499
  for (const file of await listOutput(outDir)) {
486
500
  const source = await readFile(file, "utf8");
487
- if (!source.includes(ENTRY_PLACEHOLDER) && !source.includes(MODULES_PLACEHOLDER)) continue;
501
+ if (!source.includes(ENTRY_PLACEHOLDER) && !source.includes(MODULES_PLACEHOLDER) && !source.includes(BOOT_PLACEHOLDER)) continue;
488
502
  const modules = JSON.stringify(JSON.stringify(moduleFiles)).slice(1, -1);
489
- await writeFile(file, source.replaceAll(ENTRY_PLACEHOLDER, entryUrl).replaceAll(MODULES_PLACEHOLDER, modules));
503
+ await writeFile(file, source.replaceAll(ENTRY_PLACEHOLDER, entryUrl).replaceAll(MODULES_PLACEHOLDER, modules).replaceAll(BOOT_PLACEHOLDER, bootUrl ?? ""));
490
504
  }
491
505
  }
492
506
  },
@@ -496,6 +510,7 @@ function islx(options = {}, host = {}) {
496
510
  for (const chunk of Object.values(bundle)) {
497
511
  if (chunk.type !== "chunk" || !chunk.facadeModuleId) continue;
498
512
  if (chunk.isEntry && chunk.facadeModuleId === `\0virtual:islx/client-entry`) entryUrl = url(chunk.fileName);
513
+ else if (chunk.isEntry && chunk.facadeModuleId === `\0virtual:islx/boot`) bootUrl = url(chunk.fileName);
499
514
  else if (chunk.isDynamicEntry && !chunk.facadeModuleId.startsWith("\0")) moduleFiles[relativeOf(chunk.facadeModuleId)] = [chunk.fileName, ...chunk.imports].map(url);
500
515
  }
501
516
  clientBuilt = true;
@@ -503,4 +518,4 @@ function islx(options = {}, host = {}) {
503
518
  }
504
519
  ];
505
520
  }
506
- export { serializeHooks as i, islx as n, resolveDirectives as r, ENTRY_ID as t };
521
+ export { serializeHooks as a, resolveDirectives as i, ENTRY_ID as n, islx as r, BOOT_ID as t };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @farm.js/islx/boot: the first script of a page, in place of the browser entry. It loads the entry
3
+ * (the runtime: the renderer, islx's hydration, navigation and forms) when the page first needs
4
+ * it, so a page whose islands all wait loads only this script until one is used:
5
+ *
6
+ * - at once, when an island hydrates at load (`strategy="load"`, also one that streams in later)
7
+ * or a Suspense boundary is left for the browser to finish;
8
+ * - otherwise on the first island trigger (`idle`; `visible`, with the scheduler's 200px margin;
9
+ * an interaction the inline pre-hydration script saw) or on intent to follow a link (a pointer
10
+ * over it, a press, a focus), or a focus in a form.
11
+ *
12
+ * The inline pre-hydration script holds the clicks and submits on waiting islands until then. A
13
+ * same-site link clicked before the entry has run is held too, and clicked again once it has, so
14
+ * navigation decides as for any click; if the entry fails to load, the link navigates natively.
15
+ *
16
+ * Renderer-neutral, and it imports nothing: a module it shared with the entry would change how the
17
+ * entry splits into chunks. The server names the entry in `window.__islxRuntime`.
18
+ */
19
+ /** Kept equal to the island element (protocol/wire.ts); a test compares them. */
20
+ declare const BOOT_ISLAND_ELEMENT = "f-island";
21
+ /** Kept equal to HYDRATED_ATTRIBUTE (protocol/wire.ts). */
22
+ declare const BOOT_HYDRATED_ATTRIBUTE = "hydrated";
23
+ /** Kept equal to INTERACTION_EVENT (client/prehydration.ts). */
24
+ declare const BOOT_INTERACTION_EVENT = "f:island-interaction";
25
+ /** Kept equal to the scheduler's visibility margin (client/schedule.ts). */
26
+ declare const BOOT_VISIBLE_MARGIN = "200px";
27
+ /** Where the server names the browser entry for the boot. */
28
+ declare const RUNTIME_GLOBAL = "__islxRuntime";
29
+ /** Starts the runtime when the page first needs it. `load` runs it; it starts as it runs. */
30
+ declare function startBoot(load?: () => Promise<unknown>): void;
31
+ export { BOOT_HYDRATED_ATTRIBUTE, BOOT_INTERACTION_EVENT, BOOT_ISLAND_ELEMENT, BOOT_VISIBLE_MARGIN, RUNTIME_GLOBAL, startBoot };
package/dist/boot.mjs ADDED
@@ -0,0 +1,177 @@
1
+ // src/boot.ts
2
+ /**
3
+ * @farm.js/islx/boot: the first script of a page, in place of the browser entry. It loads the entry
4
+ * (the runtime: the renderer, islx's hydration, navigation and forms) when the page first needs
5
+ * it, so a page whose islands all wait loads only this script until one is used:
6
+ *
7
+ * - at once, when an island hydrates at load (`strategy="load"`, also one that streams in later)
8
+ * or a Suspense boundary is left for the browser to finish;
9
+ * - otherwise on the first island trigger (`idle`; `visible`, with the scheduler's 200px margin;
10
+ * an interaction the inline pre-hydration script saw) or on intent to follow a link (a pointer
11
+ * over it, a press, a focus), or a focus in a form.
12
+ *
13
+ * The inline pre-hydration script holds the clicks and submits on waiting islands until then. A
14
+ * same-site link clicked before the entry has run is held too, and clicked again once it has, so
15
+ * navigation decides as for any click; if the entry fails to load, the link navigates natively.
16
+ *
17
+ * Renderer-neutral, and it imports nothing: a module it shared with the entry would change how the
18
+ * entry splits into chunks. The server names the entry in `window.__islxRuntime`.
19
+ */
20
+ /** Kept equal to the island element (protocol/wire.ts); a test compares them. */
21
+ const BOOT_ISLAND_ELEMENT = "f-island";
22
+ /** Kept equal to HYDRATED_ATTRIBUTE (protocol/wire.ts). */
23
+ const BOOT_HYDRATED_ATTRIBUTE = "hydrated";
24
+ /** Kept equal to INTERACTION_EVENT (client/prehydration.ts). */
25
+ const BOOT_INTERACTION_EVENT = "f:island-interaction";
26
+ /** Kept equal to the scheduler's visibility margin (client/schedule.ts). */
27
+ const BOOT_VISIBLE_MARGIN = "200px";
28
+ /** Where the server names the browser entry for the boot. */
29
+ const RUNTIME_GLOBAL = "__islxRuntime";
30
+ function isModified(event) {
31
+ return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
32
+ }
33
+ /** A link islx navigation takes: the same checks as its click handler. */
34
+ function navigableLink(target) {
35
+ const link = target instanceof Element ? target.closest("a[href]") : null;
36
+ if (!(link instanceof HTMLAnchorElement)) return null;
37
+ if (link.target && link.target !== "_self") return null;
38
+ if (link.hasAttribute("download") || link.relList.contains("external")) return null;
39
+ const url = new URL(link.href);
40
+ if (url.origin !== location.origin || !/^https?:$/.test(url.protocol)) return null;
41
+ if (url.pathname === location.pathname && url.search === location.search && url.hash) return null;
42
+ return link;
43
+ }
44
+ /** Runs the browser entry the server named, as a module script: the same URL, so the same module. */
45
+ function loadRuntime() {
46
+ const src = window[RUNTIME_GLOBAL];
47
+ if (typeof src !== "string" || src === "") return Promise.reject(/* @__PURE__ */ new Error(`islx: the page names no browser entry (${RUNTIME_GLOBAL}).`));
48
+ return new Promise((resolve, reject) => {
49
+ const script = document.createElement("script");
50
+ script.type = "module";
51
+ script.src = src;
52
+ const nonce = document.querySelector("script[nonce]")?.nonce;
53
+ if (nonce) script.nonce = nonce;
54
+ script.addEventListener("load", () => resolve(), { once: true });
55
+ script.addEventListener("error", () => reject(/* @__PURE__ */ new Error(`islx: ${src} failed to load.`)), { once: true });
56
+ document.head.append(script);
57
+ });
58
+ }
59
+ /** Starts the runtime when the page first needs it. `load` runs it; it starts as it runs. */
60
+ function startBoot(load = loadRuntime) {
61
+ const cleanups = [];
62
+ let loading;
63
+ const release = () => {
64
+ for (const cleanup of cleanups.splice(0)) cleanup();
65
+ };
66
+ const start = () => {
67
+ if (!loading) {
68
+ loading = load().then(release, (error) => {
69
+ release();
70
+ throw error;
71
+ });
72
+ loading.catch((error) => console.error(error));
73
+ }
74
+ return loading;
75
+ };
76
+ const listen = (type, handler, capture) => {
77
+ document.addEventListener(type, handler, capture);
78
+ cleanups.push(() => document.removeEventListener(type, handler, capture));
79
+ };
80
+ listen(BOOT_INTERACTION_EVENT, () => void start(), false);
81
+ const intent = (event) => {
82
+ if (navigableLink(event.target)) start();
83
+ else if (event.type !== "mouseover" && event.target?.closest?.("form")) start();
84
+ };
85
+ for (const type of [
86
+ "pointerdown",
87
+ "focusin",
88
+ "mouseover"
89
+ ]) listen(type, intent, true);
90
+ listen("click", (event) => {
91
+ const click = event;
92
+ if (click.defaultPrevented || click.button !== 0 || isModified(click)) return;
93
+ const link = navigableLink(click.target);
94
+ if (!link) return;
95
+ click.preventDefault();
96
+ start().then(() => {
97
+ if (link.isConnected) link.dispatchEvent(new MouseEvent("click", {
98
+ bubbles: true,
99
+ cancelable: true
100
+ }));
101
+ else location.assign(link.href);
102
+ }, () => location.assign(link.href));
103
+ }, false);
104
+ const visibility = typeof IntersectionObserver === "function" ? new IntersectionObserver((entries) => {
105
+ if (entries.some((entry) => entry.isIntersecting)) start();
106
+ }, { rootMargin: BOOT_VISIBLE_MARGIN }) : null;
107
+ if (visibility) cleanups.push(() => visibility.disconnect());
108
+ let idle = false;
109
+ const startWhenIdle = () => {
110
+ if (idle) return;
111
+ idle = true;
112
+ const scope = window;
113
+ if (typeof scope.requestIdleCallback === "function") {
114
+ const handle = scope.requestIdleCallback(() => void start(), { timeout: 2e3 });
115
+ cleanups.push(() => scope.cancelIdleCallback?.(handle));
116
+ } else {
117
+ const handle = window.setTimeout(() => void start(), 1);
118
+ cleanups.push(() => window.clearTimeout(handle));
119
+ }
120
+ };
121
+ const parsedVisible = [];
122
+ const observeVisible = (island) => {
123
+ const targets = island.getClientRects().length > 0 ? [island] : Array.from(island.children);
124
+ if (!visibility || targets.length === 0) start();
125
+ else for (const target of targets) visibility.observe(target);
126
+ };
127
+ const considerIsland = (island) => {
128
+ if (island.hasAttribute("hydrated")) return;
129
+ if (island.parentElement?.closest("f-island")) return;
130
+ const strategy = island.getAttribute("strategy");
131
+ if (strategy === "interaction") return;
132
+ if (strategy === "idle") startWhenIdle();
133
+ else if (strategy !== "visible") start();
134
+ else if (document.readyState === "loading") parsedVisible.push(island);
135
+ else observeVisible(island);
136
+ };
137
+ const consider = (node) => {
138
+ if (loading) return;
139
+ if (node.nodeType === Node.COMMENT_NODE) {
140
+ if (node.data === "$!") start();
141
+ } else if (node instanceof Element) {
142
+ if (node.matches("f-island")) considerIsland(node);
143
+ for (const island of Array.from(node.querySelectorAll(BOOT_ISLAND_ELEMENT))) considerIsland(island);
144
+ }
145
+ };
146
+ const pendingBoundary = () => {
147
+ const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
148
+ while (walker.nextNode()) if (walker.currentNode.data === "$?") return true;
149
+ return false;
150
+ };
151
+ const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
152
+ while (!loading && walker.nextNode()) consider(walker.currentNode);
153
+ consider(document.documentElement);
154
+ const parsed = () => {
155
+ if (pendingBoundary()) start();
156
+ for (const island of parsedVisible.splice(0)) if (!loading) observeVisible(island);
157
+ };
158
+ if (!loading && document.readyState === "loading") {
159
+ const observer = new MutationObserver((records) => {
160
+ for (const record of records) for (const node of Array.from(record.addedNodes)) consider(node);
161
+ });
162
+ observer.observe(document, {
163
+ childList: true,
164
+ subtree: true
165
+ });
166
+ const done = () => {
167
+ observer.disconnect();
168
+ parsed();
169
+ };
170
+ document.addEventListener("DOMContentLoaded", done, { once: true });
171
+ cleanups.push(() => {
172
+ observer.disconnect();
173
+ document.removeEventListener("DOMContentLoaded", done);
174
+ });
175
+ } else if (!loading) parsed();
176
+ }
177
+ export { BOOT_HYDRATED_ATTRIBUTE, BOOT_INTERACTION_EVENT, BOOT_ISLAND_ELEMENT, BOOT_VISIBLE_MARGIN, RUNTIME_GLOBAL, startBoot };
package/dist/client.d.mts CHANGED
@@ -57,7 +57,8 @@ declare function startNavigation(options: NavigationOptions): {
57
57
  declare function navigate(href: string, options?: NavigateOptions): Promise<void>;
58
58
  /**
59
59
  * Holding interactions that happen before the islx script has run. An inline script in the page
60
- * captures a click or a submit on an island that has not hydrated (it is prevented and queued),
60
+ * captures a click or a submit on an island that has not hydrated (it is prevented and queued; a
61
+ * click on a link only when the link stays on the page, `#…`),
61
62
  * and announces a pointerdown or a focus (which go through). The runtime then hydrates those
62
63
  * islands first and replays what was held.
63
64
  *
package/dist/client.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as defineClientHooks, c as setClientHooks, d as valueHeaders, i as setActionNavigator, l as createValueStore, n as decodeActionReference, o as emit, r as setActionInterceptor, s as hookEventName, t as createServerReference, u as getValueStore } from "./_chunks/actions-C-IwUCKU.mjs";
2
2
  import { C as RENDER_HEADER, I as readPostedAction, L as stripRuntimeFields } from "./_chunks/wire-QEQWBr5s.mjs";
3
- import { i as REPLAYABLE_SELECTOR, n as PENDING_INTERACTIONS, r as PREHYDRATION_SCRIPT, t as INTERACTION_EVENT } from "./_chunks/prehydration-B1GLTLdk.mjs";
3
+ import { i as REPLAYABLE_SELECTOR, n as PENDING_INTERACTIONS, r as PREHYDRATION_SCRIPT, t as INTERACTION_EVENT } from "./_chunks/prehydration-zEYsfFxW.mjs";
4
4
  // src/client/forms.ts
5
5
  /**
6
6
  * Forms written in server components submit without reloading the page once JavaScript runs.
@@ -1,2 +1,24 @@
1
1
  import { a as getActionId, i as TransformActionsResult, n as AstNode, o as hasModuleDirective, r as TransformActionsOptions, s as transformActions, t as ActionCompileError } from "./_chunks/actions-BCtIGvrP.mjs";
2
- export { ActionCompileError, type AstNode, type TransformActionsOptions, type TransformActionsResult, getActionId, hasModuleDirective, transformActions };
2
+
3
+ // src/compiler/infer.d.ts
4
+ interface InferOptions {
5
+ /** Parses a module's source (TypeScript and JSX included) to an ESTree program. */
6
+ parse(file: string, code: string): AstNode;
7
+ /**
8
+ * Resolves an import to a source file of the app's own, or null for a package or anything
9
+ * unknown (which keeps `"load"`). Default: relative and root-absolute paths, with the usual
10
+ * extensions and index files.
11
+ */
12
+ resolve?(specifier: string, importer: string): string | null;
13
+ }
14
+ interface Inference {
15
+ /** `"interaction"` when the islands can wait; null to keep `"load"`. */
16
+ strategy: "interaction" | null;
17
+ /** The source files the result read, the module itself first. */
18
+ files: string[];
19
+ /** Why the islands keep `"load"`. */
20
+ reason?: string;
21
+ }
22
+ /** Infers when the islands `file` exports hydrate; `code` is its source. */
23
+ declare function inferStrategy(file: string, code: string, options: InferOptions): Inference;
24
+ export { ActionCompileError, type AstNode, type InferOptions, type Inference, type TransformActionsOptions, type TransformActionsResult, getActionId, hasModuleDirective, inferStrategy, transformActions };
package/dist/compiler.mjs CHANGED
@@ -1 +1 @@
1
- export { t as ActionCompileError, n as getActionId, r as hasModuleDirective, i as transformActions } from "./_chunks/actions-Eutk6UzT.mjs";
1
+ export { n as ActionCompileError, r as getActionId, i as hasModuleDirective, t as inferStrategy, a as transformActions } from "./_chunks/infer-BIVT5aDz.mjs";
@@ -1,7 +1,7 @@
1
1
  import { a as scheduleHydration, i as isIslandStrategy, n as IslandStrategy, r as ScheduleOptions, t as ISLAND_STRATEGIES } from "../_chunks/schedule-Dw14dW0H.mjs";
2
2
  import { m as HYDRATED_ATTRIBUTE } from "../_chunks/wire-Dipqh-EB.mjs";
3
3
  import { t as ValueStore } from "../_chunks/value-store-Dm-7xJ6W.mjs";
4
- import { a as ServerTreeLayoutOptions, c as createServerTreeLayout, g as isolateIslands, i as ServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, r as ServerTreeElementOptions, s as createServerTreeElement, u as updateServerTrees, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
4
+ import { a as ServerTreeLayoutOptions, c as createServerTreeLayout, g as isolateIslands, i as ServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, r as ServerTreeElementOptions, s as createServerTreeElement, u as updateServerTrees, v as withinClientRoot } from "../_chunks/tree-CV13RsJv.mjs";
5
5
  import * as React from "react";
6
6
 
7
7
  // src/react/hydrate.d.ts
@@ -1,9 +1,9 @@
1
1
  import { d as valueHeaders, o as emit, t as createServerReference, u as getValueStore } from "../_chunks/actions-C-IwUCKU.mjs";
2
2
  import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
3
3
  import { F as getTag, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
4
- import { i as getContexts, o as isolateIslands, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
5
- import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-C47p6khN.mjs";
6
- import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
4
+ import { i as getContexts, o as isolateIslands, u as withinClientRoot } from "../_chunks/island-DXAGP4FN.mjs";
5
+ import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-BQphP23Y.mjs";
6
+ import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-Cwn9vQL0.mjs";
7
7
  import * as React from "react";
8
8
  import { hydrateRoot } from "react-dom/client";
9
9
  // src/react/hydrate.ts
@@ -1,5 +1,5 @@
1
1
  import { r as ReactFormState } from "../_chunks/handle-C0N9ZyNV.mjs";
2
- import { _ as setRenderStateResolver, d as ClientReference, f as IslandRenderState, g as isolateIslands, h as getClientReference, l as renderServerTree, m as createIsland, n as PAYLOAD_ONLY_ATTRIBUTE, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
2
+ import { _ as setRenderStateResolver, d as ClientReference, f as IslandRenderState, g as isolateIslands, h as getClientReference, l as renderServerTree, m as createIsland, n as PAYLOAD_ONLY_ATTRIBUTE, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "../_chunks/tree-CV13RsJv.mjs";
3
3
  import * as React from "react";
4
4
  import { RenderToReadableStreamOptions } from "react-dom/server";
5
5
 
@@ -1,19 +1,24 @@
1
1
  import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
2
2
  import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
3
- import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
4
- import { i as encodeActionReference } from "../_chunks/actions-BuS5jDi0.mjs";
3
+ import { RUNTIME_GLOBAL } from "../boot.mjs";
4
+ import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-zEYsfFxW.mjs";
5
+ import { i as encodeActionReference } from "../_chunks/actions-CvQ0-zjg.mjs";
5
6
  import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
7
  import { a as parseHeldValues, n as addressValues, o as selectValuesToSend, r as escapeScriptJson } from "../_chunks/values-37UIzxM0.mjs";
7
- import { c as setIslandServerHook, i as getContexts, l as setRenderStateResolver, n as createIsland, o as isolateIslands, r as getClientReference, s as runtimeTagged, t as IslandsProvider, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
8
- import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-C47p6khN.mjs";
8
+ import { c as setIslandServerHook, i as getContexts, l as setRenderStateResolver, n as createIsland, o as isolateIslands, r as getClientReference, s as runtimeTagged, t as IslandsProvider, u as withinClientRoot } from "../_chunks/island-DXAGP4FN.mjs";
9
+ import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-BQphP23Y.mjs";
9
10
  import * as React from "react";
10
11
  // src/react/server.ts
11
12
  /**
12
13
  * @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
13
14
  * server code they need (values, action references); `renderToStream` renders a page.
14
15
  */
15
- const PROMISE_PREFIX = `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
16
+ let promisePrefix;
16
17
  let promiseCount = 0;
18
+ function nextPromiseName() {
19
+ promisePrefix ??= `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
20
+ return promisePrefix + (promiseCount++).toString(36);
21
+ }
17
22
  function isThenable(value) {
18
23
  return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
19
24
  }
@@ -23,7 +28,7 @@ setIslandServerHook({
23
28
  const addressed = addressValues(props, { encodeSpecial: (value) => {
24
29
  if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
25
30
  if (isThenable(value)) {
26
- const name = PROMISE_PREFIX + (promiseCount++).toString(36);
31
+ const name = nextPromiseName();
27
32
  promises.push({
28
33
  name,
29
34
  promise: value
@@ -84,9 +89,12 @@ async function renderToStream(element, { request, formState, bootstrap = true, p
84
89
  held: held && held.size > 0 ? held : null,
85
90
  sent: /* @__PURE__ */ new Set()
86
91
  } }, element, formState ? React.createElement(FormStateScript, { formState }) : null);
87
- const entry = bootstrap ? getBuildDefaults().entry : void 0;
88
- const bootstrapModules = entry ? [...options.bootstrapModules ?? [], entry] : options.bootstrapModules;
89
- const bootstrapScriptContent = prehydration ?? entry !== void 0 ? `${PREHYDRATION_SCRIPT}${options.bootstrapScriptContent ?? ""}` : options.bootstrapScriptContent;
92
+ const defaults = getBuildDefaults();
93
+ const entry = bootstrap ? defaults.entry : void 0;
94
+ const boot = entry !== void 0 ? defaults.boot : void 0;
95
+ const bootstrapModules = entry ? [...options.bootstrapModules ?? [], boot ?? entry] : options.bootstrapModules;
96
+ const runtime = boot ? `window.${RUNTIME_GLOBAL}=${escapeScriptJson(JSON.stringify(entry))};` : "";
97
+ const bootstrapScriptContent = prehydration ?? entry !== void 0 ? `${PREHYDRATION_SCRIPT}${runtime}${options.bootstrapScriptContent ?? ""}` : runtime || options.bootstrapScriptContent ? `${runtime}${options.bootstrapScriptContent ?? ""}` : void 0;
90
98
  const { renderToReadableStream } = await import("react-dom/server");
91
99
  const stream = await renderToReadableStream(tree, {
92
100
  ...options,
package/dist/react.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { f as IslandRenderState, g as isolateIslands, l as renderServerTree, m as createIsland, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "./_chunks/tree-B4RMU2UX.mjs";
1
+ import { f as IslandRenderState, g as isolateIslands, l as renderServerTree, m as createIsland, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "./_chunks/tree-CV13RsJv.mjs";
2
2
  import { n as IslandStrategy } from "./_chunks/schedule-Dw14dW0H.mjs";
3
3
  export { type IslandRenderState, type IslandStrategy, IslandsProvider, Outlet, type ServerTreeRenderOptions, createIsland, isolateIslands, renderServerTree, withinClientRoot };
package/dist/react.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { n as createIsland, o as isolateIslands, t as IslandsProvider, u as withinClientRoot } from "./_chunks/island-DVOr-OUw.mjs";
2
- import { a as renderServerTree, t as Outlet } from "./_chunks/tree-C47p6khN.mjs";
1
+ import { n as createIsland, o as isolateIslands, t as IslandsProvider, u as withinClientRoot } from "./_chunks/island-DXAGP4FN.mjs";
2
+ import { a as renderServerTree, t as Outlet } from "./_chunks/tree-BQphP23Y.mjs";
3
3
  export { IslandsProvider, Outlet, createIsland, isolateIslands, renderServerTree, withinClientRoot };
package/dist/server.d.mts CHANGED
@@ -71,15 +71,15 @@ declare class SealError extends Error {
71
71
  interface ActionKeys {
72
72
  /** The key new values are sealed with. */
73
73
  current: CryptoKey;
74
- /** Its id, written into each sealed value (0-255). */
74
+ /** Its id, written into each sealed value (a 32-bit unsigned integer). */
75
75
  currentId: number;
76
76
  /**
77
- * Every key that opens a value, by id: the current one and earlier ones still accepted. Ids are
78
- * one byte, so two keys can share one; each is tried.
77
+ * Every key that opens a value, by id: the current one and earlier ones still accepted. Two keys
78
+ * may still share an id; each is tried.
79
79
  */
80
80
  byId: ReadonlyMap<number, readonly CryptoKey[]>;
81
81
  }
82
- /** A new random key, as base64url: what `FURROW_ACTIONS_KEY` holds. */
82
+ /** A new random key, as base64url: what `ISLX_ACTIONS_KEY` holds. */
83
83
  declare function generateActionKey(): string;
84
84
  /**
85
85
  * The keys values are sealed and opened with. `current` seals; `previous` keys still open
@@ -187,6 +187,11 @@ declare function isSameOriginRequest(request: Request, allowedOrigins: readonly
187
187
  interface BuildDefaults {
188
188
  /** The URL of the browser entry `renderToStream` adds to pages. */
189
189
  entry?: string;
190
+ /**
191
+ * The URL of the boot entry (`@farm.js/islx/boot`): pages start with it instead, and it loads
192
+ * `entry` when the page first needs it. An island that hydrates at load preloads `entry`.
193
+ */
194
+ boot?: string;
190
195
  /** A key made for this build: every instance of one build shares it. */
191
196
  key?: string;
192
197
  /**
package/dist/server.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as parseHeldValues, i as hashValue, n as addressValues, o as selectValuesToSend, r as escapeScriptJson, t as VALUE_MIN_LENGTH } from "./_chunks/values-37UIzxM0.mjs";
2
- import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-BuS5jDi0.mjs";
2
+ import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-CvQ0-zjg.mjs";
3
3
  import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
4
4
  import { I as readPostedAction, L as stripRuntimeFields, N as attachmentField, n as ACTION_KEY_FIELD, o as BOUND_FIELD, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER } from "./_chunks/wire-QEQWBr5s.mjs";
5
5
  import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
@@ -1,4 +1,4 @@
1
- import { t as IslandsProvider } from "../_chunks/island-DVOr-OUw.mjs";
1
+ import { t as IslandsProvider } from "../_chunks/island-DXAGP4FN.mjs";
2
2
  import { A as VALUES_HEADER, C as RENDER_HEADER } from "../_chunks/wire-QEQWBr5s.mjs";
3
3
  import { a as parseHeldValues } from "../_chunks/values-37UIzxM0.mjs";
4
4
  import * as React from "react";
@@ -1,4 +1,4 @@
1
- import { n as islx, t as ENTRY_ID } from "./_chunks/vite-BTzYlEiY.mjs";
1
+ import { n as ENTRY_ID, r as islx } from "./_chunks/vite-0bH_ZDys.mjs";
2
2
  import { normalizePath } from "vite";
3
3
  // src/tanstack-start/index.ts
4
4
  /**