swift-rust 1.3.0 → 1.4.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 +365 -9
- package/bin/dev-server.mjs +185 -31
- package/bin/runtime/fn-core.mjs +318 -0
- package/dist/head.d.ts +3 -3
- package/dist/head.d.ts.map +1 -1
- package/dist/head.js +14 -0
- package/dist/head.js.map +1 -0
- package/dist/link.d.ts +1 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/{link.jsx → link.js} +3 -4
- package/dist/{link.jsx.map → link.js.map} +1 -1
- package/package.json +1 -1
- package/dist/head.jsx +0 -15
- package/dist/head.jsx.map +0 -1
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
package/bin/dev-server.mjs
CHANGED
|
@@ -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
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
907
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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);
|
|
@@ -1733,15 +1882,8 @@ async function handleRequest(req, res) {
|
|
|
1733
1882
|
|
|
1734
1883
|
if (pathname.startsWith("/_swift-rust/fonts/")) {
|
|
1735
1884
|
const fontPath = join(
|
|
1736
|
-
|
|
1737
|
-
"
|
|
1738
|
-
"..",
|
|
1739
|
-
"..",
|
|
1740
|
-
"packages",
|
|
1741
|
-
"font",
|
|
1742
|
-
"src",
|
|
1743
|
-
"local",
|
|
1744
|
-
decodeURIComponent(pathname.replace("/_swift-rust/fonts/", ""))
|
|
1885
|
+
resolveLocalFontDir(),
|
|
1886
|
+
decodeURIComponent(pathname.replace("/_swift-rust/fonts/", "")),
|
|
1745
1887
|
);
|
|
1746
1888
|
if (existsSync(fontPath) && statSync(fontPath).isFile()) {
|
|
1747
1889
|
const ext = extname(fontPath).toLowerCase();
|
|
@@ -2190,7 +2332,19 @@ async function handleFetch(req) {
|
|
|
2190
2332
|
await scanFontsFromLayouts();
|
|
2191
2333
|
// seo.tsx head + metadata head
|
|
2192
2334
|
const headExtra = [metadataToHead(renderResult.metadata), renderResult.seoHead || ""].filter(Boolean).join("\n");
|
|
2193
|
-
let doc
|
|
2335
|
+
let doc;
|
|
2336
|
+
if (renderResult.fullDocument) {
|
|
2337
|
+
// shell.tsx already rendered <html>/<head>/<body>. Inject framework head
|
|
2338
|
+
// assets (metadata, fonts, globals CSS, navigator/HMR scripts) into its
|
|
2339
|
+
// <head>; body scripts below still append before </body>.
|
|
2340
|
+
const inject = `${await buildHead(headExtra)}\n<script src="/_swift-rust/navigator.js" defer></script>\n<script src="/_swift-rust/hmr-client.js" defer></script>`;
|
|
2341
|
+
doc = renderResult.html || "";
|
|
2342
|
+
if (!/^\s*<!doctype/i.test(doc)) doc = `<!DOCTYPE html>${doc}`;
|
|
2343
|
+
if (doc.includes("</head>")) doc = doc.replace("</head>", `${inject}\n</head>`);
|
|
2344
|
+
else doc = doc.replace(/<body([\s>])/i, `<head>${inject}</head><body$1`);
|
|
2345
|
+
} else {
|
|
2346
|
+
doc = await wrapInDocumentAsync({ head: headExtra, body: renderResult.html || "" });
|
|
2347
|
+
}
|
|
2194
2348
|
// state.ts → window.__SR_STATE__ for client stores
|
|
2195
2349
|
if (renderResult.serverState !== undefined) {
|
|
2196
2350
|
const json = JSON.stringify(renderResult.serverState).replace(/</g, "\\u003c");
|
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
package/dist/head.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
package/dist/head.js.map
ADDED
|
@@ -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,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, 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
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,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS
|
|
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"}
|
|
@@ -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,6 @@ 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 (
|
|
12
|
-
{children}
|
|
13
|
-
</a>);
|
|
12
|
+
return (_jsx("a", { href: href, ...dataAttrs, ...rest, children: children }));
|
|
14
13
|
}
|
|
15
|
-
//# sourceMappingURL=link.
|
|
14
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swift-rust",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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"}
|