swift-rust 1.2.1 → 1.3.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/bin/build.mjs CHANGED
@@ -412,6 +412,15 @@ async function main() {
412
412
  }
413
413
  }
414
414
 
415
+ // Client navigator runtime (SPA navigation). The rendered HTML references
416
+ // /_swift-rust/navigator.js; emit it as a static asset so deployed sites
417
+ // get client-side navigation too.
418
+ const navSrc = join(RUNTIME_DIR, "navigator.js");
419
+ if (existsSync(navSrc)) {
420
+ writeRawFile(STATIC_DIR, "_swift-rust/navigator.js", readFileSync(navSrc));
421
+ process.stdout.write(` ${paint("green", "✓")} _swift-rust/navigator.js\n`);
422
+ }
423
+
415
424
  writeConfigJson(OUT_DIR, hasPublic);
416
425
 
417
426
  const total = Date.now() - start;
@@ -1126,6 +1126,98 @@ function cacheControlFromPlan(plan) {
1126
1126
  return null;
1127
1127
  }
1128
1128
 
1129
+ // ── Parallel routes (@slot dirs → fragment / fallback / default) ────────────
1130
+ function findDynamicChild(dir) {
1131
+ try {
1132
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
1133
+ if (e.isDirectory() && /^\[.+\]$/.test(e.name)) return join(dir, e.name);
1134
+ }
1135
+ } catch {}
1136
+ return null;
1137
+ }
1138
+
1139
+ // Resolve one @slot dir against the URL segments below its layout. Returns a
1140
+ // React element (the slot's matched page/fragment, its default.tsx, wrapped in
1141
+ // any slot layouts + a fallback.tsx Suspense boundary) or null.
1142
+ async function resolveSlotElement(slotDir, segs) {
1143
+ const React = await import("react");
1144
+ let cur = slotDir;
1145
+ const slotLayouts = [];
1146
+ let matched = true;
1147
+ for (let i = 0; i < segs.length; i++) {
1148
+ const layoutFile = findFile(cur, "layout");
1149
+ if (layoutFile) slotLayouts.push(layoutFile);
1150
+ let next = join(cur, segs[i]);
1151
+ if (!existsSync(next) || !statSync(next).isDirectory()) {
1152
+ const dyn = findDynamicChild(cur);
1153
+ if (dyn) next = dyn;
1154
+ else {
1155
+ matched = false;
1156
+ break;
1157
+ }
1158
+ }
1159
+ cur = next;
1160
+ }
1161
+ let leafFile = null;
1162
+ if (matched) {
1163
+ const layoutFile = findFile(cur, "layout");
1164
+ if (layoutFile && !slotLayouts.includes(layoutFile)) slotLayouts.push(layoutFile);
1165
+ leafFile = findFile(cur, "page") || findFile(cur, "fragment");
1166
+ }
1167
+ if (!leafFile) {
1168
+ // Unmatched slot → default.tsx (Next.js semantics), else nothing.
1169
+ const def = findFile(slotDir, "default");
1170
+ if (!def) return null;
1171
+ leafFile = def;
1172
+ slotLayouts.length = 0;
1173
+ const rootLayout = findFile(slotDir, "layout");
1174
+ if (rootLayout) slotLayouts.push(rootLayout);
1175
+ }
1176
+ let mod;
1177
+ try {
1178
+ mod = await loadModuleFresh(leafFile);
1179
+ } catch {
1180
+ return null;
1181
+ }
1182
+ const Comp = mod.default ?? mod.Page ?? mod.Fragment ?? mod.page ?? mod.fragment;
1183
+ if (!Comp) return null;
1184
+ let el = React.createElement(Comp, {});
1185
+ for (let i = slotLayouts.length - 1; i >= 0; i--) {
1186
+ try {
1187
+ const lm = await loadModuleFresh(slotLayouts[i]);
1188
+ const L = lm.default ?? lm.Layout ?? lm.layout;
1189
+ if (L) el = React.createElement(L, null, el);
1190
+ } catch {}
1191
+ }
1192
+ const fallbackFile = findFile(slotDir, "fallback");
1193
+ if (fallbackFile) {
1194
+ try {
1195
+ const fb = await loadModuleFresh(fallbackFile);
1196
+ const Fallback = fb.default ?? fb.Fallback;
1197
+ if (Fallback) el = React.createElement(React.Suspense, { fallback: React.createElement(Fallback, {}) }, el);
1198
+ } catch {}
1199
+ }
1200
+ return el;
1201
+ }
1202
+
1203
+ // Collect all @slot dirs of a layout dir into named props for the layout.
1204
+ async function resolveParallelSlots(dir, segs) {
1205
+ const slots = {};
1206
+ let entries;
1207
+ try {
1208
+ entries = readdirSync(dir, { withFileTypes: true });
1209
+ } catch {
1210
+ return slots;
1211
+ }
1212
+ for (const e of entries) {
1213
+ if (e.isDirectory() && e.name.startsWith("@")) {
1214
+ const el = await resolveSlotElement(join(dir, e.name), segs);
1215
+ if (el) slots[e.name.slice(1)] = el;
1216
+ }
1217
+ }
1218
+ return slots;
1219
+ }
1220
+
1129
1221
  async function renderRoute(urlPath, req) {
1130
1222
  const segments = urlToRouteSegments(urlPath);
1131
1223
  const route = resolvePageRoute(segments);
@@ -1199,10 +1291,28 @@ async function renderRoute(urlPath, req) {
1199
1291
  tree,
1200
1292
  );
1201
1293
  }
1202
- for (let i = layouts.length - 1; i >= 0; i--) {
1203
- const layoutMod = await loadModuleFresh(layouts[i].file);
1204
- const Layout = layoutMod.default ?? layoutMod.Layout ?? layoutMod.layout;
1205
- if (Layout) tree = React.createElement(Layout, null, tree);
1294
+ // Wrap the page with each segment's template then layout, innermost dir
1295
+ // first, so the nesting is layout > template > children (Next.js order).
1296
+ // template.tsx re-mounts on navigation; on the server it just wraps.
1297
+ const dirChain = route.dirChain && route.dirChain.length ? route.dirChain : [APP_DIR];
1298
+ for (let i = dirChain.length - 1; i >= 0; i--) {
1299
+ const dir = dirChain[i];
1300
+ const templateFile = findFile(dir, "template");
1301
+ if (templateFile) {
1302
+ const tmod = await loadModuleFresh(templateFile);
1303
+ const Template = tmod.default ?? tmod.Template ?? tmod.template;
1304
+ if (Template) tree = React.createElement(Template, null, tree);
1305
+ }
1306
+ const layoutFile = findFile(dir, "layout");
1307
+ if (layoutFile) {
1308
+ const layoutMod = await loadModuleFresh(layoutFile);
1309
+ const Layout = layoutMod.default ?? layoutMod.Layout ?? layoutMod.layout;
1310
+ if (Layout) {
1311
+ // Parallel routes: @slot subdirs of this layout become named props.
1312
+ const slotProps = await resolveParallelSlots(dir, segments.slice(i));
1313
+ tree = React.createElement(Layout, slotProps, tree);
1314
+ }
1315
+ }
1206
1316
  }
1207
1317
  const html = await renderToStringCompat(tree);
1208
1318
  if (runtime?.__setRouteContext) runtime.__setRouteContext(null);
@@ -1257,10 +1367,93 @@ async function renderRoute(urlPath, req) {
1257
1367
  }
1258
1368
  } catch {}
1259
1369
  }
1370
+ // global-error.tsx — root-level boundary. It renders its own <html>/<body>,
1371
+ // so it replaces the whole document. Only kicks in for uncaught errors when
1372
+ // no closer error/error-recovery boundary handled them.
1373
+ const globalErrorFile = findFile(APP_DIR, "global-error");
1374
+ if (globalErrorFile) {
1375
+ try {
1376
+ const React = await import("react");
1377
+ const mod = await loadModule(globalErrorFile, { bust: true });
1378
+ const GlobalError = mod.default ?? mod.GlobalError;
1379
+ if (GlobalError) {
1380
+ const inner = await renderToStringCompat(
1381
+ React.createElement(GlobalError, { error: err, reset: () => {} }),
1382
+ );
1383
+ // status:200 here only gates past the dev error overlay; the
1384
+ // rawResponse carries the real 500 to the client.
1385
+ return {
1386
+ status: 200,
1387
+ html: null,
1388
+ error: null,
1389
+ segments,
1390
+ rawResponse: new Response(`<!DOCTYPE html>${inner}`, {
1391
+ status: 500,
1392
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1393
+ }),
1394
+ };
1395
+ }
1396
+ } catch {}
1397
+ }
1260
1398
  return { status: 500, html: null, error: err, segments, pageFile: route.file };
1261
1399
  }
1262
1400
  }
1263
1401
 
1402
+ async function resolveTransitionConfig(segments) {
1403
+ const file = findRouteFileUp(segments || [], "transition");
1404
+ if (!file) return null;
1405
+ try {
1406
+ const mod = await loadModuleFresh(file);
1407
+ const cfg = mod.default && typeof mod.default === "object" ? mod.default : {};
1408
+ const type = mod.type ?? cfg.type ?? "fade";
1409
+ const out = { type };
1410
+ const duration = mod.duration ?? cfg.duration;
1411
+ if (duration) out.duration = Number(duration);
1412
+ return out;
1413
+ } catch {
1414
+ return null;
1415
+ }
1416
+ }
1417
+
1418
+ async function resolvePrefetchConfig(segments) {
1419
+ const file = findRouteFileUp(segments || [], "prefetch");
1420
+ if (!file) return null;
1421
+ try {
1422
+ const mod = await loadModuleFresh(file);
1423
+ const cfg = mod.default && typeof mod.default === "object" ? mod.default : {};
1424
+ const strategy = mod.strategy ?? cfg.strategy ?? "hover";
1425
+ const out = { strategy };
1426
+ const margin = mod.margin ?? cfg.margin;
1427
+ if (margin) out.margin = String(margin);
1428
+ return out;
1429
+ } catch {
1430
+ return null;
1431
+ }
1432
+ }
1433
+
1434
+ const pendingOverlayCache = new Map();
1435
+ async function renderPendingOverlay(segments) {
1436
+ const file = findRouteFileUp(segments || [], "pending");
1437
+ if (!file) return "";
1438
+ try {
1439
+ let inner;
1440
+ const cached = pendingOverlayCache.get(file);
1441
+ if (cached && cached.gen === buildGeneration) {
1442
+ inner = cached.html;
1443
+ } else {
1444
+ const React = await import("react");
1445
+ const mod = await loadModuleFresh(file);
1446
+ const Pending = mod.default ?? mod.Pending ?? mod.pending;
1447
+ if (!Pending) return "";
1448
+ inner = await renderToStringCompat(React.createElement(Pending, {}));
1449
+ pendingOverlayCache.set(file, { gen: buildGeneration, html: inner });
1450
+ }
1451
+ return `<div id="__sr-pending" data-sr-pending hidden style="position:fixed;top:0;left:0;right:0;z-index:2147483646;pointer-events:none">${inner}</div>`;
1452
+ } catch {
1453
+ return "";
1454
+ }
1455
+ }
1456
+
1264
1457
  async function renderNotFound(segments) {
1265
1458
  const notFoundFile = findNotFound(segments);
1266
1459
  if (!notFoundFile) {
@@ -1349,6 +1542,7 @@ async function wrapInDocumentAsync({ head, body }) {
1349
1542
  <meta charset="utf-8" />
1350
1543
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1351
1544
  ${fullHead}
1545
+ <script src="/_swift-rust/navigator.js" defer></script>
1352
1546
  <script src="/_swift-rust/hmr-client.js" defer></script>
1353
1547
  </head>
1354
1548
  <body>${body}</body>
@@ -1364,6 +1558,15 @@ async function tryHmrClient() {
1364
1558
  }
1365
1559
  }
1366
1560
 
1561
+ function readNavigatorClient() {
1562
+ try {
1563
+ const file = join(dirname(new URL(import.meta.url).pathname), "runtime", "navigator.js");
1564
+ return readFileSync(file, "utf8");
1565
+ } catch {
1566
+ return null;
1567
+ }
1568
+ }
1569
+
1367
1570
  function shouldIgnoreFile(filename) {
1368
1571
  if (!filename) return true;
1369
1572
  const base = filename.split(sep).pop();
@@ -1747,6 +1950,14 @@ async function handleFetch(req) {
1747
1950
  return new Response("Not found", { status: 404 });
1748
1951
  }
1749
1952
 
1953
+ if (pathname === "/_swift-rust/navigator.js") {
1954
+ const client = readNavigatorClient();
1955
+ if (client) {
1956
+ return new Response(client, { headers: { "Content-Type": "application/javascript; charset=utf-8" } });
1957
+ }
1958
+ return new Response("// navigator unavailable", { status: 404, headers: { "Content-Type": "application/javascript" } });
1959
+ }
1960
+
1750
1961
  if (pathname === "/_swift-rust/image") {
1751
1962
  const target = url.searchParams.get("url");
1752
1963
  const w = parseInt(url.searchParams.get("w") || "0", 10);
@@ -1989,6 +2200,22 @@ async function handleFetch(req) {
1989
2200
  const src = `/_swift-rust/island.js?p=${encodeURIComponent(renderResult.pageFile)}`;
1990
2201
  doc = doc.replace("</body>", `<script type="module" src="${src}"></script>\n</body>`);
1991
2202
  }
2203
+ // pending.tsx → hidden overlay the client navigator reveals while a
2204
+ // navigation is in flight (see runtime/navigator.js).
2205
+ const pendingOverlay = await renderPendingOverlay(renderResult.segments);
2206
+ if (pendingOverlay) doc = doc.replace("</body>", `${pendingOverlay}\n</body>`);
2207
+ // prefetch.ts → client prefetch strategy for the navigator.
2208
+ const prefetchCfg = await resolvePrefetchConfig(renderResult.segments);
2209
+ if (prefetchCfg) {
2210
+ const json = JSON.stringify(prefetchCfg).replace(/</g, "\\u003c");
2211
+ doc = doc.replace("</body>", `<script>window.__SR_PREFETCH__=${json}</script>\n</body>`);
2212
+ }
2213
+ // transition.tsx → View Transitions config for the navigator's swap.
2214
+ const transitionCfg = await resolveTransitionConfig(renderResult.segments);
2215
+ if (transitionCfg) {
2216
+ const json = JSON.stringify(transitionCfg).replace(/</g, "\\u003c");
2217
+ doc = doc.replace("</body>", `<script>window.__SR_TRANSITION__=${json}</script>\n</body>`);
2218
+ }
1992
2219
  const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
1993
2220
  for (const c of renderResult.setCookies || []) headers.append("Set-Cookie", c);
1994
2221
  // config.ts headers
@@ -0,0 +1,238 @@
1
+ // Swift-Rust client navigator — turns full-page loads into SPA navigation for
2
+ // <a>/<Link>. Intercepts same-origin clicks, fetches the destination HTML,
3
+ // swaps <body>, re-runs body scripts (island bootstrap + serialized state),
4
+ // syncs <title>/meta, and manages history. Ships in dev and in the build
5
+ // output (NOT dev-only like the HMR client).
6
+ (() => {
7
+ if (window.__SR_NAV__) return;
8
+ const nav = (window.__SR_NAV__ = { cache: new Map(), inflight: new Map() });
9
+ const ORIGIN = location.origin;
10
+ const MAX_CACHE = 32;
11
+
12
+ function internalAnchor(a) {
13
+ if (!a || a.target === "_blank" || a.hasAttribute("download")) return null;
14
+ if (a.dataset.srNoNav !== undefined) return null;
15
+ const raw = a.getAttribute("href");
16
+ if (!raw || raw.startsWith("#") || raw.startsWith("mailto:") || raw.startsWith("tel:")) return null;
17
+ let url;
18
+ try {
19
+ url = new URL(a.href, location.href);
20
+ } catch {
21
+ return null;
22
+ }
23
+ if (url.origin !== ORIGIN) return null;
24
+ return url;
25
+ }
26
+
27
+ function remember(key, html) {
28
+ if (nav.cache.size >= MAX_CACHE) nav.cache.delete(nav.cache.keys().next().value);
29
+ nav.cache.set(key, html);
30
+ }
31
+
32
+ // Fetch (and cache) a route's HTML. Deduped per URL so prefetch + click share.
33
+ function fetchDoc(url) {
34
+ const key = url;
35
+ if (nav.cache.has(key)) return Promise.resolve(nav.cache.get(key));
36
+ if (nav.inflight.has(key)) return nav.inflight.get(key);
37
+ const p = fetch(url, { headers: { "x-swift-rust-nav": "1" }, credentials: "same-origin" })
38
+ .then((res) => {
39
+ if (!res.ok && res.status !== 404) throw new Error("nav fetch " + res.status);
40
+ return res.text();
41
+ })
42
+ .then((html) => {
43
+ remember(key, html);
44
+ nav.inflight.delete(key);
45
+ return html;
46
+ })
47
+ .catch((err) => {
48
+ nav.inflight.delete(key);
49
+ throw err;
50
+ });
51
+ nav.inflight.set(key, p);
52
+ return p;
53
+ }
54
+ nav.prefetch = (url) => fetchDoc(new URL(url, location.href).href).catch(() => {});
55
+
56
+ // Scripts inserted via DOM cloning don't execute; clone them into fresh nodes.
57
+ function runScripts(root) {
58
+ for (const old of root.querySelectorAll("script")) {
59
+ const s = document.createElement("script");
60
+ for (const att of old.attributes) s.setAttribute(att.name, att.value);
61
+ s.textContent = old.textContent;
62
+ old.replaceWith(s);
63
+ }
64
+ }
65
+
66
+ // transition.tsx → wrap the DOM swap in the View Transitions API. Config is
67
+ // injected as window.__SR_TRANSITION__ = { type, duration }. Falls back to a
68
+ // plain swap when unsupported, type "none", or reduced-motion is requested.
69
+ function injectTransitionStyle() {
70
+ if (document.getElementById("__sr-transition-style")) return;
71
+ const s = document.createElement("style");
72
+ s.id = "__sr-transition-style";
73
+ s.textContent =
74
+ "::view-transition-old(root),::view-transition-new(root){animation-duration:var(--sr-transition-duration,250ms)}" +
75
+ 'html[data-sr-transition="slide"]::view-transition-old(root){animation-name:sr-vt-slide-out}' +
76
+ 'html[data-sr-transition="slide"]::view-transition-new(root){animation-name:sr-vt-slide-in}' +
77
+ "@keyframes sr-vt-slide-out{to{opacity:0;transform:translateX(-24px)}}" +
78
+ "@keyframes sr-vt-slide-in{from{opacity:0;transform:translateX(24px)}}";
79
+ (document.head || document.documentElement).appendChild(s);
80
+ }
81
+
82
+ async function withTransition(apply) {
83
+ const t = window.__SR_TRANSITION__;
84
+ const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
85
+ if (!t || !t.type || t.type === "none" || reduce || typeof document.startViewTransition !== "function") {
86
+ apply();
87
+ return;
88
+ }
89
+ const root = document.documentElement;
90
+ root.dataset.srTransition = t.type;
91
+ if (t.duration) root.style.setProperty("--sr-transition-duration", t.duration + "ms");
92
+ try {
93
+ const vt = document.startViewTransition(() => apply());
94
+ await vt.finished;
95
+ } catch {
96
+ // DOM was already updated inside the callback; nothing to recover.
97
+ } finally {
98
+ delete root.dataset.srTransition;
99
+ }
100
+ }
101
+
102
+ function syncHead(doc) {
103
+ const title = doc.querySelector("title");
104
+ if (title) document.title = title.textContent || document.title;
105
+ const selectors = ['meta[name="description"]', 'meta[property^="og:"]', 'meta[name^="twitter:"]', 'link[rel="canonical"]'];
106
+ for (const sel of selectors) {
107
+ document.head.querySelectorAll(sel).forEach((m) => m.remove());
108
+ doc.head.querySelectorAll(sel).forEach((m) => document.head.appendChild(m.cloneNode(true)));
109
+ }
110
+ }
111
+
112
+ // pending.tsx overlay: revealed only if a navigation outlasts the threshold,
113
+ // so fast (cached) navigations don't flash it.
114
+ let pendingTimer = null;
115
+ function showPending() {
116
+ const el = document.getElementById("__sr-pending");
117
+ if (el) el.hidden = false;
118
+ }
119
+ function clearPending() {
120
+ if (pendingTimer) {
121
+ clearTimeout(pendingTimer);
122
+ pendingTimer = null;
123
+ }
124
+ const el = document.getElementById("__sr-pending");
125
+ if (el) el.hidden = true;
126
+ }
127
+
128
+ async function navigate(href, { push = true, scroll = true, replace = false } = {}) {
129
+ let html;
130
+ nav.active = href;
131
+ if (pendingTimer) clearTimeout(pendingTimer);
132
+ const delay = typeof window.__SR_NAV_PENDING_DELAY === "number" ? window.__SR_NAV_PENDING_DELAY : 120;
133
+ pendingTimer = setTimeout(showPending, delay);
134
+ window.dispatchEvent(new CustomEvent("sr:navigate-start", { detail: { url: href } }));
135
+ try {
136
+ html = await fetchDoc(href);
137
+ } catch {
138
+ location.href = href; // hard fallback
139
+ return;
140
+ }
141
+ if (nav.active !== href) return; // superseded by a newer navigation
142
+ const doc = new DOMParser().parseFromString(html, "text/html");
143
+ if (!doc.body) {
144
+ location.href = href;
145
+ return;
146
+ }
147
+ const apply = () => {
148
+ document.body.replaceWith(doc.body);
149
+ runScripts(document.body);
150
+ syncHead(doc);
151
+ };
152
+ await withTransition(apply);
153
+ if (push) {
154
+ if (replace) history.replaceState({ srNav: true }, "", href);
155
+ else history.pushState({ srNav: true }, "", href);
156
+ }
157
+ if (scroll) window.scrollTo(0, 0);
158
+ clearPending();
159
+ window.dispatchEvent(new CustomEvent("sr:navigate-end", { detail: { url: href } }));
160
+ }
161
+ nav.navigate = navigate;
162
+
163
+ document.addEventListener(
164
+ "click",
165
+ (e) => {
166
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
167
+ const a = e.target.closest && e.target.closest("a");
168
+ const url = internalAnchor(a);
169
+ if (!url) return;
170
+ e.preventDefault();
171
+ if (url.href === location.href) return;
172
+ navigate(url.href, {
173
+ push: true,
174
+ scroll: a.dataset.srScroll !== "false",
175
+ replace: a.dataset.srReplace === "true",
176
+ });
177
+ },
178
+ false,
179
+ );
180
+
181
+ window.addEventListener("popstate", () => {
182
+ navigate(location.href, { push: false, scroll: false });
183
+ });
184
+
185
+ // ── Prefetch (prefetch.ts) ────────────────────────────────────────────────
186
+ // Strategy comes from the per-route prefetch.ts, injected as
187
+ // window.__SR_PREFETCH__ = { strategy, margin? }. Default: "hover".
188
+ // Per-link opt-out via data-sr-prefetch="false".
189
+ function strategy() {
190
+ const c = window.__SR_PREFETCH__;
191
+ return (c && c.strategy) || "hover";
192
+ }
193
+ function prefetchableURL(a) {
194
+ if (!a || a.dataset.srPrefetch === "false") return null;
195
+ return internalAnchor(a);
196
+ }
197
+ function intent(e) {
198
+ if (strategy() !== "hover") return;
199
+ const a = e.target.closest && e.target.closest("a");
200
+ const url = prefetchableURL(a);
201
+ if (url && url.href !== location.href) nav.prefetch(url.href);
202
+ }
203
+ document.addEventListener("mouseover", intent, { passive: true });
204
+ document.addEventListener("focusin", intent);
205
+ document.addEventListener("touchstart", intent, { passive: true });
206
+
207
+ let io = null;
208
+ function scanViewport() {
209
+ if (io) {
210
+ io.disconnect();
211
+ io = null;
212
+ }
213
+ if (strategy() !== "viewport" || !("IntersectionObserver" in window)) return;
214
+ const margin = (window.__SR_PREFETCH__ && window.__SR_PREFETCH__.margin) || "200px";
215
+ io = new IntersectionObserver(
216
+ (entries) => {
217
+ for (const en of entries) {
218
+ if (!en.isIntersecting) continue;
219
+ const url = prefetchableURL(en.target);
220
+ if (url && url.href !== location.href) nav.prefetch(url.href);
221
+ io.unobserve(en.target);
222
+ }
223
+ },
224
+ { rootMargin: margin },
225
+ );
226
+ document.querySelectorAll("a[href]").forEach((a) => {
227
+ if (prefetchableURL(a)) io.observe(a);
228
+ });
229
+ }
230
+ window.addEventListener("sr:navigate-end", scanViewport);
231
+ if (document.readyState === "loading") {
232
+ document.addEventListener("DOMContentLoaded", scanViewport, { once: true });
233
+ } else {
234
+ scanViewport();
235
+ }
236
+
237
+ injectTransitionStyle();
238
+ })();
package/dist/link.d.ts CHANGED
@@ -6,5 +6,5 @@ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
6
6
  replace?: boolean;
7
7
  scroll?: boolean;
8
8
  }
9
- export declare function Link({ href, prefetch: _prefetch, replace: _replace, scroll: _scroll, children, ...rest }: LinkProps): import("react").JSX.Element;
9
+ export declare function Link({ href, prefetch, replace, scroll, children, ...rest }: LinkProps): import("react").JSX.Element;
10
10
  //# sourceMappingURL=link.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7D,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,QAAQ,EAAE,SAAS,EACnB,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,OAAO,EACf,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,SAAS,+BAMX"}
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7D,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,+BAYrF"}
package/dist/link.jsx CHANGED
@@ -1,5 +1,14 @@
1
- export function Link({ href, prefetch: _prefetch, replace: _replace, scroll: _scroll, children, ...rest }) {
2
- return (<a href={href} {...rest}>
1
+ export function Link({ href, prefetch, replace, scroll, children, ...rest }) {
2
+ // The client navigator (runtime/navigator.js) reads these data-* hints to
3
+ // drive SPA navigation. Plain <a> semantics are preserved when JS is off.
4
+ const dataAttrs = {};
5
+ if (replace)
6
+ dataAttrs["data-sr-replace"] = "true";
7
+ if (scroll === false)
8
+ dataAttrs["data-sr-scroll"] = "false";
9
+ if (prefetch === false)
10
+ dataAttrs["data-sr-prefetch"] = "false";
11
+ return (<a href={href} {...dataAttrs} {...rest}>
3
12
  {children}
4
13
  </a>);
5
14
  }
package/dist/link.jsx.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"link.jsx","sourceRoot":"","sources":["../src/link.tsx"],"names":[],"mappings":"AAUA,MAAM,UAAU,IAAI,CAAC,EACnB,IAAI,EACJ,QAAQ,EAAE,SAAS,EACnB,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,OAAO,EACf,QAAQ,EACR,GAAG,IAAI,EACG;IACV,OAAO,CACL,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CACtB;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,CAAC,CAAC,CACL,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"link.jsx","sourceRoot":"","sources":["../src/link.tsx"],"names":[],"mappings":"AAUA,MAAM,UAAU,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAa;IACpF,0EAA0E;IAC1E,0EAA0E;IAC1E,MAAM,SAAS,GAA2B,EAAE,CAAC;IAC7C,IAAI,OAAO;QAAE,SAAS,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC;IACnD,IAAI,MAAM,KAAK,KAAK;QAAE,SAAS,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC;IAC5D,IAAI,QAAQ,KAAK,KAAK;QAAE,SAAS,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC;IAChE,OAAO,CACL,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,IAAI,IAAI,CAAC,CACrC;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,CAAC,CAAC,CACL,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swift-rust",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "The full-stack React framework powered with Rust + Bun. TSX-first, Rust rendering, 10x faster than Next.js, single binary deploy.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://swift-rust.dev",