bosia 0.9.10 → 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/README.md +1 -1
- package/package.json +4 -1
- package/src/core/build.ts +46 -9
- package/src/core/client/App.svelte +6 -3
- package/src/core/client/page.svelte.ts +2 -22
- package/src/core/dev.ts +2 -2
- package/src/core/html.ts +10 -4
- package/src/core/plugins/inspector/index.ts +5 -14
- package/src/core/plugins/inspector/overlay.ts +15 -6
- package/src/core/plugins/inspector/sourcemap.ts +19 -10
- package/src/core/renderer.ts +6 -17
- package/src/core/routeTypes.ts +3 -1
- package/templates/default/package.json +3 -0
- package/templates/demo/package.json +3 -0
- package/templates/shop/package.json +3 -0
- package/templates/store/package.json +3 -0
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ bun run start
|
|
|
52
52
|
|
|
53
53
|
| Layer | Technology |
|
|
54
54
|
| ----------- | ------------------------------------------ |
|
|
55
|
-
| Runtime | [Bun](https://bun.sh)
|
|
55
|
+
| Runtime | [Bun](https://bun.sh) ≥ 1.4 |
|
|
56
56
|
| HTTP Server | Built-in (`Bun.serve`) |
|
|
57
57
|
| UI | [Svelte 5](https://svelte.dev) (Runes) |
|
|
58
58
|
| CSS | [Tailwind CSS v4](https://tailwindcss.com) |
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"bun": ">=1.4"
|
|
7
|
+
},
|
|
5
8
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun. File-based routing. No Node.js, no Vite. Runs on Bun or Cloudflare Workers.",
|
|
6
9
|
"keywords": [
|
|
7
10
|
"bun",
|
package/src/core/build.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
writeFileSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
rmdirSync,
|
|
10
|
+
} from "fs";
|
|
11
|
+
import { basename, join, relative, resolve } from "path";
|
|
3
12
|
import type { RouteManifest } from "./types.ts";
|
|
4
13
|
|
|
5
14
|
import { scanRoutes, RouteConflictError } from "./scanner.ts";
|
|
@@ -92,9 +101,39 @@ if (process.env.BOSIA_SKIP_ROUTE_SCAN === "1") {
|
|
|
92
101
|
// would clobber a concurrently-running `bosia dev` whose compiled server lives
|
|
93
102
|
// at .bosia/dev/ — the codegen files (routes*.ts, env.*.ts, types/) are the
|
|
94
103
|
// only things this build needs to clear to avoid stale entries on route renames.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
// Windows won't delete a file another process holds open (a running `bosia
|
|
105
|
+
// start`, an editor, antivirus scanning fresh files); warn instead of silently
|
|
106
|
+
// building over stale output. rmSync is only a first try: on Windows, Bun 1.4.2
|
|
107
|
+
// threw ENOENT for an existing "./dist" (and with `force` hid it, deleting
|
|
108
|
+
// nothing). So resolve the path, check what's left, and finish entry by entry —
|
|
109
|
+
// a real failure then names the exact file.
|
|
110
|
+
function clearOutput(path: string): void {
|
|
111
|
+
const abs = resolve(path);
|
|
112
|
+
if (!existsSync(abs)) return;
|
|
113
|
+
try {
|
|
114
|
+
rmSync(abs, { recursive: true, maxRetries: 5, retryDelay: 100 });
|
|
115
|
+
} catch {}
|
|
116
|
+
if (!existsSync(abs)) return;
|
|
117
|
+
try {
|
|
118
|
+
removeTree(abs);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const e = err as NodeJS.ErrnoException;
|
|
121
|
+
console.warn(
|
|
122
|
+
`⚠️ Could not delete ${e.path ?? path} (${e.code ?? String(err)}) — stop whatever is using it and rebuild.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function removeTree(path: string): void {
|
|
127
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
128
|
+
const child = join(path, entry.name);
|
|
129
|
+
if (entry.isDirectory()) removeTree(child);
|
|
130
|
+
else unlinkSync(child);
|
|
131
|
+
}
|
|
132
|
+
rmdirSync(path);
|
|
133
|
+
}
|
|
134
|
+
clearOutput(OUT_DIR);
|
|
135
|
+
// A Bun build must not ship a leftover Workers bundle from an earlier target.
|
|
136
|
+
if (target !== "workers" && existsSync(`${OUT_DIR}/worker`)) clearOutput(`${OUT_DIR}/worker`);
|
|
98
137
|
for (const p of [
|
|
99
138
|
".bosia/routes.ts",
|
|
100
139
|
".bosia/routes.client.ts",
|
|
@@ -104,9 +143,7 @@ for (const p of [
|
|
|
104
143
|
".bosia/runtime.workers.ts",
|
|
105
144
|
".bosia/types",
|
|
106
145
|
]) {
|
|
107
|
-
|
|
108
|
-
rmSync(p, { recursive: true, force: true });
|
|
109
|
-
} catch {}
|
|
146
|
+
clearOutput(p);
|
|
110
147
|
}
|
|
111
148
|
|
|
112
149
|
// 1. Scan routes (or reuse the cached manifest — see 0b-pre)
|
|
@@ -287,7 +324,7 @@ for (const output of clientResult.outputs) {
|
|
|
287
324
|
const rel = toPosix(relative(`${OUT_DIR}/client`, output.path)); // URL path, not fs path
|
|
288
325
|
if (output.path.endsWith(".js")) jsFiles.push(rel);
|
|
289
326
|
if (output.path.endsWith(".css")) cssFiles.push(rel);
|
|
290
|
-
if (
|
|
327
|
+
if (output.kind === "entry-point" && output.path.endsWith(".js")) {
|
|
291
328
|
clientEntry = rel;
|
|
292
329
|
}
|
|
293
330
|
}
|
|
@@ -147,7 +147,10 @@
|
|
|
147
147
|
}
|
|
148
148
|
lastTick = currentTick;
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
// Only a real navigation drops the action result. An invalidation re-run
|
|
151
|
+
// (same URL, new tick) is how `use:enhance` refreshes the page after a
|
|
152
|
+
// successful action — clearing here wiped the `form` it had just set.
|
|
153
|
+
if (navKey !== lastNavKey) appState.form = null;
|
|
151
154
|
if (navDoneTimer) {
|
|
152
155
|
clearTimeout(navDoneTimer);
|
|
153
156
|
navDoneTimer = null;
|
|
@@ -457,7 +460,7 @@
|
|
|
457
460
|
{#if depth > 0 && layoutComponents.length > 0}
|
|
458
461
|
{@render renderLayout(0, depth)}
|
|
459
462
|
{:else}
|
|
460
|
-
<ErrorComponent {...errorProps ?? {}} />
|
|
463
|
+
<ErrorComponent {...errorProps ?? {}} {params} />
|
|
461
464
|
{/if}
|
|
462
465
|
{:else if layoutComponents.length > 0}
|
|
463
466
|
{@render renderLayout(0, layoutComponents.length)}
|
|
@@ -490,7 +493,7 @@
|
|
|
490
493
|
{:else}
|
|
491
494
|
<Layout {data} {params}>
|
|
492
495
|
{#if ErrorComponent}
|
|
493
|
-
<ErrorComponent {...errorProps ?? {}} />
|
|
496
|
+
<ErrorComponent {...errorProps ?? {}} {params} />
|
|
494
497
|
{:else if PageComponent}
|
|
495
498
|
<PageComponent bind:this={pageInstance} data={pageData} {params} form={formData} />
|
|
496
499
|
{:else}
|
|
@@ -6,16 +6,11 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Bosia passes `params` as a prop to `+page.svelte` / `+layout.svelte` (see
|
|
8
8
|
// App.svelte), mirroring the modern SvelteKit `$app/state` direction. Route
|
|
9
|
-
// components should destructure `params` from `$props()`. `page.params`
|
|
10
|
-
//
|
|
11
|
-
// `appState.routeParams`) so legacy code keeps running — it will be removed in
|
|
12
|
-
// 1.0.0.
|
|
9
|
+
// components should destructure `params` from `$props()`. `page.params` was
|
|
10
|
+
// removed in 1.1.0.
|
|
13
11
|
|
|
14
|
-
import { appState } from "./appState.svelte.ts";
|
|
15
12
|
import { router } from "./router.svelte.ts";
|
|
16
13
|
|
|
17
|
-
let paramsWarned = false;
|
|
18
|
-
|
|
19
14
|
class Page {
|
|
20
15
|
// Real on the server too: the renderer seeds `router.currentRoute`/`.origin`
|
|
21
16
|
// from the request immediately before each render (see `renderWithPageContext`
|
|
@@ -29,21 +24,6 @@ class Page {
|
|
|
29
24
|
get pathname() {
|
|
30
25
|
return this.#url.pathname;
|
|
31
26
|
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @deprecated Read route params from `$props()` instead:
|
|
35
|
-
* `let { params } = $props()` in `+page.svelte` / `+layout.svelte`.
|
|
36
|
-
* `page.params` is a temporary fallback and will be removed in 1.0.0.
|
|
37
|
-
*/
|
|
38
|
-
get params() {
|
|
39
|
-
if (process.env.NODE_ENV !== "production" && !paramsWarned) {
|
|
40
|
-
paramsWarned = true;
|
|
41
|
-
console.warn(
|
|
42
|
-
"[bosia] page.params is deprecated — destructure `params` from $props() in your route component. It will be removed in 1.0.0.",
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return appState.routeParams;
|
|
46
|
-
}
|
|
47
27
|
}
|
|
48
28
|
|
|
49
29
|
export const page = new Page();
|
package/src/core/dev.ts
CHANGED
|
@@ -423,8 +423,8 @@ const devServer = Bun.serve({
|
|
|
423
423
|
// the eBPF cgroup filter only permits packets to/from 127.0.0.0/8 + ::1.
|
|
424
424
|
// "localhost" resolves to both ::1 and 127.0.0.1; bun's fetch() picks IPv6
|
|
425
425
|
// first, but the inner Bun.serve() listens IPv4-only, so the v6 connect
|
|
426
|
-
// fails fast and (under bun 1.3) doesn't fall back to v4. Pinning to
|
|
427
|
-
// avoids the failure path entirely.
|
|
426
|
+
// fails fast and (seen under bun 1.3) doesn't fall back to v4. Pinning to
|
|
427
|
+
// v4 avoids the failure path entirely, whatever bun's resolver does.
|
|
428
428
|
target.hostname = "127.0.0.1";
|
|
429
429
|
target.port = String(APP_PORT);
|
|
430
430
|
|
package/src/core/html.ts
CHANGED
|
@@ -38,6 +38,12 @@ const TW_CSS = `${B}/bosia-tw.css`;
|
|
|
38
38
|
const FAVICON = `${B}/favicon.svg`;
|
|
39
39
|
const SSE = `${B}/__bosia/sse`;
|
|
40
40
|
|
|
41
|
+
// The client entry, emitted verbatim — never with a `?v=` buster. Split chunks
|
|
42
|
+
// import it by bare relative URL (Bun 1.4 keeps shared code in the entry), so a
|
|
43
|
+
// query here makes the browser load it twice: two Svelte runtimes, and hydration
|
|
44
|
+
// dies with "reading 'call'". The content-hashed name is already the buster.
|
|
45
|
+
const ENTRY = `${DIST}/${distManifest.entry}`;
|
|
46
|
+
|
|
41
47
|
/**
|
|
42
48
|
* Handed to the client bundle so its router strips the same prefix the server
|
|
43
49
|
* added. Emitted before the module script, and omitted entirely at the origin
|
|
@@ -195,7 +201,7 @@ export function buildHtml(
|
|
|
195
201
|
: "";
|
|
196
202
|
|
|
197
203
|
const scripts = csr
|
|
198
|
-
? `${baseScript(nonce)}${envScript}${dataIslands}${sysScript}\n <script${n} type="module" src="${
|
|
204
|
+
? `${baseScript(nonce)}${envScript}${dataIslands}${sysScript}\n <script${n} type="module" src="${ENTRY}"></script>`
|
|
199
205
|
: isDev
|
|
200
206
|
? `\n <script${n}>!function r(){var e=new EventSource("${SSE}");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`
|
|
201
207
|
: "";
|
|
@@ -264,7 +270,7 @@ export function buildHtmlShellOpen(
|
|
|
264
270
|
`\n ${faviconLine}${twCssLink()}\n` +
|
|
265
271
|
componentCssLinks() +
|
|
266
272
|
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
267
|
-
` <link rel="modulepreload" href="${
|
|
273
|
+
` <link rel="modulepreload" href="${ENTRY}">`
|
|
268
274
|
);
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -276,7 +282,7 @@ export function buildHtmlShellOpen(
|
|
|
276
282
|
` ${twCssLink()}\n` +
|
|
277
283
|
componentCssLinks() +
|
|
278
284
|
` <script${n}>${THEME_INIT_JS}</script>\n` +
|
|
279
|
-
` <link rel="modulepreload" href="${
|
|
285
|
+
` <link rel="modulepreload" href="${ENTRY}">`
|
|
280
286
|
);
|
|
281
287
|
}
|
|
282
288
|
|
|
@@ -401,7 +407,7 @@ export function buildHtmlTail(
|
|
|
401
407
|
if (ssrFlag || depsInject) {
|
|
402
408
|
out += `\n<script${n}>${ssrFlag}${depsInject}</script>`;
|
|
403
409
|
}
|
|
404
|
-
out += `\n<script${n} type="module" src="${
|
|
410
|
+
out += `\n<script${n} type="module" src="${ENTRY}"></script>`;
|
|
405
411
|
} else if (isDev) {
|
|
406
412
|
out += `\n<script${n}>!function r(){var e=new EventSource("${SSE}");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
|
|
407
413
|
}
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { createInspectorBunPlugin } from "./bun-plugin.ts";
|
|
4
4
|
import { getOverlayScript } from "./overlay.ts";
|
|
5
|
-
import { resolveFrame, resolveStack } from "./sourcemap.ts";
|
|
5
|
+
import { parseTopFrame, resolveFrame, resolveStack } from "./sourcemap.ts";
|
|
6
6
|
import type { BosiaPlugin } from "../../types/plugin.ts";
|
|
7
7
|
|
|
8
8
|
export interface InspectorOptions {
|
|
@@ -34,18 +34,6 @@ function buildEditorArgs(editor: string, file: string, line: number, col: number
|
|
|
34
34
|
return ["-g", `${file}:${line}:${col}`];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Parse the top frame out of a stack trace string. Best-effort.
|
|
38
|
-
function parseTopFrame(
|
|
39
|
-
stack: string | undefined,
|
|
40
|
-
): { file: string; line: number; col: number } | null {
|
|
41
|
-
if (!stack) return null;
|
|
42
|
-
const m =
|
|
43
|
-
/\((https?:\/\/[^)]+|\/[^)]+):(\d+):(\d+)\)/.exec(stack) ||
|
|
44
|
-
/at\s+(\S+):(\d+):(\d+)/.exec(stack) ||
|
|
45
|
-
/@(\S+):(\d+):(\d+)/.exec(stack);
|
|
46
|
-
return m ? { file: m[1], line: Number(m[2]), col: Number(m[3]) } : null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
37
|
// Module-scoped state for the error feature. Initialised lazily inside the
|
|
50
38
|
// plugin so importing this module doesn't attach process-wide listeners.
|
|
51
39
|
let processListenersInstalled = false;
|
|
@@ -293,6 +281,9 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
293
281
|
return;
|
|
294
282
|
}
|
|
295
283
|
flushReplay(ctrl);
|
|
284
|
+
// Under the app server's 10s default idle timeout, or Bun drops the
|
|
285
|
+
// stream (ECONNRESET) and EventSource reconnects. Still breaks if
|
|
286
|
+
// IDLE_TIMEOUT < 5; use server.timeout(req, 0) if that ever matters.
|
|
296
287
|
const ping = setInterval(() => {
|
|
297
288
|
try {
|
|
298
289
|
ctrl.enqueue(encode(":ping\n\n"));
|
|
@@ -300,7 +291,7 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
300
291
|
clearInterval(ping);
|
|
301
292
|
sseClients.delete(ctrl);
|
|
302
293
|
}
|
|
303
|
-
},
|
|
294
|
+
}, 5_000);
|
|
304
295
|
|
|
305
296
|
request.signal.addEventListener("abort", () => {
|
|
306
297
|
clearInterval(ping);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { safeJsonStringify } from "../../html.ts";
|
|
2
|
+
import { FRAME_RE } from "./sourcemap.ts";
|
|
2
3
|
|
|
3
4
|
export interface OverlayConfig {
|
|
4
5
|
aiEndpoint?: string;
|
|
@@ -209,12 +210,10 @@ if(ERR_ENABLED){
|
|
|
209
210
|
function uid(){return "c-"+Math.random().toString(36).slice(2)+Date.now().toString(36)}
|
|
210
211
|
function escapeHtml(s){return String(s==null?"":s).replace(/[&<>"']/g,function(c){return{"&":"&","<":"<",">":">","\\"":""","'":"'"}[c]})}
|
|
211
212
|
|
|
213
|
+
var FRAME_RE=new RegExp(${JSON.stringify(FRAME_RE.source)});
|
|
212
214
|
function parseTopFrame(stack){
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
||/at\\s+(\\S+):(\\d+):(\\d+)/.exec(stack)
|
|
216
|
-
||/@(\\S+):(\\d+):(\\d+)/.exec(stack);
|
|
217
|
-
return m?{file:m[1],line:+m[2],col:+m[3]}:null;
|
|
215
|
+
var m=stack?FRAME_RE.exec(stack):null;
|
|
216
|
+
return m?{file:m[2],line:+m[3],col:+m[4]}:null;
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
function trackInteraction(e){
|
|
@@ -255,7 +254,7 @@ if(ERR_ENABLED){
|
|
|
255
254
|
var locStr=er.file?(er.file+(er.line?":"+er.line:"")):er.source;
|
|
256
255
|
rows+='<div data-row="'+er.id+'" style="padding:10px 12px;border-bottom:1px solid #f4f4f5">'
|
|
257
256
|
+'<div style="font:11px ui-monospace,monospace;color:#71717a;margin-bottom:4px">['+escapeHtml(er.source)+'] '+escapeHtml(locStr)+'</div>'
|
|
258
|
-
+'<div style="margin-bottom:6px;font-weight:500;word-break:break-word">'+escapeHtml(er.message)+'</div>'
|
|
257
|
+
+'<div style="margin-bottom:6px;font-weight:500;word-break:break-word">'+escapeHtml(er.message)+(er.count>1?' <span style="color:#71717a;font-weight:400">×'+er.count+'</span>':"")+'</div>'
|
|
259
258
|
+(er.stack?'<details><summary style="cursor:pointer;color:#71717a;font-size:11px">Stack</summary><pre style="margin:6px 0 0;padding:8px;background:#f4f4f5;border-radius:4px;font:11px/1.4 ui-monospace,monospace;white-space:pre-wrap;overflow:auto;max-height:200px">'+escapeHtml(er.stack)+'</pre></details>':"")
|
|
260
259
|
+'<div style="margin-top:8px;display:flex;gap:6px;justify-content:flex-end">'
|
|
261
260
|
+(AI?'<button data-send="'+er.id+'" style="padding:4px 10px;border:0;background:#111;color:#fff;border-radius:4px;cursor:pointer;font-size:12px">Send to AI</button>':"")
|
|
@@ -304,7 +303,17 @@ if(ERR_ENABLED){
|
|
|
304
303
|
var id=e.id||uid();
|
|
305
304
|
if(seenIds[id])return;
|
|
306
305
|
seenIds[id]=true;
|
|
306
|
+
// Same key as the server's dedup (source, message, top frame). Repeats
|
|
307
|
+
// bump a counter on the existing row instead of adding a new one.
|
|
308
|
+
var key=(e.source||"unknown")+"|"+(e.message||"")+"|"+(e.file||"")+":"+(e.line||"");
|
|
309
|
+
for(var i=0;i<errors.length;i++)if(errors[i].key===key){
|
|
310
|
+
errors[i].count++;
|
|
311
|
+
errors[i].ts=e.ts||Date.now();
|
|
312
|
+
renderBadge();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
307
315
|
var entry={
|
|
316
|
+
key:key,count:1,
|
|
308
317
|
id:id,
|
|
309
318
|
ts:e.ts||Date.now(),
|
|
310
319
|
source:e.source||"unknown",
|
|
@@ -123,15 +123,24 @@ function relToCwd(abs: string): string {
|
|
|
123
123
|
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? toPosix(rel) : abs;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
126
|
+
// One stack frame: "(file:L:C)", "at file:L:C", "@file:L:C". The file body is
|
|
127
|
+
// lazy up to the first ":L:C", so ")" in route groups like `(public)` and the
|
|
128
|
+
// port in `http://host:9000/...` stay inside the file. Groups: lead, file, L, C, tail.
|
|
129
|
+
// The browser overlay reuses this source too (see overlay.ts).
|
|
130
|
+
export const FRAME_RE = /(\(|\bat\s+|@)((?:https?:\/\/|\/|[A-Za-z]:[\\/])[^\n]*?):(\d+):(\d+)(\)?)/;
|
|
131
|
+
|
|
132
|
+
// Top frame of a stack trace string. Best-effort.
|
|
133
|
+
export function parseTopFrame(
|
|
134
|
+
stack: string | undefined,
|
|
135
|
+
): { file: string; line: number; col: number } | null {
|
|
136
|
+
const m = stack ? FRAME_RE.exec(stack) : null;
|
|
137
|
+
return m ? { file: m[2], line: Number(m[3]), col: Number(m[4]) } : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Rewrite every frame in a stack string to its original source position.
|
|
129
141
|
export function resolveStack(stack: string): string {
|
|
130
|
-
return stack.replace(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return r ? `${lead}${r.file}:${r.line}:${r.col}${tail}` : _m;
|
|
135
|
-
},
|
|
136
|
-
);
|
|
142
|
+
return stack.replace(new RegExp(FRAME_RE.source, "g"), (_m, lead, file, l, c, tail) => {
|
|
143
|
+
const r = resolveFrame(file, Number(l), Number(c));
|
|
144
|
+
return r ? `${lead}${r.file}:${r.line}:${r.col}${tail}` : _m;
|
|
145
|
+
});
|
|
137
146
|
}
|
package/src/core/renderer.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { HttpError, Redirect, isHttpError, isRedirect } from "./errors.ts";
|
|
|
23
23
|
import { pickErrorPage, type ErrorOrigin } from "./errorMatch.ts";
|
|
24
24
|
import App from "./client/App.svelte";
|
|
25
25
|
import { router } from "./client/router.svelte.ts";
|
|
26
|
-
import { appState } from "./client/appState.svelte.ts";
|
|
27
26
|
import { withBase } from "./basePath.ts";
|
|
28
27
|
import { currentBase } from "./appBase.ts";
|
|
29
28
|
import {
|
|
@@ -92,15 +91,9 @@ const enc = new TextEncoder();
|
|
|
92
91
|
// `render`'s own signature is conditional on the component's prop type, so it
|
|
93
92
|
// collapses to `never` behind a generic wrapper — hence `any` on both, matching
|
|
94
93
|
// how the call sites already type `pageMod.default` / `layoutMods`.
|
|
95
|
-
function renderWithPageContext(
|
|
96
|
-
component: any,
|
|
97
|
-
options: { props?: Record<string, any> },
|
|
98
|
-
url: URL,
|
|
99
|
-
params: Record<string, string> = {},
|
|
100
|
-
) {
|
|
94
|
+
function renderWithPageContext(component: any, options: { props?: Record<string, any> }, url: URL) {
|
|
101
95
|
router.origin = url.origin;
|
|
102
96
|
router.currentRoute = withBase(currentBase(), url.pathname) + url.search + url.hash;
|
|
103
|
-
appState.routeParams = params;
|
|
104
97
|
return render(component, options as any);
|
|
105
98
|
}
|
|
106
99
|
|
|
@@ -805,7 +798,6 @@ export async function renderSSRStream(
|
|
|
805
798
|
},
|
|
806
799
|
},
|
|
807
800
|
url,
|
|
808
|
-
params,
|
|
809
801
|
));
|
|
810
802
|
} catch (err) {
|
|
811
803
|
if (isDev) console.error("SSR render error:", err);
|
|
@@ -1056,7 +1048,6 @@ export async function renderPageWithFormData(
|
|
|
1056
1048
|
},
|
|
1057
1049
|
},
|
|
1058
1050
|
url,
|
|
1059
|
-
params,
|
|
1060
1051
|
);
|
|
1061
1052
|
|
|
1062
1053
|
const html = buildHtml(
|
|
@@ -1117,10 +1108,9 @@ export async function renderErrorPage(
|
|
|
1117
1108
|
};
|
|
1118
1109
|
const bodyEndExtras = await pluginRenderFragments("bodyEnd", renderCtx);
|
|
1119
1110
|
|
|
1120
|
-
// The failing route's own params, so
|
|
1121
|
-
// /blog/[slug]
|
|
1122
|
-
//
|
|
1123
|
-
// request's values.
|
|
1111
|
+
// The failing route's own params, so +error.svelte (and the layouts wrapping
|
|
1112
|
+
// a nested one) under /blog/[slug] get `params.slug`. A 404 matches nothing
|
|
1113
|
+
// and yields {}.
|
|
1124
1114
|
const errorParams = findMatch(serverRoutes, url.pathname)?.params ?? {};
|
|
1125
1115
|
|
|
1126
1116
|
// 1. Nested boundary
|
|
@@ -1150,10 +1140,10 @@ export async function renderErrorPage(
|
|
|
1150
1140
|
ssrErrorComponent: errorMod.default,
|
|
1151
1141
|
ssrErrorProps: { error: { status, message } },
|
|
1152
1142
|
ssrErrorDepth: K,
|
|
1143
|
+
ssrPageData: { params: errorParams },
|
|
1153
1144
|
},
|
|
1154
1145
|
},
|
|
1155
1146
|
url,
|
|
1156
|
-
errorParams,
|
|
1157
1147
|
);
|
|
1158
1148
|
// csr=false: no client hydration on the error page itself.
|
|
1159
1149
|
const html = buildHtml(
|
|
@@ -1191,9 +1181,8 @@ export async function renderErrorPage(
|
|
|
1191
1181
|
// expects `error` as a direct prop: `let { error } = $props()`.
|
|
1192
1182
|
const { body, head } = renderWithPageContext(
|
|
1193
1183
|
mod.default,
|
|
1194
|
-
{ props: { error: { status, message } } },
|
|
1184
|
+
{ props: { error: { status, message }, params: errorParams } },
|
|
1195
1185
|
url,
|
|
1196
|
-
errorParams,
|
|
1197
1186
|
);
|
|
1198
1187
|
const html = buildHtml(
|
|
1199
1188
|
body,
|
package/src/core/routeTypes.ts
CHANGED
|
@@ -123,7 +123,9 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
123
123
|
if (info.hasErrorPage) {
|
|
124
124
|
lines.push(``);
|
|
125
125
|
lines.push(`export type PageError = { status: number; message: string };`);
|
|
126
|
-
lines.push(
|
|
126
|
+
lines.push(
|
|
127
|
+
`export type ErrorProps = { error: PageError; params: Params & Record<string, string | undefined> };`,
|
|
128
|
+
);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
// ActionData — union of all action return types, unwrapping ActionFailure
|