swift-rust 1.0.1 → 1.1.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 +36 -1
- package/bin/dev-server.mjs +717 -24
- package/bin/runtime/hmr-client.js +102 -0
- package/dist/router.d.ts +103 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +58 -0
- package/dist/router.js.map +1 -1
- package/package.json +9 -8
package/bin/build.mjs
CHANGED
|
@@ -214,6 +214,41 @@ function writeRawFile(outDir, name, contents) {
|
|
|
214
214
|
writeFileSync(outPath, contents);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// Client-island hydration: a "use client" page's HTML references the dev-only
|
|
218
|
+
// bundle at /_swift-rust/island.js?p=<file>. For the static export we fetch
|
|
219
|
+
// that bundle, write it as a real file, and rewrite the script src to point at
|
|
220
|
+
// it — so hydration works on the deployed site, not just in `bun dev`.
|
|
221
|
+
const islandWritten = new Map();
|
|
222
|
+
function simpleHash(s) {
|
|
223
|
+
let h = 2166136261;
|
|
224
|
+
for (let i = 0; i < s.length; i++) {
|
|
225
|
+
h ^= s.charCodeAt(i);
|
|
226
|
+
h = Math.imul(h, 16777619);
|
|
227
|
+
}
|
|
228
|
+
return (h >>> 0).toString(36);
|
|
229
|
+
}
|
|
230
|
+
async function localizeIslands(html) {
|
|
231
|
+
const re = /\/_swift-rust\/island\.js\?p=([^"]+)/g;
|
|
232
|
+
const found = new Set();
|
|
233
|
+
let m;
|
|
234
|
+
while ((m = re.exec(html)) !== null) found.add(m[1]);
|
|
235
|
+
if (found.size === 0) return html;
|
|
236
|
+
let out = html;
|
|
237
|
+
for (const enc of found) {
|
|
238
|
+
let staticUrl = islandWritten.get(enc);
|
|
239
|
+
if (!staticUrl) {
|
|
240
|
+
const { status, body } = await fetchRoute(`/_swift-rust/island.js?p=${enc}`);
|
|
241
|
+
if (status !== 200) continue;
|
|
242
|
+
const rel = `_swift-rust/island/${simpleHash(enc)}.js`;
|
|
243
|
+
writeRawFile(STATIC_DIR, rel, body);
|
|
244
|
+
staticUrl = `/${rel}`;
|
|
245
|
+
islandWritten.set(enc, staticUrl);
|
|
246
|
+
}
|
|
247
|
+
out = out.split(`/_swift-rust/island.js?p=${enc}`).join(staticUrl);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
217
252
|
function writeConfigJson(outDir, _hasPublic) {
|
|
218
253
|
// Build Output API v3 config. Only schema-valid fields here — unknown
|
|
219
254
|
// top-level fields or route properties are rejected at "Deploying outputs".
|
|
@@ -290,7 +325,7 @@ async function main() {
|
|
|
290
325
|
try {
|
|
291
326
|
const { status, body } = await fetchRoute(route);
|
|
292
327
|
if (status === 200) {
|
|
293
|
-
const cleaned = stripHmrScript(body);
|
|
328
|
+
const cleaned = await localizeIslands(stripHmrScript(body));
|
|
294
329
|
writeStaticFile(STATIC_DIR, route, cleaned);
|
|
295
330
|
okCount++;
|
|
296
331
|
process.stdout.write(` ${paint("green", "✓")} ${route}\n`);
|
package/bin/dev-server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, statSync, readFileSync, readdirSync, watch as fsWatch } from "node:fs";
|
|
2
|
+
import { existsSync, statSync, readFileSync, readdirSync, writeFileSync, unlinkSync, watch as fsWatch } from "node:fs";
|
|
3
3
|
import { join, resolve, extname, relative, dirname, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
@@ -42,7 +42,14 @@ const PUBLIC_DIR = resolve(cwd, "public");
|
|
|
42
42
|
const SWIFT_RUST_CONFIG = resolve(cwd, "swift-rust.config.json");
|
|
43
43
|
|
|
44
44
|
const PAGE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
45
|
-
const SPECIAL_FILES = new Set([
|
|
45
|
+
const SPECIAL_FILES = new Set([
|
|
46
|
+
"layout", "loading", "error", "not-found", "template", "default", "route",
|
|
47
|
+
// RFC 0001 routing files
|
|
48
|
+
"guard", "loader", "action", "config", "schema", "proxy", "pending",
|
|
49
|
+
"revalidate", "shell", "fragment", "transition", "fallback", "prefetch",
|
|
50
|
+
"error-recovery", "i18n", "rpc", "stream", "edge", "worker", "query",
|
|
51
|
+
"state", "seo", "variant", "global-error",
|
|
52
|
+
]);
|
|
46
53
|
|
|
47
54
|
const moduleCache = new Map();
|
|
48
55
|
const compileTimings = new Map();
|
|
@@ -148,22 +155,22 @@ function findFile(dir, basename) {
|
|
|
148
155
|
function resolvePageRoute(segments) {
|
|
149
156
|
if (segments.length === 0) {
|
|
150
157
|
const file = findFile(APP_DIR, "page");
|
|
151
|
-
if (file) return { file, params: {}, segments: [] };
|
|
158
|
+
if (file) return { file, params: {}, segments: [], dirChain: [APP_DIR] };
|
|
152
159
|
return null;
|
|
153
160
|
}
|
|
154
|
-
return resolveRoute(APP_DIR, segments, 0, {});
|
|
161
|
+
return resolveRoute(APP_DIR, segments, 0, {}, [APP_DIR]);
|
|
155
162
|
}
|
|
156
163
|
|
|
157
|
-
function resolveRoute(dir, segments, idx, params) {
|
|
164
|
+
function resolveRoute(dir, segments, idx, params, chain) {
|
|
158
165
|
if (idx === segments.length) {
|
|
159
166
|
const file = findFile(dir, "page");
|
|
160
|
-
if (file) return { file, params, segments: segments.slice(0, idx) };
|
|
167
|
+
if (file) return { file, params, segments: segments.slice(0, idx), dirChain: chain };
|
|
161
168
|
return null;
|
|
162
169
|
}
|
|
163
170
|
const seg = segments[idx];
|
|
164
171
|
const directDir = join(dir, seg);
|
|
165
172
|
if (existsSync(directDir) && statSync(directDir).isDirectory()) {
|
|
166
|
-
const result = resolveRoute(directDir, segments, idx + 1, params);
|
|
173
|
+
const result = resolveRoute(directDir, segments, idx + 1, params, [...chain, directDir]);
|
|
167
174
|
if (result) return result;
|
|
168
175
|
}
|
|
169
176
|
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
@@ -175,7 +182,7 @@ function resolveRoute(dir, segments, idx, params) {
|
|
|
175
182
|
if (paramName.startsWith("...")) continue;
|
|
176
183
|
const paramDir = join(dir, e.name);
|
|
177
184
|
const newParams = { ...params, [paramName]: seg };
|
|
178
|
-
const result = resolveRoute(paramDir, segments, idx + 1, newParams);
|
|
185
|
+
const result = resolveRoute(paramDir, segments, idx + 1, newParams, [...chain, paramDir]);
|
|
179
186
|
if (result) return result;
|
|
180
187
|
}
|
|
181
188
|
}
|
|
@@ -183,6 +190,42 @@ function resolveRoute(dir, segments, idx, params) {
|
|
|
183
190
|
return null;
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
/** Resolve the leaf directory for URL segments (incl. dynamic), even when the
|
|
194
|
+
* segment has no page.tsx — used for stream.ts / rpc.ts handlers. */
|
|
195
|
+
function resolveLeafDir(segments) {
|
|
196
|
+
return walkLeafDir(APP_DIR, segments, 0, {});
|
|
197
|
+
}
|
|
198
|
+
function walkLeafDir(dir, segments, idx, params) {
|
|
199
|
+
if (idx === segments.length) return { dir, params };
|
|
200
|
+
const seg = segments[idx];
|
|
201
|
+
const directDir = join(dir, seg);
|
|
202
|
+
if (existsSync(directDir) && statSync(directDir).isDirectory()) {
|
|
203
|
+
const r = walkLeafDir(directDir, segments, idx + 1, params);
|
|
204
|
+
if (r) return r;
|
|
205
|
+
}
|
|
206
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
207
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
208
|
+
if (e.isDirectory() && e.name.startsWith("[") && e.name.endsWith("]") && !e.name.includes("...")) {
|
|
209
|
+
const pn = e.name.slice(1, -1);
|
|
210
|
+
const r = walkLeafDir(join(dir, e.name), segments, idx + 1, { ...params, [pn]: seg });
|
|
211
|
+
if (r) return r;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Collect a routing file (guard/loader/action/config/schema/…) along the
|
|
219
|
+
* matched directory chain, outermost → innermost. */
|
|
220
|
+
function collectRouteFiles(dirChain, basename) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const dir of dirChain || []) {
|
|
223
|
+
const f = findFile(dir, basename);
|
|
224
|
+
if (f) out.push({ file: f, dir });
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
186
229
|
function resolveApiRoute(segments) {
|
|
187
230
|
if (segments.length === 0 || segments[0] !== "api") return null;
|
|
188
231
|
const apiDir = join(APP_DIR, "api");
|
|
@@ -256,6 +299,16 @@ function findLoading(segments) {
|
|
|
256
299
|
return findFile(APP_DIR, "loading");
|
|
257
300
|
}
|
|
258
301
|
|
|
302
|
+
/** Generic: find the nearest `<basename>` file walking the URL segments up. */
|
|
303
|
+
function findRouteFileUp(segments, basename) {
|
|
304
|
+
for (let i = segments?.length ?? 0; i >= 0; i--) {
|
|
305
|
+
const dir = i === 0 ? APP_DIR : join(APP_DIR, ...segments.slice(0, i));
|
|
306
|
+
const file = findFile(dir, basename);
|
|
307
|
+
if (file) return file;
|
|
308
|
+
}
|
|
309
|
+
return findFile(APP_DIR, basename);
|
|
310
|
+
}
|
|
311
|
+
|
|
259
312
|
function findRouteSegmentsForDir(dir) {
|
|
260
313
|
const rel = relative(APP_DIR, dir);
|
|
261
314
|
if (rel === "" || rel === ".") return [];
|
|
@@ -271,6 +324,57 @@ function bustCache(file) {
|
|
|
271
324
|
}
|
|
272
325
|
}
|
|
273
326
|
|
|
327
|
+
// Bumped on every file change. Used to invalidate the per-render SSR bundles
|
|
328
|
+
// below so that edits to *transitively imported* modules (components/, lib/)
|
|
329
|
+
// are picked up — Bun's module cache is path-keyed and ignores ?query busting,
|
|
330
|
+
// so a plain re-import of the page would still serve stale child modules.
|
|
331
|
+
let buildGeneration = 1;
|
|
332
|
+
|
|
333
|
+
// Externalize everything that isn't the app's own source (relative imports and
|
|
334
|
+
// the @/ alias). React and the framework stay shared/cached; only app code is
|
|
335
|
+
// re-bundled, so every render reflects the latest component/lib edits.
|
|
336
|
+
const externalizeDepsPlugin = {
|
|
337
|
+
name: "sr-externalize-deps",
|
|
338
|
+
setup(build) {
|
|
339
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
340
|
+
const p = args.path;
|
|
341
|
+
// App's own source (relative, absolute, or the @/ alias) → bundle fresh.
|
|
342
|
+
if (p.startsWith(".") || p.startsWith("/") || p.startsWith("@/")) return undefined;
|
|
343
|
+
// Everything else (react, swift-rust, node:*, @scope/*) → keep external.
|
|
344
|
+
return { path: p, external: true };
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const ssrBundleCache = new Map(); // file -> { gen, mod }
|
|
350
|
+
|
|
351
|
+
async function loadModuleFresh(filePath) {
|
|
352
|
+
if (typeof Bun === "undefined" || typeof Bun.build !== "function") {
|
|
353
|
+
return loadModule(filePath, { bust: true });
|
|
354
|
+
}
|
|
355
|
+
const cached = ssrBundleCache.get(filePath);
|
|
356
|
+
if (cached && cached.gen === buildGeneration) return cached.mod;
|
|
357
|
+
|
|
358
|
+
const result = await Bun.build({
|
|
359
|
+
entrypoints: [filePath],
|
|
360
|
+
target: "bun",
|
|
361
|
+
plugins: [externalizeDepsPlugin],
|
|
362
|
+
});
|
|
363
|
+
if (!result.success) {
|
|
364
|
+
throw new Error(result.logs.map((l) => l.message).join("\n"));
|
|
365
|
+
}
|
|
366
|
+
const code = await result.outputs[0].text();
|
|
367
|
+
const tmp = join(dirname(filePath), `.__sr_ssr_${buildGeneration}_${Math.random().toString(36).slice(2)}.mjs`);
|
|
368
|
+
writeFileSync(tmp, code);
|
|
369
|
+
try {
|
|
370
|
+
const mod = await import(pathToFileURL(tmp).href);
|
|
371
|
+
ssrBundleCache.set(filePath, { gen: buildGeneration, mod });
|
|
372
|
+
return mod;
|
|
373
|
+
} finally {
|
|
374
|
+
try { unlinkSync(tmp); } catch {}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
274
378
|
async function loadModule(filePath, { bust = false } = {}) {
|
|
275
379
|
const baseUrl = pathToFileURL(filePath).href;
|
|
276
380
|
const url = bust ? `${baseUrl}?t=${Date.now()}-${Math.random().toString(36).slice(2)}` : baseUrl;
|
|
@@ -605,7 +709,393 @@ async function renderToStringCompat(tree) {
|
|
|
605
709
|
return renderToString(tree);
|
|
606
710
|
}
|
|
607
711
|
|
|
608
|
-
|
|
712
|
+
// ── Client islands: page-level "use client" hydration ──────────────────────
|
|
713
|
+
// A page whose first meaningful line is `"use client"` is bundled for the
|
|
714
|
+
// browser and hydrated, so interactive components (e.g. the Pdf viewer) run.
|
|
715
|
+
// Static pages are untouched and ship zero JS.
|
|
716
|
+
|
|
717
|
+
const islandBundleCache = new Map(); // pageFile -> { mtime, code }
|
|
718
|
+
|
|
719
|
+
export function isClientPage(file) {
|
|
720
|
+
try {
|
|
721
|
+
for (const raw of readFileSync(file, "utf8").split("\n")) {
|
|
722
|
+
const t = raw.trim();
|
|
723
|
+
if (!t || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
|
|
724
|
+
return /^["']use client["'];?$/.test(t);
|
|
725
|
+
}
|
|
726
|
+
} catch {}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function buildIslandBundle(pageFile) {
|
|
731
|
+
if (typeof Bun === "undefined" || typeof Bun.build !== "function") {
|
|
732
|
+
throw new Error("client islands require the Bun runtime");
|
|
733
|
+
}
|
|
734
|
+
const cached = islandBundleCache.get(pageFile);
|
|
735
|
+
if (cached && cached.gen === buildGeneration) return cached.code;
|
|
736
|
+
|
|
737
|
+
// Temp hydration entry, written next to the page so module resolution works.
|
|
738
|
+
const entryPath = join(dirname(pageFile), `.__sr_island_${process.pid}_${Date.now()}.js`);
|
|
739
|
+
const entry = `import { createRoot } from "react-dom/client";
|
|
740
|
+
import { createElement } from "react";
|
|
741
|
+
import Page from ${JSON.stringify(pageFile)};
|
|
742
|
+
const el = document.getElementById("__sr_island_root");
|
|
743
|
+
if (el) {
|
|
744
|
+
let params = {};
|
|
745
|
+
try { params = JSON.parse(el.getAttribute("data-sr-params") || "{}"); } catch {}
|
|
746
|
+
// Client-render (not hydrate): "use client" pages contain client-only
|
|
747
|
+
// widgets (e.g. the pdf.js viewer) whose SSR output intentionally differs
|
|
748
|
+
// from the first client render, so hydration would mismatch.
|
|
749
|
+
el.innerHTML = "";
|
|
750
|
+
createRoot(el).render(createElement(Page, { params }));
|
|
751
|
+
}
|
|
752
|
+
`;
|
|
753
|
+
writeFileSync(entryPath, entry);
|
|
754
|
+
try {
|
|
755
|
+
const result = await Bun.build({
|
|
756
|
+
entrypoints: [entryPath],
|
|
757
|
+
target: "browser",
|
|
758
|
+
minify: true,
|
|
759
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
760
|
+
});
|
|
761
|
+
if (!result.success) {
|
|
762
|
+
throw new Error(result.logs.map((l) => l.message).join("\n"));
|
|
763
|
+
}
|
|
764
|
+
const code = await result.outputs[0].text();
|
|
765
|
+
islandBundleCache.set(pageFile, { gen: buildGeneration, code });
|
|
766
|
+
return code;
|
|
767
|
+
} finally {
|
|
768
|
+
try { unlinkSync(entryPath); } catch {}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ── Routing-file error handling & convention validation ─────────────────────
|
|
773
|
+
|
|
774
|
+
class RoutingFileError extends Error {
|
|
775
|
+
constructor(kind, file, cause) {
|
|
776
|
+
super(`Error in ${kind} (${relative(cwd, file)}): ${cause?.message ?? cause}`);
|
|
777
|
+
this.name = "RoutingFileError";
|
|
778
|
+
this.kind = kind;
|
|
779
|
+
this.file = file;
|
|
780
|
+
this.cause = cause;
|
|
781
|
+
if (cause?.stack) this.stack = cause.stack;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const warnedRouting = new Set();
|
|
786
|
+
function warnRoutingFile(file, msg) {
|
|
787
|
+
const key = `${file}::${msg}`;
|
|
788
|
+
if (warnedRouting.has(key)) return;
|
|
789
|
+
warnedRouting.add(key);
|
|
790
|
+
process.stderr.write(` ${paint("yellow", "⚠")} ${paint("dim", "[routing]")} ${msg}\n`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Warn when routing files are placed where the router will never find them
|
|
794
|
+
* (e.g. proxy.ts at the project root, or routing files in app/ when the app
|
|
795
|
+
* uses an app/src/ directory). Helps catch a very common mistake early. */
|
|
796
|
+
function validateRoutingConventions() {
|
|
797
|
+
const names = [...SPECIAL_FILES, "page"];
|
|
798
|
+
const appRel = relative(cwd, APP_DIR) || "app";
|
|
799
|
+
const usesSrc = APP_DIR.endsWith(`${sep}src`);
|
|
800
|
+
const scan = (dir, label) => {
|
|
801
|
+
if (!existsSync(dir) || resolve(dir) === resolve(APP_DIR)) return;
|
|
802
|
+
for (const name of names) {
|
|
803
|
+
const f = findFile(dir, name);
|
|
804
|
+
if (f) {
|
|
805
|
+
warnRoutingFile(
|
|
806
|
+
f,
|
|
807
|
+
`"${name}" found in ${label} — routing files belong under "${appRel}/". ` +
|
|
808
|
+
`Move ${relative(cwd, f)} into ${appRel}/ (the router only scans there).`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
scan(cwd, "the project root");
|
|
814
|
+
if (usesSrc) scan(dirname(APP_DIR), `"app/" (outside "src/")`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── RFC 0001 route pipeline: config → schema → guard → loader/action ────────
|
|
818
|
+
|
|
819
|
+
let routerRuntimePromise = null;
|
|
820
|
+
function routerRuntime() {
|
|
821
|
+
if (!routerRuntimePromise) routerRuntimePromise = import("swift-rust/router").catch(() => null);
|
|
822
|
+
return routerRuntimePromise;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function parseCookieHeader(header) {
|
|
826
|
+
const map = new Map();
|
|
827
|
+
for (const part of (header || "").split(";")) {
|
|
828
|
+
const i = part.indexOf("=");
|
|
829
|
+
if (i < 0) continue;
|
|
830
|
+
const k = part.slice(0, i).trim();
|
|
831
|
+
if (k) map.set(k, decodeURIComponent(part.slice(i + 1).trim()));
|
|
832
|
+
}
|
|
833
|
+
return map;
|
|
834
|
+
}
|
|
835
|
+
function serializeCookie(name, value, opts = {}) {
|
|
836
|
+
let s = `${name}=${encodeURIComponent(value)}`;
|
|
837
|
+
if (opts.maxAge != null) s += `; Max-Age=${opts.maxAge}`;
|
|
838
|
+
if (opts.path) s += `; Path=${opts.path}`;
|
|
839
|
+
else s += "; Path=/";
|
|
840
|
+
if (opts.domain) s += `; Domain=${opts.domain}`;
|
|
841
|
+
if (opts.httpOnly) s += "; HttpOnly";
|
|
842
|
+
if (opts.secure) s += "; Secure";
|
|
843
|
+
if (opts.sameSite) s += `; SameSite=${opts.sameSite[0].toUpperCase()}${opts.sameSite.slice(1)}`;
|
|
844
|
+
return s;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function buildRouteCtx(req, url, params, searchParams) {
|
|
848
|
+
const cookieMap = parseCookieHeader(req?.headers?.get?.("cookie") || "");
|
|
849
|
+
const setCookies = [];
|
|
850
|
+
const cookies = {
|
|
851
|
+
get: (n) => cookieMap.get(n),
|
|
852
|
+
set: (n, v, o) => { cookieMap.set(n, v); setCookies.push(serializeCookie(n, v, o)); },
|
|
853
|
+
delete: (n, o) => { cookieMap.delete(n); setCookies.push(serializeCookie(n, "", { ...o, maxAge: 0 })); },
|
|
854
|
+
all: () => cookieMap,
|
|
855
|
+
};
|
|
856
|
+
const localsMap = new Map();
|
|
857
|
+
return {
|
|
858
|
+
url,
|
|
859
|
+
method: req?.method || "GET",
|
|
860
|
+
headers: req?.headers || new Headers(),
|
|
861
|
+
cookies,
|
|
862
|
+
params,
|
|
863
|
+
searchParams,
|
|
864
|
+
runtime: "node",
|
|
865
|
+
locals: { get: (k) => localsMap.get(k), set: (k, v) => localsMap.set(k, v) },
|
|
866
|
+
request: req,
|
|
867
|
+
__setCookies: setCookies,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Turn a returned RouteControl object into a thrown control the catch handles.
|
|
872
|
+
function applyControl(c) {
|
|
873
|
+
if (!c || typeof c !== "object" || !c.kind) return;
|
|
874
|
+
if (c.kind === "next") return;
|
|
875
|
+
const e = new Error(`route control: ${c.kind}`);
|
|
876
|
+
if (c.kind === "redirect") e.digest = `REDIRECT;${c.status || 307};${c.to}`;
|
|
877
|
+
else if (c.kind === "rewrite") e.digest = `REWRITE;${c.to}`;
|
|
878
|
+
else if (c.kind === "notFound") e.digest = "NOT_FOUND";
|
|
879
|
+
else if (c.kind === "response") e.__response = c.response;
|
|
880
|
+
else if (c.kind === "error") { throw c.error ?? new Error("route error"); }
|
|
881
|
+
throw e;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Merge config.ts along the chain (inner overrides outer). */
|
|
885
|
+
async function readMergedConfig(chain) {
|
|
886
|
+
let config = {};
|
|
887
|
+
for (const { file } of collectRouteFiles(chain, "config")) {
|
|
888
|
+
const mod = await loadModuleFresh(file);
|
|
889
|
+
const c = mod.config ?? mod.default;
|
|
890
|
+
if (c && typeof c === "object") config = { ...config, ...c, headers: { ...config.headers, ...c.headers } };
|
|
891
|
+
}
|
|
892
|
+
// edge.ts / worker.ts force a runtime.
|
|
893
|
+
const edge = await collectFirst(chain, "edge");
|
|
894
|
+
if (edge && (edge.edge || edge.default)) config.runtime = "edge";
|
|
895
|
+
const worker = await collectFirst(chain, "worker");
|
|
896
|
+
if (worker && (worker.default || worker.bindings)) config.runtime = "worker";
|
|
897
|
+
return config;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function runRoutePipeline(route, ctx) {
|
|
901
|
+
const chain = route.dirChain || [];
|
|
902
|
+
|
|
903
|
+
// proxy.ts — phase 1, outer → inner. Cheap, data-free interception
|
|
904
|
+
// (Next.js calls this "proxy"; "middleware" is accepted with a warning).
|
|
905
|
+
for (const { file } of collectRouteFiles(chain, "proxy")) {
|
|
906
|
+
const mod = await loadModuleFresh(file);
|
|
907
|
+
const fn = mod.default ?? mod.proxy;
|
|
908
|
+
if (typeof fn === "function") {
|
|
909
|
+
const matcher = mod.matcher;
|
|
910
|
+
const matched = !matcher || matchesMatcher(ctx.url.pathname, matcher);
|
|
911
|
+
if (matched) {
|
|
912
|
+
try {
|
|
913
|
+
applyControl(await fn(ctx));
|
|
914
|
+
} catch (e) {
|
|
915
|
+
if (e?.digest || e?.__response) throw e; // control flow, re-throw
|
|
916
|
+
throw new RoutingFileError("proxy", file, e);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// Back-compat: warn if the old name is used.
|
|
922
|
+
for (const { file } of collectRouteFiles(chain, "middleware")) {
|
|
923
|
+
warnRoutingFile(file, `"middleware.ts" was renamed to "proxy.ts" — rename ${relative(cwd, file)}.`);
|
|
924
|
+
const mod = await loadModuleFresh(file);
|
|
925
|
+
const fn = mod.default ?? mod.middleware;
|
|
926
|
+
if (typeof fn === "function") {
|
|
927
|
+
const matcher = mod.matcher;
|
|
928
|
+
if (!matcher || matchesMatcher(ctx.url.pathname, matcher)) applyControl(await fn(ctx));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// config.ts — merged, applied to the response by the caller.
|
|
933
|
+
const config = await readMergedConfig(chain);
|
|
934
|
+
|
|
935
|
+
// i18n.ts — resolve the active locale into locals (cookie/header/default).
|
|
936
|
+
const i18nMod = await collectFirst(chain, "i18n");
|
|
937
|
+
const i18nCfg = i18nMod?.i18n ?? i18nMod?.default;
|
|
938
|
+
if (i18nCfg && Array.isArray(i18nCfg.locales)) {
|
|
939
|
+
let locale = i18nCfg.defaultLocale ?? i18nCfg.locales[0];
|
|
940
|
+
if (typeof i18nCfg.resolve === "function") locale = (await i18nCfg.resolve(ctx)) || locale;
|
|
941
|
+
else if (i18nCfg.strategy === "cookie") locale = ctx.cookies.get("locale") || locale;
|
|
942
|
+
else if (i18nCfg.strategy === "header") {
|
|
943
|
+
const al = ctx.headers.get?.("accept-language")?.split(",")[0]?.split("-")[0];
|
|
944
|
+
if (al && i18nCfg.locales.includes(al)) locale = al;
|
|
945
|
+
}
|
|
946
|
+
ctx.locals.set("locale", locale);
|
|
947
|
+
ctx.__locale = locale;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// schema.ts — validate/brand params + searchParams (Standard-Schema/Zod-like)
|
|
951
|
+
for (const { file } of collectRouteFiles(chain, "schema")) {
|
|
952
|
+
const mod = await loadModuleFresh(file);
|
|
953
|
+
if (mod.params?.safeParse) {
|
|
954
|
+
const r = mod.params.safeParse(ctx.params);
|
|
955
|
+
if (!r.success) { const e = new Error("Invalid params"); e.__response = jsonResponse({ error: "Invalid params", issues: r.error?.issues ?? r.error }, 400); throw e; }
|
|
956
|
+
Object.assign(ctx.params, r.data);
|
|
957
|
+
}
|
|
958
|
+
const querySpec = (await collectFirst(chain, "query"))?.query;
|
|
959
|
+
const searchSchema = querySpec?.parse ?? mod.searchParams;
|
|
960
|
+
if (searchSchema?.safeParse) {
|
|
961
|
+
const r = searchSchema.safeParse(ctx.searchParams);
|
|
962
|
+
if (r.success) Object.assign(ctx.searchParams, r.data);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// guard.ts — outer → inner
|
|
967
|
+
for (const { file } of collectRouteFiles(chain, "guard")) {
|
|
968
|
+
const mod = await loadModuleFresh(file);
|
|
969
|
+
const fn = mod.default ?? mod.guard;
|
|
970
|
+
if (typeof fn === "function") {
|
|
971
|
+
try {
|
|
972
|
+
applyControl(await fn(ctx));
|
|
973
|
+
} catch (e) {
|
|
974
|
+
if (e?.digest || e?.__response) throw e;
|
|
975
|
+
throw new RoutingFileError("guard", file, e);
|
|
976
|
+
}
|
|
977
|
+
} else if (mod.default !== undefined || mod.guard !== undefined) {
|
|
978
|
+
warnRoutingFile(file, `guard.ts must export a function (default export). ${relative(cwd, file)}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
let actionData;
|
|
983
|
+
// action.ts on mutating requests (leaf only)
|
|
984
|
+
if (ctx.method !== "GET" && ctx.method !== "HEAD") {
|
|
985
|
+
const actions = collectRouteFiles(chain, "action");
|
|
986
|
+
const leaf = actions[actions.length - 1];
|
|
987
|
+
if (leaf) {
|
|
988
|
+
const mod = await loadModuleFresh(leaf.file);
|
|
989
|
+
const fn = mod.default ?? mod.action;
|
|
990
|
+
if (typeof fn === "function") {
|
|
991
|
+
const actx = Object.assign({}, ctx, {
|
|
992
|
+
formData: () => ctx.request.formData(),
|
|
993
|
+
json: () => ctx.request.json(),
|
|
994
|
+
});
|
|
995
|
+
try {
|
|
996
|
+
const result = await fn(actx);
|
|
997
|
+
applyControl(result);
|
|
998
|
+
actionData = result;
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
if (e?.digest || e?.__response) throw e;
|
|
1001
|
+
throw new RoutingFileError("action", leaf.file, e);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// loader.ts — run in parallel along the chain
|
|
1008
|
+
const loaders = collectRouteFiles(chain, "loader");
|
|
1009
|
+
const loaded = await Promise.all(
|
|
1010
|
+
loaders.map(async ({ file, dir }) => {
|
|
1011
|
+
const mod = await loadModuleFresh(file);
|
|
1012
|
+
const fn = mod.default ?? mod.loader;
|
|
1013
|
+
if (typeof fn !== "function") return [dir, undefined];
|
|
1014
|
+
const lctx = Object.assign({}, ctx, { parent: () => undefined });
|
|
1015
|
+
try {
|
|
1016
|
+
return [dir, await fn(lctx)];
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
if (e?.digest || e?.__response) throw e;
|
|
1019
|
+
throw new RoutingFileError("loader", file, e);
|
|
1020
|
+
}
|
|
1021
|
+
}),
|
|
1022
|
+
);
|
|
1023
|
+
const loadersMap = {};
|
|
1024
|
+
for (const [dir, data] of loaded) loadersMap[relative(cwd, dir)] = data;
|
|
1025
|
+
const loaderData = loaded.length ? loaded[loaded.length - 1][1] : undefined;
|
|
1026
|
+
|
|
1027
|
+
// state.ts — server-side state to hydrate a client store.
|
|
1028
|
+
let serverState;
|
|
1029
|
+
const stateMod = await collectFirst(chain, "state");
|
|
1030
|
+
const stateFn = stateMod?.default ?? stateMod?.state;
|
|
1031
|
+
if (typeof stateFn === "function") serverState = await stateFn(ctx);
|
|
1032
|
+
|
|
1033
|
+
// seo.tsx — structured data / head injection (has loader data).
|
|
1034
|
+
let seoHead = "";
|
|
1035
|
+
const seoMod = await collectFirst(chain, "seo");
|
|
1036
|
+
const seoFn = seoMod?.default ?? seoMod?.seo;
|
|
1037
|
+
if (typeof seoFn === "function") {
|
|
1038
|
+
seoHead = buildSeoHead(await seoFn(Object.assign({}, ctx, { data: loaderData })));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// revalidate.ts — leaf decides cache TTL / tags.
|
|
1042
|
+
let revalidatePlan = config.revalidate != null ? { ttl: config.revalidate } : undefined;
|
|
1043
|
+
const rev = await collectFirst(chain, "revalidate");
|
|
1044
|
+
const revFn = rev?.default ?? rev?.revalidate;
|
|
1045
|
+
if (typeof revFn === "function") {
|
|
1046
|
+
const plan = await revFn(Object.assign({}, ctx, { data: loaderData, afterAction: ctx.method !== "GET" }));
|
|
1047
|
+
if (plan && typeof plan === "object") revalidatePlan = { ...revalidatePlan, ...plan };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return { actionData, loaderData, loaders: loadersMap, setCookies: ctx.__setCookies, config, revalidatePlan, serverState, seoHead };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function escAttr(s) {
|
|
1054
|
+
return String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
1055
|
+
}
|
|
1056
|
+
function buildSeoHead(r) {
|
|
1057
|
+
if (!r || typeof r !== "object") return "";
|
|
1058
|
+
const out = [];
|
|
1059
|
+
if (r.title) out.push(`<title>${escAttr(r.title)}</title>`);
|
|
1060
|
+
if (r.description) out.push(`<meta name="description" content="${escAttr(r.description)}" />`);
|
|
1061
|
+
if (r.canonical) out.push(`<link rel="canonical" href="${escAttr(r.canonical)}" />`);
|
|
1062
|
+
if (r.robots) out.push(`<meta name="robots" content="${escAttr(r.robots)}" />`);
|
|
1063
|
+
for (const [k, v] of Object.entries(r.openGraph || {})) out.push(`<meta property="og:${escAttr(k)}" content="${escAttr(v)}" />`);
|
|
1064
|
+
for (const a of r.alternates || []) out.push(`<link rel="alternate" hreflang="${escAttr(a.hreflang)}" href="${escAttr(a.href)}" />`);
|
|
1065
|
+
const jsonLd = r.jsonLd ? (Array.isArray(r.jsonLd) ? r.jsonLd : [r.jsonLd]) : [];
|
|
1066
|
+
for (const ld of jsonLd) out.push(`<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, "\\u003c")}</script>`);
|
|
1067
|
+
return out.join("\n");
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function collectFirst(chain, basename) {
|
|
1071
|
+
const files = collectRouteFiles(chain, basename);
|
|
1072
|
+
if (files.length === 0) return null;
|
|
1073
|
+
return await loadModuleFresh(files[files.length - 1].file);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function matchesMatcher(pathname, matcher) {
|
|
1077
|
+
const list = Array.isArray(matcher) ? matcher : [matcher];
|
|
1078
|
+
return list.some((m) => {
|
|
1079
|
+
if (typeof m !== "string") return false;
|
|
1080
|
+
// simple glob: * → [^/]*, ** → .*
|
|
1081
|
+
const re = new RegExp("^" + m.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::").replace(/\*/g, "[^/]*").replace(/::/g, ".*") + "$");
|
|
1082
|
+
return re.test(pathname);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function jsonResponse(obj, status = 200) {
|
|
1087
|
+
return new Response(JSON.stringify(obj), { status, headers: { "Content-Type": "application/json" } });
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function cacheControlFromPlan(plan) {
|
|
1091
|
+
if (!plan) return null;
|
|
1092
|
+
if (plan.ttl === false) return "public, max-age=31536000, immutable";
|
|
1093
|
+
if (plan.ttl === 0) return "no-store";
|
|
1094
|
+
if (typeof plan.ttl === "number") return `public, max-age=0, s-maxage=${plan.ttl}, stale-while-revalidate`;
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function renderRoute(urlPath, req) {
|
|
609
1099
|
const segments = urlToRouteSegments(urlPath);
|
|
610
1100
|
const route = resolvePageRoute(segments);
|
|
611
1101
|
if (!route) {
|
|
@@ -618,8 +1108,48 @@ async function renderRoute(urlPath) {
|
|
|
618
1108
|
|
|
619
1109
|
try {
|
|
620
1110
|
const React = await import("react");
|
|
621
|
-
const
|
|
622
|
-
const
|
|
1111
|
+
const url = req ? new URL(req.url) : new URL(urlPath, "http://localhost");
|
|
1112
|
+
const searchParams = Object.fromEntries(url.searchParams.entries());
|
|
1113
|
+
const ctx = buildRouteCtx(req, url, { ...route.params }, searchParams);
|
|
1114
|
+
|
|
1115
|
+
// Run the RFC 0001 pipeline (config/schema/guard/loader/action).
|
|
1116
|
+
const pipeline = await runRoutePipeline(route, ctx);
|
|
1117
|
+
route.params = ctx.params; // validated/branded params flow to the page
|
|
1118
|
+
|
|
1119
|
+
const runtime = await routerRuntime();
|
|
1120
|
+
if (runtime?.__setRouteContext) {
|
|
1121
|
+
runtime.__setRouteContext({
|
|
1122
|
+
request: ctx,
|
|
1123
|
+
loaderData: pipeline.loaderData,
|
|
1124
|
+
actionData: pipeline.actionData,
|
|
1125
|
+
loaders: pipeline.loaders,
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// variant.tsx — pick an A/B variant component (bucket from middleware/
|
|
1130
|
+
// cookie/assign), else fall back to page.tsx.
|
|
1131
|
+
let Page;
|
|
1132
|
+
const leafDir = (route.dirChain || [])[(route.dirChain || []).length - 1];
|
|
1133
|
+
const variantFile = leafDir && findFile(leafDir, "variant");
|
|
1134
|
+
if (variantFile) {
|
|
1135
|
+
const vmod = await loadModuleFresh(variantFile);
|
|
1136
|
+
if (vmod.variants && typeof vmod.variants === "object") {
|
|
1137
|
+
const bucket =
|
|
1138
|
+
(typeof vmod.assign === "function" ? vmod.assign(ctx) : null) ||
|
|
1139
|
+
ctx.locals.get("bucket") ||
|
|
1140
|
+
ctx.cookies.get("bucket") ||
|
|
1141
|
+
Object.keys(vmod.variants)[0];
|
|
1142
|
+
const loader = vmod.variants[bucket];
|
|
1143
|
+
if (typeof loader === "function") {
|
|
1144
|
+
const m = await loader();
|
|
1145
|
+
Page = m.default ?? m;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (!Page) {
|
|
1150
|
+
const pageMod = await loadModuleFresh(route.file);
|
|
1151
|
+
Page = pageMod.default ?? pageMod.Page ?? pageMod.page;
|
|
1152
|
+
}
|
|
623
1153
|
if (!Page) {
|
|
624
1154
|
return { status: 500, html: null, error: new Error(`page ${route.file} has no default export`) };
|
|
625
1155
|
}
|
|
@@ -628,19 +1158,44 @@ async function renderRoute(urlPath) {
|
|
|
628
1158
|
get: (t, k) => (k in t ? t[k] : dynamicParams.get(String(k))),
|
|
629
1159
|
});
|
|
630
1160
|
|
|
1161
|
+
const clientPage = isClientPage(route.file);
|
|
631
1162
|
let tree = React.createElement(Page, { params: paramsProxy });
|
|
1163
|
+
if (clientPage) {
|
|
1164
|
+
// Wrap in a hydration root so the client bundle can mount into it.
|
|
1165
|
+
tree = React.createElement(
|
|
1166
|
+
"div",
|
|
1167
|
+
{ id: "__sr_island_root", "data-sr-params": JSON.stringify(route.params || {}) },
|
|
1168
|
+
tree,
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
632
1171
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
633
|
-
const layoutMod = await
|
|
1172
|
+
const layoutMod = await loadModuleFresh(layouts[i].file);
|
|
634
1173
|
const Layout = layoutMod.default ?? layoutMod.Layout ?? layoutMod.layout;
|
|
635
1174
|
if (Layout) tree = React.createElement(Layout, null, tree);
|
|
636
1175
|
}
|
|
637
1176
|
const html = await renderToStringCompat(tree);
|
|
1177
|
+
if (runtime?.__setRouteContext) runtime.__setRouteContext(null);
|
|
638
1178
|
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 };
|
|
1179
|
+
return { status: 200, html, metadata, error: null, clientPage, pageFile: route.file, layoutFiles: layouts.map((l) => l.file), notFoundFile, errorFile, loadingFile, segments, setCookies: pipeline.setCookies, actionData: pipeline.actionData, config: pipeline.config, revalidatePlan: pipeline.revalidatePlan, seoHead: pipeline.seoHead, serverState: pipeline.serverState };
|
|
640
1180
|
} catch (err) {
|
|
1181
|
+
const rt = await routerRuntime();
|
|
1182
|
+
if (rt?.__setRouteContext) rt.__setRouteContext(null);
|
|
1183
|
+
if (err?.__response instanceof Response) {
|
|
1184
|
+
return { status: err.__response.status, rawResponse: err.__response, html: null, error: null, segments };
|
|
1185
|
+
}
|
|
641
1186
|
if (err?.digest === "NOT_FOUND" || err?.name === "NotFoundError") {
|
|
642
1187
|
return await renderNotFound(segments);
|
|
643
1188
|
}
|
|
1189
|
+
if (err?.digest === "FORBIDDEN" || err?.name === "ForbiddenError") {
|
|
1190
|
+
return { status: 403, html: null, error: null, segments, rawResponse: new Response("Forbidden", { status: 403, headers: { "Content-Type": "text/plain" } }) };
|
|
1191
|
+
}
|
|
1192
|
+
if (err?.digest === "UNAUTHORIZED" || err?.name === "UnauthorizedError") {
|
|
1193
|
+
return { status: 401, html: null, error: null, segments, rawResponse: new Response("Unauthorized", { status: 401, headers: { "Content-Type": "text/plain" } }) };
|
|
1194
|
+
}
|
|
1195
|
+
if (err?.digest && String(err.digest).startsWith("REWRITE")) {
|
|
1196
|
+
const to = String(err.digest).slice("REWRITE;".length);
|
|
1197
|
+
return await renderRoute(to, req);
|
|
1198
|
+
}
|
|
644
1199
|
if (err?.digest && String(err.digest).startsWith("REDIRECT")) {
|
|
645
1200
|
const [, statusStr, ...rest] = String(err.digest).split(";");
|
|
646
1201
|
return { status: parseInt(statusStr, 10) || 307, redirect: rest.join(";"), html: null, error: null, segments };
|
|
@@ -656,6 +1211,21 @@ async function renderRoute(urlPath) {
|
|
|
656
1211
|
}
|
|
657
1212
|
} catch {}
|
|
658
1213
|
}
|
|
1214
|
+
// error-recovery.tsx — richer boundary with retry/reset (SSR: stub callbacks).
|
|
1215
|
+
const recoveryFile = findRouteFileUp(segments, "error-recovery");
|
|
1216
|
+
if (recoveryFile) {
|
|
1217
|
+
try {
|
|
1218
|
+
const React = await import("react");
|
|
1219
|
+
const mod = await loadModule(recoveryFile, { bust: true });
|
|
1220
|
+
const Recovery = mod.default ?? mod.ErrorRecovery;
|
|
1221
|
+
if (Recovery) {
|
|
1222
|
+
const html = await renderToStringCompat(
|
|
1223
|
+
React.createElement(Recovery, { error: err, attempt: 1, retry: () => {}, reset: () => {} }),
|
|
1224
|
+
);
|
|
1225
|
+
return { status: 500, html, metadata: null, error: err, segments, pageFile: route.file };
|
|
1226
|
+
}
|
|
1227
|
+
} catch {}
|
|
1228
|
+
}
|
|
659
1229
|
return { status: 500, html: null, error: err, segments, pageFile: route.file };
|
|
660
1230
|
}
|
|
661
1231
|
}
|
|
@@ -800,6 +1370,7 @@ function setupWatcher() {
|
|
|
800
1370
|
}
|
|
801
1371
|
} catch {}
|
|
802
1372
|
logEvent("change", full);
|
|
1373
|
+
buildGeneration++;
|
|
803
1374
|
bustCache(full);
|
|
804
1375
|
if (full.includes(`${sep}globals.${"css"}`)) {
|
|
805
1376
|
globalsCssCache = { file: null, mtime: 0, css: "" };
|
|
@@ -823,6 +1394,12 @@ function setupWatcher() {
|
|
|
823
1394
|
}
|
|
824
1395
|
}
|
|
825
1396
|
walk(APP_DIR);
|
|
1397
|
+
// Also watch sibling source dirs that pages/layouts import from — without
|
|
1398
|
+
// this, edits to components/, lib/, etc. never trigger a reload.
|
|
1399
|
+
for (const extra of ["components", "lib", "app"]) {
|
|
1400
|
+
const p = resolve(cwd, extra);
|
|
1401
|
+
if (existsSync(p)) walk(p);
|
|
1402
|
+
}
|
|
826
1403
|
}
|
|
827
1404
|
|
|
828
1405
|
const networkUrls = [];
|
|
@@ -1050,7 +1627,7 @@ async function handleRequest(req, res) {
|
|
|
1050
1627
|
}
|
|
1051
1628
|
|
|
1052
1629
|
const renderStart = performance.now();
|
|
1053
|
-
const renderResult = await renderRoute(pathname);
|
|
1630
|
+
const renderResult = await renderRoute(pathname, req);
|
|
1054
1631
|
const renderMs = performance.now() - renderStart;
|
|
1055
1632
|
const total = performance.now() - reqStart;
|
|
1056
1633
|
|
|
@@ -1165,10 +1742,48 @@ async function handleFetch(req) {
|
|
|
1165
1742
|
return new Response("Image not found", { status: 404 });
|
|
1166
1743
|
}
|
|
1167
1744
|
|
|
1745
|
+
// Client-island hydration bundle for a "use client" page.
|
|
1746
|
+
if (pathname === "/_swift-rust/island.js") {
|
|
1747
|
+
const p = url.searchParams.get("p");
|
|
1748
|
+
if (p && existsSync(p)) {
|
|
1749
|
+
try {
|
|
1750
|
+
const code = await buildIslandBundle(p);
|
|
1751
|
+
return new Response(code, {
|
|
1752
|
+
headers: { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" },
|
|
1753
|
+
});
|
|
1754
|
+
} catch (e) {
|
|
1755
|
+
logError(e, `island bundle failed for ${p}`);
|
|
1756
|
+
return new Response(`/* island build error: ${String(e?.message || e).replace(/\*\//g, "* /")} */`, {
|
|
1757
|
+
status: 500,
|
|
1758
|
+
headers: { "Content-Type": "application/javascript; charset=utf-8" },
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return new Response("// island not found", { status: 404, headers: { "Content-Type": "application/javascript" } });
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1168
1765
|
if (pathname.startsWith("/_swift-rust/")) {
|
|
1169
1766
|
return new Response("Not found", { status: 404 });
|
|
1170
1767
|
}
|
|
1171
1768
|
|
|
1769
|
+
// App-directory metadata icons (app/favicon.ico, app/favicon.svg, …) served
|
|
1770
|
+
// from the site root, Next.js style.
|
|
1771
|
+
{
|
|
1772
|
+
const iconName = pathname.replace(/^\/+/, "");
|
|
1773
|
+
if (APP_ICON_FILES.includes(iconName)) {
|
|
1774
|
+
const candidate = join(APP_DIR, iconName);
|
|
1775
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
1776
|
+
const ext = extname(candidate).toLowerCase();
|
|
1777
|
+
const mime =
|
|
1778
|
+
{ ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png" }[ext] ||
|
|
1779
|
+
"application/octet-stream";
|
|
1780
|
+
const file = Bun?.file ? Bun.file(candidate) : readFileSync(candidate);
|
|
1781
|
+
logRequest({ method, url: pathname, status: 200, duration: performance.now() - reqStart, compileMs: 0 });
|
|
1782
|
+
return new Response(file, { headers: { "Content-Type": mime } });
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1172
1787
|
if (existsSync(PUBLIC_DIR)) {
|
|
1173
1788
|
const safe = pathname.replace(/\.\.+/g, "").replace(/^\/+/, "");
|
|
1174
1789
|
const candidate = join(PUBLIC_DIR, safe);
|
|
@@ -1187,7 +1802,59 @@ async function handleFetch(req) {
|
|
|
1187
1802
|
return await handleApiRoute(req, segments, method, reqStart);
|
|
1188
1803
|
}
|
|
1189
1804
|
|
|
1190
|
-
|
|
1805
|
+
// rpc.ts / stream.ts handlers (leaf, route.ts-like).
|
|
1806
|
+
{
|
|
1807
|
+
const leaf = resolveLeafDir(segments);
|
|
1808
|
+
if (leaf) {
|
|
1809
|
+
const sp = Object.fromEntries(url.searchParams.entries());
|
|
1810
|
+
const baseCtx = buildRouteCtx(req, url, leaf.params, sp);
|
|
1811
|
+
const rpcFile = findFile(leaf.dir, "rpc");
|
|
1812
|
+
if (rpcFile) {
|
|
1813
|
+
try {
|
|
1814
|
+
const mod = await loadModuleFresh(rpcFile);
|
|
1815
|
+
const procs = mod.procedures ?? mod.default;
|
|
1816
|
+
if (procs && typeof procs === "object") {
|
|
1817
|
+
if (method === "POST") {
|
|
1818
|
+
const body = await req.json().catch(() => ({}));
|
|
1819
|
+
const proc = procs[body.procedure];
|
|
1820
|
+
if (!proc) return jsonResponse({ error: `Unknown procedure: ${body.procedure}` }, 404);
|
|
1821
|
+
let input = body.input;
|
|
1822
|
+
if (proc.input?.safeParse) {
|
|
1823
|
+
const r = proc.input.safeParse(input);
|
|
1824
|
+
if (!r.success) return jsonResponse({ error: "Invalid input", issues: r.error?.issues ?? r.error }, 400);
|
|
1825
|
+
input = r.data;
|
|
1826
|
+
}
|
|
1827
|
+
return jsonResponse({ data: await proc.handler(input, baseCtx) });
|
|
1828
|
+
}
|
|
1829
|
+
if (method === "GET") return jsonResponse({ procedures: Object.keys(procs) });
|
|
1830
|
+
}
|
|
1831
|
+
} catch (e) {
|
|
1832
|
+
return jsonResponse({ error: String(e?.message || e) }, 500);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const streamFile = findFile(leaf.dir, "stream");
|
|
1836
|
+
if (streamFile && !findFile(leaf.dir, "page")) {
|
|
1837
|
+
try {
|
|
1838
|
+
const mod = await loadModuleFresh(streamFile);
|
|
1839
|
+
const fn = mod.default ?? mod.stream;
|
|
1840
|
+
if (typeof fn === "function") {
|
|
1841
|
+
const result = await fn(baseCtx);
|
|
1842
|
+
if (result instanceof Response) return result;
|
|
1843
|
+
return new Response(result, {
|
|
1844
|
+
headers: { "Content-Type": mod.contentType || "text/event-stream", "Cache-Control": "no-cache" },
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
} catch (e) {
|
|
1848
|
+
return new Response(String(e?.message || e), { status: 500 });
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// Non-GET requests are allowed to reach the page render so action.ts can run;
|
|
1855
|
+
// pages without an action simply ignore the body.
|
|
1856
|
+
const ALLOWED_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
|
|
1857
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
1191
1858
|
return new Response("Method not allowed", { status: 405 });
|
|
1192
1859
|
}
|
|
1193
1860
|
|
|
@@ -1232,7 +1899,7 @@ async function handleFetch(req) {
|
|
|
1232
1899
|
}
|
|
1233
1900
|
|
|
1234
1901
|
const renderStart = performance.now();
|
|
1235
|
-
const renderResult = await renderRoute(pathname);
|
|
1902
|
+
const renderResult = await renderRoute(pathname, req);
|
|
1236
1903
|
const renderMs = performance.now() - renderStart;
|
|
1237
1904
|
const total = performance.now() - reqStart;
|
|
1238
1905
|
|
|
@@ -1264,18 +1931,43 @@ async function handleFetch(req) {
|
|
|
1264
1931
|
|
|
1265
1932
|
if (renderResult.redirect) {
|
|
1266
1933
|
logRequest({ method, url: pathname, status: renderResult.status, duration: total, compileMs });
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1934
|
+
const rh = new Headers({ Location: renderResult.redirect });
|
|
1935
|
+
for (const c of renderResult.setCookies || []) rh.append("Set-Cookie", c);
|
|
1936
|
+
return new Response(null, { status: renderResult.status, headers: rh });
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// Raw Response from a guard/action (RouteControl response, 401, 403, …).
|
|
1940
|
+
if (renderResult.rawResponse instanceof Response) {
|
|
1941
|
+
logRequest({ method, url: pathname, status: renderResult.rawResponse.status, duration: total, compileMs });
|
|
1942
|
+
const r = renderResult.rawResponse;
|
|
1943
|
+
for (const c of renderResult.setCookies || []) r.headers.append("Set-Cookie", c);
|
|
1944
|
+
return r;
|
|
1271
1945
|
}
|
|
1272
1946
|
|
|
1273
1947
|
logRequest({ method, url: pathname, status: 200, duration: total, compileMs });
|
|
1274
1948
|
await scanFontsFromLayouts();
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1949
|
+
// seo.tsx head + metadata head
|
|
1950
|
+
const headExtra = [metadataToHead(renderResult.metadata), renderResult.seoHead || ""].filter(Boolean).join("\n");
|
|
1951
|
+
let doc = await wrapInDocumentAsync({ head: headExtra, body: renderResult.html || "" });
|
|
1952
|
+
// state.ts → window.__SR_STATE__ for client stores
|
|
1953
|
+
if (renderResult.serverState !== undefined) {
|
|
1954
|
+
const json = JSON.stringify(renderResult.serverState).replace(/</g, "\\u003c");
|
|
1955
|
+
doc = doc.replace("</body>", `<script>window.__SR_STATE__=${json}</script>\n</body>`);
|
|
1956
|
+
}
|
|
1957
|
+
if (renderResult.clientPage && renderResult.pageFile) {
|
|
1958
|
+
const src = `/_swift-rust/island.js?p=${encodeURIComponent(renderResult.pageFile)}`;
|
|
1959
|
+
doc = doc.replace("</body>", `<script type="module" src="${src}"></script>\n</body>`);
|
|
1960
|
+
}
|
|
1961
|
+
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
1962
|
+
for (const c of renderResult.setCookies || []) headers.append("Set-Cookie", c);
|
|
1963
|
+
// config.ts headers
|
|
1964
|
+
for (const [k, v] of Object.entries(renderResult.config?.headers || {})) headers.set(k, String(v));
|
|
1965
|
+
// revalidate.ts → Cache-Control + cache tags
|
|
1966
|
+
const cc = cacheControlFromPlan(renderResult.revalidatePlan);
|
|
1967
|
+
if (cc) headers.set("Cache-Control", cc);
|
|
1968
|
+
const tags = renderResult.revalidatePlan?.tags || renderResult.revalidatePlan?.invalidate;
|
|
1969
|
+
if (Array.isArray(tags) && tags.length) headers.set("x-vercel-cache-tags", tags.join(","));
|
|
1970
|
+
return new Response(doc, { status: 200, headers });
|
|
1279
1971
|
}
|
|
1280
1972
|
|
|
1281
1973
|
async function handleApiRoute(req, segments, method, reqStart) {
|
|
@@ -1352,6 +2044,7 @@ const MIME = {
|
|
|
1352
2044
|
};
|
|
1353
2045
|
|
|
1354
2046
|
await checkAppDir();
|
|
2047
|
+
try { validateRoutingConventions(); } catch {}
|
|
1355
2048
|
await detectNetworkUrls();
|
|
1356
2049
|
logStartupBanner(`http://localhost:${port}`, networkUrls);
|
|
1357
2050
|
logLine([` ${paint("dim", "› setupWatcher…")}`]);
|
|
@@ -66,7 +66,12 @@
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
es.addEventListener("open", function () {
|
|
70
|
+
if (window.__sr_setStatus) window.__sr_setStatus(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
69
73
|
es.addEventListener("error", function () {
|
|
74
|
+
if (window.__sr_setStatus) window.__sr_setStatus(false);
|
|
70
75
|
setTimeout(function () {
|
|
71
76
|
if (es.readyState === EventSource.CLOSED) {
|
|
72
77
|
console.warn("[swift-rust] HMR connection lost, retrying…");
|
|
@@ -74,4 +79,101 @@
|
|
|
74
79
|
}
|
|
75
80
|
}, 1000);
|
|
76
81
|
});
|
|
82
|
+
|
|
83
|
+
// ── Dev indicator: a sleek, glassy button at the bottom-left (Next.js-style).
|
|
84
|
+
// Click to toggle a popover with route / status / version info. ─────────
|
|
85
|
+
function mountDevWidget() {
|
|
86
|
+
if (document.getElementById("__sr_devwidget")) return;
|
|
87
|
+
|
|
88
|
+
var root = document.createElement("div");
|
|
89
|
+
root.id = "__sr_devwidget";
|
|
90
|
+
root.setAttribute("data-sr-dev", "");
|
|
91
|
+
root.style.cssText =
|
|
92
|
+
"position:fixed;left:16px;bottom:16px;z-index:2147483646;" +
|
|
93
|
+
"font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;" +
|
|
94
|
+
"color-scheme:dark;";
|
|
95
|
+
|
|
96
|
+
var glass =
|
|
97
|
+
"background:rgba(10,10,10,0.55);-webkit-backdrop-filter:blur(12px) saturate(160%);" +
|
|
98
|
+
"backdrop-filter:blur(12px) saturate(160%);border:1px solid rgba(255,255,255,0.12);";
|
|
99
|
+
|
|
100
|
+
// Popover (hidden by default, opens above the button)
|
|
101
|
+
var pop = document.createElement("div");
|
|
102
|
+
pop.style.cssText =
|
|
103
|
+
glass +
|
|
104
|
+
"position:absolute;left:0;bottom:46px;width:236px;border-radius:14px;padding:12px;" +
|
|
105
|
+
"box-shadow:0 12px 40px rgba(0,0,0,0.5);color:#ededed;font-size:12px;line-height:1.5;" +
|
|
106
|
+
"opacity:0;transform:translateY(6px) scale(.97);pointer-events:none;transform-origin:bottom left;" +
|
|
107
|
+
"transition:opacity .16s ease,transform .16s ease;";
|
|
108
|
+
function row(label, value, color) {
|
|
109
|
+
var r = document.createElement("div");
|
|
110
|
+
r.style.cssText = "display:flex;justify-content:space-between;gap:12px;padding:5px 4px;";
|
|
111
|
+
var l = document.createElement("span");
|
|
112
|
+
l.textContent = label; l.style.cssText = "color:#8f8f8f;";
|
|
113
|
+
var v = document.createElement("span");
|
|
114
|
+
v.textContent = value;
|
|
115
|
+
v.style.cssText = "color:" + (color || "#ededed") + ";font-weight:500;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
|
116
|
+
r.appendChild(l); r.appendChild(v);
|
|
117
|
+
return { el: r, set: function (t, c) { v.textContent = t; if (c) v.style.color = c; } };
|
|
118
|
+
}
|
|
119
|
+
var head = document.createElement("div");
|
|
120
|
+
head.style.cssText = "display:flex;align-items:center;gap:7px;padding:2px 4px 8px;border-bottom:1px solid rgba(255,255,255,0.08);margin-bottom:4px;";
|
|
121
|
+
head.innerHTML =
|
|
122
|
+
'<span style="display:inline-flex;width:18px;height:18px;border-radius:5px;background:#fb923c;align-items:center;justify-content:center;">' +
|
|
123
|
+
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#0a0a0a" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4.7"/><path d="M13.3 6.8 L8.9 12.6 H11.5 L10.7 16.6 L15.1 10.8 H12.5 Z" fill="#0a0a0a" stroke="none"/></svg></span>' +
|
|
124
|
+
'<span style="font-weight:600;color:#fafafa;">Swift Rust</span>' +
|
|
125
|
+
'<span style="margin-left:auto;font-size:10px;color:#8f8f8f;">dev</span>';
|
|
126
|
+
var statusRow = row("Status", "Connected", "#4ade80");
|
|
127
|
+
var routeRow = row("Route", location.pathname);
|
|
128
|
+
var saveRow = row("Hot reload", "On save", "#8f8f8f");
|
|
129
|
+
pop.appendChild(head);
|
|
130
|
+
pop.appendChild(statusRow.el);
|
|
131
|
+
pop.appendChild(routeRow.el);
|
|
132
|
+
pop.appendChild(saveRow.el);
|
|
133
|
+
|
|
134
|
+
// Button (the always-visible trigger)
|
|
135
|
+
var btn = document.createElement("button");
|
|
136
|
+
btn.type = "button";
|
|
137
|
+
btn.setAttribute("aria-label", "Swift Rust dev tools");
|
|
138
|
+
btn.style.cssText =
|
|
139
|
+
glass +
|
|
140
|
+
"display:flex;align-items:center;gap:8px;height:34px;padding:0 11px;border-radius:999px;" +
|
|
141
|
+
"cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,0.35);transition:transform .12s ease,background .2s ease;";
|
|
142
|
+
btn.onmouseenter = function () { btn.style.transform = "translateY(-1px)"; };
|
|
143
|
+
btn.onmouseleave = function () { btn.style.transform = "none"; };
|
|
144
|
+
var dot = document.createElement("span");
|
|
145
|
+
dot.style.cssText = "width:8px;height:8px;border-radius:999px;background:#4ade80;box-shadow:0 0 8px #4ade80;flex-shrink:0;";
|
|
146
|
+
var mark = document.createElement("span");
|
|
147
|
+
mark.style.cssText = "display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;";
|
|
148
|
+
mark.innerHTML =
|
|
149
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fb923c" stroke-width="1.8" stroke-linecap="round">' +
|
|
150
|
+
'<circle cx="12" cy="12" r="4.7"/>' +
|
|
151
|
+
'<path d="M12 3.3v1.9M12 18.8v1.9M3.3 12h1.9M18.8 12h1.9M6 6l1.4 1.4M16.6 16.6l1.4 1.4M6 18l1.4-1.4M16.6 7.4l1.4-1.4"/>' +
|
|
152
|
+
'<path d="M13.3 6.8 L8.9 12.6 H11.5 L10.7 16.6 L15.1 10.8 H12.5 Z" fill="#fb923c" stroke="none"/></svg>';
|
|
153
|
+
btn.appendChild(dot);
|
|
154
|
+
btn.appendChild(mark);
|
|
155
|
+
|
|
156
|
+
root.appendChild(pop);
|
|
157
|
+
root.appendChild(btn);
|
|
158
|
+
document.body.appendChild(root);
|
|
159
|
+
|
|
160
|
+
var openState = false;
|
|
161
|
+
function setOpen(v) {
|
|
162
|
+
openState = v;
|
|
163
|
+
pop.style.opacity = v ? "1" : "0";
|
|
164
|
+
pop.style.transform = v ? "translateY(0) scale(1)" : "translateY(6px) scale(.97)";
|
|
165
|
+
pop.style.pointerEvents = v ? "auto" : "none";
|
|
166
|
+
}
|
|
167
|
+
btn.addEventListener("click", function (e) { e.stopPropagation(); setOpen(!openState); });
|
|
168
|
+
document.addEventListener("click", function () { if (openState) setOpen(false); });
|
|
169
|
+
pop.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
170
|
+
|
|
171
|
+
window.__sr_setStatus = function (ok) {
|
|
172
|
+
dot.style.background = ok ? "#4ade80" : "#f87171";
|
|
173
|
+
dot.style.boxShadow = "0 0 8px " + (ok ? "#4ade80" : "#f87171");
|
|
174
|
+
statusRow.set(ok ? "Connected" : "Disconnected", ok ? "#4ade80" : "#f87171");
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (document.body) mountDevWidget();
|
|
178
|
+
else document.addEventListener("DOMContentLoaded", mountDevWidget);
|
|
77
179
|
})();
|
package/dist/router.d.ts
CHANGED
|
@@ -27,4 +27,107 @@ export declare class RedirectError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
export declare function redirect(location: string, status?: 307 | 308 | 302 | 303): never;
|
|
29
29
|
export declare function permanentRedirect(location: string): never;
|
|
30
|
+
export declare class UnauthorizedError extends Error {
|
|
31
|
+
readonly digest = "UNAUTHORIZED";
|
|
32
|
+
readonly status = 401;
|
|
33
|
+
constructor(message?: string);
|
|
34
|
+
}
|
|
35
|
+
export declare class ForbiddenError extends Error {
|
|
36
|
+
readonly digest = "FORBIDDEN";
|
|
37
|
+
readonly status = 403;
|
|
38
|
+
constructor(message?: string);
|
|
39
|
+
}
|
|
40
|
+
export declare class RewriteError extends Error {
|
|
41
|
+
readonly digest: string;
|
|
42
|
+
readonly to: string;
|
|
43
|
+
constructor(to: string);
|
|
44
|
+
}
|
|
45
|
+
export declare function unauthorized(message?: string): never;
|
|
46
|
+
export declare function forbidden(message?: string): never;
|
|
47
|
+
export declare function rewrite(to: string): never;
|
|
48
|
+
export type Runtime = "node" | "edge" | "worker";
|
|
49
|
+
export interface CookieOptions {
|
|
50
|
+
path?: string;
|
|
51
|
+
domain?: string;
|
|
52
|
+
maxAge?: number;
|
|
53
|
+
expires?: Date;
|
|
54
|
+
httpOnly?: boolean;
|
|
55
|
+
secure?: boolean;
|
|
56
|
+
sameSite?: "strict" | "lax" | "none";
|
|
57
|
+
}
|
|
58
|
+
export interface CookieJar {
|
|
59
|
+
get(name: string): string | undefined;
|
|
60
|
+
set(name: string, value: string, opts?: CookieOptions): void;
|
|
61
|
+
delete(name: string, opts?: Pick<CookieOptions, "path" | "domain">): void;
|
|
62
|
+
all(): ReadonlyMap<string, string>;
|
|
63
|
+
}
|
|
64
|
+
/** App augments this via declaration merging for typed `locals`. */
|
|
65
|
+
export interface RouteLocalsMap {
|
|
66
|
+
}
|
|
67
|
+
export interface RouteLocals {
|
|
68
|
+
get<K extends keyof RouteLocalsMap>(key: K): RouteLocalsMap[K] | undefined;
|
|
69
|
+
set<K extends keyof RouteLocalsMap>(key: K, value: RouteLocalsMap[K]): void;
|
|
70
|
+
}
|
|
71
|
+
export interface RouteRequest<TParams extends Record<string, string> = Record<string, string>, TSearch extends Record<string, unknown> = Record<string, unknown>> {
|
|
72
|
+
readonly url: URL;
|
|
73
|
+
readonly method: string;
|
|
74
|
+
readonly headers: Headers;
|
|
75
|
+
readonly cookies: CookieJar;
|
|
76
|
+
readonly params: TParams;
|
|
77
|
+
readonly searchParams: TSearch;
|
|
78
|
+
readonly runtime: Runtime;
|
|
79
|
+
readonly locals: RouteLocals;
|
|
80
|
+
readonly request: Request;
|
|
81
|
+
}
|
|
82
|
+
export type RouteControl = {
|
|
83
|
+
kind: "next";
|
|
84
|
+
} | {
|
|
85
|
+
kind: "redirect";
|
|
86
|
+
to: string;
|
|
87
|
+
status?: 301 | 302 | 303 | 307 | 308;
|
|
88
|
+
} | {
|
|
89
|
+
kind: "rewrite";
|
|
90
|
+
to: string;
|
|
91
|
+
} | {
|
|
92
|
+
kind: "response";
|
|
93
|
+
response: Response;
|
|
94
|
+
} | {
|
|
95
|
+
kind: "notFound";
|
|
96
|
+
} | {
|
|
97
|
+
kind: "error";
|
|
98
|
+
error: unknown;
|
|
99
|
+
status?: number;
|
|
100
|
+
};
|
|
101
|
+
export type GuardContext<P extends Record<string, string> = Record<string, string>, Q extends Record<string, unknown> = Record<string, unknown>> = RouteRequest<P, Q>;
|
|
102
|
+
export type GuardResult = RouteControl | void;
|
|
103
|
+
export interface LoaderContext<P extends Record<string, string> = Record<string, string>, Q extends Record<string, unknown> = Record<string, unknown>> extends RouteRequest<P, Q> {
|
|
104
|
+
parent: <T = unknown>() => T | undefined;
|
|
105
|
+
}
|
|
106
|
+
export interface ActionContext<P extends Record<string, string> = Record<string, string>, Q extends Record<string, unknown> = Record<string, unknown>> extends RouteRequest<P, Q> {
|
|
107
|
+
formData: () => Promise<FormData>;
|
|
108
|
+
json: <T = unknown>() => Promise<T>;
|
|
109
|
+
}
|
|
110
|
+
export interface RouteConfig {
|
|
111
|
+
runtime?: Runtime;
|
|
112
|
+
rendering?: "static" | "dynamic" | "ssr-stream" | "isr";
|
|
113
|
+
revalidate?: number | false;
|
|
114
|
+
dynamicParams?: boolean;
|
|
115
|
+
cache?: "force-cache" | "no-store" | "default";
|
|
116
|
+
headers?: Record<string, string>;
|
|
117
|
+
maxDuration?: number;
|
|
118
|
+
}
|
|
119
|
+
export type LoaderData<L> = L extends (...a: never[]) => infer R ? Awaited<R> extends Response ? never : Awaited<R> : L;
|
|
120
|
+
export type ActionData<A> = A extends (...a: never[]) => infer R ? Awaited<R> : A;
|
|
121
|
+
export interface RouteRenderContext {
|
|
122
|
+
request: RouteRequest;
|
|
123
|
+
loaderData: unknown;
|
|
124
|
+
actionData: unknown;
|
|
125
|
+
loaders: Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
/** @internal — framework use only. */
|
|
128
|
+
export declare function __setRouteContext(ctx: RouteRenderContext | null): void;
|
|
129
|
+
export declare function useLoaderData<L = unknown>(): LoaderData<L>;
|
|
130
|
+
export declare function useActionData<A = unknown>(): ActionData<A> | undefined;
|
|
131
|
+
export declare function useRouteRequest(): RouteRequest | undefined;
|
|
132
|
+
export declare function useLoaderDataFor<T = unknown>(segment: string): T | undefined;
|
|
30
133
|
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IACnD,MAAM,EAAE,CAAC,CAAC;IACV,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAC7D,MAAM,EAAE,CAAC,CAAC;IACV,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CACrD,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAC5B,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,eAAe;gBAClB,OAAO,SAAc;CAIlC;AAED,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAEhD;AAED,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACd,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAM;CAO3C;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAS,GAAG,KAAK,CAErF;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAEzD"}
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/dist/router.js
CHANGED
|
@@ -26,4 +26,62 @@ export function redirect(location, status = 307) {
|
|
|
26
26
|
export function permanentRedirect(location) {
|
|
27
27
|
throw new RedirectError(location, 308);
|
|
28
28
|
}
|
|
29
|
+
// ── Access-control control flow (used by guard.ts / loader.ts / action.ts) ──
|
|
30
|
+
export class UnauthorizedError extends Error {
|
|
31
|
+
digest = "UNAUTHORIZED";
|
|
32
|
+
status = 401;
|
|
33
|
+
constructor(message = "Unauthorized") {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "UnauthorizedError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class ForbiddenError extends Error {
|
|
39
|
+
digest = "FORBIDDEN";
|
|
40
|
+
status = 403;
|
|
41
|
+
constructor(message = "Forbidden") {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "ForbiddenError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class RewriteError extends Error {
|
|
47
|
+
digest;
|
|
48
|
+
to;
|
|
49
|
+
constructor(to) {
|
|
50
|
+
super(`Rewrite to ${to}`);
|
|
51
|
+
this.name = "RewriteError";
|
|
52
|
+
this.to = to;
|
|
53
|
+
this.digest = `REWRITE;${to}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function unauthorized(message) {
|
|
57
|
+
throw new UnauthorizedError(message);
|
|
58
|
+
}
|
|
59
|
+
export function forbidden(message) {
|
|
60
|
+
throw new ForbiddenError(message);
|
|
61
|
+
}
|
|
62
|
+
export function rewrite(to) {
|
|
63
|
+
throw new RewriteError(to);
|
|
64
|
+
}
|
|
65
|
+
function ctxBox() {
|
|
66
|
+
const g = globalThis;
|
|
67
|
+
if (!g.__SR_ROUTE_CTX__)
|
|
68
|
+
g.__SR_ROUTE_CTX__ = { current: null };
|
|
69
|
+
return g.__SR_ROUTE_CTX__;
|
|
70
|
+
}
|
|
71
|
+
/** @internal — framework use only. */
|
|
72
|
+
export function __setRouteContext(ctx) {
|
|
73
|
+
ctxBox().current = ctx;
|
|
74
|
+
}
|
|
75
|
+
export function useLoaderData() {
|
|
76
|
+
return (ctxBox().current?.loaderData);
|
|
77
|
+
}
|
|
78
|
+
export function useActionData() {
|
|
79
|
+
return (ctxBox().current?.actionData);
|
|
80
|
+
}
|
|
81
|
+
export function useRouteRequest() {
|
|
82
|
+
return ctxBox().current?.request;
|
|
83
|
+
}
|
|
84
|
+
export function useLoaderDataFor(segment) {
|
|
85
|
+
return ctxBox().current?.loaders[segment];
|
|
86
|
+
}
|
|
29
87
|
//# sourceMappingURL=router.js.map
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,GAAG,WAAW,CAAC;IAC9B,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,UAAU,QAAQ,CAAC,OAAgB;IACvC,MAAM,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAS;IACf,MAAM,CAAS;IACf,QAAQ,CAAS;IAC1B,YAAY,QAAgB,EAAE,MAAM,GAAG,GAAG;QACxC,KAAK,CAAC,eAAe,QAAQ,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,YAAY,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,SAAgC,GAAG;IAC5E,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AACzC,CAAC"}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,GAAG,WAAW,CAAC;IAC9B,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,UAAU,QAAQ,CAAC,OAAgB;IACvC,MAAM,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAS;IACf,MAAM,CAAS;IACf,QAAQ,CAAS;IAC1B,YAAY,QAAgB,EAAE,MAAM,GAAG,GAAG;QACxC,KAAK,CAAC,eAAe,QAAQ,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,YAAY,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,SAAgC,GAAG;IAC5E,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AACzC,CAAC;AAED,+EAA+E;AAE/E,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,MAAM,GAAG,cAAc,CAAC;IACxB,MAAM,GAAG,GAAG,CAAC;IACtB,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AACD,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,MAAM,GAAG,WAAW,CAAC;IACrB,MAAM,GAAG,GAAG,CAAC;IACtB,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AACD,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,MAAM,CAAS;IACf,EAAE,CAAS;IACpB,YAAY,EAAU;QACpB,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,GAAG,WAAW,EAAE,EAAE,CAAC;IAChC,CAAC;CACF;AACD,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,MAAM,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC;AACD,MAAM,UAAU,SAAS,CAAC,OAAgB;IACxC,MAAM,IAAI,cAAc,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AACD,MAAM,UAAU,OAAO,CAAC,EAAU;IAChC,MAAM,IAAI,YAAY,CAAC,EAAE,CAAC,CAAC;AAC7B,CAAC;AAmGD,SAAS,MAAM;IACb,MAAM,CAAC,GAAG,UAAsD,CAAC;IACjE,IAAI,CAAC,CAAC,CAAC,gBAAgB;QAAE,CAAC,CAAC,gBAAgB,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAChE,OAAO,CAAC,CAAC,gBAAgB,CAAC;AAC5B,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,iBAAiB,CAAC,GAA8B;IAC9D,MAAM,EAAE,CAAC,OAAO,GAAG,GAAG,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,UAAU,CAAkB,CAAC;AACzD,CAAC;AACD,MAAM,UAAU,aAAa;IAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,UAAU,CAA8B,CAAC;AACrE,CAAC;AACD,MAAM,UAAU,eAAe;IAC7B,OAAO,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;AACnC,CAAC;AACD,MAAM,UAAU,gBAAgB,CAAc,OAAe;IAC3D,OAAO,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAkB,CAAC;AAC7D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swift-rust",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The full-stack React framework powered with Rust + Bun. TSX-first, Rust rendering, 10x faster than Next.js, single binary deploy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://swift-rust.dev",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/colesites/swift-rust.git",
|
|
10
10
|
"directory": "packages/swift-rust"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"module": "./dist/index.js",
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"exports": {
|
|
24
|
+
"./package.json": "./package.json",
|
|
24
25
|
".": {
|
|
25
26
|
"types": "./dist/index.d.ts",
|
|
26
27
|
"import": "./dist/index.js"
|
|
@@ -87,11 +88,11 @@
|
|
|
87
88
|
},
|
|
88
89
|
"dependencies": {
|
|
89
90
|
"@dotenvx/dotenvx": "^1.0.0",
|
|
90
|
-
"@swift-rust/env": "
|
|
91
|
-
"@swift-rust/font": "
|
|
92
|
-
"@swift-rust/image": "
|
|
93
|
-
"@swift-rust/pdf": "
|
|
94
|
-
"@swift-rust/video": "
|
|
91
|
+
"@swift-rust/env": "^0.2.0",
|
|
92
|
+
"@swift-rust/font": "^0.2.0",
|
|
93
|
+
"@swift-rust/image": "^1.0.0",
|
|
94
|
+
"@swift-rust/pdf": "^0.2.0",
|
|
95
|
+
"@swift-rust/video": "^0.2.1",
|
|
95
96
|
"react": "^19.0.0",
|
|
96
97
|
"react-dom": "^19.0.0"
|
|
97
98
|
},
|
|
@@ -122,6 +123,6 @@
|
|
|
122
123
|
"access": "public"
|
|
123
124
|
},
|
|
124
125
|
"bugs": {
|
|
125
|
-
"url": "https://github.com/
|
|
126
|
+
"url": "https://github.com/colesites/swift-rust/issues"
|
|
126
127
|
}
|
|
127
128
|
}
|