swift-rust 0.2.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/build.mjs CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  mkdirSync,
6
6
  readdirSync,
7
7
  writeFileSync,
8
+ readFileSync,
8
9
  rmSync,
9
10
  cpSync,
10
11
  openSync,
@@ -27,9 +28,9 @@ let PORT = PORT_START;
27
28
  const ROUTE_TIMEOUT_MS = 30_000;
28
29
  const HEALTH_TIMEOUT_MS = 30_000;
29
30
 
30
- const PAGE_EXTENSIONS = new Set(["page.tsx", "page.ts", "page.jsx", "page.js"]);
31
31
  const CATCH_PARAM = /^\[\.{3}([^\]]+)\]$/;
32
32
  const NAMED_PARAM = /^\[([^\]]+)\]$/;
33
+ const PAGE_EXTENSIONS = ["page.tsx", "page.ts", "page.jsx", "page.js"];
33
34
  const NOT_FOUND_FILES = ["not-found.tsx", "not-found.ts", "not-found.jsx", "not-found.js"];
34
35
 
35
36
  const c = {
@@ -41,6 +42,16 @@ const useColor = process.stdout.isTTY !== false && !process.env.NO_COLOR;
41
42
  const paint = (color, s) => (useColor ? `${c[color]}${s}${c.reset}` : s);
42
43
  const fmtMs = (ms) => (ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`);
43
44
 
45
+ let activeLogFile = null;
46
+
47
+ function findPageFile(dir) {
48
+ for (const ext of PAGE_EXTENSIONS) {
49
+ const p = join(dir, ext);
50
+ if (existsSync(p)) return p;
51
+ }
52
+ return null;
53
+ }
54
+
44
55
  function discoverPages(dir, base = "") {
45
56
  const pages = [];
46
57
  if (!existsSync(dir)) return pages;
@@ -48,15 +59,24 @@ function discoverPages(dir, base = "") {
48
59
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
49
60
  const full = join(dir, entry.name);
50
61
  if (entry.isDirectory()) {
51
- const match = entry.name.match(CATCH_PARAM);
52
- if (match) {
53
- pages.push({ type: "catchall", dir: full, base, paramName: match[1] });
62
+ const catchAll = entry.name.match(CATCH_PARAM);
63
+ if (catchAll) {
64
+ const pageFile = findPageFile(full);
65
+ if (pageFile) {
66
+ pages.push({ type: "catchall", dir: full, file: pageFile, base, paramName: catchAll[1] });
67
+ }
54
68
  continue;
55
69
  }
56
70
  const named = entry.name.match(NAMED_PARAM);
57
- const segment = named ? `[${named[1]}]` : entry.name;
58
- pages.push(...discoverPages(full, base + "/" + segment));
59
- } else if (PAGE_EXTENSIONS.has(entry.name)) {
71
+ if (named) {
72
+ const pageFile = findPageFile(full);
73
+ if (pageFile) {
74
+ pages.push({ type: "dynamic", dir: full, file: pageFile, base, paramName: named[1] });
75
+ }
76
+ continue;
77
+ }
78
+ pages.push(...discoverPages(full, base + "/" + entry.name));
79
+ } else if (PAGE_EXTENSIONS.includes(entry.name)) {
60
80
  pages.push({ type: "static", file: full, route: base || "/" });
61
81
  }
62
82
  }
@@ -71,9 +91,9 @@ function findNotFoundFile(dir) {
71
91
  return null;
72
92
  }
73
93
 
74
- async function enumerateCatchAll(page) {
75
- for (const ext of ["tsx", "ts", "jsx", "js"]) {
76
- const p = join(page.dir, `page.${ext}`);
94
+ async function enumerateParams(page) {
95
+ for (const ext of PAGE_EXTENSIONS) {
96
+ const p = join(page.dir, ext);
77
97
  if (!existsSync(p)) continue;
78
98
  try {
79
99
  const mod = await import(`${p}?t=${Date.now()}`);
@@ -85,13 +105,12 @@ async function enumerateCatchAll(page) {
85
105
  return [];
86
106
  }
87
107
 
88
- function routesFromCatchAllParams(catchall, params) {
108
+ function routesFromParams(base, paramName, params) {
89
109
  return params
90
110
  .map((p) => {
91
- const v = p[catchall.paramName];
92
- const arr = Array.isArray(v) ? v : v != null ? [String(v)] : [];
93
- if (arr.length === 0) return null;
94
- return catchall.base + "/" + arr.join("/");
111
+ const v = p[paramName];
112
+ if (v == null) return null;
113
+ return base + "/" + (Array.isArray(v) ? v.join("/") : String(v));
95
114
  })
96
115
  .filter(Boolean);
97
116
  }
@@ -130,7 +149,7 @@ function startDevServer(port, logFile) {
130
149
  const runtime = findBun();
131
150
  const stdio = logFile
132
151
  ? ["ignore", openSync(logFile, "w"), "inherit"]
133
- : ["ignore", "ignore", "inherit"];
152
+ : ["ignore", "inherit", "inherit"];
134
153
  const proc = spawn(runtime, [devServer, "--port", String(port), "--hostname", HOST], {
135
154
  stdio,
136
155
  env: { ...process.env, NO_COLOR: "1", SWIFT_RUST_BUILD: "1" },
@@ -167,6 +186,16 @@ async function fetchRoute(pathname, timeoutMs = ROUTE_TIMEOUT_MS) {
167
186
  }
168
187
  }
169
188
 
189
+ function tailDevLog(lines = 40) {
190
+ if (!activeLogFile || !existsSync(activeLogFile)) return [];
191
+ try {
192
+ const all = readFileSync(activeLogFile, "utf8").split("\n");
193
+ return all.slice(-lines);
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
198
+
170
199
  function stripHmrScript(html) {
171
200
  return html.replace(/\s*<script src="\/_swift-rust\/hmr-client\.js"[^>]*>\s*<\/script>/g, "");
172
201
  }
@@ -178,27 +207,28 @@ function writeStaticFile(outDir, pathname, html) {
178
207
  writeFileSync(outPath, html);
179
208
  }
180
209
 
181
- function writeConfigJson(outDir, hasPublic) {
182
- const headers = [
183
- { key: "Cache-Control", value: "public, max-age=0, must-revalidate" },
184
- ];
210
+ // Writes a literal file (e.g. 404.html) at static/<name>, NOT static/<name>/index.html.
211
+ function writeRawFile(outDir, name, contents) {
212
+ const outPath = join(outDir, name);
213
+ mkdirSync(dirname(outPath), { recursive: true });
214
+ writeFileSync(outPath, contents);
215
+ }
216
+
217
+ function writeConfigJson(outDir, _hasPublic) {
218
+ // Build Output API v3 config. Only schema-valid fields here — unknown
219
+ // top-level fields or route properties are rejected at "Deploying outputs".
185
220
  const config = {
186
221
  version: 3,
187
- framework: { slug: "swift-rust", name: "Swift Rust" },
188
222
  routes: [
189
223
  { src: "/_swift-rust/static/(.*)", headers: { "Cache-Control": "public, max-age=31536000, immutable" } },
190
224
  { src: "/fonts/(.*)", headers: { "Cache-Control": "public, max-age=31536000, immutable" } },
191
- ...(hasPublic
192
- ? [{ src: "/(.*)", headers: { "Cache-Control": "public, max-age=31536000, immutable" }, "isr-per-page": false }]
193
- : []),
194
225
  { handle: "filesystem" },
195
226
  { src: "^(.*)$", status: 404, dest: "/404.html" },
196
227
  ],
197
228
  overrides: {
198
- "404": { path: "404", contentType: "text/html; charset=utf-8" },
229
+ "404.html": { path: "404", contentType: "text/html; charset=utf-8" },
199
230
  },
200
231
  };
201
- if (!hasPublic) config.routes = config.routes.filter((r) => !r["isr-per-page"]);
202
232
  writeFileSync(join(outDir, "config.json"), `${JSON.stringify(config, null, 2)}\n`);
203
233
  }
204
234
 
@@ -220,13 +250,25 @@ async function main() {
220
250
  const pages = discoverPages(APP_DIR);
221
251
  const staticRoutes = pages.filter((p) => p.type === "static").map((p) => p.route);
222
252
  const catchalls = pages.filter((p) => p.type === "catchall");
253
+ const dynamics = pages.filter((p) => p.type === "dynamic");
223
254
  process.stdout.write(` ${paint("dim", "•")} static routes: ${paint("bold", String(staticRoutes.length))}\n`);
255
+
224
256
  for (const ca of catchalls) {
225
- const params = await enumerateCatchAll(ca);
226
- const routes = routesFromCatchAllParams(ca, params);
257
+ const params = await enumerateParams(ca);
258
+ const routes = routesFromParams(ca.base, ca.paramName, params);
227
259
  for (const r of routes) staticRoutes.push(r);
228
260
  process.stdout.write(` ${paint("dim", "•")} catch-all ${paint("cyan", ca.base + "/[...]")}: ${paint("bold", String(routes.length))}\n`);
229
261
  }
262
+ for (const dyn of dynamics) {
263
+ const params = await enumerateParams(dyn);
264
+ if (params.length === 0) {
265
+ process.stdout.write(` ${paint("dim", "•")} dynamic ${paint("cyan", dyn.base + "/[" + dyn.paramName + "]")}: ${paint("yellow", "needs serverless function (skipped for v0.1.0)")}\n`);
266
+ continue;
267
+ }
268
+ const routes = routesFromParams(dyn.base, dyn.paramName, params);
269
+ for (const r of routes) staticRoutes.push(r);
270
+ process.stdout.write(` ${paint("dim", "•")} dynamic ${paint("cyan", dyn.base + "/[" + dyn.paramName + "]")}: ${paint("bold", String(routes.length))}\n`);
271
+ }
230
272
 
231
273
  const allRoutes = [...new Set(staticRoutes)].sort();
232
274
  process.stdout.write(` ${paint("dim", "•")} total: ${paint("bold", String(allRoutes.length))}\n\n`);
@@ -234,7 +276,9 @@ async function main() {
234
276
  process.stdout.write(` ${paint("dim", "starting dev server on " + HOST + ":" + PORT_START + "…")}\n`);
235
277
  PORT = await findFreePort(PORT_START);
236
278
  process.stdout.write(` ${paint("dim", "using port " + PORT + "\n")}\n`);
237
- const proc = startDevServer(PORT, null);
279
+ activeLogFile = process.env.SWIFT_RUST_BUILD_LOG || "/tmp/swift-rust-build-dev.log";
280
+ try { unlinkSync(activeLogFile); } catch {}
281
+ const proc = startDevServer(PORT, activeLogFile);
238
282
  let okCount = 0;
239
283
  let skipCount = 0;
240
284
  let failCount = 0;
@@ -255,7 +299,9 @@ async function main() {
255
299
  process.stdout.write(` ${paint("dim", "○")} ${route} ${paint("dim", "(404, skipped)")}\n`);
256
300
  } else {
257
301
  failCount++;
302
+ const snippet = body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
258
303
  process.stdout.write(` ${paint("yellow", "!")} ${route} ${paint("dim", `(${status})`)}\n`);
304
+ if (snippet) process.stdout.write(` ${paint("dim", snippet)}\n`);
259
305
  }
260
306
  } catch (e) {
261
307
  failCount++;
@@ -263,13 +309,21 @@ async function main() {
263
309
  }
264
310
  }
265
311
 
312
+ if (failCount > 0) {
313
+ const tail = tailDevLog(30);
314
+ if (tail.length) {
315
+ process.stdout.write(`\n ${paint("dim", "dev server tail:")}\n`);
316
+ for (const line of tail) process.stdout.write(` ${paint("dim", line)}\n`);
317
+ }
318
+ }
319
+
266
320
  const notFoundFile = findNotFoundFile(APP_DIR);
267
321
  if (notFoundFile) {
268
322
  try {
269
323
  const { status, body } = await fetchRoute("/_not_found_");
270
324
  if (status === 200 || status === 404) {
271
325
  const html = stripHmrScript(body).replace(/<title>[^<]*<\/title>/, "<title>404 · Swift Rust</title>");
272
- writeStaticFile(STATIC_DIR, "/404.html", html);
326
+ writeRawFile(STATIC_DIR, "404.html", html);
273
327
  process.stdout.write(`\n ${paint("green", "✓")} 404.html\n`);
274
328
  }
275
329
  } catch (e) {
@@ -277,7 +331,7 @@ async function main() {
277
331
  }
278
332
  }
279
333
  if (!existsSync(join(STATIC_DIR, "404.html"))) {
280
- writeStaticFile(STATIC_DIR, "/404.html", `<!DOCTYPE html>
334
+ writeRawFile(STATIC_DIR, "404.html", `<!DOCTYPE html>
281
335
  <html lang="en"><head><meta charset="utf-8" /><title>404 · Swift Rust</title></head>
282
336
  <body><main style="font-family:system-ui;padding:4rem;text-align:center">
283
337
  <h1>404</h1><p>This page could not be found.</p><a href="/">← Back home</a>
@@ -293,6 +347,17 @@ async function main() {
293
347
  process.stdout.write(` ${paint("green", "✓")} copied public/\n`);
294
348
  }
295
349
 
350
+ // App-directory metadata icons (app/favicon.ico, app/icon.svg, …) → static root.
351
+ // The <link> tags are already baked into the rendered HTML by the dev server.
352
+ const APP_ICON_FILES = ["favicon.ico", "favicon.svg", "icon.svg", "icon.png", "apple-icon.png"];
353
+ for (const name of APP_ICON_FILES) {
354
+ const src = join(APP_DIR, name);
355
+ if (existsSync(src)) {
356
+ cpSync(src, join(STATIC_DIR, name));
357
+ process.stdout.write(` ${paint("green", "✓")} ${name}\n`);
358
+ }
359
+ }
360
+
296
361
  writeConfigJson(OUT_DIR, hasPublic);
297
362
 
298
363
  const total = Date.now() - start;
@@ -690,11 +690,38 @@ function errorOverlayHTML(message, stack, extra) {
690
690
  return renderErrorOverlay({ message, stack, ...(extra || {}) });
691
691
  }
692
692
 
693
+ // Metadata icon files looked up at the root of the app directory (app/ or
694
+ // app/src/), Next.js App Router style. Found files are served from the site
695
+ // root and auto-linked in <head>.
696
+ const APP_ICON_FILES = ["favicon.ico", "favicon.svg", "icon.svg", "icon.png", "apple-icon.png"];
697
+
698
+ function discoverAppIcons() {
699
+ const icons = [];
700
+ for (const name of APP_ICON_FILES) {
701
+ const file = join(APP_DIR, name);
702
+ if (existsSync(file) && statSync(file).isFile()) icons.push({ name, file });
703
+ }
704
+ return icons;
705
+ }
706
+
707
+ function buildAppIconsLinkTags() {
708
+ return discoverAppIcons()
709
+ .map(({ name }) => {
710
+ const ext = extname(name).toLowerCase();
711
+ if (name.startsWith("apple-icon")) return `<link rel="apple-touch-icon" href="/${name}" />`;
712
+ if (ext === ".ico") return `<link rel="icon" href="/${name}" sizes="any" />`;
713
+ if (ext === ".svg") return `<link rel="icon" href="/${name}" type="image/svg+xml" />`;
714
+ return `<link rel="icon" href="/${name}" />`;
715
+ })
716
+ .join("\n");
717
+ }
718
+
693
719
  async function buildHead(head) {
694
720
  const css = await getProcessedGlobalsCss();
695
721
  const fontLink = buildGoogleFontsLinkTag();
696
722
  return [
697
723
  head || "",
724
+ buildAppIconsLinkTags(),
698
725
  fontLink,
699
726
  css ? `<style data-swift-rust-globals>${escapeForStyleTag(css)}</style>` : "",
700
727
  ].filter(Boolean).join("\n");
@@ -954,6 +981,22 @@ async function handleRequest(req, res) {
954
981
  return;
955
982
  }
956
983
 
984
+ // App-directory metadata icons (app/favicon.ico, app/icon.svg, …) served
985
+ // from the site root, Next.js style.
986
+ {
987
+ const iconName = pathname.replace(/^\/+/, "");
988
+ if (APP_ICON_FILES.includes(iconName)) {
989
+ const candidate = join(APP_DIR, iconName);
990
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
991
+ const ext = extname(candidate).toLowerCase();
992
+ const mime = { ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png" }[ext] || "application/octet-stream";
993
+ res.writeHead(200, { "Content-Type": mime });
994
+ res.end(readFileSync(candidate));
995
+ return;
996
+ }
997
+ }
998
+ }
999
+
957
1000
  if (existsSync(PUBLIC_DIR)) {
958
1001
  const safe = pathname.replace(/\.\.+/g, "").replace(/^\/+/, "");
959
1002
  const candidate = join(PUBLIC_DIR, safe);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swift-rust",
3
- "version": "0.2.1",
3
+ "version": "1.0.1",
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",
@@ -87,11 +87,11 @@
87
87
  },
88
88
  "dependencies": {
89
89
  "@dotenvx/dotenvx": "^1.0.0",
90
- "@swift-rust/env": "0.2.0",
91
- "@swift-rust/font": "0.2.0",
92
- "@swift-rust/image": "1.0.0",
93
- "@swift-rust/pdf": "0.2.0",
94
- "@swift-rust/video": "0.2.0",
90
+ "@swift-rust/env": "workspace:*",
91
+ "@swift-rust/font": "workspace:*",
92
+ "@swift-rust/image": "workspace:*",
93
+ "@swift-rust/pdf": "workspace:*",
94
+ "@swift-rust/video": "workspace:*",
95
95
  "react": "^19.0.0",
96
96
  "react-dom": "^19.0.0"
97
97
  },