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 +9 -0
- package/bin/dev-server.mjs +231 -4
- package/bin/runtime/navigator.js +238 -0
- package/dist/link.d.ts +1 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/link.jsx +11 -2
- package/dist/link.jsx.map +1 -1
- package/package.json +1 -1
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;
|
package/bin/dev-server.mjs
CHANGED
|
@@ -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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
|
9
|
+
export declare function Link({ href, prefetch, replace, scroll, children, ...rest }: LinkProps): import("react").JSX.Element;
|
|
10
10
|
//# sourceMappingURL=link.d.ts.map
|
package/dist/link.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
2
|
-
|
|
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,
|
|
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.
|
|
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",
|