swift-rust 0.2.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.
Files changed (56) hide show
  1. package/README.md +78 -0
  2. package/bin/dev-server.mjs +1360 -0
  3. package/bin/error-overlay.mjs +529 -0
  4. package/bin/runtime/hmr-client.js +77 -0
  5. package/bin/swift-rust.js +96 -0
  6. package/dist/env.d.ts +4 -0
  7. package/dist/env.d.ts.map +1 -0
  8. package/dist/env.js +3 -0
  9. package/dist/env.js.map +1 -0
  10. package/dist/font-google.d.ts +2 -0
  11. package/dist/font-google.d.ts.map +1 -0
  12. package/dist/font-google.js +2 -0
  13. package/dist/font-google.js.map +1 -0
  14. package/dist/font-local.d.ts +3 -0
  15. package/dist/font-local.d.ts.map +1 -0
  16. package/dist/font-local.js +2 -0
  17. package/dist/font-local.js.map +1 -0
  18. package/dist/font.d.ts +3 -0
  19. package/dist/font.d.ts.map +1 -0
  20. package/dist/font.js +2 -0
  21. package/dist/font.js.map +1 -0
  22. package/dist/head.d.ts +21 -0
  23. package/dist/head.d.ts.map +1 -0
  24. package/dist/head.jsx +15 -0
  25. package/dist/head.jsx.map +1 -0
  26. package/dist/image.d.ts +3 -0
  27. package/dist/image.d.ts.map +1 -0
  28. package/dist/image.js +2 -0
  29. package/dist/image.js.map +1 -0
  30. package/dist/index.d.ts +79 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +12 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/link.d.ts +10 -0
  35. package/dist/link.d.ts.map +1 -0
  36. package/dist/link.jsx +6 -0
  37. package/dist/link.jsx.map +1 -0
  38. package/dist/pdf.d.ts +3 -0
  39. package/dist/pdf.d.ts.map +1 -0
  40. package/dist/pdf.js +2 -0
  41. package/dist/pdf.js.map +1 -0
  42. package/dist/router.d.ts +30 -0
  43. package/dist/router.d.ts.map +1 -0
  44. package/dist/router.js +29 -0
  45. package/dist/router.js.map +1 -0
  46. package/dist/smoke.test.d.ts +2 -0
  47. package/dist/smoke.test.d.ts.map +1 -0
  48. package/dist/smoke.test.js +6 -0
  49. package/dist/smoke.test.js.map +1 -0
  50. package/dist/video.d.ts +4 -0
  51. package/dist/video.d.ts.map +1 -0
  52. package/dist/video.js +3 -0
  53. package/dist/video.js.map +1 -0
  54. package/package.json +127 -0
  55. package/scripts/package-native.ts +113 -0
  56. package/scripts/postinstall.js +151 -0
@@ -0,0 +1,1360 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, statSync, readFileSync, readdirSync, watch as fsWatch } from "node:fs";
3
+ import { join, resolve, extname, relative, dirname, sep } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { performance } from "node:perf_hooks";
6
+ import { errorOverlayHTML as renderErrorOverlay } from "./error-overlay.mjs";
7
+
8
+ const cwd = process.cwd();
9
+ const args = process.argv.slice(2);
10
+
11
+ function getArg(name, fallback) {
12
+ const flag = `--${name}`;
13
+ const eq = args.find((a) => a.startsWith(`${flag}=`));
14
+ if (eq) return eq.slice(flag.length + 1);
15
+ const i = args.indexOf(flag);
16
+ if (i >= 0 && args[i + 1]) return args[i + 1];
17
+ return fallback;
18
+ }
19
+
20
+ const port = parseInt(getArg("port", process.env.PORT || "3000"), 10);
21
+ const hostname = getArg("hostname", "0.0.0.0");
22
+
23
+ const c = {
24
+ reset: "\x1b[0m",
25
+ bold: "\x1b[1m",
26
+ dim: "\x1b[2m",
27
+ red: "\x1b[31m",
28
+ green: "\x1b[32m",
29
+ yellow: "\x1b[33m",
30
+ blue: "\x1b[34m",
31
+ magenta: "\x1b[35m",
32
+ cyan: "\x1b[36m",
33
+ gray: "\x1b[90m",
34
+ };
35
+ const useColor = process.stdout.isTTY !== false && !process.env.NO_COLOR;
36
+ const paint = (color, s) => (useColor ? `${c[color]}${s}${c.reset}` : s);
37
+
38
+ const VERSION = "0.1.0";
39
+ const APP_DIR_CANDIDATES = [resolve(cwd, "app", "src"), resolve(cwd, "app")];
40
+ const APP_DIR = APP_DIR_CANDIDATES.find((p) => existsSync(p)) ?? resolve(cwd, "app");
41
+ const PUBLIC_DIR = resolve(cwd, "public");
42
+ const SWIFT_RUST_CONFIG = resolve(cwd, "swift-rust.config.json");
43
+
44
+ const PAGE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
45
+ const SPECIAL_FILES = new Set(["layout", "loading", "error", "not-found", "template", "default", "route"]);
46
+
47
+ const moduleCache = new Map();
48
+ const compileTimings = new Map();
49
+ const lastCompiledAt = new Map();
50
+ const hmrClients = new Set();
51
+ let lastError = null;
52
+ const dynamicParams = new Map();
53
+
54
+ const startedAt = performance.now();
55
+ const bootTime = Date.now();
56
+
57
+ function fmtMs(ms) {
58
+ if (ms < 1) return "<1ms";
59
+ if (ms < 1000) return `${Math.round(ms)}ms`;
60
+ return `${(ms / 1000).toFixed(2)}s`;
61
+ }
62
+
63
+ function logLine(parts, indent = 0) {
64
+ const prefix = `${"│ ".repeat(indent)}`;
65
+ process.stdout.write(`${prefix}${parts.join("")}\n`);
66
+ }
67
+
68
+ function logStartupBanner(localUrl, networkUrls) {
69
+ const lines = [];
70
+ lines.push("");
71
+ lines.push(` ${paint("cyan", "▲")} ${paint("bold", "Swift Rust")} ${paint("dim", `v${VERSION}`)}`);
72
+ lines.push(` ${paint("dim", "─".repeat(50))}`);
73
+ lines.push(` ${paint("dim", "Local:".padEnd(10))} ${paint("cyan", localUrl)}`);
74
+ for (const url of networkUrls) {
75
+ lines.push(` ${paint("dim", "Network:".padEnd(10))} ${paint("cyan", url)}`);
76
+ }
77
+ lines.push(` ${paint("dim", "─".repeat(50))}`);
78
+ lines.push("");
79
+ process.stdout.write(lines.join("\n") + "\n");
80
+ }
81
+
82
+ function logReady() {
83
+ const total = performance.now() - startedAt;
84
+ logLine([` ${paint("green", "✓")} ${paint("bold", "Ready")} ${paint("dim", `in ${fmtMs(total)}`)}`]);
85
+ }
86
+
87
+ function logCompile(route, ms) {
88
+ const safe = route || "/";
89
+ logLine([` ${paint("green", "✓")} ${paint("dim", "Compiled")} ${paint("cyan", safe)} ${paint("dim", `in ${fmtMs(ms)}`)}`]);
90
+ }
91
+
92
+ function logCompiling(route) {
93
+ const safe = route || "/";
94
+ logLine([` ${paint("yellow", "○")} ${paint("dim", "Compiling")} ${paint("cyan", safe)} ${paint("dim", "…")}`]);
95
+ }
96
+
97
+ function logError(err, context) {
98
+ const msg = err?.message || String(err);
99
+ const lines = msg.split("\n");
100
+ logLine([` ${paint("red", "✗")} ${paint("red", context || "Error")}`]);
101
+ for (const line of lines) {
102
+ logLine([paint("red", line)], 1);
103
+ }
104
+ if (err?.stack) {
105
+ const stackLines = err.stack.split("\n").slice(1, 6);
106
+ for (const line of stackLines) {
107
+ logLine([paint("dim", line.trim())], 1);
108
+ }
109
+ }
110
+ }
111
+
112
+ function logRequest({ method, url, status, duration, compileMs }) {
113
+ const statusColor = status < 300 ? "green" : status < 400 ? "cyan" : status < 500 ? "yellow" : "red";
114
+ const timing = compileMs > 0 ? ` ${paint("dim", `(${fmtMs(compileMs)} compile, ${fmtMs(duration - compileMs)} render)`)}` : "";
115
+ logLine([
116
+ `${paint("bold", method.padEnd(6))} `,
117
+ `${paint("dim", url)} `,
118
+ `${paint(statusColor, String(status))} `,
119
+ `${paint("dim", `in ${fmtMs(duration)}`)}`,
120
+ timing,
121
+ ]);
122
+ }
123
+
124
+ function logHmr(file) {
125
+ logLine([` ${paint("magenta", "↻")} ${paint("dim", "HMR")} ${paint("dim", "│")} ${paint("magenta", relative(cwd, file))}`]);
126
+ }
127
+
128
+ function logEvent(type, msg) {
129
+ const colors = { add: "green", change: "yellow", unlink: "red" };
130
+ const icons = { add: "+", change: "~", unlink: "-" };
131
+ logLine([` ${paint(colors[type] || "dim", icons[type] || "?")} ${paint("dim", relative(cwd, msg))}`]);
132
+ }
133
+
134
+ function urlToRouteSegments(urlPath) {
135
+ const clean = urlPath.split("?")[0].split("#")[0];
136
+ if (clean === "/" || clean === "") return [];
137
+ return clean.replace(/^\//, "").split("/").filter(Boolean);
138
+ }
139
+
140
+ function findFile(dir, basename) {
141
+ for (const ext of PAGE_EXTENSIONS) {
142
+ const candidate = join(dir, `${basename}${ext}`);
143
+ if (existsSync(candidate)) return candidate;
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function resolvePageRoute(segments) {
149
+ if (segments.length === 0) {
150
+ const file = findFile(APP_DIR, "page");
151
+ if (file) return { file, params: {}, segments: [] };
152
+ return null;
153
+ }
154
+ return resolveRoute(APP_DIR, segments, 0, {});
155
+ }
156
+
157
+ function resolveRoute(dir, segments, idx, params) {
158
+ if (idx === segments.length) {
159
+ const file = findFile(dir, "page");
160
+ if (file) return { file, params, segments: segments.slice(0, idx) };
161
+ return null;
162
+ }
163
+ const seg = segments[idx];
164
+ const directDir = join(dir, seg);
165
+ if (existsSync(directDir) && statSync(directDir).isDirectory()) {
166
+ const result = resolveRoute(directDir, segments, idx + 1, params);
167
+ if (result) return result;
168
+ }
169
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
170
+ const entries = readdirSync(dir, { withFileTypes: true });
171
+ for (const e of entries) {
172
+ if (!e.isDirectory()) continue;
173
+ if (e.name.startsWith("[") && e.name.endsWith("]")) {
174
+ const paramName = e.name.slice(1, -1);
175
+ if (paramName.startsWith("...")) continue;
176
+ const paramDir = join(dir, e.name);
177
+ const newParams = { ...params, [paramName]: seg };
178
+ const result = resolveRoute(paramDir, segments, idx + 1, newParams);
179
+ if (result) return result;
180
+ }
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function resolveApiRoute(segments) {
187
+ if (segments.length === 0 || segments[0] !== "api") return null;
188
+ const apiDir = join(APP_DIR, "api");
189
+ if (!existsSync(apiDir)) return null;
190
+ return resolveApi(apiDir, segments.slice(1), 0, {});
191
+ }
192
+
193
+ function resolveApi(dir, segments, idx, params) {
194
+ if (idx === segments.length) {
195
+ const file = findFile(dir, "route");
196
+ if (file) return { file, params };
197
+ return null;
198
+ }
199
+ const seg = segments[idx];
200
+ const directDir = join(dir, seg);
201
+ if (existsSync(directDir) && statSync(directDir).isDirectory()) {
202
+ const result = resolveApi(directDir, segments, idx + 1, params);
203
+ if (result) return result;
204
+ }
205
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
206
+ const entries = readdirSync(dir, { withFileTypes: true });
207
+ for (const e of entries) {
208
+ if (!e.isDirectory()) continue;
209
+ if (e.name.startsWith("[") && e.name.endsWith("]")) {
210
+ const paramName = e.name.slice(1, -1);
211
+ if (paramName.startsWith("...")) continue;
212
+ const paramDir = join(dir, e.name);
213
+ const newParams = { ...params, [paramName]: seg };
214
+ const result = resolveApi(paramDir, segments, idx + 1, newParams);
215
+ if (result) return result;
216
+ }
217
+ }
218
+ }
219
+ return null;
220
+ }
221
+
222
+ function findLayoutsFor(segments) {
223
+ const layouts = [];
224
+ for (let i = 0; i <= segments.length; i++) {
225
+ const dir = i === 0 ? APP_DIR : join(APP_DIR, ...segments.slice(0, i));
226
+ const file = findFile(dir, "layout");
227
+ if (file) layouts.push({ file, dir });
228
+ }
229
+ return layouts;
230
+ }
231
+
232
+ function findNotFound(segments) {
233
+ for (let i = segments?.length ?? 0; i >= 0; i--) {
234
+ const dir = i === 0 ? APP_DIR : join(APP_DIR, ...segments.slice(0, i));
235
+ const file = findFile(dir, "not-found");
236
+ if (file) return file;
237
+ }
238
+ return findFile(APP_DIR, "not-found");
239
+ }
240
+
241
+ function findErrorBoundary(segments) {
242
+ for (let i = segments?.length ?? 0; i >= 0; i--) {
243
+ const dir = i === 0 ? APP_DIR : join(APP_DIR, ...segments.slice(0, i));
244
+ const file = findFile(dir, "error");
245
+ if (file) return file;
246
+ }
247
+ return findFile(APP_DIR, "error");
248
+ }
249
+
250
+ function findLoading(segments) {
251
+ for (let i = segments?.length ?? 0; i >= 0; i--) {
252
+ const dir = i === 0 ? APP_DIR : join(APP_DIR, ...segments.slice(0, i));
253
+ const file = findFile(dir, "loading");
254
+ if (file) return file;
255
+ }
256
+ return findFile(APP_DIR, "loading");
257
+ }
258
+
259
+ function findRouteSegmentsForDir(dir) {
260
+ const rel = relative(APP_DIR, dir);
261
+ if (rel === "" || rel === ".") return [];
262
+ return rel.split(sep);
263
+ }
264
+
265
+ function bustCache(file) {
266
+ const urlPrefix = pathToFileURL(file).href;
267
+ for (const key of moduleCache.keys()) {
268
+ if (key === urlPrefix || key.startsWith(`${urlPrefix}?`) || key.startsWith(`${urlPrefix}#`)) {
269
+ moduleCache.delete(key);
270
+ }
271
+ }
272
+ }
273
+
274
+ async function loadModule(filePath, { bust = false } = {}) {
275
+ const baseUrl = pathToFileURL(filePath).href;
276
+ const url = bust ? `${baseUrl}?t=${Date.now()}-${Math.random().toString(36).slice(2)}` : baseUrl;
277
+ try {
278
+ const mod = await import(url);
279
+ moduleCache.set(url, { mod, file: filePath, loadedAt: Date.now() });
280
+ return mod;
281
+ } catch (err) {
282
+ lastError = err;
283
+ throw err;
284
+ }
285
+ }
286
+
287
+ async function compileRoute(urlPath) {
288
+ const start = performance.now();
289
+ const segments = urlToRouteSegments(urlPath);
290
+ const route = resolvePageRoute(segments);
291
+ if (!route) {
292
+ compileTimings.set(urlPath, performance.now() - start);
293
+ return { ok: false, reason: "not_found", pageFile: null, layoutFile: null, params: {}, segments };
294
+ }
295
+ const layouts = findLayoutsFor(segments);
296
+ const notFoundFile = findNotFound(segments);
297
+ const errorFile = findErrorBoundary(segments);
298
+ const loadingFile = findLoading(segments);
299
+
300
+ if (lastCompiledAt.has(urlPath) && Date.now() - (lastCompiledAt.get(urlPath) || 0) < 100) {
301
+ return { ok: true, pageFile: route.file, layoutFiles: layouts.map((l) => l.file), notFoundFile, errorFile, loadingFile, params: route.params, segments, cached: true };
302
+ }
303
+
304
+ try {
305
+ await loadModule(route.file, { bust: true });
306
+ for (const layout of layouts) await loadModule(layout.file, { bust: true });
307
+ if (notFoundFile) await loadModule(notFoundFile, { bust: true });
308
+ if (errorFile) await loadModule(errorFile, { bust: true });
309
+ if (loadingFile) await loadModule(loadingFile, { bust: true });
310
+ } catch (err) {
311
+ compileTimings.set(urlPath, performance.now() - start);
312
+ return { ok: false, reason: "compile_error", error: err, pageFile: route.file, layoutFile: layouts[0]?.file, params: route.params, segments };
313
+ }
314
+
315
+ const ms = performance.now() - start;
316
+ compileTimings.set(urlPath, ms);
317
+ lastCompiledAt.set(urlPath, Date.now());
318
+ return { ok: true, pageFile: route.file, layoutFiles: layouts.map((l) => l.file), notFoundFile, errorFile, loadingFile, params: route.params, segments };
319
+ }
320
+
321
+ function escapeForStyleTag(css) {
322
+ return String(css).replace(/<\/style/gi, "<\\/style").replace(/<!--/g, "<\\!--");
323
+ }
324
+
325
+ function findAppGlobalsCss() {
326
+ for (const ext of [".css", ".scss", ".pcss"]) {
327
+ const candidate = join(APP_DIR, `globals${ext}`);
328
+ if (existsSync(candidate)) return candidate;
329
+ }
330
+ return null;
331
+ }
332
+
333
+ let postcssInstance = null;
334
+ async function getPostcss() {
335
+ if (postcssInstance !== null) return postcssInstance;
336
+ try {
337
+ const candidates = [
338
+ join(cwd, "node_modules", "postcss"),
339
+ join(cwd, "node_modules", "postcss", "lib", "postcss.mjs"),
340
+ join(cwd, "node_modules", "postcss", "lib", "postcss.js"),
341
+ ];
342
+ for (const candidate of candidates) {
343
+ if (existsSync(candidate)) {
344
+ const mod = await import(pathToFileURL(candidate).href);
345
+ postcssInstance = { available: true, default: mod.default ?? mod, mod };
346
+ return postcssInstance;
347
+ }
348
+ }
349
+ const mod = await import("postcss");
350
+ postcssInstance = { available: true, default: mod.default ?? mod, mod };
351
+ return postcssInstance;
352
+ } catch {
353
+ postcssInstance = { available: false };
354
+ return postcssInstance;
355
+ }
356
+ }
357
+
358
+ async function loadPostcssPluginByName(name) {
359
+ const candidates = [
360
+ join(cwd, "node_modules", name),
361
+ join(cwd, "..", "..", "node_modules", name),
362
+ ];
363
+ for (const candidate of candidates) {
364
+ const pkgJsonPath = join(candidate, "package.json");
365
+ if (existsSync(pkgJsonPath)) {
366
+ try {
367
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
368
+ const exportsRoot = pkg.exports?.["."] ?? {};
369
+ const entry = exportsRoot.import ?? exportsRoot.default ?? pkg.main ?? pkg.module;
370
+ if (typeof entry === "string") {
371
+ const entryPath = join(candidate, entry);
372
+ if (existsSync(entryPath)) {
373
+ const mod = await import(pathToFileURL(entryPath).href);
374
+ const result = mod.default ?? mod;
375
+ logLine([` ${paint("dim", "css plugin loaded:")} ${paint("cyan", name)} ${paint("dim", "from " + relative(cwd, entryPath))}`], 1);
376
+ return result;
377
+ }
378
+ }
379
+ } catch (err) {
380
+ logLine([` ${paint("dim", "css plugin error:")} ${paint("red", err.message)}`], 1);
381
+ }
382
+ }
383
+ }
384
+ logLine([` ${paint("dim", "css plugin not found:")} ${paint("yellow", name)}`], 1);
385
+ return null;
386
+ }
387
+
388
+ async function loadPostcssPlugins() {
389
+ const plugins = [];
390
+ const tailwind = await loadPostcssPluginByName("@tailwindcss/postcss");
391
+ if (tailwind) {
392
+ if (typeof tailwind === "function") {
393
+ plugins.push(tailwind());
394
+ } else if (tailwind && typeof tailwind === "object") {
395
+ if (typeof tailwind.postcss === "function") plugins.push(tailwind.postcss());
396
+ else if (typeof tailwind.default === "function") plugins.push(tailwind.default());
397
+ else if (typeof tailwind === "function") plugins.push(tailwind);
398
+ }
399
+ }
400
+ return plugins;
401
+ }
402
+
403
+ async function processCss(css) {
404
+ const pc = await getPostcss();
405
+ if (!pc.available) {
406
+ logLine([` ${paint("dim", "css: postcss not found, inlining raw")}`], 1);
407
+ return css;
408
+ }
409
+ const plugins = await loadPostcssPlugins();
410
+ if (plugins.length === 0) {
411
+ logLine([` ${paint("dim", "css: no plugins loaded, inlining raw")}`], 1);
412
+ return css;
413
+ }
414
+ try {
415
+ const processor = pc.default(plugins);
416
+ const result = await processor.process(css, { from: undefined, to: undefined });
417
+ return result.css;
418
+ } catch (err) {
419
+ logLine([` ${paint("dim", "css process:")} ${paint("red", err.message)}`], 1);
420
+ return css;
421
+ }
422
+ }
423
+
424
+ let globalsCssCache = { file: null, mtime: 0, css: "" };
425
+ async function getProcessedGlobalsCss() {
426
+ const file = findAppGlobalsCss();
427
+ if (!file) return "";
428
+ const stat = statSync(file);
429
+ if (globalsCssCache.file === file && globalsCssCache.mtime === stat.mtimeMs) {
430
+ return globalsCssCache.css;
431
+ }
432
+ const raw = readFileSync(file, "utf8");
433
+ const processed = await processCss(raw);
434
+ globalsCssCache = { file, mtime: stat.mtimeMs, css: processed };
435
+ return processed;
436
+ }
437
+
438
+ const GOOGLE_FONT_FAMILIES = new Set();
439
+ let fontsScannedFromLayout = false;
440
+
441
+ const FONT_FAMILY_HINT = /\b(?:Inter|Geist|GeistMono|Geist_Mono|Roboto|Poppins|Manrope|JetBrainsMono|JetbrainsMono|FiraCode|SpaceGrotesk|PlayfairDisplay|Lora|Outfit|Sora|Figtree|PlusJakartaSans|BricolageGrotesque|Cinzel|Caveat|BebasNeue|DmSans|DmSerifDisplay|SourceCodePro|SourceCode3|IBMPlex|IbmPlex|Swift_Rust|Lora|Plus_Jakarta_Sans|Bricolage_Grotesque|Bebas_Neue|DM_Sans|DM_Serif_Display|Source_Code_Pro|IBM_Plex|JetBrains_Mono|Space_Grotesk|Plus_Jakarta_Sans|Bricolage_Grotesque)\b/g;
442
+ const FONT_HUMAN_NAME = {
443
+ Geist: "Geist", GeistMono: "Geist Mono", Geist_Mono: "Geist Mono",
444
+ Inter: "Inter", Roboto: "Roboto", Poppins: "Poppins", Manrope: "Manrope",
445
+ JetBrainsMono: "JetBrains Mono", JetbrainsMono: "JetBrains Mono", JetBrains_Mono: "JetBrains Mono",
446
+ FiraCode: "Fira Code", Fira_Code: "Fira Code",
447
+ SpaceGrotesk: "Space Grotesk", Space_Grotesk: "Space Grotesk",
448
+ PlayfairDisplay: "Playfair Display", Playfair_Display: "Playfair Display",
449
+ Lora: "Lora", Outfit: "Outfit", Sora: "Sora", Figtree: "Figtree",
450
+ PlusJakartaSans: "Plus Jakarta Sans", Plus_Jakarta_Sans: "Plus Jakarta Sans",
451
+ BricolageGrotesque: "Bricolage Grotesque", Bricolage_Grotesque: "Bricolage Grotesque",
452
+ Cinzel: "Cinzel", Caveat: "Caveat", BebasNeue: "Bebas Neue", Bebas_Neue: "Bebas Neue",
453
+ DmSans: "DM Sans", DM_Sans: "DM Sans",
454
+ DmSerifDisplay: "DM Serif Display", DM_Serif_Display: "DM Serif Display",
455
+ SourceCodePro: "Source Code Pro", Source_Code_Pro: "Source Code Pro", SourceCode3: "Source Code Pro",
456
+ IBMPlexSans: "IBM Plex Sans", IBM_Plex_Sans: "IBM Plex Sans",
457
+ IbmPlexSans: "IBM Plex Sans",
458
+ IBMPlexMono: "IBM Plex Mono", IBM_Plex_Mono: "IBM Plex Mono",
459
+ IbmPlexMono: "IBM Plex Mono",
460
+ IBMPlexSerif: "IBM Plex Serif", IBM_Plex_Serif: "IBM Plex Serif",
461
+ IbmPlexSerif: "IBM Plex Serif",
462
+ };
463
+
464
+ function scanFontImportsInSource(src) {
465
+ for (const match of src.matchAll(FONT_FAMILY_HINT)) {
466
+ const key = match[0];
467
+ const family = FONT_HUMAN_NAME[key];
468
+ if (family) GOOGLE_FONT_FAMILIES.add(family);
469
+ }
470
+ }
471
+
472
+ async function scanFontsFromLayouts() {
473
+ if (fontsScannedFromLayout) return;
474
+ fontsScannedFromLayout = true;
475
+ const dirs = [APP_DIR, ...(existsSync(join(APP_DIR, "blog")) ? [join(APP_DIR, "blog")] : [])];
476
+ for (const dir of dirs) {
477
+ if (!existsSync(dir)) continue;
478
+ const layoutFile = findFile(dir, "layout");
479
+ if (layoutFile) {
480
+ try {
481
+ const src = readFileSync(layoutFile, "utf8");
482
+ scanFontImportsInSource(src);
483
+ } catch {}
484
+ }
485
+ }
486
+ }
487
+
488
+ function buildGoogleFontsLinkTag() {
489
+ if (GOOGLE_FONT_FAMILIES.size === 0) return "";
490
+ const families = Array.from(GOOGLE_FONT_FAMILIES)
491
+ .map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@300..900`)
492
+ .join("&");
493
+ return `<link rel="preconnect" href="https://fonts.googleapis.com" />
494
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
495
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?${families}&display=swap" />`;
496
+ }
497
+
498
+ function mergeMetadata(...metas) {
499
+ const out = {};
500
+ for (const m of metas) {
501
+ if (!m) continue;
502
+ if (m.title) {
503
+ if (typeof m.title === "string") {
504
+ out.title = m.title;
505
+ } else if (m.title.template && m.title.default) {
506
+ out.title = m.title.default;
507
+ } else if (m.title.default) {
508
+ out.title = m.title.default;
509
+ } else if (typeof m.title === "object") {
510
+ out.title = m.title;
511
+ }
512
+ }
513
+ if (m.description) out.description = m.description;
514
+ if (m.keywords) out.keywords = m.keywords;
515
+ if (m.openGraph) out.openGraph = { ...(out.openGraph || {}), ...m.openGraph };
516
+ }
517
+ return out;
518
+ }
519
+
520
+ function metadataToHead(meta) {
521
+ if (!meta) return "";
522
+ const parts = [];
523
+ if (meta.title) {
524
+ if (typeof meta.title === "string") parts.push(`<title>${escapeHtml(meta.title)}</title>`);
525
+ }
526
+ if (meta.description) parts.push(`<meta name="description" content="${escapeHtml(meta.description)}" />`);
527
+ if (meta.keywords) {
528
+ const kw = Array.isArray(meta.keywords) ? meta.keywords.join(", ") : meta.keywords;
529
+ parts.push(`<meta name="keywords" content="${escapeHtml(kw)}" />`);
530
+ }
531
+ if (meta.openGraph) {
532
+ if (meta.openGraph.title) parts.push(`<meta property="og:title" content="${escapeHtml(meta.openGraph.title)}" />`);
533
+ if (meta.openGraph.description) parts.push(`<meta property="og:description" content="${escapeHtml(meta.openGraph.description)}" />`);
534
+ if (meta.openGraph.type) parts.push(`<meta property="og:type" content="${escapeHtml(meta.openGraph.type)}" />`);
535
+ if (meta.openGraph.url) parts.push(`<meta property="og:url" content="${escapeHtml(meta.openGraph.url)}" />`);
536
+ if (meta.openGraph.images) {
537
+ for (const img of meta.openGraph.images) {
538
+ const url = typeof img === "string" ? img : img.url;
539
+ if (url) parts.push(`<meta property="og:image" content="${escapeHtml(url)}" />`);
540
+ }
541
+ }
542
+ }
543
+ return parts.join("\n");
544
+ }
545
+
546
+ function escapeHtml(s) {
547
+ if (s == null) return "";
548
+ return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
549
+ }
550
+
551
+ async function resolveMetadata(layoutFiles, pageFile, params, segments) {
552
+ const metas = [];
553
+ for (const layoutFile of layoutFiles || []) {
554
+ try {
555
+ const mod = await loadModule(layoutFile, { bust: true });
556
+ if (mod.metadata) metas.push(mod.metadata);
557
+ } catch {}
558
+ }
559
+ if (pageFile) {
560
+ try {
561
+ const mod = await loadModule(pageFile, { bust: true });
562
+ if (mod.generateMetadata) {
563
+ const m = await mod.generateMetadata({ params: params || {}, searchParams: {} });
564
+ if (m) metas.push(m);
565
+ } else if (mod.metadata) {
566
+ metas.push(mod.metadata);
567
+ }
568
+ } catch {}
569
+ }
570
+ return mergeMetadata(...metas);
571
+ }
572
+
573
+ async function renderToStringCompat(tree) {
574
+ try {
575
+ const edge = await import("react-dom/server.edge");
576
+ if (typeof edge.renderToReadableStream === "function") {
577
+ const stream = await edge.renderToReadableStream(tree, {
578
+ signal: AbortSignal.timeout(10000),
579
+ });
580
+ if (stream.allReady) await stream.allReady;
581
+ const reader = stream.getReader();
582
+ let html = "";
583
+ while (true) {
584
+ const { done, value } = await reader.read();
585
+ if (done) break;
586
+ html += new TextDecoder().decode(value);
587
+ }
588
+ return html;
589
+ }
590
+ } catch (err) {
591
+ if (err?.digest === "NOT_FOUND" || err?.name === "NotFoundError") throw err;
592
+ if (err?.digest && String(err.digest).startsWith("REDIRECT")) throw err;
593
+ try {
594
+ const React = await import("react");
595
+ const { renderToString } = await import("react-dom/server");
596
+ return renderToString(tree);
597
+ } catch (innerErr) {
598
+ if (innerErr?.digest === "NOT_FOUND" || innerErr?.name === "NotFoundError") throw innerErr;
599
+ if (innerErr?.digest && String(innerErr.digest).startsWith("REDIRECT")) throw innerErr;
600
+ throw err;
601
+ }
602
+ }
603
+ const React = await import("react");
604
+ const { renderToString } = await import("react-dom/server");
605
+ return renderToString(tree);
606
+ }
607
+
608
+ async function renderRoute(urlPath) {
609
+ const segments = urlToRouteSegments(urlPath);
610
+ const route = resolvePageRoute(segments);
611
+ if (!route) {
612
+ return await renderNotFound(segments);
613
+ }
614
+ const layouts = findLayoutsFor(segments);
615
+ const notFoundFile = findNotFound(segments);
616
+ const errorFile = findErrorBoundary(segments);
617
+ const loadingFile = findLoading(segments);
618
+
619
+ try {
620
+ const React = await import("react");
621
+ const pageMod = await loadModule(route.file, { bust: true });
622
+ const Page = pageMod.default ?? pageMod.Page ?? pageMod.page;
623
+ if (!Page) {
624
+ return { status: 500, html: null, error: new Error(`page ${route.file} has no default export`) };
625
+ }
626
+
627
+ const paramsProxy = new Proxy(route.params || {}, {
628
+ get: (t, k) => (k in t ? t[k] : dynamicParams.get(String(k))),
629
+ });
630
+
631
+ let tree = React.createElement(Page, { params: paramsProxy });
632
+ for (let i = layouts.length - 1; i >= 0; i--) {
633
+ const layoutMod = await loadModule(layouts[i].file, { bust: true });
634
+ const Layout = layoutMod.default ?? layoutMod.Layout ?? layoutMod.layout;
635
+ if (Layout) tree = React.createElement(Layout, null, tree);
636
+ }
637
+ const html = await renderToStringCompat(tree);
638
+ const metadata = await resolveMetadata(layouts.map((l) => l.file), route.file, route.params, segments);
639
+ return { status: 200, html, metadata, error: null, pageFile: route.file, layoutFiles: layouts.map((l) => l.file), notFoundFile, errorFile, loadingFile, segments };
640
+ } catch (err) {
641
+ if (err?.digest === "NOT_FOUND" || err?.name === "NotFoundError") {
642
+ return await renderNotFound(segments);
643
+ }
644
+ if (err?.digest && String(err.digest).startsWith("REDIRECT")) {
645
+ const [, statusStr, ...rest] = String(err.digest).split(";");
646
+ return { status: parseInt(statusStr, 10) || 307, redirect: rest.join(";"), html: null, error: null, segments };
647
+ }
648
+ if (errorFile) {
649
+ try {
650
+ const React = await import("react");
651
+ const errorMod = await loadModule(errorFile, { bust: true });
652
+ const ErrorBoundary = errorMod.default ?? errorMod.ErrorBoundary ?? errorMod.error;
653
+ if (ErrorBoundary) {
654
+ const html = await renderToStringCompat(React.createElement(ErrorBoundary, { error: err }));
655
+ return { status: 500, html, metadata: null, error: err, segments, pageFile: route.file, errorFile };
656
+ }
657
+ } catch {}
658
+ }
659
+ return { status: 500, html: null, error: err, segments, pageFile: route.file };
660
+ }
661
+ }
662
+
663
+ async function renderNotFound(segments) {
664
+ const notFoundFile = findNotFound(segments);
665
+ if (!notFoundFile) {
666
+ return { status: 404, html: null, error: null, segments };
667
+ }
668
+ try {
669
+ const React = await import("react");
670
+ const layouts = findLayoutsFor(segments || []);
671
+ const mod = await loadModule(notFoundFile, { bust: true });
672
+ const NotFound = mod.default ?? mod.NotFound ?? mod.notFound;
673
+ if (!NotFound) {
674
+ return { status: 404, html: null, error: null, segments };
675
+ }
676
+ let tree = React.createElement(NotFound);
677
+ for (let i = layouts.length - 1; i >= 0; i--) {
678
+ const layoutMod = await loadModule(layouts[i].file, { bust: true });
679
+ const Layout = layoutMod.default ?? layoutMod.Layout ?? layoutMod.layout;
680
+ if (Layout) tree = React.createElement(Layout, null, tree);
681
+ }
682
+ const html = await renderToStringCompat(tree);
683
+ return { status: 404, html, metadata: null, error: null, pageFile: notFoundFile, layoutFiles: layouts.map((l) => l.file), segments };
684
+ } catch (err) {
685
+ return { status: 404, html: null, error: err, segments };
686
+ }
687
+ }
688
+
689
+ function errorOverlayHTML(message, stack, extra) {
690
+ return renderErrorOverlay({ message, stack, ...(extra || {}) });
691
+ }
692
+
693
+ async function buildHead(head) {
694
+ const css = await getProcessedGlobalsCss();
695
+ const fontLink = buildGoogleFontsLinkTag();
696
+ return [
697
+ head || "",
698
+ fontLink,
699
+ css ? `<style data-swift-rust-globals>${escapeForStyleTag(css)}</style>` : "",
700
+ ].filter(Boolean).join("\n");
701
+ }
702
+
703
+ function wrapInDocument({ head, body }) {
704
+ return `<!DOCTYPE html>
705
+ <html lang="en">
706
+ <head>
707
+ <meta charset="utf-8" />
708
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
709
+ ${head || ""}
710
+ <script src="/_swift-rust/hmr-client.js" defer></script>
711
+ </head>
712
+ <body>${body}</body>
713
+ </html>`;
714
+ }
715
+
716
+ async function wrapInDocumentAsync({ head, body }) {
717
+ const fullHead = await buildHead(head);
718
+ return `<!DOCTYPE html>
719
+ <html lang="en">
720
+ <head>
721
+ <meta charset="utf-8" />
722
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
723
+ ${fullHead}
724
+ <script src="/_swift-rust/hmr-client.js" defer></script>
725
+ </head>
726
+ <body>${body}</body>
727
+ </html>`;
728
+ }
729
+
730
+ async function tryHmrClient() {
731
+ try {
732
+ const file = join(dirname(new URL(import.meta.url).pathname), "runtime", "hmr-client.js");
733
+ return readFileSync(file, "utf8");
734
+ } catch {
735
+ return null;
736
+ }
737
+ }
738
+
739
+ function shouldIgnoreFile(filename) {
740
+ if (!filename) return true;
741
+ const base = filename.split(sep).pop();
742
+ if (!base) return true;
743
+ if (base.startsWith(".")) return true;
744
+ if (base.endsWith(".bak") || base.endsWith(".tmp") || base.endsWith("~")) return true;
745
+ if (base === "node_modules") return true;
746
+ return false;
747
+ }
748
+
749
+ function setupWatcher() {
750
+ if (!existsSync(APP_DIR)) return;
751
+ const watchers = new Map();
752
+
753
+ function walk(dir) {
754
+ if (watchers.has(dir)) return;
755
+ try {
756
+ const entries = readdirSync(dir, { withFileTypes: true });
757
+ for (const e of entries) {
758
+ if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
759
+ walk(join(dir, e.name));
760
+ }
761
+ }
762
+ let debounce;
763
+ const w = fsWatch(dir, (event, filename) => {
764
+ if (shouldIgnoreFile(filename)) return;
765
+ const full = join(dir, filename.toString());
766
+ if (debounce) clearTimeout(debounce);
767
+ debounce = setTimeout(() => {
768
+ try {
769
+ const s = statSync(full);
770
+ if (s.isDirectory()) {
771
+ walk(full);
772
+ return;
773
+ }
774
+ } catch {}
775
+ logEvent("change", full);
776
+ bustCache(full);
777
+ if (full.includes(`${sep}globals.${"css"}`)) {
778
+ globalsCssCache = { file: null, mtime: 0, css: "" };
779
+ }
780
+ if (full.endsWith(`${sep}layout.tsx`) || full.endsWith(`${sep}layout.ts`)) {
781
+ fontsScannedFromLayout = false;
782
+ GOOGLE_FONT_FAMILIES.clear();
783
+ }
784
+ const payload = { type: "reload", file: relative(cwd, full), at: Date.now() };
785
+ for (const send of hmrClients) {
786
+ try {
787
+ send(JSON.stringify({ event: "change", data: payload }));
788
+ } catch {}
789
+ }
790
+ logHmr(full);
791
+ }, 30);
792
+ });
793
+ watchers.set(dir, w);
794
+ } catch (err) {
795
+ logLine([` ${paint("dim", "watch error:")} ${paint("red", err.message)}`], 1);
796
+ }
797
+ }
798
+ walk(APP_DIR);
799
+ }
800
+
801
+ const networkUrls = [];
802
+ async function detectNetworkUrls() {
803
+ const { networkInterfaces } = await import("node:os");
804
+ const ifaces = networkInterfaces();
805
+ for (const [name, list] of Object.entries(ifaces)) {
806
+ for (const iface of list || []) {
807
+ if (iface.family === "IPv4" && !iface.internal) {
808
+ networkUrls.push(`http://${iface.address}:${port}`);
809
+ }
810
+ }
811
+ }
812
+ }
813
+
814
+ async function checkAppDir() {
815
+ if (!existsSync(APP_DIR)) {
816
+ process.stdout.write(`\n ${paint("red", "✗")} ${paint("bold", "No app/ directory found")}\n`);
817
+ process.stdout.write(` ${paint("dim", `Create ${paint("cyan", "app/page.tsx")} to get started`)}\n\n`);
818
+ process.exit(1);
819
+ }
820
+ }
821
+
822
+ let serverHandle = null;
823
+
824
+ async function handleRequest(req, res) {
825
+ const reqStart = performance.now();
826
+ const url = new URL(req.url, `http://${req.headers.host}`);
827
+ const pathname = url.pathname;
828
+ const method = req.method || "GET";
829
+
830
+ if (pathname === "/_swift-rust/hmr") {
831
+ res.writeHead(200, {
832
+ "Content-Type": "text/event-stream",
833
+ "Cache-Control": "no-cache",
834
+ Connection: "keep-alive",
835
+ "X-Accel-Buffering": "no",
836
+ });
837
+ res.write(": ping\n\n");
838
+ const send = (data) => res.write(`data: ${data}\n\n`);
839
+ hmrClients.add(send);
840
+ const ping = setInterval(() => {
841
+ try {
842
+ res.write(": ping\n\n");
843
+ } catch {
844
+ clearInterval(ping);
845
+ }
846
+ }, 15000);
847
+ req.on("close", () => {
848
+ hmrClients.delete(send);
849
+ clearInterval(ping);
850
+ });
851
+ return;
852
+ }
853
+
854
+ if (pathname === "/sw.js" || pathname === "/sw.js.map") {
855
+ res.writeHead(204);
856
+ res.end();
857
+ return;
858
+ }
859
+
860
+ if (pathname === "/_swift-rust/info") {
861
+ res.writeHead(200, { "Content-Type": "application/json" });
862
+ res.end(JSON.stringify({ version: VERSION, appDir: relative(cwd, APP_DIR), port, uptime: Date.now() - bootTime }));
863
+ return;
864
+ }
865
+
866
+ if (pathname === "/_swift-rust/health") {
867
+ res.writeHead(200, { "Content-Type": "text/plain" });
868
+ res.end("ok");
869
+ return;
870
+ }
871
+
872
+ if (method !== "GET" && method !== "HEAD") {
873
+ res.writeHead(405);
874
+ res.end("Method not allowed");
875
+ return;
876
+ }
877
+
878
+ if (pathname.startsWith("/_swift-rust/")) {
879
+ res.writeHead(404);
880
+ res.end("Not found");
881
+ return;
882
+ }
883
+
884
+ if (pathname === "/_swift-rust/hmr-client.js") {
885
+ const client = await tryHmrClient();
886
+ if (client) {
887
+ res.writeHead(200, { "Content-Type": "application/javascript" });
888
+ res.end(client);
889
+ return;
890
+ }
891
+ res.writeHead(404);
892
+ res.end("Not found");
893
+ return;
894
+ }
895
+
896
+ if (pathname.startsWith("/_swift-rust/fonts/")) {
897
+ const fontPath = join(
898
+ import.meta.dirname,
899
+ "..",
900
+ "..",
901
+ "..",
902
+ "packages",
903
+ "font",
904
+ "src",
905
+ "local",
906
+ decodeURIComponent(pathname.replace("/_swift-rust/fonts/", ""))
907
+ );
908
+ if (existsSync(fontPath) && statSync(fontPath).isFile()) {
909
+ const ext = extname(fontPath).toLowerCase();
910
+ const mime = { ".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf", ".otf": "font/otf" }[ext] ?? "application/octet-stream";
911
+ res.writeHead(200, { "Content-Type": mime, "Cache-Control": "public, max-age=31536000, immutable" });
912
+ res.end(readFileSync(fontPath));
913
+ return;
914
+ }
915
+ res.writeHead(404);
916
+ res.end("Font not found");
917
+ return;
918
+ }
919
+
920
+ if (pathname === "/_swift-rust/image") {
921
+ const target = url.searchParams.get("url");
922
+ const w = parseInt(url.searchParams.get("w") || "0", 10);
923
+ if (!target) {
924
+ res.writeHead(400);
925
+ res.end("Missing url");
926
+ return;
927
+ }
928
+ const decoded = decodeURIComponent(target);
929
+ if (existsSync(PUBLIC_DIR)) {
930
+ const safe = decoded.replace(/\.\.+/g, "").replace(/^\/+/, "");
931
+ const candidate = join(PUBLIC_DIR, safe);
932
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
933
+ const ext = extname(candidate).toLowerCase();
934
+ const mime = MIME[ext] || "application/octet-stream";
935
+ const buf = readFileSync(candidate);
936
+ res.writeHead(200, {
937
+ "Content-Type": mime,
938
+ "Cache-Control": "public, max-age=3600",
939
+ "X-Swift-Rust-Image-Optimization": "passthrough",
940
+ ...(w > 0 ? { "X-Swift-Rust-Image-Width": String(w) } : {}),
941
+ });
942
+ res.end(buf);
943
+ return;
944
+ }
945
+ }
946
+ res.writeHead(404);
947
+ res.end("Image not found");
948
+ return;
949
+ }
950
+
951
+ if (pathname.startsWith("/_static/") || pathname.startsWith("/__swift-rust/")) {
952
+ res.writeHead(404);
953
+ res.end("Not found");
954
+ return;
955
+ }
956
+
957
+ if (existsSync(PUBLIC_DIR)) {
958
+ const safe = pathname.replace(/\.\.+/g, "").replace(/^\/+/, "");
959
+ const candidate = join(PUBLIC_DIR, safe);
960
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
961
+ const ext = extname(candidate).toLowerCase();
962
+ const mime = {
963
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
964
+ ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif",
965
+ ".svg": "image/svg+xml", ".ico": "image/x-icon",
966
+ ".css": "text/css", ".js": "application/javascript", ".mjs": "application/javascript",
967
+ ".json": "application/json", ".txt": "text/plain", ".woff": "font/woff",
968
+ ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf",
969
+ ".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg",
970
+ }[ext] || "application/octet-stream";
971
+ res.writeHead(200, { "Content-Type": mime });
972
+ res.end(readFileSync(candidate));
973
+ return;
974
+ }
975
+ }
976
+
977
+ const compileStart = performance.now();
978
+ const compileResult = await compileRoute(pathname);
979
+ const compileMs = performance.now() - compileStart;
980
+
981
+ if (compileResult.ok && !compileResult.cached) {
982
+ logCompile(pathname, compileMs);
983
+ }
984
+
985
+ if (!compileResult.ok) {
986
+ if (compileResult.reason === "not_found") {
987
+ const total = performance.now() - reqStart;
988
+ logRequest({ method, url: pathname, status: 404, duration: total, compileMs: 0 });
989
+ res.writeHead(404, { "Content-Type": "text/plain" });
990
+ res.end(`404 not found: ${pathname}`);
991
+ return;
992
+ }
993
+ if (compileResult.reason === "compile_error") {
994
+ const err = compileResult.error;
995
+ logError(err, `Failed to compile ${pathname}`);
996
+ const total = performance.now() - reqStart;
997
+ logRequest({ method, url: pathname, status: 500, duration: total, compileMs });
998
+ res.writeHead(500, { "Content-Type": "text/html" });
999
+ res.end(errorOverlayHTML(err?.message || "Compilation error", err?.stack || ""));
1000
+ for (const send of hmrClients) {
1001
+ try {
1002
+ send(JSON.stringify({ event: "change", data: { type: "error", message: err?.message || "compile error" } }));
1003
+ } catch {}
1004
+ }
1005
+ return;
1006
+ }
1007
+ }
1008
+
1009
+ const renderStart = performance.now();
1010
+ const renderResult = await renderRoute(pathname);
1011
+ const renderMs = performance.now() - renderStart;
1012
+ const total = performance.now() - reqStart;
1013
+
1014
+ if (renderResult.status === 500) {
1015
+ logError(renderResult.error, `Failed to render ${pathname}`);
1016
+ logRequest({ method, url: pathname, status: 500, duration: total, compileMs });
1017
+ res.writeHead(500, { "Content-Type": "text/html" });
1018
+ res.end(errorOverlayHTML(renderResult.error?.message || "Render error", renderResult.error?.stack || ""));
1019
+ for (const send of hmrClients) {
1020
+ try {
1021
+ send(JSON.stringify({ event: "change", data: { type: "error", message: renderResult.error?.message || "render error" } }));
1022
+ } catch {}
1023
+ }
1024
+ return;
1025
+ }
1026
+
1027
+ if (renderResult.status === 404) {
1028
+ logRequest({ method, url: pathname, status: 404, duration: total, compileMs: 0 });
1029
+ res.writeHead(404, { "Content-Type": "text/html" });
1030
+ res.end(errorOverlayHTML("Not found", `No page found for ${pathname}`));
1031
+ return;
1032
+ }
1033
+
1034
+ logRequest({ method, url: pathname, status: 200, duration: total, compileMs });
1035
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1036
+ res.end(await wrapInDocumentAsync({ head: "", body: renderResult.html || "" }));
1037
+ }
1038
+
1039
+ async function handleFetch(req) {
1040
+ const reqStart = performance.now();
1041
+ const url = new URL(req.url);
1042
+ const pathname = url.pathname;
1043
+ const method = req.method || "GET";
1044
+
1045
+ if (pathname === "/_swift-rust/hmr") {
1046
+ const stream = new ReadableStream({
1047
+ start(controller) {
1048
+ const enc = new TextEncoder();
1049
+ controller.enqueue(enc.encode(": ping\n\n"));
1050
+ const send = (data) => controller.enqueue(enc.encode(`data: ${data}\n\n`));
1051
+ hmrClients.add(send);
1052
+ const ping = setInterval(() => {
1053
+ try {
1054
+ controller.enqueue(enc.encode(": ping\n\n"));
1055
+ } catch {
1056
+ clearInterval(ping);
1057
+ hmrClients.delete(send);
1058
+ }
1059
+ }, 15000);
1060
+ req.signal?.addEventListener("abort", () => {
1061
+ hmrClients.delete(send);
1062
+ clearInterval(ping);
1063
+ try {
1064
+ controller.close();
1065
+ } catch {}
1066
+ });
1067
+ },
1068
+ });
1069
+ return new Response(stream, {
1070
+ headers: {
1071
+ "Content-Type": "text/event-stream",
1072
+ "Cache-Control": "no-cache",
1073
+ Connection: "keep-alive",
1074
+ "X-Accel-Buffering": "no",
1075
+ },
1076
+ });
1077
+ }
1078
+
1079
+ if (pathname === "/sw.js" || pathname === "/sw.js.map") {
1080
+ return new Response(null, { status: 204 });
1081
+ }
1082
+
1083
+ if (pathname === "/_swift-rust/info") {
1084
+ return Response.json({ version: VERSION, appDir: relative(cwd, APP_DIR), port, uptime: Date.now() - bootTime });
1085
+ }
1086
+
1087
+ if (pathname === "/_swift-rust/health") {
1088
+ return new Response("ok", { headers: { "Content-Type": "text/plain" } });
1089
+ }
1090
+
1091
+ if (pathname === "/_swift-rust/hmr-client.js") {
1092
+ const client = await tryHmrClient();
1093
+ if (client) {
1094
+ return new Response(client, { headers: { "Content-Type": "application/javascript" } });
1095
+ }
1096
+ return new Response("Not found", { status: 404 });
1097
+ }
1098
+
1099
+ if (pathname === "/_swift-rust/image") {
1100
+ const target = url.searchParams.get("url");
1101
+ const w = parseInt(url.searchParams.get("w") || "0", 10);
1102
+ if (!target) return new Response("Missing url", { status: 400 });
1103
+ const decoded = decodeURIComponent(target);
1104
+ if (existsSync(PUBLIC_DIR)) {
1105
+ const safe = decoded.replace(/\.\.+/g, "").replace(/^\/+/, "");
1106
+ const candidate = join(PUBLIC_DIR, safe);
1107
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
1108
+ const ext = extname(candidate).toLowerCase();
1109
+ const mime = MIME[ext] || "application/octet-stream";
1110
+ const file = Bun?.file ? Bun.file(candidate) : readFileSync(candidate);
1111
+ const headers = {
1112
+ "Content-Type": mime,
1113
+ "Cache-Control": "public, max-age=3600",
1114
+ "X-Swift-Rust-Image-Optimization": "passthrough",
1115
+ ...(w > 0 ? { "X-Swift-Rust-Image-Width": String(w) } : {}),
1116
+ };
1117
+ const total = performance.now() - reqStart;
1118
+ logRequest({ method, url: pathname, status: 200, duration: total, compileMs: 0 });
1119
+ return new Response(file, { headers });
1120
+ }
1121
+ }
1122
+ return new Response("Image not found", { status: 404 });
1123
+ }
1124
+
1125
+ if (pathname.startsWith("/_swift-rust/")) {
1126
+ return new Response("Not found", { status: 404 });
1127
+ }
1128
+
1129
+ if (existsSync(PUBLIC_DIR)) {
1130
+ const safe = pathname.replace(/\.\.+/g, "").replace(/^\/+/, "");
1131
+ const candidate = join(PUBLIC_DIR, safe);
1132
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
1133
+ const ext = extname(candidate).toLowerCase();
1134
+ const mime = MIME[ext] || "application/octet-stream";
1135
+ const file = Bun?.file ? Bun.file(candidate) : readFileSync(candidate);
1136
+ const total = performance.now() - reqStart;
1137
+ logRequest({ method, url: pathname, status: 200, duration: total, compileMs: 0 });
1138
+ return new Response(file, { headers: { "Content-Type": mime } });
1139
+ }
1140
+ }
1141
+
1142
+ const segments = urlToRouteSegments(pathname);
1143
+ if (segments[0] === "api") {
1144
+ return await handleApiRoute(req, segments, method, reqStart);
1145
+ }
1146
+
1147
+ if (method !== "GET" && method !== "HEAD") {
1148
+ return new Response("Method not allowed", { status: 405 });
1149
+ }
1150
+
1151
+ const compileStart = performance.now();
1152
+ const compileResult = await compileRoute(pathname);
1153
+ const compileMs = performance.now() - compileStart;
1154
+
1155
+ if (compileResult.ok && !compileResult.cached) {
1156
+ logCompile(pathname, compileMs);
1157
+ }
1158
+
1159
+ if (!compileResult.ok) {
1160
+ if (compileResult.reason === "not_found") {
1161
+ const notFoundResult = await renderNotFound(segments);
1162
+ const total = performance.now() - reqStart;
1163
+ const status = notFoundResult.html ? 404 : 404;
1164
+ logRequest({ method, url: pathname, status, duration: total, compileMs: 0 });
1165
+ if (notFoundResult.html) {
1166
+ return new Response(await wrapInDocumentAsync({ head: "", body: notFoundResult.html }), {
1167
+ status: 404,
1168
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1169
+ });
1170
+ }
1171
+ return new Response(errorOverlayHTML("Not found", `No page found for ${pathname}`), {
1172
+ status: 404,
1173
+ headers: { "Content-Type": "text/html" },
1174
+ });
1175
+ }
1176
+ if (compileResult.reason === "compile_error") {
1177
+ const err = compileResult.error;
1178
+ logError(err, `Failed to compile ${pathname}`);
1179
+ const total = performance.now() - reqStart;
1180
+ logRequest({ method, url: pathname, status: 500, duration: total, compileMs });
1181
+ const overlay = errorOverlayHTML(err?.message || "Compilation error", err?.stack || "");
1182
+ for (const send of hmrClients) {
1183
+ try {
1184
+ send(JSON.stringify({ event: "change", data: { type: "error", message: err?.message || "compile error" } }));
1185
+ } catch {}
1186
+ }
1187
+ return new Response(overlay, { status: 500, headers: { "Content-Type": "text/html" } });
1188
+ }
1189
+ }
1190
+
1191
+ const renderStart = performance.now();
1192
+ const renderResult = await renderRoute(pathname);
1193
+ const renderMs = performance.now() - renderStart;
1194
+ const total = performance.now() - reqStart;
1195
+
1196
+ if (renderResult.status === 500) {
1197
+ logError(renderResult.error, `Failed to render ${pathname}`);
1198
+ logRequest({ method, url: pathname, status: 500, duration: total, compileMs });
1199
+ const overlay = errorOverlayHTML(renderResult.error?.message || "Render error", renderResult.error?.stack || "");
1200
+ for (const send of hmrClients) {
1201
+ try {
1202
+ send(JSON.stringify({ event: "change", data: { type: "error", message: renderResult.error?.message || "render error" } }));
1203
+ } catch {}
1204
+ }
1205
+ return new Response(overlay, { status: 500, headers: { "Content-Type": "text/html" } });
1206
+ }
1207
+
1208
+ if (renderResult.status === 404) {
1209
+ logRequest({ method, url: pathname, status: 404, duration: total, compileMs: 0 });
1210
+ if (renderResult.html) {
1211
+ return new Response(await wrapInDocumentAsync({ head: "", body: renderResult.html }), {
1212
+ status: 404,
1213
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1214
+ });
1215
+ }
1216
+ return new Response(errorOverlayHTML("Not found", `No page found for ${pathname}`), {
1217
+ status: 404,
1218
+ headers: { "Content-Type": "text/html" },
1219
+ });
1220
+ }
1221
+
1222
+ if (renderResult.redirect) {
1223
+ logRequest({ method, url: pathname, status: renderResult.status, duration: total, compileMs });
1224
+ return new Response(null, {
1225
+ status: renderResult.status,
1226
+ headers: { Location: renderResult.redirect },
1227
+ });
1228
+ }
1229
+
1230
+ logRequest({ method, url: pathname, status: 200, duration: total, compileMs });
1231
+ await scanFontsFromLayouts();
1232
+ return new Response(await wrapInDocumentAsync({ head: metadataToHead(renderResult.metadata), body: renderResult.html || "" }), {
1233
+ status: 200,
1234
+ headers: { "Content-Type": "text/html; charset=utf-8" },
1235
+ });
1236
+ }
1237
+
1238
+ async function handleApiRoute(req, segments, method, reqStart) {
1239
+ const route = resolveApiRoute(segments);
1240
+ if (!route) {
1241
+ const total = performance.now() - reqStart;
1242
+ logRequest({ method, url: "/" + segments.join("/"), status: 404, duration: total, compileMs: 0 });
1243
+ return new Response(JSON.stringify({ error: "Not found" }), {
1244
+ status: 404,
1245
+ headers: { "Content-Type": "application/json" },
1246
+ });
1247
+ }
1248
+ const handlerName = method.toUpperCase();
1249
+ try {
1250
+ const mod = await loadModule(route.file, { bust: true });
1251
+ const handler = mod[handlerName] || mod[method.toLowerCase()];
1252
+ if (typeof handler !== "function") {
1253
+ const total = performance.now() - reqStart;
1254
+ logRequest({ method, url: "/" + segments.join("/"), status: 405, duration: total, compileMs: 0 });
1255
+ return new Response(JSON.stringify({ error: `Method ${method} not allowed` }), {
1256
+ status: 405,
1257
+ headers: { "Content-Type": "application/json", Allow: Object.keys(mod).filter((k) => typeof mod[k] === "function" && k === k.toUpperCase()).join(", ") },
1258
+ });
1259
+ }
1260
+ const url = new URL(req.url);
1261
+ const body = method === "GET" || method === "HEAD" ? null : await req.text().catch(() => null);
1262
+ let parsedBody = null;
1263
+ if (body) {
1264
+ try {
1265
+ parsedBody = JSON.parse(body);
1266
+ } catch {
1267
+ parsedBody = body;
1268
+ }
1269
+ }
1270
+ const result = await handler({
1271
+ request: req,
1272
+ params: route.params,
1273
+ query: Object.fromEntries(url.searchParams),
1274
+ body: parsedBody,
1275
+ searchParams: Object.fromEntries(url.searchParams),
1276
+ });
1277
+ const total = performance.now() - reqStart;
1278
+ logRequest({ method, url: "/" + segments.join("/"), status: result?.status || 200, duration: total, compileMs: 0 });
1279
+ if (result instanceof Response) return result;
1280
+ if (result && typeof result === "object") {
1281
+ return new Response(JSON.stringify(result), {
1282
+ status: result.status || 200,
1283
+ headers: { "Content-Type": "application/json", ...(result.headers || {}) },
1284
+ });
1285
+ }
1286
+ return new Response(String(result ?? ""), {
1287
+ status: 200,
1288
+ headers: { "Content-Type": "text/plain" },
1289
+ });
1290
+ } catch (err) {
1291
+ logError(err, `Failed to handle ${method} /${segments.join("/")}`);
1292
+ const total = performance.now() - reqStart;
1293
+ logRequest({ method, url: "/" + segments.join("/"), status: 500, duration: total, compileMs: 0 });
1294
+ return new Response(JSON.stringify({ error: err?.message || "Internal error" }), {
1295
+ status: 500,
1296
+ headers: { "Content-Type": "application/json" },
1297
+ });
1298
+ }
1299
+ }
1300
+
1301
+ const MIME = {
1302
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
1303
+ ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif",
1304
+ ".svg": "image/svg+xml", ".ico": "image/x-icon",
1305
+ ".css": "text/css", ".js": "application/javascript", ".mjs": "application/javascript",
1306
+ ".json": "application/json", ".txt": "text/plain", ".woff": "font/woff",
1307
+ ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf",
1308
+ ".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg",
1309
+ };
1310
+
1311
+ await checkAppDir();
1312
+ await detectNetworkUrls();
1313
+ logStartupBanner(`http://localhost:${port}`, networkUrls);
1314
+ logLine([` ${paint("dim", "› setupWatcher…")}`]);
1315
+ try {
1316
+ setupWatcher();
1317
+ logLine([` ${paint("dim", "› watcher ready")}`]);
1318
+ } catch (err) {
1319
+ logLine([` ${paint("red", "watcher error:")} ${err.message}`]);
1320
+ }
1321
+
1322
+ logLine([` ${paint("dim", `› listen on ${hostname}:${port}…`)}`]);
1323
+ try {
1324
+ if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
1325
+ serverHandle = Bun.serve({
1326
+ port,
1327
+ hostname,
1328
+ async fetch(req) {
1329
+ return await handleFetch(req);
1330
+ },
1331
+ });
1332
+ logReady();
1333
+ await new Promise(() => {});
1334
+ } else {
1335
+ const http = await import("node:http");
1336
+ const srv = http.createServer(async (req, res) => {
1337
+ await handleRequest(req, res);
1338
+ });
1339
+ srv.on("error", (err) => logError(err, "server error"));
1340
+ srv.listen(port, hostname, () => {
1341
+ logReady();
1342
+ });
1343
+ serverHandle = srv;
1344
+ }
1345
+ } catch (err) {
1346
+ logError(err, "listen error");
1347
+ }
1348
+
1349
+ process.on("SIGINT", () => {
1350
+ logLine([`\n ${paint("yellow", "■")} ${paint("dim", "Stopping dev server…")}\n`]);
1351
+ if (serverHandle && typeof serverHandle.close === "function") {
1352
+ serverHandle.close(() => process.exit(0));
1353
+ } else if (serverHandle && typeof serverHandle.stop === "function") {
1354
+ serverHandle.stop();
1355
+ process.exit(0);
1356
+ } else {
1357
+ process.exit(0);
1358
+ }
1359
+ setTimeout(() => process.exit(0), 2000).unref();
1360
+ });