swift-rust 1.3.0 → 1.5.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
@@ -13,6 +13,7 @@ import {
13
13
  } from "node:fs";
14
14
  import { join, resolve, dirname, sep } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { createRequire } from "node:module";
16
17
 
17
18
  const cwd = process.cwd();
18
19
  const APP_DIR_CANDIDATES = [resolve(cwd, "src", "app"), resolve(cwd, "app")];
@@ -22,6 +23,260 @@ const OUT_DIR = resolve(cwd, ".vercel", "output");
22
23
  const STATIC_DIR = join(OUT_DIR, "static");
23
24
  const RUNTIME_DIR = resolve(fileURLToPath(import.meta.url), "..", "runtime");
24
25
 
26
+ const ROUTING_EXTS = [".tsx", ".ts", ".jsx", ".js"];
27
+
28
+ // Scan a file's leading directives for a runtime / guard opt-in.
29
+ function scanFileDirectives(file) {
30
+ const out = { runtime: null, guard: false };
31
+ try {
32
+ for (const line of readFileSync(file, "utf8").split("\n")) {
33
+ const t = line.trim();
34
+ if (!t || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
35
+ const m = t.match(/^["']use (bun|edge|node|worker|guard)["'];?$/);
36
+ if (m) {
37
+ if (m[1] === "guard") out.guard = true;
38
+ else if (!out.runtime) out.runtime = m[1];
39
+ continue;
40
+ }
41
+ if (/^["']use [a-z]+["'];?$/.test(t)) continue;
42
+ break;
43
+ }
44
+ } catch {}
45
+ return out;
46
+ }
47
+
48
+ // Source-level detection of a route's runtime/guard opt-in (page + layouts),
49
+ // so dynamic routes are emitted even when a build-time GET redirects.
50
+ function scanDirectives(pageFile) {
51
+ const { layouts } = collectChainFiles(pageFile);
52
+ let runtime = null;
53
+ let guard = false;
54
+ for (const f of [pageFile, ...layouts]) {
55
+ const d = scanFileDirectives(f);
56
+ if (d.runtime && !runtime) runtime = d.runtime;
57
+ if (d.guard) guard = true;
58
+ }
59
+ return { runtime, guardEnabled: guard };
60
+ }
61
+
62
+ function findRoutingFile(dir, base) {
63
+ for (const ext of ROUTING_EXTS) {
64
+ const f = join(dir, `${base}${ext}`);
65
+ if (existsSync(f)) return f;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ // Collect the routing files along a page's dir chain (outer → inner) plus the
71
+ // URL pattern segments (with [param] markers preserved). Mirrors the dev
72
+ // server's pipeline so the emitted function has the same behavior.
73
+ function collectChainFiles(pageFile) {
74
+ const layouts = []; // { file, slots: [{ name, file }] }
75
+ const proxies = [];
76
+ const schemas = [];
77
+ const guards = [];
78
+ const loaders = [];
79
+ let actionFile = null;
80
+ let queryFile = null;
81
+ let stateFile = null;
82
+ let seoFile = null;
83
+ let i18nFile = null;
84
+ let revalidateFile = null;
85
+ const pattern = [];
86
+ const rel = dirname(pageFile).slice(APP_DIR.length).replace(/^[/\\]/, "");
87
+ const segs = rel ? rel.split(sep) : [];
88
+ if (APP_DIR.endsWith(`${sep}app`) && dirname(APP_DIR).endsWith(`${sep}src`)) {
89
+ const rp = findRoutingFile(dirname(APP_DIR), "proxy");
90
+ if (rp) proxies.push(rp);
91
+ }
92
+ const slotsFor = (d) => {
93
+ const out = [];
94
+ try {
95
+ for (const e of readdirSync(d, { withFileTypes: true })) {
96
+ if (e.isDirectory() && e.name.startsWith("@")) {
97
+ const sd = join(d, e.name);
98
+ const file = findRoutingFile(sd, "page") || findRoutingFile(sd, "fragment") || findRoutingFile(sd, "default");
99
+ if (file) out.push({ name: e.name.slice(1), file });
100
+ }
101
+ }
102
+ } catch {}
103
+ return out;
104
+ };
105
+ let dir = APP_DIR;
106
+ const scan = (d, isLeaf) => {
107
+ const l = findRoutingFile(d, "layout");
108
+ if (l) layouts.push({ file: l, slots: slotsFor(d) });
109
+ const p = findRoutingFile(d, "proxy");
110
+ if (p) proxies.push(p);
111
+ const s = findRoutingFile(d, "schema");
112
+ if (s) schemas.push(s);
113
+ const g = findRoutingFile(d, "guard");
114
+ if (g) guards.push(g);
115
+ const ld = findRoutingFile(d, "loader");
116
+ if (ld) loaders.push(ld);
117
+ const q = findRoutingFile(d, "query");
118
+ if (q) queryFile = q;
119
+ const st = findRoutingFile(d, "state");
120
+ if (st) stateFile = st;
121
+ const se = findRoutingFile(d, "seo");
122
+ if (se) seoFile = se;
123
+ const i = findRoutingFile(d, "i18n");
124
+ if (i) i18nFile = i;
125
+ const rv = findRoutingFile(d, "revalidate");
126
+ if (rv) revalidateFile = rv;
127
+ if (isLeaf) {
128
+ const a = findRoutingFile(d, "action");
129
+ if (a) actionFile = a;
130
+ }
131
+ };
132
+ scan(dir, segs.length === 0);
133
+ for (let i = 0; i < segs.length; i++) {
134
+ dir = join(dir, segs[i]);
135
+ pattern.push(segs[i]);
136
+ scan(dir, i === segs.length - 1);
137
+ }
138
+ return { layouts, proxies, schemas, guards, loaders, actionFile, queryFile, stateFile, seoFile, i18nFile, revalidateFile, pattern };
139
+ }
140
+
141
+ function isClientPageFile(file) {
142
+ return scanFileDirectivesRaw(file).includes("client");
143
+ }
144
+ function scanFileDirectivesRaw(file) {
145
+ const found = [];
146
+ try {
147
+ for (const line of readFileSync(file, "utf8").split("\n")) {
148
+ const t = line.trim();
149
+ if (!t || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
150
+ const m = t.match(/^["']use ([a-z]+)["'];?$/);
151
+ if (m) {
152
+ found.push(m[1]);
153
+ continue;
154
+ }
155
+ break;
156
+ }
157
+ } catch {}
158
+ return found;
159
+ }
160
+
161
+ // Build a client-island bundle for a "use client" page (served by the dev
162
+ // server during build) and write it to the static output. Returns its URL.
163
+ async function buildIslandAsset(pageFile) {
164
+ try {
165
+ const res = await fetch(`http://${HOST}:${PORT}/_swift-rust/island.js?p=${encodeURIComponent(pageFile)}`);
166
+ if (!res.ok) return null;
167
+ const code = await res.text();
168
+ const rel = `_swift-rust/island/${simpleHash(pageFile)}.js`;
169
+ writeRawFile(STATIC_DIR, rel, code);
170
+ return `/${rel}`;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ // Generate + bundle a Vercel function (.func) for one dynamic route.
177
+ async function emitFunction({ route, funcRel, pageFile, runtime, head, guardEnabled, isClient, islandSrc }) {
178
+ if (typeof Bun === "undefined" || typeof Bun.build !== "function") return false;
179
+ const f = collectChainFiles(pageFile);
180
+ const fnCore = join(RUNTIME_DIR, "fn-core.mjs");
181
+ const imports = [`import { makeRouteHandler } from ${JSON.stringify(fnCore)};`];
182
+ let uid = 0;
183
+ const ns = (file) => {
184
+ const name = `__m${uid++}`;
185
+ imports.push(`import * as ${name} from ${JSON.stringify(file)};`);
186
+ return name;
187
+ };
188
+ const arr = (files) => `[${files.map((file) => ns(file)).join(", ")}]`;
189
+ const opt = (file) => (file ? ns(file) : "undefined");
190
+
191
+ // IMPORTANT: resolve every ns()/arr()/opt() (which append to `imports`)
192
+ // BEFORE joining `imports` — otherwise body-only modules go unimported.
193
+ const pageNs = ns(pageFile);
194
+ const layoutsArr = `[${f.layouts.map((l) => ns(l.file)).join(", ")}]`;
195
+ const slotsArr = `[${f.layouts.map((l) => `[${l.slots.map((s) => `{ name: ${JSON.stringify(s.name)}, mod: ${ns(s.file)} }`).join(", ")}]`).join(", ")}]`;
196
+ const proxiesArr = arr(f.proxies);
197
+ const schemasArr = arr(f.schemas);
198
+ const guardsArr = arr(f.guards);
199
+ const loadersArr = arr(f.loaders);
200
+ const actionNs = opt(f.actionFile);
201
+ const queryNs = opt(f.queryFile);
202
+ const stateNs = opt(f.stateFile);
203
+ const seoNs = opt(f.seoFile);
204
+ const i18nNs = opt(f.i18nFile);
205
+ const revNs = opt(f.revalidateFile);
206
+
207
+ const entry =
208
+ `${imports.join("\n")}\n` +
209
+ `const handler = makeRouteHandler({\n` +
210
+ ` page: ${pageNs}, layouts: ${layoutsArr}, layoutMetas: ${layoutsArr}, slots: ${slotsArr},\n` +
211
+ ` proxies: ${proxiesArr}, schemas: ${schemasArr}, guards: ${guardsArr}, guardEnabled: ${guardEnabled ? "true" : "false"},\n` +
212
+ ` action: ${actionNs}, loaders: ${loadersArr}, query: ${queryNs}, state: ${stateNs},\n` +
213
+ ` seo: ${seoNs}, i18n: ${i18nNs}, revalidate: ${revNs},\n` +
214
+ ` isClient: ${isClient ? "true" : "false"}, islandSrc: ${JSON.stringify(islandSrc || "")},\n` +
215
+ ` pattern: ${JSON.stringify(f.pattern)}, runtime: ${JSON.stringify(runtime)}, head: ${JSON.stringify(head)},\n` +
216
+ `});\n` +
217
+ (runtime === "bun" ? `export default { fetch: handler };\n` : `export default handler;\n`);
218
+
219
+ const rel = funcRel || (route === "/" ? "index.func" : `${route.replace(/^\//, "")}.func`);
220
+ const funcDir = join(OUT_DIR, "functions", rel);
221
+ mkdirSync(funcDir, { recursive: true });
222
+ const entryTmp = join(dirname(pageFile), `.__sr_fn_${process.pid}_${simpleHash(rel)}.js`);
223
+ writeFileSync(entryTmp, entry);
224
+ try {
225
+ const isEdge = runtime === "edge";
226
+ const result = await Bun.build({
227
+ entrypoints: [entryTmp],
228
+ target: isEdge ? "browser" : "node",
229
+ format: "esm",
230
+ minify: true,
231
+ external: isEdge ? ["react-dom/server"] : [],
232
+ });
233
+ if (!result.success) throw new AggregateError(result.logs, "function bundle failed");
234
+ const code = await result.outputs[0].text();
235
+ writeFileSync(join(funcDir, "index.js"), code);
236
+ const vcConfig = isEdge
237
+ ? { runtime: "edge", entrypoint: "index.js" }
238
+ : { runtime: "nodejs22.x", handler: "index.js", launcherType: "Nodejs", supportsResponseStreaming: true };
239
+ writeFileSync(join(funcDir, ".vc-config.json"), `${JSON.stringify(vcConfig, null, 2)}\n`);
240
+ return true;
241
+ } finally {
242
+ try {
243
+ unlinkSync(entryTmp);
244
+ } catch {}
245
+ }
246
+ }
247
+
248
+ // Vercel runs functions on Bun when vercel.json sets bunVersion. Ensure it's
249
+ // present (preserving the rest of the file) so 'use bun' routes run on Bun.
250
+ function ensureBunVersion() {
251
+ const file = resolve(cwd, "vercel.json");
252
+ let json = {};
253
+ if (existsSync(file)) {
254
+ try {
255
+ json = JSON.parse(readFileSync(file, "utf8"));
256
+ } catch {
257
+ json = {};
258
+ }
259
+ }
260
+ if (json.bunVersion) return;
261
+ json.bunVersion = "1.x";
262
+ writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
263
+ process.stdout.write(` ${paint("cyan", "•")} set ${paint("bold", 'vercel.json "bunVersion": "1.x"')} for Bun functions\n`);
264
+ }
265
+
266
+ // Locate the bundled local fonts (installed @swift-rust/font, else monorepo).
267
+ function resolveLocalFontDir() {
268
+ const candidates = [];
269
+ try {
270
+ const req = createRequire(import.meta.url);
271
+ const pkg = req.resolve("@swift-rust/font/package.json");
272
+ candidates.push(join(dirname(pkg), "src", "local"), join(dirname(pkg), "dist", "local"));
273
+ } catch {}
274
+ candidates.push(
275
+ resolve(fileURLToPath(import.meta.url), "..", "..", "..", "..", "packages", "font", "src", "local"),
276
+ );
277
+ return candidates.find((d) => existsSync(d)) ?? null;
278
+ }
279
+
25
280
  const PORT_START = parseInt(process.env.SWIFT_RUST_BUILD_PORT || "47321", 10);
26
281
  const HOST = "127.0.0.1";
27
282
  let PORT = PORT_START;
@@ -57,6 +312,8 @@ function discoverPages(dir, base = "") {
57
312
  if (!existsSync(dir)) return pages;
58
313
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
59
314
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
315
+ // @slot dirs (parallel routes) are not URL segments — composed into layouts.
316
+ if (entry.name.startsWith("@")) continue;
60
317
  const full = join(dir, entry.name);
61
318
  if (entry.isDirectory()) {
62
319
  const catchAll = entry.name.match(CATCH_PARAM);
@@ -180,7 +437,13 @@ async function fetchRoute(pathname, timeoutMs = ROUTE_TIMEOUT_MS) {
180
437
  signal: controller.signal,
181
438
  redirect: "manual",
182
439
  });
183
- return { status: res.status, body: await res.text() };
440
+ return {
441
+ status: res.status,
442
+ body: await res.text(),
443
+ runtime: res.headers.get("x-swift-rust-runtime") || "bun",
444
+ dynamic: res.headers.get("x-swift-rust-dynamic") === "1",
445
+ guardEnabled: res.headers.get("x-swift-rust-guard") === "1",
446
+ };
184
447
  } finally {
185
448
  clearTimeout(timer);
186
449
  }
@@ -258,7 +521,7 @@ async function localizeIslands(html) {
258
521
  return out;
259
522
  }
260
523
 
261
- function writeConfigJson(outDir, _hasPublic) {
524
+ function writeConfigJson(outDir, _hasPublic, fnRoutes = []) {
262
525
  // Build Output API v3 config. Only schema-valid fields here — unknown
263
526
  // top-level fields or route properties are rejected at "Deploying outputs".
264
527
  const config = {
@@ -266,6 +529,8 @@ function writeConfigJson(outDir, _hasPublic) {
266
529
  routes: [
267
530
  { src: "/_swift-rust/static/(.*)", headers: { "Cache-Control": "public, max-age=31536000, immutable" } },
268
531
  { src: "/fonts/(.*)", headers: { "Cache-Control": "public, max-age=31536000, immutable" } },
532
+ // [param] / catch-all routes → their request-time function.
533
+ ...fnRoutes,
269
534
  { handle: "filesystem" },
270
535
  { src: "^(.*)$", status: 404, dest: "/404.html" },
271
536
  ],
@@ -313,10 +578,12 @@ async function main() {
313
578
  for (const r of routes) staticRoutes.push(r);
314
579
  process.stdout.write(` ${paint("dim", "•")} catch-all ${paint("cyan", ca.base + "/[...]")}: ${paint("bold", String(routes.length))}\n`);
315
580
  }
581
+ const paramFns = []; // dynamic pages with no static params → request-time functions
316
582
  for (const dyn of dynamics) {
317
583
  const params = await enumerateParams(dyn);
318
584
  if (params.length === 0) {
319
- process.stdout.write(` ${paint("dim", "•")} dynamic ${paint("cyan", dyn.base + "/[" + dyn.paramName + "]")}: ${paint("yellow", "needs serverless function (skipped for v0.1.0)")}\n`);
585
+ paramFns.push(dyn);
586
+ process.stdout.write(` ${paint("dim", "•")} dynamic ${paint("cyan", dyn.base + "/[" + dyn.paramName + "]")}: ${paint("cyan", "→ function")}\n`);
320
587
  continue;
321
588
  }
322
589
  const routes = routesFromParams(dyn.base, dyn.paramName, params);
@@ -324,6 +591,10 @@ async function main() {
324
591
  process.stdout.write(` ${paint("dim", "•")} dynamic ${paint("cyan", dyn.base + "/[" + dyn.paramName + "]")}: ${paint("bold", String(routes.length))}\n`);
325
592
  }
326
593
 
594
+ // route → page file (for static-type routes that may opt into a runtime fn).
595
+ const routeToFile = new Map();
596
+ for (const p of pages) if (p.type === "static") routeToFile.set(p.route, p.file);
597
+
327
598
  const allRoutes = [...new Set(staticRoutes)].sort();
328
599
  process.stdout.write(` ${paint("dim", "•")} total: ${paint("bold", String(allRoutes.length))}\n\n`);
329
600
 
@@ -336,18 +607,64 @@ async function main() {
336
607
  let okCount = 0;
337
608
  let skipCount = 0;
338
609
  let failCount = 0;
610
+ let fnCount = 0;
611
+ let usesBunFns = false;
612
+ let fallbackHead = null;
613
+ const fnConfigRoutes = [];
339
614
  try {
340
615
  await waitForServer();
341
616
  process.stdout.write(` ${paint("green", "✓")} server ready\n\n`);
342
617
 
343
618
  for (const route of allRoutes) {
344
619
  try {
345
- const { status, body } = await fetchRoute(route);
620
+ const { status, body, runtime, dynamic, guardEnabled } = await fetchRoute(route);
621
+ const pageFile = routeToFile.get(route);
622
+ const cleaned = status === 200 ? rewriteImageUrls(await localizeIslands(stripHmrScript(body))) : null;
623
+ if (cleaned && fallbackHead == null) {
624
+ fallbackHead = (cleaned.match(/<head>([\s\S]*?)<\/head>/i) || [, ""])[1];
625
+ }
626
+
627
+ // Decide if this route is a request-time function. Prefer the live
628
+ // headers (200 render); else fall back to a source directive scan so
629
+ // guarded routes that redirect a build-time GET still get a function.
630
+ let fnRuntime = null;
631
+ let fnGuard = false;
632
+ let fnHead = null;
633
+ if (dynamic && pageFile) {
634
+ fnRuntime = runtime;
635
+ fnGuard = guardEnabled;
636
+ fnHead = (cleaned?.match(/<head>([\s\S]*?)<\/head>/i) || [, ""])[1];
637
+ } else if (pageFile && status !== 404) {
638
+ const d = scanDirectives(pageFile);
639
+ if (d.runtime) {
640
+ fnRuntime = d.runtime;
641
+ fnGuard = d.guardEnabled;
642
+ fnHead = fallbackHead || "";
643
+ }
644
+ }
645
+
646
+ if (fnRuntime && pageFile) {
647
+ let emitted = false;
648
+ const isClient = isClientPageFile(pageFile);
649
+ const islandSrc = isClient ? await buildIslandAsset(pageFile) : null;
650
+ try {
651
+ emitted = await emitFunction({ route, pageFile, runtime: fnRuntime, head: fnHead, guardEnabled: fnGuard, isClient, islandSrc });
652
+ } catch (e) {
653
+ process.stdout.write(` ${paint("dim", `fn emit failed: ${e?.message || e}`)}\n`);
654
+ }
655
+ if (emitted) {
656
+ fnCount++;
657
+ if (fnRuntime === "bun") usesBunFns = true;
658
+ process.stdout.write(` ${paint("cyan", "ƒ")} ${route} ${paint("yellow", `[${fnRuntime}]`)}\n`);
659
+ continue;
660
+ }
661
+ }
662
+
346
663
  if (status === 200) {
347
- const cleaned = rewriteImageUrls(await localizeIslands(stripHmrScript(body)));
348
664
  writeStaticFile(STATIC_DIR, route, cleaned);
349
665
  okCount++;
350
- process.stdout.write(` ${paint("green", "✓")} ${route}\n`);
666
+ const rt = runtime && runtime !== "bun" ? ` ${paint("yellow", `[${runtime}]`)}` : "";
667
+ process.stdout.write(` ${paint("green", "✓")} ${route}${rt}\n`);
351
668
  } else if (status === 404) {
352
669
  skipCount++;
353
670
  process.stdout.write(` ${paint("dim", "○")} ${route} ${paint("dim", "(404, skipped)")}\n`);
@@ -363,6 +680,32 @@ async function main() {
363
680
  }
364
681
  }
365
682
 
683
+ // [param] / catch-all routes with no static params → one request-time
684
+ // function each, matched via a config.json route.
685
+ for (const dyn of paramFns) {
686
+ const pageFile = dyn.file;
687
+ const relDir = dirname(pageFile).slice(APP_DIR.length).replace(/^[/\\]/, "").split(sep).join("/");
688
+ const funcRel = `${relDir}.func`;
689
+ const { runtime: dirRt, guardEnabled: dirGuard } = scanDirectives(pageFile);
690
+ const runtime = dirRt || "bun";
691
+ const isClient = isClientPageFile(pageFile);
692
+ const islandSrc = isClient ? await buildIslandAsset(pageFile) : null;
693
+ let emitted = false;
694
+ try {
695
+ emitted = await emitFunction({ funcRel, pageFile, runtime, head: fallbackHead || "", guardEnabled: dirGuard, isClient, islandSrc });
696
+ } catch (e) {
697
+ process.stdout.write(` ${paint("dim", `fn emit failed: ${e?.message || e}`)}\n`);
698
+ }
699
+ if (emitted) {
700
+ fnCount++;
701
+ if (runtime === "bun") usesBunFns = true;
702
+ // src regex from the pattern segments; dest is the literal function path.
703
+ const src = `^/${relDir.split("/").map((s) => (s.startsWith("[...") ? "(.*)" : s.startsWith("[") ? "([^/]+)" : s)).join("/")}/?$`;
704
+ fnConfigRoutes.push({ src, dest: `/${relDir}` });
705
+ process.stdout.write(` ${paint("cyan", "ƒ")} /${relDir} ${paint("yellow", `[${runtime}]`)}\n`);
706
+ }
707
+ }
708
+
366
709
  if (failCount > 0) {
367
710
  const tail = tailDevLog(30);
368
711
  if (tail.length) {
@@ -421,16 +764,29 @@ async function main() {
421
764
  process.stdout.write(` ${paint("green", "✓")} _swift-rust/navigator.js\n`);
422
765
  }
423
766
 
424
- writeConfigJson(OUT_DIR, hasPublic);
767
+ // Bundled local fonts — the dev server serves these from
768
+ // /_swift-rust/fonts/; emit them so deployed @font-face URLs resolve.
769
+ const fontDir = resolveLocalFontDir();
770
+ if (fontDir) {
771
+ cpSync(fontDir, join(STATIC_DIR, "_swift-rust", "fonts"), { recursive: true });
772
+ process.stdout.write(` ${paint("green", "✓")} _swift-rust/fonts/\n`);
773
+ }
774
+
775
+ writeConfigJson(OUT_DIR, hasPublic, fnConfigRoutes);
425
776
 
426
777
  const total = Date.now() - start;
427
778
  const outRel = OUT_DIR.startsWith(cwd + sep) ? OUT_DIR.slice(cwd.length + 1) : OUT_DIR;
428
779
  const skippedPart = skipCount > 0 ? paint("dim", `${skipCount} skipped`) : "";
429
780
  const failedPart = failCount > 0 ? paint("yellow", `${failCount} failed`) : paint("dim", "0 failed");
430
- const summary = ` ${paint("green", "✓")} ${okCount} ok ${skippedPart} ${failedPart}\n`;
781
+ const fnPart = fnCount > 0 ? ` ${paint("cyan", `${fnCount} function${fnCount === 1 ? "" : "s"}`)}` : "";
782
+ const summary = ` ${paint("green", "✓")} ${okCount} ok ${skippedPart} ${failedPart}${fnPart}\n`;
431
783
  process.stdout.write(`\n ${paint("bold", "done")} ${paint("dim", "in " + fmtMs(total))}\n`);
432
784
  process.stdout.write(summary);
433
- process.stdout.write(` ${paint("dim", "output: " + outRel)}\n\n`);
785
+ process.stdout.write(` ${paint("dim", "output: " + outRel)}\n`);
786
+ if (usesBunFns) {
787
+ ensureBunVersion();
788
+ }
789
+ process.stdout.write("\n");
434
790
 
435
791
  const treatFailuresAsWarning = okCount > 0;
436
792
  process.exit(treatFailuresAsWarning || failCount === 0 ? 0 : 1);
@@ -2,9 +2,27 @@
2
2
  import { existsSync, statSync, readFileSync, readdirSync, writeFileSync, unlinkSync, watch as fsWatch } from "node:fs";
3
3
  import { join, resolve, extname, relative, dirname, basename, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { createRequire } from "node:module";
5
6
  import { performance } from "node:perf_hooks";
6
7
  import { errorOverlayHTML as renderErrorOverlay } from "./error-overlay.mjs";
7
8
 
9
+ // Locate the bundled local fonts. Prefers the installed @swift-rust/font
10
+ // package (so it works when swift-rust is installed from npm), falling back to
11
+ // the monorepo source dir during local development.
12
+ let _localFontDir;
13
+ export function resolveLocalFontDir() {
14
+ if (_localFontDir) return _localFontDir;
15
+ const candidates = [];
16
+ try {
17
+ const req = createRequire(import.meta.url);
18
+ const pkg = req.resolve("@swift-rust/font/package.json");
19
+ candidates.push(join(dirname(pkg), "src", "local"), join(dirname(pkg), "dist", "local"));
20
+ } catch {}
21
+ candidates.push(join(import.meta.dirname, "..", "..", "..", "packages", "font", "src", "local"));
22
+ _localFontDir = candidates.find((d) => existsSync(d)) ?? candidates[candidates.length - 1];
23
+ return _localFontDir;
24
+ }
25
+
8
26
  const cwd = process.cwd();
9
27
  const args = process.argv.slice(2);
10
28
 
@@ -590,8 +608,12 @@ async function scanFontsFromLayouts() {
590
608
  }
591
609
 
592
610
  function buildGoogleFontsLinkTag() {
593
- if (GOOGLE_FONT_FAMILIES.size === 0) return "";
594
- const families = Array.from(GOOGLE_FONT_FAMILIES)
611
+ // Layout-scanned families + families any factory registered during this
612
+ // render (globalThis.__SR_GOOGLE_FONTS__), so page-only fonts get a <link>.
613
+ const registered = globalThis.__SR_GOOGLE_FONTS__ instanceof Set ? globalThis.__SR_GOOGLE_FONTS__ : null;
614
+ const all = registered ? new Set([...GOOGLE_FONT_FAMILIES, ...registered]) : GOOGLE_FONT_FAMILIES;
615
+ if (all.size === 0) return "";
616
+ const families = Array.from(all)
595
617
  .map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@300..900`)
596
618
  .join("&");
597
619
  return `<link rel="preconnect" href="https://fonts.googleapis.com" />
@@ -740,7 +762,44 @@ export function isClientPage(file) {
740
762
  for (const raw of readFileSync(file, "utf8").split("\n")) {
741
763
  const t = raw.trim();
742
764
  if (!t || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
743
- return /^["']use client["'];?$/.test(t);
765
+ if (/^["']use client["'];?$/.test(t)) return true;
766
+ // allow other leading directives (e.g. 'use bun') before 'use client'
767
+ if (/^["']use [a-z]+["'];?$/.test(t)) continue;
768
+ return false;
769
+ }
770
+ } catch {}
771
+ return false;
772
+ }
773
+
774
+ const VALID_RUNTIMES = new Set(["bun", "edge", "node", "worker"]);
775
+
776
+ // Detect a `'use bun' | 'use edge' | 'use node'` directive among a file's
777
+ // leading string-literal directives (alongside an optional 'use client').
778
+ export function detectRuntimeDirective(file) {
779
+ try {
780
+ for (const raw of readFileSync(file, "utf8").split("\n")) {
781
+ const t = raw.trim();
782
+ if (!t || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
783
+ const m = t.match(/^["']use (bun|edge|node|worker)["'];?$/);
784
+ if (m) return m[1];
785
+ if (/^["']use [a-z]+["'];?$/.test(t)) continue; // other directive, keep scanning
786
+ return null; // first non-directive line ends the directive prologue
787
+ }
788
+ } catch {}
789
+ return null;
790
+ }
791
+
792
+ // True if a file declares a `'use <name>'` directive among its leading
793
+ // string-literal directives (coexists with 'use client' / 'use bun' / etc.).
794
+ export function hasUseDirective(file, name) {
795
+ try {
796
+ const re = new RegExp(`^["']use ${name}["'];?$`);
797
+ for (const raw of readFileSync(file, "utf8").split("\n")) {
798
+ const t = raw.trim();
799
+ if (!t || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
800
+ if (re.test(t)) return true;
801
+ if (/^["']use [a-z]+["'];?$/.test(t)) continue;
802
+ return false;
744
803
  }
745
804
  } catch {}
746
805
  return false;
@@ -883,7 +942,7 @@ function buildRouteCtx(req, url, params, searchParams) {
883
942
  cookies,
884
943
  params,
885
944
  searchParams,
886
- runtime: "node",
945
+ runtime: "bun",
887
946
  locals: { get: (k) => localsMap.get(k), set: (k, v) => localsMap.set(k, v) },
888
947
  request: req,
889
948
  __setCookies: setCookies,
@@ -903,19 +962,85 @@ function applyControl(c) {
903
962
  throw e;
904
963
  }
905
964
 
906
- /** Merge config.ts along the chain (inner overrides outer). */
907
- async function readMergedConfig(chain) {
965
+ // swift-rust.config.json (read once) — provides the project default runtime.
966
+ let _globalConfig;
967
+ function loadGlobalConfig() {
968
+ if (_globalConfig !== undefined) return _globalConfig;
969
+ try {
970
+ _globalConfig = existsSync(SWIFT_RUST_CONFIG) ? JSON.parse(readFileSync(SWIFT_RUST_CONFIG, "utf8")) : {};
971
+ } catch {
972
+ _globalConfig = {};
973
+ }
974
+ return _globalConfig;
975
+ }
976
+
977
+ // Resolve a `'use bun'|'use edge'|'use node'` directive for a route's tree:
978
+ // the page wins, then layouts innermost → outermost.
979
+ function resolveTreeRuntimeDirective(route, chain) {
980
+ if (route?.file) {
981
+ const d = detectRuntimeDirective(route.file);
982
+ if (d) return d;
983
+ }
984
+ const layouts = collectRouteFiles(chain, "layout"); // outer → inner
985
+ for (let i = layouts.length - 1; i >= 0; i--) {
986
+ const d = detectRuntimeDirective(layouts[i].file);
987
+ if (d) return d;
988
+ }
989
+ return null;
990
+ }
991
+
992
+ // guard.ts runs only when explicitly opted in: a `'use guard'` directive in the
993
+ // route tree (page/layout/config), config.ts `{ guard: true }`, or the global
994
+ // `autoApplyGuard: true`. Default is off (autoApplyGuard defaults to false).
995
+ function shouldRunGuard(route, chain, config) {
996
+ if (route?.file && hasUseDirective(route.file, "guard")) return true;
997
+ for (const { file } of collectRouteFiles(chain, "layout")) {
998
+ if (hasUseDirective(file, "guard")) return true;
999
+ }
1000
+ for (const { file } of collectRouteFiles(chain, "config")) {
1001
+ if (hasUseDirective(file, "guard")) return true;
1002
+ }
1003
+ if (config?.guard === true) return true;
1004
+ if (loadGlobalConfig().autoApplyGuard === true) return true;
1005
+ return false;
1006
+ }
1007
+
1008
+ /** Merge config.ts along the chain (inner overrides outer) + resolve runtime. */
1009
+ async function readMergedConfig(chain, route) {
908
1010
  let config = {};
909
1011
  for (const { file } of collectRouteFiles(chain, "config")) {
910
1012
  const mod = await loadModuleFresh(file);
911
1013
  const c = mod.config ?? mod.default;
912
1014
  if (c && typeof c === "object") config = { ...config, ...c, headers: { ...config.headers, ...c.headers } };
913
1015
  }
914
- // edge.ts / worker.ts force a runtime.
1016
+ // edge.ts / worker.ts force a runtime (file-based, like a directive).
915
1017
  const edge = await collectFirst(chain, "edge");
916
1018
  if (edge && (edge.edge || edge.default)) config.runtime = "edge";
917
1019
  const worker = await collectFirst(chain, "worker");
918
1020
  if (worker && (worker.default || worker.bindings)) config.runtime = "worker";
1021
+
1022
+ // Runtime resolution (highest priority first):
1023
+ // 'use bun|edge|node' directive → config.ts / edge.ts / worker.ts
1024
+ // → swift-rust.config.json "runtime" → default "bun".
1025
+ const explicitRuntime = config.runtime != null; // set by config.ts / edge / worker
1026
+ const directive = resolveTreeRuntimeDirective(route, chain);
1027
+ const fromJson = loadGlobalConfig().runtime;
1028
+ let runtime = directive ?? config.runtime ?? fromJson ?? "bun";
1029
+ if (!VALID_RUNTIMES.has(runtime)) {
1030
+ process.stderr.write(
1031
+ ` ${paint("yellow", "⚠")} invalid runtime ${JSON.stringify(runtime)} — falling back to "bun"\n`,
1032
+ );
1033
+ runtime = "bun";
1034
+ }
1035
+ config.runtime = runtime;
1036
+ // A route is "dynamic" (emitted as a request-time function) when it explicitly
1037
+ // opts into a runtime via a directive, config.ts/edge/worker, or config.dynamic.
1038
+ config.dynamic = Boolean(directive) || explicitRuntime || config.dynamic === true;
1039
+ config.headers = {
1040
+ "x-swift-rust-runtime": runtime,
1041
+ ...(config.dynamic ? { "x-swift-rust-dynamic": "1" } : {}),
1042
+ ...config.headers,
1043
+ };
919
1044
  return config;
920
1045
  }
921
1046
 
@@ -961,7 +1086,8 @@ async function runRoutePipeline(route, ctx) {
961
1086
  }
962
1087
 
963
1088
  // config.ts — merged, applied to the response by the caller.
964
- const config = await readMergedConfig(chain);
1089
+ const config = await readMergedConfig(chain, route);
1090
+ ctx.runtime = config.runtime;
965
1091
 
966
1092
  // i18n.ts — resolve the active locale into locals (cookie/header/default).
967
1093
  const i18nMod = await collectFirst(chain, "i18n");
@@ -994,19 +1120,23 @@ async function runRoutePipeline(route, ctx) {
994
1120
  }
995
1121
  }
996
1122
 
997
- // guard.ts — outer → inner
998
- for (const { file } of collectRouteFiles(chain, "guard")) {
999
- const mod = await loadModuleFresh(file);
1000
- const fn = mod.default ?? mod.guard;
1001
- if (typeof fn === "function") {
1002
- try {
1003
- applyControl(await fn(ctx));
1004
- } catch (e) {
1005
- if (e?.digest || e?.__response) throw e;
1006
- throw new RoutingFileError("guard", file, e);
1123
+ // guard.ts — outer → inner. Opt-in via 'use guard' / config.guard /
1124
+ // global autoApplyGuard (see shouldRunGuard).
1125
+ if (shouldRunGuard(route, chain, config)) {
1126
+ config.headers = { ...config.headers, "x-swift-rust-guard": "1" };
1127
+ for (const { file } of collectRouteFiles(chain, "guard")) {
1128
+ const mod = await loadModuleFresh(file);
1129
+ const fn = mod.default ?? mod.guard;
1130
+ if (typeof fn === "function") {
1131
+ try {
1132
+ applyControl(await fn(ctx));
1133
+ } catch (e) {
1134
+ if (e?.digest || e?.__response) throw e;
1135
+ throw new RoutingFileError("guard", file, e);
1136
+ }
1137
+ } else if (mod.default !== undefined || mod.guard !== undefined) {
1138
+ warnRoutingFile(file, `guard.ts must export a function (default export). ${relative(cwd, file)}`);
1007
1139
  }
1008
- } else if (mod.default !== undefined || mod.guard !== undefined) {
1009
- warnRoutingFile(file, `guard.ts must export a function (default export). ${relative(cwd, file)}`);
1010
1140
  }
1011
1141
  }
1012
1142
 
@@ -1229,6 +1359,10 @@ async function renderRoute(urlPath, req) {
1229
1359
  const errorFile = findErrorBoundary(segments);
1230
1360
  const loadingFile = findLoading(segments);
1231
1361
 
1362
+ // Reset per-render Google font registry so each page only requests the
1363
+ // families it actually uses (factories re-register during render).
1364
+ globalThis.__SR_GOOGLE_FONTS__ = new Set();
1365
+
1232
1366
  try {
1233
1367
  const React = await import("react");
1234
1368
  const url = req ? new URL(req.url) : new URL(urlPath, "http://localhost");
@@ -1314,10 +1448,25 @@ async function renderRoute(urlPath, req) {
1314
1448
  }
1315
1449
  }
1316
1450
  }
1451
+ // shell.tsx — root-only. Lets the app own the outer document
1452
+ // (<html>/<body>/providers); the framework injects head assets + body
1453
+ // scripts into what it renders (see the fullDocument branch in handleFetch).
1454
+ let fullDocument = false;
1455
+ const shellFile = findFile(APP_DIR, "shell");
1456
+ if (shellFile) {
1457
+ try {
1458
+ const sm = await loadModuleFresh(shellFile);
1459
+ const Shell = sm.default ?? sm.Shell ?? sm.shell;
1460
+ if (Shell) {
1461
+ tree = React.createElement(Shell, null, tree);
1462
+ fullDocument = true;
1463
+ }
1464
+ } catch {}
1465
+ }
1317
1466
  const html = await renderToStringCompat(tree);
1318
1467
  if (runtime?.__setRouteContext) runtime.__setRouteContext(null);
1319
1468
  const metadata = await resolveMetadata(layouts.map((l) => l.file), route.file, route.params, segments);
1320
- return { status: 200, html, metadata, error: null, clientPage, pageFile: route.file, layoutFiles: layouts.map((l) => l.file), notFoundFile, errorFile, loadingFile, segments, setCookies: pipeline.setCookies, actionData: pipeline.actionData, config: pipeline.config, revalidatePlan: pipeline.revalidatePlan, seoHead: pipeline.seoHead, serverState: pipeline.serverState };
1469
+ return { status: 200, html, metadata, error: null, clientPage, pageFile: route.file, layoutFiles: layouts.map((l) => l.file), notFoundFile, errorFile, loadingFile, segments, setCookies: pipeline.setCookies, actionData: pipeline.actionData, config: pipeline.config, revalidatePlan: pipeline.revalidatePlan, seoHead: pipeline.seoHead, serverState: pipeline.serverState, fullDocument };
1321
1470
  } catch (err) {
1322
1471
  const rt = await routerRuntime();
1323
1472
  if (rt?.__setRouteContext) rt.__setRouteContext(null);
@@ -1581,12 +1730,25 @@ function setupWatcher() {
1581
1730
  if (!existsSync(APP_DIR)) return;
1582
1731
  const watchers = new Map();
1583
1732
 
1733
+ const IGNORE_DIRS = new Set([
1734
+ "node_modules",
1735
+ "dist",
1736
+ "build",
1737
+ "out",
1738
+ "coverage",
1739
+ "target",
1740
+ ".git",
1741
+ ".vercel",
1742
+ ".turbo",
1743
+ ".swift-rust",
1744
+ ".next",
1745
+ ]);
1584
1746
  function walk(dir) {
1585
1747
  if (watchers.has(dir)) return;
1586
1748
  try {
1587
1749
  const entries = readdirSync(dir, { withFileTypes: true });
1588
1750
  for (const e of entries) {
1589
- if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
1751
+ if (e.isDirectory() && !e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) {
1590
1752
  walk(join(dir, e.name));
1591
1753
  }
1592
1754
  }
@@ -1627,13 +1789,10 @@ function setupWatcher() {
1627
1789
  logLine([` ${paint("dim", "watch error:")} ${paint("red", err.message)}`], 1);
1628
1790
  }
1629
1791
  }
1630
- walk(APP_DIR);
1631
- // Also watch sibling source dirs that pages/layouts import fromwithout
1632
- // this, edits to components/, lib/, etc. never trigger a reload.
1633
- for (const extra of ["components", "lib", "app"]) {
1634
- const p = resolve(cwd, extra);
1635
- if (existsSync(p)) walk(p);
1636
- }
1792
+ // Watch the WHOLE project (minus dep/build dirs) so a change to *any* source
1793
+ // file pages, components, lib, hooks, content, config, wherevertriggers
1794
+ // a recompile + reload. No more "edit this folder and nothing happens".
1795
+ walk(cwd);
1637
1796
  }
1638
1797
 
1639
1798
  const networkUrls = [];
@@ -1733,15 +1892,8 @@ async function handleRequest(req, res) {
1733
1892
 
1734
1893
  if (pathname.startsWith("/_swift-rust/fonts/")) {
1735
1894
  const fontPath = join(
1736
- import.meta.dirname,
1737
- "..",
1738
- "..",
1739
- "..",
1740
- "packages",
1741
- "font",
1742
- "src",
1743
- "local",
1744
- decodeURIComponent(pathname.replace("/_swift-rust/fonts/", ""))
1895
+ resolveLocalFontDir(),
1896
+ decodeURIComponent(pathname.replace("/_swift-rust/fonts/", "")),
1745
1897
  );
1746
1898
  if (existsSync(fontPath) && statSync(fontPath).isFile()) {
1747
1899
  const ext = extname(fontPath).toLowerCase();
@@ -2190,7 +2342,19 @@ async function handleFetch(req) {
2190
2342
  await scanFontsFromLayouts();
2191
2343
  // seo.tsx head + metadata head
2192
2344
  const headExtra = [metadataToHead(renderResult.metadata), renderResult.seoHead || ""].filter(Boolean).join("\n");
2193
- let doc = await wrapInDocumentAsync({ head: headExtra, body: renderResult.html || "" });
2345
+ let doc;
2346
+ if (renderResult.fullDocument) {
2347
+ // shell.tsx already rendered <html>/<head>/<body>. Inject framework head
2348
+ // assets (metadata, fonts, globals CSS, navigator/HMR scripts) into its
2349
+ // <head>; body scripts below still append before </body>.
2350
+ const inject = `${await buildHead(headExtra)}\n<script src="/_swift-rust/navigator.js" defer></script>\n<script src="/_swift-rust/hmr-client.js" defer></script>`;
2351
+ doc = renderResult.html || "";
2352
+ if (!/^\s*<!doctype/i.test(doc)) doc = `<!DOCTYPE html>${doc}`;
2353
+ if (doc.includes("</head>")) doc = doc.replace("</head>", `${inject}\n</head>`);
2354
+ else doc = doc.replace(/<body([\s>])/i, `<head>${inject}</head><body$1`);
2355
+ } else {
2356
+ doc = await wrapInDocumentAsync({ head: headExtra, body: renderResult.html || "" });
2357
+ }
2194
2358
  // state.ts → window.__SR_STATE__ for client stores
2195
2359
  if (renderResult.serverState !== undefined) {
2196
2360
  const json = JSON.stringify(renderResult.serverState).replace(/</g, "\\u003c");
@@ -2319,6 +2483,9 @@ try {
2319
2483
  serverHandle = Bun.serve({
2320
2484
  port,
2321
2485
  hostname,
2486
+ // Bun defaults to a 10s idle timeout, which aborts slow renders / large
2487
+ // payloads (e.g. the first compile of a heavy page). 255s is Bun's max.
2488
+ idleTimeout: 255,
2322
2489
  async fetch(req) {
2323
2490
  return await handleFetch(req);
2324
2491
  },
@@ -0,0 +1,318 @@
1
+ // Portable request-time SSR handler used by emitted Vercel functions
2
+ // (edge / node / bun). No Bun-only APIs — renders with react-dom/server.
3
+ // Mirrors the dev-server route pipeline so emitted functions behave the same.
4
+ import { createElement } from "react";
5
+
6
+ async function renderTree(tree) {
7
+ try {
8
+ const { renderToReadableStream } = await import("react-dom/server.edge");
9
+ const stream = await renderToReadableStream(tree);
10
+ if (stream.allReady) await stream.allReady;
11
+ return await new Response(stream).text();
12
+ } catch {
13
+ const { renderToString } = await import("react-dom/server");
14
+ return renderToString(tree);
15
+ }
16
+ }
17
+
18
+ function parseCookies(request) {
19
+ const header = request.headers.get("cookie") || "";
20
+ const map = new Map();
21
+ for (const part of header.split(";")) {
22
+ const i = part.indexOf("=");
23
+ if (i > 0) map.set(part.slice(0, i).trim(), decodeURIComponent(part.slice(i + 1).trim()));
24
+ }
25
+ return { get: (k) => map.get(k), has: (k) => map.has(k) };
26
+ }
27
+
28
+ function matchParams(pattern, pathname) {
29
+ const segs = pathname.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
30
+ const params = {};
31
+ for (let i = 0; i < pattern.length; i++) {
32
+ const p = pattern[i];
33
+ if (p.startsWith("[...") && p.endsWith("]")) {
34
+ params[p.slice(4, -1)] = segs.slice(i);
35
+ return params;
36
+ }
37
+ if (p.startsWith("[") && p.endsWith("]")) params[p.slice(1, -1)] = segs[i];
38
+ }
39
+ return params;
40
+ }
41
+
42
+ const pick = (mod, ...keys) => {
43
+ for (const k of keys) if (mod && mod[k] != null) return mod[k];
44
+ return undefined;
45
+ };
46
+ const matchesMatcher = (pathname, matcher) => {
47
+ const list = Array.isArray(matcher) ? matcher : [matcher];
48
+ return list.some((m) => new RegExp(`^${String(m).replace(/\*\*/g, ".*").replace(/(?<!\.)\*/g, "[^/]*")}$`).test(pathname));
49
+ };
50
+ const esc = (s) =>
51
+ String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
52
+
53
+ // ── metadata (per-request) ──────────────────────────────────────────────────
54
+ function mergeMetadata(...metas) {
55
+ const out = {};
56
+ for (const m of metas) {
57
+ if (!m) continue;
58
+ if (m.title) out.title = typeof m.title === "string" ? m.title : (m.title.default ?? out.title);
59
+ if (m.description) out.description = m.description;
60
+ if (m.keywords) out.keywords = m.keywords;
61
+ if (m.openGraph) out.openGraph = { ...(out.openGraph || {}), ...m.openGraph };
62
+ if (m.twitter) out.twitter = { ...(out.twitter || {}), ...m.twitter };
63
+ }
64
+ return out;
65
+ }
66
+ function metadataToHead(meta) {
67
+ if (!meta) return "";
68
+ const p = [];
69
+ if (typeof meta.title === "string") p.push(`<title>${esc(meta.title)}</title>`);
70
+ if (meta.description) p.push(`<meta name="description" content="${esc(meta.description)}" />`);
71
+ if (meta.keywords) p.push(`<meta name="keywords" content="${esc(Array.isArray(meta.keywords) ? meta.keywords.join(", ") : meta.keywords)}" />`);
72
+ const og = meta.openGraph;
73
+ if (og) {
74
+ if (og.title) p.push(`<meta property="og:title" content="${esc(og.title)}" />`);
75
+ if (og.description) p.push(`<meta property="og:description" content="${esc(og.description)}" />`);
76
+ if (og.type) p.push(`<meta property="og:type" content="${esc(og.type)}" />`);
77
+ if (og.url) p.push(`<meta property="og:url" content="${esc(og.url)}" />`);
78
+ for (const img of og.images || []) {
79
+ const url = typeof img === "string" ? img : img.url;
80
+ if (url) p.push(`<meta property="og:image" content="${esc(url)}" />`);
81
+ if (typeof img === "object" && img) {
82
+ if (img.width) p.push(`<meta property="og:image:width" content="${esc(img.width)}" />`);
83
+ if (img.height) p.push(`<meta property="og:image:height" content="${esc(img.height)}" />`);
84
+ if (img.alt) p.push(`<meta property="og:image:alt" content="${esc(img.alt)}" />`);
85
+ }
86
+ }
87
+ }
88
+ const tw = meta.twitter;
89
+ if (tw) {
90
+ if (tw.card) p.push(`<meta name="twitter:card" content="${esc(tw.card)}" />`);
91
+ if (tw.title) p.push(`<meta name="twitter:title" content="${esc(tw.title)}" />`);
92
+ if (tw.description) p.push(`<meta name="twitter:description" content="${esc(tw.description)}" />`);
93
+ for (const img of tw.images || []) p.push(`<meta name="twitter:image" content="${esc(typeof img === "string" ? img : img.url)}" />`);
94
+ }
95
+ return p.join("\n");
96
+ }
97
+ function buildSeoHead(r) {
98
+ if (!r || typeof r !== "object") return "";
99
+ const out = [];
100
+ if (r.title) out.push(`<title>${esc(r.title)}</title>`);
101
+ if (r.description) out.push(`<meta name="description" content="${esc(r.description)}" />`);
102
+ if (r.canonical) out.push(`<link rel="canonical" href="${esc(r.canonical)}" />`);
103
+ if (r.robots) out.push(`<meta name="robots" content="${esc(r.robots)}" />`);
104
+ for (const [k, v] of Object.entries(r.openGraph || {})) out.push(`<meta property="og:${esc(k)}" content="${esc(v)}" />`);
105
+ const jsonLd = r.jsonLd ? (Array.isArray(r.jsonLd) ? r.jsonLd : [r.jsonLd]) : [];
106
+ for (const ld of jsonLd) out.push(`<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, "\\u003c")}</script>`);
107
+ return out.join("\n");
108
+ }
109
+ // Drop captured metadata tags so per-request metadata/seo can replace them.
110
+ function stripMeta(head) {
111
+ return head
112
+ .replace(/<title>[\s\S]*?<\/title>/gi, "")
113
+ .replace(/<meta\s+name="description"[^>]*>/gi, "")
114
+ .replace(/<meta\s+name="keywords"[^>]*>/gi, "")
115
+ .replace(/<meta\s+property="og:[^"]*"[^>]*>/gi, "")
116
+ .replace(/<meta\s+name="twitter:[^"]*"[^>]*>/gi, "");
117
+ }
118
+
119
+ function cacheControl(plan) {
120
+ if (!plan) return null;
121
+ if (plan.ttl === 0 || plan.noStore) return "no-store";
122
+ const parts = [];
123
+ if (typeof plan.ttl === "number") parts.push(`s-maxage=${plan.ttl}`, "stale-while-revalidate");
124
+ return parts.length ? `public, ${parts.join(", ")}` : null;
125
+ }
126
+
127
+ function controlToResponse(c, url) {
128
+ if (!c || typeof c !== "object") return null;
129
+ if (c.kind === "redirect") return new Response(null, { status: c.status || 307, headers: { Location: new URL(c.to, url).toString() } });
130
+ if (c.kind === "notFound") return new Response("Not Found", { status: 404 });
131
+ if (c.kind === "response" && c.response) return c.response;
132
+ return null;
133
+ }
134
+ function thrownToResponse(err, url) {
135
+ if (err?.__response instanceof Response) return err.__response;
136
+ const d = err?.digest;
137
+ if (d && String(d).startsWith("REDIRECT")) {
138
+ const [, status, ...rest] = String(d).split(";");
139
+ return new Response(null, { status: Number(status) || 307, headers: { Location: new URL(rest.join(";"), url).toString() } });
140
+ }
141
+ if (d === "NOT_FOUND" || err?.name === "NotFoundError") return new Response("Not Found", { status: 404 });
142
+ if (d === "FORBIDDEN" || err?.name === "ForbiddenError") return new Response("Forbidden", { status: 403 });
143
+ if (d === "UNAUTHORIZED" || err?.name === "UnauthorizedError") return new Response("Unauthorized", { status: 401 });
144
+ return null;
145
+ }
146
+
147
+ export function makeRouteHandler(opts) {
148
+ return async function fetch(request) {
149
+ const url = new URL(request.url);
150
+ const params = matchParams(opts.pattern || [], url.pathname);
151
+ const setCookies = [];
152
+ const localsMap = new Map();
153
+ const ctx = {
154
+ url,
155
+ request,
156
+ method: request.method,
157
+ headers: request.headers,
158
+ params,
159
+ searchParams: Object.fromEntries(url.searchParams.entries()),
160
+ cookies: parseCookies(request),
161
+ locals: { get: (k) => localsMap.get(k), set: (k, v) => localsMap.set(k, v) },
162
+ runtime: opts.runtime,
163
+ __setCookies: setCookies,
164
+ };
165
+
166
+ try {
167
+ // proxy
168
+ for (const mod of opts.proxies || []) {
169
+ const fn = pick(mod, "default", "proxy");
170
+ if (typeof fn !== "function") continue;
171
+ if (mod.matcher && !matchesMatcher(url.pathname, mod.matcher)) continue;
172
+ const r = controlToResponse(await fn(ctx), url);
173
+ if (r) return r;
174
+ }
175
+
176
+ // i18n — resolve locale into locals
177
+ const i18nCfg = pick(opts.i18n || {}, "i18n", "default");
178
+ if (i18nCfg && Array.isArray(i18nCfg.locales)) {
179
+ let locale = i18nCfg.defaultLocale ?? i18nCfg.locales[0];
180
+ if (typeof i18nCfg.resolve === "function") locale = (await i18nCfg.resolve(ctx)) || locale;
181
+ else if (i18nCfg.strategy === "cookie") locale = ctx.cookies.get("locale") || locale;
182
+ else if (i18nCfg.strategy === "header") {
183
+ const al = request.headers.get("accept-language")?.split(",")[0]?.split("-")[0];
184
+ if (al && i18nCfg.locales.includes(al)) locale = al;
185
+ }
186
+ ctx.locals.set("locale", locale);
187
+ ctx.__locale = locale;
188
+ }
189
+
190
+ // schema / query
191
+ const querySpec = pick(opts.query || {}, "query");
192
+ for (const mod of opts.schemas || []) {
193
+ if (mod.params?.safeParse) {
194
+ const r = mod.params.safeParse(ctx.params);
195
+ if (!r.success) return Response.json({ error: "Invalid params", issues: r.error?.issues ?? r.error }, { status: 400 });
196
+ Object.assign(ctx.params, r.data);
197
+ }
198
+ const searchSchema = querySpec?.parse ?? mod.searchParams;
199
+ if (searchSchema?.safeParse) {
200
+ const r = searchSchema.safeParse(ctx.searchParams);
201
+ if (r.success) Object.assign(ctx.searchParams, r.data);
202
+ }
203
+ }
204
+
205
+ // guard (opt-in)
206
+ if (opts.guardEnabled) {
207
+ for (const mod of opts.guards || []) {
208
+ const fn = pick(mod, "default", "guard");
209
+ if (typeof fn !== "function") continue;
210
+ const r = controlToResponse(await fn(ctx), url);
211
+ if (r) return r;
212
+ }
213
+ }
214
+
215
+ // action (mutations)
216
+ let actionData;
217
+ if (ctx.method !== "GET" && ctx.method !== "HEAD" && opts.action) {
218
+ const fn = pick(opts.action, "default", "action");
219
+ if (typeof fn === "function") {
220
+ const actx = Object.assign({}, ctx, { formData: () => request.formData(), json: () => request.json() });
221
+ const result = await fn(actx);
222
+ const r = controlToResponse(result, url);
223
+ if (r) return r;
224
+ actionData = result;
225
+ }
226
+ }
227
+
228
+ // loaders (parallel)
229
+ const loaded = await Promise.all(
230
+ (opts.loaders || []).map(async (mod) => {
231
+ const fn = pick(mod, "default", "loader");
232
+ return typeof fn === "function" ? fn(ctx) : undefined;
233
+ }),
234
+ );
235
+ const loaderData = loaded.length ? loaded[loaded.length - 1] : undefined;
236
+
237
+ // state
238
+ let serverState;
239
+ const stateFn = pick(opts.state || {}, "default", "state");
240
+ if (typeof stateFn === "function") serverState = await stateFn(ctx);
241
+
242
+ // seo
243
+ let seoHead = "";
244
+ const seoFn = pick(opts.seo || {}, "default", "seo");
245
+ if (typeof seoFn === "function") seoHead = buildSeoHead(await seoFn(Object.assign({}, ctx, { data: loaderData })));
246
+
247
+ // per-request metadata (layout metadata + page metadata/generateMetadata)
248
+ const metas = [];
249
+ for (const m of opts.layoutMetas || []) if (m?.metadata) metas.push(m.metadata);
250
+ const pageMod = opts.page;
251
+ if (typeof pageMod.generateMetadata === "function") {
252
+ metas.push(await pageMod.generateMetadata({ params: ctx.params, searchParams: ctx.searchParams }));
253
+ } else if (pageMod.metadata) {
254
+ metas.push(pageMod.metadata);
255
+ }
256
+ const metaHead = metadataToHead(mergeMetadata(...metas));
257
+
258
+ // Expose loader/action data to useLoaderData()/useActionData() via the
259
+ // router's globalThis context box (same channel the dev server uses).
260
+ const g = globalThis;
261
+ g.__SR_ROUTE_CTX__ = g.__SR_ROUTE_CTX__ || { current: null };
262
+ g.__SR_ROUTE_CTX__.current = { request: ctx, loaderData, actionData, loaders: {} };
263
+
264
+ // render: page → (slots) → layouts
265
+ const Page = pick(opts.page, "default", "Page", "page");
266
+ let tree = createElement(Page, { params: ctx.params, loaderData });
267
+ if (opts.isClient) {
268
+ tree = createElement("div", { id: "__sr_island_root", "data-sr-params": JSON.stringify(ctx.params) }, tree);
269
+ }
270
+ const layouts = opts.layouts || [];
271
+ const slotsByLayout = opts.slots || [];
272
+ for (let i = layouts.length - 1; i >= 0; i--) {
273
+ const Layout = pick(layouts[i], "default", "Layout", "layout");
274
+ if (!Layout) continue;
275
+ const slotProps = {};
276
+ for (const slot of slotsByLayout[i] || []) {
277
+ const SlotComp = pick(slot.mod, "default", "Page", "Fragment", "page", "fragment");
278
+ if (SlotComp) slotProps[slot.name] = createElement(SlotComp, {});
279
+ }
280
+ tree = createElement(Layout, slotProps, tree);
281
+ }
282
+ const body = await renderTree(tree);
283
+ g.__SR_ROUTE_CTX__.current = null;
284
+
285
+ // head: captured assets (metadata stripped) + per-request metadata + seo
286
+ let head = stripMeta(opts.head || "");
287
+ head += metaHead;
288
+ if (/<title/i.test(seoHead)) head = head.replace(/<title>[\s\S]*?<\/title>/i, "");
289
+ head += seoHead;
290
+
291
+ const stateScript =
292
+ serverState !== undefined ? `<script>window.__SR_STATE__=${JSON.stringify(serverState).replace(/</g, "\\u003c")}</script>` : "";
293
+ const islandScript = opts.isClient && opts.islandSrc ? `<script type="module" src="${opts.islandSrc}"></script>` : "";
294
+ const html = `<!DOCTYPE html><html lang="${ctx.__locale || "en"}"><head>${head}</head><body>${body}${opts.bodyScripts || ""}${stateScript}${islandScript}</body></html>`;
295
+
296
+ // revalidate → Cache-Control + tags
297
+ let plan = opts.configRevalidate != null ? { ttl: opts.configRevalidate } : undefined;
298
+ const revFn = pick(opts.revalidate || {}, "default", "revalidate");
299
+ if (typeof revFn === "function") {
300
+ const p = await revFn(Object.assign({}, ctx, { data: loaderData, afterAction: ctx.method !== "GET" }));
301
+ if (p && typeof p === "object") plan = { ...plan, ...p };
302
+ }
303
+
304
+ const headers = new Headers({ "content-type": "text/html; charset=utf-8", "x-swift-rust-runtime": opts.runtime || "bun" });
305
+ for (const [k, v] of Object.entries(opts.config?.headers || {})) headers.set(k, String(v));
306
+ const cc = cacheControl(plan);
307
+ if (cc) headers.set("cache-control", cc);
308
+ const tags = plan?.tags || plan?.invalidate;
309
+ if (Array.isArray(tags) && tags.length) headers.set("x-vercel-cache-tags", tags.join(","));
310
+ for (const c of setCookies) headers.append("set-cookie", c);
311
+ return new Response(html, { headers });
312
+ } catch (err) {
313
+ const resp = thrownToResponse(err, url);
314
+ if (resp) return resp;
315
+ return new Response(`Internal Error: ${err?.message || err}`, { status: 500 });
316
+ }
317
+ };
318
+ }
package/dist/head.d.ts CHANGED
@@ -9,13 +9,13 @@ export interface HeadProps {
9
9
  export declare function Head({ children }: HeadProps): ReactNode;
10
10
  export declare function Title({ children }: {
11
11
  children: ReactNode;
12
- }): import("react").JSX.Element;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
13
  export declare function Meta({ name, content, property, }: {
14
14
  name?: string;
15
15
  property?: string;
16
16
  content: string;
17
- }): import("react").JSX.Element;
17
+ }): import("react/jsx-runtime").JSX.Element;
18
18
  export declare function Style({ children }: {
19
19
  children: ReactNode;
20
- }): import("react").JSX.Element;
20
+ }): import("react/jsx-runtime").JSX.Element;
21
21
  //# sourceMappingURL=head.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,aAE3C;AAED,wBAAgB,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAE1D;AAED,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,OAAO,EACP,QAAQ,GACT,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,+BAMA;AAED,wBAAgB,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAK1D"}
1
+ {"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,aAE3C;AAED,wBAAgB,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAE1D;AAED,wBAAgB,IAAI,CAAC,EACnB,IAAI,EACJ,OAAO,EACP,QAAQ,GACT,EAAE;IACD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,2CAMA;AAED,wBAAgB,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAK1D"}
package/dist/head.js ADDED
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Head({ children }) {
3
+ return children;
4
+ }
5
+ export function Title({ children }) {
6
+ return _jsx("title", { children: children });
7
+ }
8
+ export function Meta({ name, content, property, }) {
9
+ return name ? (_jsx("meta", { name: name, content: content })) : (_jsx("meta", { property: property, content: content }));
10
+ }
11
+ export function Style({ children }) {
12
+ return (_jsx("style", { dangerouslySetInnerHTML: { __html: typeof children === "string" ? children : "" } }));
13
+ }
14
+ //# sourceMappingURL=head.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.tsx"],"names":[],"mappings":";AAUA,MAAM,UAAU,IAAI,CAAC,EAAE,QAAQ,EAAa;IAC1C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAE,QAAQ,EAA2B;IACzD,OAAO,0BAAQ,QAAQ,GAAS,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,EACnB,IAAI,EACJ,OAAO,EACP,QAAQ,GAKT;IACC,OAAO,IAAI,CAAC,CAAC,CAAC,CACZ,eAAM,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAI,CACvC,CAAC,CAAC,CAAC,CACF,eAAM,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAI,CAC/C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAE,QAAQ,EAA2B;IACzD,OAAO,CAEL,gBAAO,uBAAuB,EAAE,EAAE,MAAM,EAAE,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,GAAI,CAC7F,CAAC;AACJ,CAAC"}
package/dist/link.d.ts CHANGED
@@ -6,5 +6,6 @@ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
6
6
  replace?: boolean;
7
7
  scroll?: boolean;
8
8
  }
9
- export declare function Link({ href, prefetch, replace, scroll, children, ...rest }: LinkProps): import("react").JSX.Element;
9
+ export declare function Link({ href, prefetch, replace, scroll, children, ...rest }: LinkProps): import("react/jsx-runtime").JSX.Element;
10
+ export default Link;
10
11
  //# 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,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,+BAYrF"}
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,2CAYrF;AAID,eAAe,IAAI,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  export function Link({ href, prefetch, replace, scroll, children, ...rest }) {
2
3
  // The client navigator (runtime/navigator.js) reads these data-* hints to
3
4
  // drive SPA navigation. Plain <a> semantics are preserved when JS is off.
@@ -8,8 +9,9 @@ export function Link({ href, prefetch, replace, scroll, children, ...rest }) {
8
9
  dataAttrs["data-sr-scroll"] = "false";
9
10
  if (prefetch === false)
10
11
  dataAttrs["data-sr-prefetch"] = "false";
11
- return (<a href={href} {...dataAttrs} {...rest}>
12
- {children}
13
- </a>);
12
+ return (_jsx("a", { href: href, ...dataAttrs, ...rest, children: children }));
14
13
  }
15
- //# sourceMappingURL=link.jsx.map
14
+ // Default export too, so both `import { Link } from "swift-rust"` and the
15
+ // Next.js-style `import Link from "swift-rust/link"` work.
16
+ export default Link;
17
+ //# sourceMappingURL=link.js.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"link.js","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,YAAG,IAAI,EAAE,IAAI,KAAM,SAAS,KAAM,IAAI,YACnC,QAAQ,GACP,CACL,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,2DAA2D;AAC3D,eAAe,IAAI,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swift-rust",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",
package/dist/head.jsx DELETED
@@ -1,15 +0,0 @@
1
- export function Head({ children }) {
2
- return children;
3
- }
4
- export function Title({ children }) {
5
- return <title>{children}</title>;
6
- }
7
- export function Meta({ name, content, property, }) {
8
- return name ? (<meta name={name} content={content}/>) : (<meta property={property} content={content}/>);
9
- }
10
- export function Style({ children }) {
11
- return (
12
- // biome-ignore lint/security/noDangerouslySetInnerHtml: <style> contents are static CSS at build time
13
- <style dangerouslySetInnerHTML={{ __html: typeof children === "string" ? children : "" }}/>);
14
- }
15
- //# sourceMappingURL=head.jsx.map
package/dist/head.jsx.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"head.jsx","sourceRoot":"","sources":["../src/head.tsx"],"names":[],"mappings":"AAUA,MAAM,UAAU,IAAI,CAAC,EAAE,QAAQ,EAAa;IAC1C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAE,QAAQ,EAA2B;IACzD,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,EACnB,IAAI,EACJ,OAAO,EACP,QAAQ,GAKT;IACC,OAAO,IAAI,CAAC,CAAC,CAAC,CACZ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAG,CACvC,CAAC,CAAC,CAAC,CACF,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAG,CAC/C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAE,QAAQ,EAA2B;IACzD,OAAO;IACL,sGAAsG;IACtG,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAG,CAC7F,CAAC;AACJ,CAAC"}