swift-rust 1.2.1 → 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 +374 -9
- package/bin/dev-server.mjs +416 -35
- package/bin/runtime/fn-core.mjs +318 -0
- package/bin/runtime/navigator.js +238 -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.js +14 -0
- package/dist/link.js.map +1 -0
- package/package.json +1 -1
- package/dist/head.jsx +0 -15
- package/dist/head.jsx.map +0 -1
- package/dist/link.jsx +0 -6
- package/dist/link.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) {
|
|
@@ -412,16 +755,38 @@ async function main() {
|
|
|
412
755
|
}
|
|
413
756
|
}
|
|
414
757
|
|
|
415
|
-
|
|
758
|
+
// Client navigator runtime (SPA navigation). The rendered HTML references
|
|
759
|
+
// /_swift-rust/navigator.js; emit it as a static asset so deployed sites
|
|
760
|
+
// get client-side navigation too.
|
|
761
|
+
const navSrc = join(RUNTIME_DIR, "navigator.js");
|
|
762
|
+
if (existsSync(navSrc)) {
|
|
763
|
+
writeRawFile(STATIC_DIR, "_swift-rust/navigator.js", readFileSync(navSrc));
|
|
764
|
+
process.stdout.write(` ${paint("green", "✓")} _swift-rust/navigator.js\n`);
|
|
765
|
+
}
|
|
766
|
+
|
|
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);
|
|
416
776
|
|
|
417
777
|
const total = Date.now() - start;
|
|
418
778
|
const outRel = OUT_DIR.startsWith(cwd + sep) ? OUT_DIR.slice(cwd.length + 1) : OUT_DIR;
|
|
419
779
|
const skippedPart = skipCount > 0 ? paint("dim", `${skipCount} skipped`) : "";
|
|
420
780
|
const failedPart = failCount > 0 ? paint("yellow", `${failCount} failed`) : paint("dim", "0 failed");
|
|
421
|
-
const
|
|
781
|
+
const fnPart = fnCount > 0 ? ` ${paint("cyan", `${fnCount} function${fnCount === 1 ? "" : "s"}`)}` : "";
|
|
782
|
+
const summary = ` ${paint("green", "✓")} ${okCount} ok ${skippedPart} ${failedPart}${fnPart}\n`;
|
|
422
783
|
process.stdout.write(`\n ${paint("bold", "done")} ${paint("dim", "in " + fmtMs(total))}\n`);
|
|
423
784
|
process.stdout.write(summary);
|
|
424
|
-
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");
|
|
425
790
|
|
|
426
791
|
const treatFailuresAsWarning = okCount > 0;
|
|
427
792
|
process.exit(treatFailuresAsWarning || failCount === 0 ? 0 : 1);
|