swift-rust 1.1.0 → 1.2.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
@@ -15,7 +15,7 @@ import { join, resolve, dirname, sep } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
 
17
17
  const cwd = process.cwd();
18
- const APP_DIR_CANDIDATES = [resolve(cwd, "app", "src"), resolve(cwd, "app")];
18
+ const APP_DIR_CANDIDATES = [resolve(cwd, "src", "app"), resolve(cwd, "app")];
19
19
  const APP_DIR = APP_DIR_CANDIDATES.find((p) => existsSync(p)) ?? resolve(cwd, "app");
20
20
  const PUBLIC_DIR = resolve(cwd, "public");
21
21
  const OUT_DIR = resolve(cwd, ".vercel", "output");
@@ -200,6 +200,15 @@ function stripHmrScript(html) {
200
200
  return html.replace(/\s*<script src="\/_swift-rust\/hmr-client\.js"[^>]*>\s*<\/script>/g, "");
201
201
  }
202
202
 
203
+ // The dev-time image endpoint (/_swift-rust/image) only exists while the dev
204
+ // server runs. On Vercel the platform serves an optimizing endpoint at
205
+ // /_vercel/image with the exact same query contract (url, w, q), so we swap the
206
+ // path prefix at build time. The `images` config written into config.json
207
+ // enables (and bounds) that optimizer. Handles `src`, `srcset`, and `&amp;`.
208
+ function rewriteImageUrls(html) {
209
+ return html.split("/_swift-rust/image?").join("/_vercel/image?");
210
+ }
211
+
203
212
  function writeStaticFile(outDir, pathname, html) {
204
213
  const rel = pathname === "/" ? "index.html" : `${pathname.replace(/^\//, "")}/index.html`;
205
214
  const outPath = join(outDir, rel);
@@ -263,6 +272,16 @@ function writeConfigJson(outDir, _hasPublic) {
263
272
  overrides: {
264
273
  "404.html": { path: "404", contentType: "text/html; charset=utf-8" },
265
274
  },
275
+ // Enables Vercel's image optimizer (/_vercel/image). `sizes` MUST match the
276
+ // widths <Image> requests (packages/image DEVICE_SIZES) or requests 400.
277
+ images: {
278
+ sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
279
+ formats: ["image/avif", "image/webp"],
280
+ minimumCacheTTL: 86400,
281
+ // Allow optimizing local SVG assets (e.g. blog covers); sandboxed by CSP.
282
+ dangerouslyAllowSVG: true,
283
+ contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
284
+ },
266
285
  };
267
286
  writeFileSync(join(outDir, "config.json"), `${JSON.stringify(config, null, 2)}\n`);
268
287
  }
@@ -325,7 +344,7 @@ async function main() {
325
344
  try {
326
345
  const { status, body } = await fetchRoute(route);
327
346
  if (status === 200) {
328
- const cleaned = await localizeIslands(stripHmrScript(body));
347
+ const cleaned = rewriteImageUrls(await localizeIslands(stripHmrScript(body)));
329
348
  writeStaticFile(STATIC_DIR, route, cleaned);
330
349
  okCount++;
331
350
  process.stdout.write(` ${paint("green", "✓")} ${route}\n`);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, statSync, readFileSync, readdirSync, writeFileSync, unlinkSync, watch as fsWatch } from "node:fs";
3
- import { join, resolve, extname, relative, dirname, sep } from "node:path";
3
+ import { join, resolve, extname, relative, dirname, basename, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { errorOverlayHTML as renderErrorOverlay } from "./error-overlay.mjs";
@@ -17,7 +17,7 @@ function getArg(name, fallback) {
17
17
  return fallback;
18
18
  }
19
19
 
20
- const port = parseInt(getArg("port", process.env.PORT || "3000"), 10);
20
+ const port = parseInt(getArg("port", process.env.PORT || "3210"), 10);
21
21
  const hostname = getArg("hostname", "0.0.0.0");
22
22
 
23
23
  const c = {
@@ -36,7 +36,7 @@ const useColor = process.stdout.isTTY !== false && !process.env.NO_COLOR;
36
36
  const paint = (color, s) => (useColor ? `${c[color]}${s}${c.reset}` : s);
37
37
 
38
38
  const VERSION = "0.1.0";
39
- const APP_DIR_CANDIDATES = [resolve(cwd, "app", "src"), resolve(cwd, "app")];
39
+ const APP_DIR_CANDIDATES = [resolve(cwd, "src", "app"), resolve(cwd, "app")];
40
40
  const APP_DIR = APP_DIR_CANDIDATES.find((p) => existsSync(p)) ?? resolve(cwd, "app");
41
41
  const PUBLIC_DIR = resolve(cwd, "public");
42
42
  const SWIFT_RUST_CONFIG = resolve(cwd, "swift-rust.config.json");
@@ -617,6 +617,7 @@ function mergeMetadata(...metas) {
617
617
  if (m.description) out.description = m.description;
618
618
  if (m.keywords) out.keywords = m.keywords;
619
619
  if (m.openGraph) out.openGraph = { ...(out.openGraph || {}), ...m.openGraph };
620
+ if (m.twitter) out.twitter = { ...(out.twitter || {}), ...m.twitter };
620
621
  }
621
622
  return out;
622
623
  }
@@ -641,6 +642,24 @@ function metadataToHead(meta) {
641
642
  for (const img of meta.openGraph.images) {
642
643
  const url = typeof img === "string" ? img : img.url;
643
644
  if (url) parts.push(`<meta property="og:image" content="${escapeHtml(url)}" />`);
645
+ if (typeof img === "object" && img) {
646
+ if (img.width) parts.push(`<meta property="og:image:width" content="${escapeHtml(img.width)}" />`);
647
+ if (img.height) parts.push(`<meta property="og:image:height" content="${escapeHtml(img.height)}" />`);
648
+ if (img.alt) parts.push(`<meta property="og:image:alt" content="${escapeHtml(img.alt)}" />`);
649
+ }
650
+ }
651
+ }
652
+ }
653
+ if (meta.twitter) {
654
+ if (meta.twitter.card) parts.push(`<meta name="twitter:card" content="${escapeHtml(meta.twitter.card)}" />`);
655
+ if (meta.twitter.site) parts.push(`<meta name="twitter:site" content="${escapeHtml(meta.twitter.site)}" />`);
656
+ if (meta.twitter.creator) parts.push(`<meta name="twitter:creator" content="${escapeHtml(meta.twitter.creator)}" />`);
657
+ if (meta.twitter.title) parts.push(`<meta name="twitter:title" content="${escapeHtml(meta.twitter.title)}" />`);
658
+ if (meta.twitter.description) parts.push(`<meta name="twitter:description" content="${escapeHtml(meta.twitter.description)}" />`);
659
+ if (meta.twitter.images) {
660
+ for (const img of meta.twitter.images) {
661
+ const url = typeof img === "string" ? img : img.url;
662
+ if (url) parts.push(`<meta name="twitter:image" content="${escapeHtml(url)}" />`);
644
663
  }
645
664
  }
646
665
  }
@@ -796,10 +815,12 @@ function warnRoutingFile(file, msg) {
796
815
  function validateRoutingConventions() {
797
816
  const names = [...SPECIAL_FILES, "page"];
798
817
  const appRel = relative(cwd, APP_DIR) || "app";
799
- const usesSrc = APP_DIR.endsWith(`${sep}src`);
800
- const scan = (dir, label) => {
818
+ const srcRoot = dirname(APP_DIR);
819
+ const usesSrc = basename(APP_DIR) === "app" && basename(srcRoot) === "src";
820
+ const scan = (dir, label, allow = []) => {
801
821
  if (!existsSync(dir) || resolve(dir) === resolve(APP_DIR)) return;
802
822
  for (const name of names) {
823
+ if (allow.includes(name)) continue;
803
824
  const f = findFile(dir, name);
804
825
  if (f) {
805
826
  warnRoutingFile(
@@ -811,7 +832,8 @@ function validateRoutingConventions() {
811
832
  }
812
833
  };
813
834
  scan(cwd, "the project root");
814
- if (usesSrc) scan(dirname(APP_DIR), `"app/" (outside "src/")`);
835
+ // proxy.ts is allowed to live at the src/ root (sibling of src/app).
836
+ if (usesSrc) scan(srcRoot, `"src/" (outside "app/")`, ["proxy"]);
815
837
  }
816
838
 
817
839
  // ── RFC 0001 route pipeline: config → schema → guard → loader/action ────────
@@ -902,7 +924,16 @@ async function runRoutePipeline(route, ctx) {
902
924
 
903
925
  // proxy.ts — phase 1, outer → inner. Cheap, data-free interception
904
926
  // (Next.js calls this "proxy"; "middleware" is accepted with a warning).
905
- for (const { file } of collectRouteFiles(chain, "proxy")) {
927
+ // When using a src/ directory, a root proxy lives at src/proxy.ts (sibling
928
+ // of src/app), so it runs before any in-app proxy.
929
+ const proxyFiles = [];
930
+ const srcRoot = dirname(APP_DIR);
931
+ if (basename(APP_DIR) === "app" && basename(srcRoot) === "src") {
932
+ const rootProxy = findFile(srcRoot, "proxy");
933
+ if (rootProxy) proxyFiles.push({ file: rootProxy, dir: srcRoot });
934
+ }
935
+ proxyFiles.push(...collectRouteFiles(chain, "proxy"));
936
+ for (const { file } of proxyFiles) {
906
937
  const mod = await loadModuleFresh(file);
907
938
  const fn = mod.default ?? mod.proxy;
908
939
  if (typeof fn === "function") {
@@ -113,8 +113,8 @@ function classifyError(message) {
113
113
  "The dev server could not bind to the requested port, " +
114
114
  "or it ran out of system resources.",
115
115
  suggestions: [
116
- "Close any other process using this port (usually 3000)",
117
- "Run with --port 3001 to use a different port",
116
+ "Close any other process using this port (usually 3210)",
117
+ "Run with --port <number> to use a different port",
118
118
  "Check disk space with `df -h`",
119
119
  ],
120
120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swift-rust",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -22,6 +22,7 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "exports": {
24
24
  "./package.json": "./package.json",
25
+ "./tsconfig.base.json": "./tsconfig.base.json",
25
26
  ".": {
26
27
  "types": "./dist/index.d.ts",
27
28
  "import": "./dist/index.js"
@@ -74,7 +75,8 @@
74
75
  "dist",
75
76
  "bin",
76
77
  "scripts",
77
- "native"
78
+ "native",
79
+ "tsconfig.base.json"
78
80
  ],
79
81
  "scripts": {
80
82
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "display": "swift-rust base",
4
+ "compilerOptions": {
5
+ "target": "ES2022",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "moduleResolution": "bundler",
9
+ "moduleDetection": "force",
10
+ "jsx": "react-jsx",
11
+ "allowJs": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "strict": true,
17
+ "noUncheckedIndexedAccess": true,
18
+ "forceConsistentCasingInFileNames": true,
19
+ "noEmit": true
20
+ }
21
+ }