bosia 0.5.7 → 0.5.9
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/package.json +1 -1
- package/src/core/appHtml.ts +106 -0
- package/src/core/build.ts +28 -1
- package/src/core/html.ts +70 -4
- package/src/core/plugins/inspector/bun-plugin.ts +51 -8
- package/src/core/plugins/inspector/sourcemap.ts +61 -1
- package/src/core/renderer.ts +19 -4
- package/src/core/svelteCompiler.ts +43 -1
- package/templates/default/src/app.html +11 -0
- package/templates/demo/src/app.html +11 -0
- package/templates/todo/src/app.html +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
// ─── Types ────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export type AppHtmlSegments = {
|
|
7
|
+
headOpen: string;
|
|
8
|
+
headClose: string;
|
|
9
|
+
tail: string;
|
|
10
|
+
hasCustomFavicon: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// ─── Cached Singleton ─────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
let cachedSegments: AppHtmlSegments | undefined;
|
|
16
|
+
|
|
17
|
+
// ─── Parse & Validate ─────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function loadAppHtmlTemplate(cwd: string = process.cwd()): AppHtmlSegments {
|
|
20
|
+
const templatePath = join(cwd, "src", "app.html");
|
|
21
|
+
|
|
22
|
+
if (!existsSync(templatePath)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`src/app.html is required but not found. Create src/app.html with %bosia.head% and %bosia.body% placeholders.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
29
|
+
|
|
30
|
+
// Replace static placeholders at parse time
|
|
31
|
+
template = replaceStaticPlaceholders(template);
|
|
32
|
+
|
|
33
|
+
// Validate required placeholders
|
|
34
|
+
const missingPlaceholders: string[] = [];
|
|
35
|
+
if (!template.includes("%bosia.head%")) missingPlaceholders.push("%bosia.head%");
|
|
36
|
+
if (!template.includes("%bosia.body%")) missingPlaceholders.push("%bosia.body%");
|
|
37
|
+
|
|
38
|
+
if (missingPlaceholders.length > 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`src/app.html is missing required placeholder(s): ${missingPlaceholders.join(", ")}. ` +
|
|
41
|
+
`Both %bosia.head% and %bosia.body% must be present.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Split into segments
|
|
46
|
+
const [headOpen, rest1] = template.split("%bosia.head%", 2);
|
|
47
|
+
const [headClose, tail] = rest1.split("%bosia.body%", 2);
|
|
48
|
+
|
|
49
|
+
// Check for custom favicon
|
|
50
|
+
const hasCustomFavicon = headOpen.includes('rel="icon"');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
headOpen,
|
|
54
|
+
headClose,
|
|
55
|
+
tail,
|
|
56
|
+
hasCustomFavicon,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function replaceStaticPlaceholders(template: string): string {
|
|
61
|
+
// Replace %bosia.assets% with BOSIA_ASSETS_BASE env var
|
|
62
|
+
const assetsBase = process.env.BOSIA_ASSETS_BASE || "";
|
|
63
|
+
template = template.replaceAll("%bosia.assets%", assetsBase);
|
|
64
|
+
|
|
65
|
+
// Replace %bosia.env.PUBLIC_FOO% with process.env.PUBLIC_FOO
|
|
66
|
+
template = template.replace(/%bosia\.env\.(PUBLIC_[A-Z0-9_]+)%/g, (_match, key: string) => {
|
|
67
|
+
return process.env[key] ?? "";
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return template;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Cached Getter ────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function getAppHtmlSegments(cwd: string = process.cwd()): AppHtmlSegments {
|
|
76
|
+
if (cachedSegments !== undefined) {
|
|
77
|
+
return cachedSegments;
|
|
78
|
+
}
|
|
79
|
+
cachedSegments = loadAppHtmlTemplate(cwd);
|
|
80
|
+
return cachedSegments;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Cache Invalidation ───────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export function invalidateAppHtmlCache(): void {
|
|
86
|
+
cachedSegments = undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Runtime Interpolation ────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export function interpolateSegment(
|
|
92
|
+
segment: string,
|
|
93
|
+
vars: { nonce?: string; lang?: string },
|
|
94
|
+
): string {
|
|
95
|
+
let result = segment;
|
|
96
|
+
|
|
97
|
+
if (vars.lang) {
|
|
98
|
+
result = result.replaceAll("%bosia.lang%", vars.lang);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (vars.nonce) {
|
|
102
|
+
result = result.replaceAll("%bosia.nonce%", vars.nonce);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
package/src/core/build.ts
CHANGED
|
@@ -5,13 +5,14 @@ import { scanRoutes } from "./scanner.ts";
|
|
|
5
5
|
import { generateRoutesFile } from "./routeFile.ts";
|
|
6
6
|
import { generateRouteTypes, ensureRootDirs } from "./routeTypes.ts";
|
|
7
7
|
import { makeBosiaPlugin } from "./plugin.ts";
|
|
8
|
-
import { makeBosiaSvelteCompiler } from "./svelteCompiler.ts";
|
|
8
|
+
import { makeBosiaSvelteCompiler, svelteMapCache } from "./svelteCompiler.ts";
|
|
9
9
|
import { prerenderStaticRoutes, generateStaticSite } from "./prerender.ts";
|
|
10
10
|
import { loadEnv, classifyEnvVars } from "./env.ts";
|
|
11
11
|
import { generateEnvModules } from "./envCodegen.ts";
|
|
12
12
|
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin } from "./paths.ts";
|
|
13
13
|
import { loadPlugins } from "./config.ts";
|
|
14
14
|
import type { BuildContext } from "./types/plugin.ts";
|
|
15
|
+
import { loadAppHtmlTemplate } from "./appHtml.ts";
|
|
15
16
|
|
|
16
17
|
// Resolved from this file's location inside the bosia package
|
|
17
18
|
const CORE_DIR = import.meta.dir;
|
|
@@ -79,6 +80,20 @@ if (manifest.apis.length > 0) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// 1b. Load & validate src/app.html template (required)
|
|
84
|
+
let appHtml: any;
|
|
85
|
+
try {
|
|
86
|
+
appHtml = loadAppHtmlTemplate(process.cwd());
|
|
87
|
+
console.log(
|
|
88
|
+
"📄 Loaded src/app.html (favicon override: " +
|
|
89
|
+
(appHtml.hasCustomFavicon ? "yes" : "no") +
|
|
90
|
+
")",
|
|
91
|
+
);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`❌ src/app.html validation failed:\n${(err as Error).message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
for (const p of userPlugins) {
|
|
83
98
|
if (p.build?.postScan) {
|
|
84
99
|
await p.build.postScan(manifest, buildCtx);
|
|
@@ -187,6 +202,18 @@ if (!serverResult.success) {
|
|
|
187
202
|
process.exit(1);
|
|
188
203
|
}
|
|
189
204
|
|
|
205
|
+
// Persist the per-file Svelte compile maps so the inspector's runtime stack
|
|
206
|
+
// resolver can chain bundle-map → svelte-map to land on original source. We
|
|
207
|
+
// cannot chain at bundle time because Bun's bundle maps reference intermediate
|
|
208
|
+
// JS positions that are mostly absent from Svelte's sparse compile map —
|
|
209
|
+
// post-build remapping nukes mappings. Instead we keep both maps separate and
|
|
210
|
+
// do a two-stage lookup with `bias` interpolation in the resolver.
|
|
211
|
+
if (!isProduction && svelteMapCache.size > 0) {
|
|
212
|
+
const entries: Record<string, unknown> = {};
|
|
213
|
+
for (const [k, v] of svelteMapCache) entries[k] = v;
|
|
214
|
+
writeFileSync(`${OUT_DIR}/svelte-maps.json`, JSON.stringify(entries));
|
|
215
|
+
}
|
|
216
|
+
|
|
190
217
|
// 6. Collect output files for dist/manifest.json
|
|
191
218
|
const jsFiles: string[] = [];
|
|
192
219
|
const cssFiles: string[] = [];
|
package/src/core/html.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from "fs";
|
|
|
2
2
|
import { getDeclaredEnvKeys } from "./env.ts";
|
|
3
3
|
import { nonceAttr } from "./csp.ts";
|
|
4
4
|
import { OUT_DIR } from "./paths.ts";
|
|
5
|
+
import type { AppHtmlSegments } from "./appHtml.ts";
|
|
6
|
+
import { interpolateSegment } from "./appHtml.ts";
|
|
5
7
|
|
|
6
8
|
// ─── Dist Manifest ───────────────────────────────────────
|
|
7
9
|
// Maps hashed filenames → script/link tags.
|
|
@@ -98,6 +100,7 @@ export function buildHtml(
|
|
|
98
100
|
pageDeps: any = null,
|
|
99
101
|
layoutDeps: any[] | null = null,
|
|
100
102
|
bodyEndExtras?: string[],
|
|
103
|
+
segments?: AppHtmlSegments,
|
|
101
104
|
): string {
|
|
102
105
|
const cssLinks = (distManifest.css ?? [])
|
|
103
106
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
@@ -138,6 +141,31 @@ export function buildHtml(
|
|
|
138
141
|
|
|
139
142
|
const bodyEnd = bodyEndExtras?.length ? "\n " + bodyEndExtras.join("\n ") : "";
|
|
140
143
|
|
|
144
|
+
if (segments) {
|
|
145
|
+
const safeKey = safeLang(lang);
|
|
146
|
+
const headOpenInterpolated = interpolateSegment(segments.headOpen, {
|
|
147
|
+
lang: safeKey,
|
|
148
|
+
nonce,
|
|
149
|
+
});
|
|
150
|
+
const headCloseInterpolated = interpolateSegment(segments.headClose, { nonce });
|
|
151
|
+
const tailInterpolated = interpolateSegment(segments.tail, { nonce });
|
|
152
|
+
const faviconLine = segments.hasCustomFavicon
|
|
153
|
+
? ""
|
|
154
|
+
: ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n`;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
headOpenInterpolated +
|
|
158
|
+
`\n ${faviconLine}${cssLinks}\n` +
|
|
159
|
+
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
160
|
+
` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
161
|
+
` ${fallbackTitle}${head}` +
|
|
162
|
+
headCloseInterpolated +
|
|
163
|
+
`\n${SPINNER}` +
|
|
164
|
+
`\n <div id="app">${body}</div>${scripts}${bodyEnd}` +
|
|
165
|
+
tailInterpolated
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
return `<!DOCTYPE html>
|
|
142
170
|
<html lang="${safeLang(lang)}">
|
|
143
171
|
<head>
|
|
@@ -161,12 +189,31 @@ export function buildHtml(
|
|
|
161
189
|
import type { Metadata } from "./hooks.ts";
|
|
162
190
|
|
|
163
191
|
/** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
|
|
164
|
-
export function buildHtmlShellOpen(
|
|
192
|
+
export function buildHtmlShellOpen(
|
|
193
|
+
lang?: string,
|
|
194
|
+
nonce?: string,
|
|
195
|
+
segments?: AppHtmlSegments,
|
|
196
|
+
): string {
|
|
165
197
|
const key = safeLang(lang);
|
|
166
198
|
const n = nonceAttr(nonce);
|
|
167
199
|
const cssLinks = (distManifest.css ?? [])
|
|
168
200
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
169
201
|
.join("\n ");
|
|
202
|
+
|
|
203
|
+
if (segments) {
|
|
204
|
+
const headOpenInterpolated = interpolateSegment(segments.headOpen, { lang: key, nonce });
|
|
205
|
+
const faviconLine = segments.hasCustomFavicon
|
|
206
|
+
? ""
|
|
207
|
+
: ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n`;
|
|
208
|
+
return (
|
|
209
|
+
headOpenInterpolated +
|
|
210
|
+
`\n ${faviconLine}${cssLinks}\n` +
|
|
211
|
+
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
212
|
+
` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
213
|
+
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
170
217
|
return (
|
|
171
218
|
`<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
|
|
172
219
|
` <meta charset="UTF-8">\n` +
|
|
@@ -188,7 +235,11 @@ const SPINNER =
|
|
|
188
235
|
`@keyframes __bs__{to{transform:rotate(360deg)}}</style><i></i></div>`;
|
|
189
236
|
|
|
190
237
|
/** Chunk 2: metadata tags + close </head> + open <body> + spinner */
|
|
191
|
-
export function buildMetadataChunk(
|
|
238
|
+
export function buildMetadataChunk(
|
|
239
|
+
metadata: Metadata | null,
|
|
240
|
+
headExtras?: string[],
|
|
241
|
+
segments?: AppHtmlSegments,
|
|
242
|
+
): string {
|
|
192
243
|
let out = "\n";
|
|
193
244
|
if (metadata) {
|
|
194
245
|
if (metadata.title) out += ` <title>${escapeHtml(metadata.title)}</title>\n`;
|
|
@@ -218,7 +269,14 @@ export function buildMetadataChunk(metadata: Metadata | null, headExtras?: strin
|
|
|
218
269
|
if (fragment) out += ` ${fragment}\n`;
|
|
219
270
|
}
|
|
220
271
|
}
|
|
221
|
-
|
|
272
|
+
|
|
273
|
+
if (segments) {
|
|
274
|
+
const headCloseInterpolated = interpolateSegment(segments.headClose, {});
|
|
275
|
+
out += headCloseInterpolated + `\n${SPINNER}`;
|
|
276
|
+
} else {
|
|
277
|
+
out += `</head>\n<body>\n${SPINNER}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
222
280
|
return out;
|
|
223
281
|
}
|
|
224
282
|
|
|
@@ -246,6 +304,7 @@ export function buildHtmlTail(
|
|
|
246
304
|
nonce?: string,
|
|
247
305
|
pageDeps: any = null,
|
|
248
306
|
layoutDeps: any[] | null = null,
|
|
307
|
+
segments?: AppHtmlSegments,
|
|
249
308
|
): string {
|
|
250
309
|
const n = nonceAttr(nonce);
|
|
251
310
|
let out = `<script${n}>document.getElementById('__bs__').remove()</script>`;
|
|
@@ -279,7 +338,14 @@ export function buildHtmlTail(
|
|
|
279
338
|
if (fragment) out += `\n${fragment}`;
|
|
280
339
|
}
|
|
281
340
|
}
|
|
282
|
-
|
|
341
|
+
|
|
342
|
+
if (segments) {
|
|
343
|
+
const tailInterpolated = interpolateSegment(segments.tail, { nonce });
|
|
344
|
+
out += `\n${tailInterpolated}`;
|
|
345
|
+
} else {
|
|
346
|
+
out += `\n</body>\n</html>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
283
349
|
return out;
|
|
284
350
|
}
|
|
285
351
|
|
|
@@ -2,6 +2,7 @@ import { parse, compile } from "svelte/compiler";
|
|
|
2
2
|
import MagicString from "magic-string";
|
|
3
3
|
import { basename, relative } from "node:path";
|
|
4
4
|
import type { BunPlugin } from "bun";
|
|
5
|
+
import { svelteMapCache } from "../../svelteCompiler.ts";
|
|
5
6
|
|
|
6
7
|
const VIRTUAL_NS = "bosia-inspector-css";
|
|
7
8
|
|
|
@@ -87,6 +88,21 @@ export interface InspectorBunPluginOptions {
|
|
|
87
88
|
dev: boolean;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// Svelte 5 dev compile emits named `function get()` / `function set($$value)`
|
|
92
|
+
// expressions inside `$.bind_*` calls (for nicer `$inspect` stack traces). Bun's
|
|
93
|
+
// bundler destructures `import * as $ from "svelte/internal/client"` into named
|
|
94
|
+
// imports, so `$.get(search)` becomes plain `get(search)` — which collides with
|
|
95
|
+
// the wrapping function name and recurses into itself → RangeError. Prod compile
|
|
96
|
+
// uses anonymous arrow functions and is unaffected.
|
|
97
|
+
//
|
|
98
|
+
// Rename to `$$g` / `$$s` (3 chars — length-preserving so cached svelte source
|
|
99
|
+
// map columns stay accurate). These names aren't present in svelte/internal/client.
|
|
100
|
+
function fixBindShadow(code: string): string {
|
|
101
|
+
return code
|
|
102
|
+
.replace(/\bfunction get\(\)/g, () => "function $$g()")
|
|
103
|
+
.replace(/\bfunction set\(\$\$value\)/g, () => "function $$s($$value)");
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
const fnv = (s: string): string => {
|
|
91
107
|
let h = 2166136261;
|
|
92
108
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -120,13 +136,37 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
120
136
|
cssHash: ({ css }) => `svelte-${fnv(css)}`,
|
|
121
137
|
});
|
|
122
138
|
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
139
|
+
// Store Svelte's compile-step map. Bun's bundler doesn't chain
|
|
140
|
+
// onLoad sourcemaps, so the final bundle map resolves stack frames
|
|
141
|
+
// to the post-svelte-compile JS intermediate (`$.next()` /
|
|
142
|
+
// `$.append()` calls) using the .svelte filename — line numbers
|
|
143
|
+
// land past EOF. The inspector's runtime resolver chases bundle
|
|
144
|
+
// map → this svelte map (with bias-interpolated lookup) to refine
|
|
145
|
+
// to original source. `injectLocs` only shifts column offsets
|
|
146
|
+
// inside HTML open tags, never script-block line numbers, so we
|
|
147
|
+
// accept the small column drift to skip a second map chain.
|
|
148
|
+
//
|
|
149
|
+
// Only cache for the client target — server (Bun) and client builds
|
|
150
|
+
// share `svelteMapCache` keyed by abs path, but their compile output
|
|
151
|
+
// line numbers differ. The resolver translates browser-side stack
|
|
152
|
+
// frames (delivered via SSE), which run client code.
|
|
153
|
+
if (dev && generate === "client" && result.js.map) {
|
|
154
|
+
const m =
|
|
155
|
+
typeof result.js.map === "string"
|
|
156
|
+
? JSON.parse(result.js.map)
|
|
157
|
+
: result.js.map;
|
|
158
|
+
svelteMapCache.set(args.path, m);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let js = dev ? fixBindShadow(result.js.code) : result.js.code;
|
|
162
|
+
// Inject component <style> blocks via runtime JS, not CSS chunks.
|
|
163
|
+
// Bun's `splitting: true` names CSS chunks after the importing JS
|
|
164
|
+
// chunk's `[name]`, not the virtual module's uid — so when several
|
|
165
|
+
// routes (e.g. multiple `+page.svelte`) transitively import the same
|
|
166
|
+
// styled component, each route emits its own CSS sidecar named
|
|
167
|
+
// `+page-<contentHash>.css`. Identical content → identical hash →
|
|
168
|
+
// "Multiple files share the same output path". Runtime injection
|
|
169
|
+
// avoids CSS chunking entirely.
|
|
130
170
|
if (result.css?.code?.trim() && generate !== "server") {
|
|
131
171
|
const safeBase = basename(args.path).replace(/\./g, "_");
|
|
132
172
|
const uid = `${safeBase}-${fnv(args.path)}-style.css`;
|
|
@@ -146,7 +186,10 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
146
186
|
build.onLoad({ filter: /.*/, namespace: VIRTUAL_NS }, (args) => {
|
|
147
187
|
const css = virtualCss.get(args.path) ?? "";
|
|
148
188
|
virtualCss.delete(args.path);
|
|
149
|
-
|
|
189
|
+
const contents = css
|
|
190
|
+
? `(()=>{const s=document.createElement('style');s.textContent=${JSON.stringify(css)};document.head.appendChild(s);})();`
|
|
191
|
+
: "";
|
|
192
|
+
return { contents, loader: "js" };
|
|
150
193
|
});
|
|
151
194
|
},
|
|
152
195
|
};
|
|
@@ -1,10 +1,37 @@
|
|
|
1
|
-
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
1
|
+
import { TraceMap, originalPositionFor, GREATEST_LOWER_BOUND } from "@jridgewell/trace-mapping";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve as pathResolve } from "node:path";
|
|
4
4
|
import { OUT_DIR } from "../../paths.ts";
|
|
5
5
|
|
|
6
6
|
const cache = new Map<string, TraceMap | null>();
|
|
7
7
|
|
|
8
|
+
// Per-`.svelte` (absolute path) compile maps written by the build step. The
|
|
9
|
+
// bundle map only resolves a stack frame to the post-svelte-compile JS
|
|
10
|
+
// position labeled with the .svelte filename; a second lookup against this
|
|
11
|
+
// map (with `bias: GREATEST_LOWER_BOUND` to interpolate sparse mappings)
|
|
12
|
+
// translates that intermediate position to original source line/col.
|
|
13
|
+
let svelteMaps: Map<string, TraceMap> | null = null;
|
|
14
|
+
let svelteMapsLoaded = false;
|
|
15
|
+
|
|
16
|
+
function loadSvelteMaps(): Map<string, TraceMap> | null {
|
|
17
|
+
if (svelteMapsLoaded) return svelteMaps;
|
|
18
|
+
svelteMapsLoaded = true;
|
|
19
|
+
try {
|
|
20
|
+
const p = pathResolve(process.cwd(), OUT_DIR, "svelte-maps.json");
|
|
21
|
+
if (!existsSync(p)) return null;
|
|
22
|
+
const raw = JSON.parse(readFileSync(p, "utf8")) as Record<string, unknown>;
|
|
23
|
+
svelteMaps = new Map();
|
|
24
|
+
for (const [absPath, m] of Object.entries(raw)) {
|
|
25
|
+
try {
|
|
26
|
+
svelteMaps.set(absPath, new TraceMap(m as never));
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
return svelteMaps;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
function loadMap(mapPath: string): TraceMap | null {
|
|
9
36
|
if (cache.has(mapPath)) return cache.get(mapPath)!;
|
|
10
37
|
try {
|
|
@@ -57,6 +84,39 @@ export function resolveFrame(
|
|
|
57
84
|
const pos = originalPositionFor(tm, { line, column: col });
|
|
58
85
|
if (!pos.source || pos.line == null) return null;
|
|
59
86
|
const abs = pathResolve(dirname(mp), pos.source);
|
|
87
|
+
|
|
88
|
+
// Bundle map points at the post-svelte-compile JS position labeled with the
|
|
89
|
+
// .svelte filename. Refine by chasing through the cached svelte compile map
|
|
90
|
+
// to the real source position. Svelte's map is sparse — a given line may
|
|
91
|
+
// only carry mappings starting at some column. Try the exact column first,
|
|
92
|
+
// then fall back to the rightmost mapping on the same line, so we never lose
|
|
93
|
+
// a frame just because the bundle's reported column lands in a gap.
|
|
94
|
+
if (abs.endsWith(".svelte") || abs.endsWith(".svelte.ts") || abs.endsWith(".svelte.js")) {
|
|
95
|
+
const maps = loadSvelteMaps();
|
|
96
|
+
const svelteMap = maps?.get(abs);
|
|
97
|
+
if (svelteMap) {
|
|
98
|
+
let refined = originalPositionFor(svelteMap, {
|
|
99
|
+
line: pos.line,
|
|
100
|
+
column: pos.column ?? 0,
|
|
101
|
+
bias: GREATEST_LOWER_BOUND,
|
|
102
|
+
});
|
|
103
|
+
if (!refined.source) {
|
|
104
|
+
refined = originalPositionFor(svelteMap, {
|
|
105
|
+
line: pos.line,
|
|
106
|
+
column: Number.MAX_SAFE_INTEGER,
|
|
107
|
+
bias: GREATEST_LOWER_BOUND,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (refined.source && refined.line != null) {
|
|
111
|
+
const refinedAbs = pathResolve(dirname(abs), refined.source);
|
|
112
|
+
const rel = refinedAbs.startsWith(process.cwd() + "/")
|
|
113
|
+
? refinedAbs.slice(process.cwd().length + 1)
|
|
114
|
+
: refinedAbs;
|
|
115
|
+
return { file: rel, line: refined.line, col: refined.column ?? 1 };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
60
120
|
const rel = abs.startsWith(process.cwd() + "/") ? abs.slice(process.cwd().length + 1) : abs;
|
|
61
121
|
return { file: rel, line: pos.line, col: pos.column ?? 1 };
|
|
62
122
|
}
|
package/src/core/renderer.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type { Metadata } from "./hooks.ts";
|
|
|
20
20
|
import { loadPlugins } from "./config.ts";
|
|
21
21
|
import { reportDevErrorFromCatch } from "./devErrorReport.ts";
|
|
22
22
|
import type { BosiaPlugin, RenderContext } from "./types/plugin.ts";
|
|
23
|
+
import { getAppHtmlSegments } from "./appHtml.ts";
|
|
24
|
+
import type { AppHtmlSegments } from "./appHtml.ts";
|
|
23
25
|
|
|
24
26
|
// Plugins are loaded once per process at module init via top-level await elsewhere
|
|
25
27
|
// (server.ts), but renderer is also reachable from build/prerender contexts where
|
|
@@ -99,6 +101,11 @@ if (INTERNAL_HOSTS.size > 0) {
|
|
|
99
101
|
console.log(`🍪 Internal hosts (cookies forwarded): ${[...INTERNAL_HOSTS].join(", ")}`);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
// ─── App HTML Template ───────────────────────────────────
|
|
105
|
+
// Required; loaded once per process; cached singleton with invalidation for HMR
|
|
106
|
+
|
|
107
|
+
const appHtmlSegments: AppHtmlSegments = getAppHtmlSegments();
|
|
108
|
+
|
|
102
109
|
// ─── Session-Aware Fetch ─────────────────────────────────
|
|
103
110
|
// Passed to load() functions so they can call internal APIs with the
|
|
104
111
|
// current user's cookies automatically forwarded. Cookie is attached
|
|
@@ -607,8 +614,8 @@ export async function renderSSRStream(
|
|
|
607
614
|
);
|
|
608
615
|
}
|
|
609
616
|
const html =
|
|
610
|
-
buildHtmlShellOpen(metadata?.lang, nonce) +
|
|
611
|
-
buildMetadataChunk(metadata, headExtras) +
|
|
617
|
+
buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments) +
|
|
618
|
+
buildMetadataChunk(metadata, headExtras, appHtmlSegments) +
|
|
612
619
|
buildHtmlTail(
|
|
613
620
|
"",
|
|
614
621
|
"",
|
|
@@ -621,6 +628,7 @@ export async function renderSSRStream(
|
|
|
621
628
|
nonce,
|
|
622
629
|
data.pageDeps,
|
|
623
630
|
data.layoutDeps,
|
|
631
|
+
appHtmlSegments,
|
|
624
632
|
);
|
|
625
633
|
return new Response(html, {
|
|
626
634
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
@@ -659,8 +667,8 @@ export async function renderSSRStream(
|
|
|
659
667
|
|
|
660
668
|
// Pre-compute all chunks; pull-based stream gives Bun native backpressure.
|
|
661
669
|
const chunks: Uint8Array[] = [
|
|
662
|
-
enc.encode(buildHtmlShellOpen(metadata?.lang, nonce)),
|
|
663
|
-
enc.encode(buildMetadataChunk(metadata, headExtras)),
|
|
670
|
+
enc.encode(buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments)),
|
|
671
|
+
enc.encode(buildMetadataChunk(metadata, headExtras, appHtmlSegments)),
|
|
664
672
|
enc.encode(
|
|
665
673
|
buildHtmlTail(
|
|
666
674
|
body,
|
|
@@ -674,6 +682,7 @@ export async function renderSSRStream(
|
|
|
674
682
|
nonce,
|
|
675
683
|
data.pageDeps,
|
|
676
684
|
data.layoutDeps,
|
|
685
|
+
appHtmlSegments,
|
|
677
686
|
),
|
|
678
687
|
),
|
|
679
688
|
];
|
|
@@ -781,6 +790,8 @@ export async function renderPageWithFormData(
|
|
|
781
790
|
nonce,
|
|
782
791
|
data.pageDeps,
|
|
783
792
|
data.layoutDeps,
|
|
793
|
+
undefined,
|
|
794
|
+
appHtmlSegments,
|
|
784
795
|
);
|
|
785
796
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
786
797
|
}
|
|
@@ -808,6 +819,8 @@ export async function renderPageWithFormData(
|
|
|
808
819
|
nonce,
|
|
809
820
|
data.pageDeps,
|
|
810
821
|
data.layoutDeps,
|
|
822
|
+
undefined,
|
|
823
|
+
appHtmlSegments,
|
|
811
824
|
);
|
|
812
825
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
813
826
|
}
|
|
@@ -887,6 +900,7 @@ export async function renderErrorPage(
|
|
|
887
900
|
null,
|
|
888
901
|
null,
|
|
889
902
|
bodyEndExtras,
|
|
903
|
+
appHtmlSegments,
|
|
890
904
|
);
|
|
891
905
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
892
906
|
} catch (err) {
|
|
@@ -924,6 +938,7 @@ export async function renderErrorPage(
|
|
|
924
938
|
null,
|
|
925
939
|
null,
|
|
926
940
|
bodyEndExtras,
|
|
941
|
+
appHtmlSegments,
|
|
927
942
|
);
|
|
928
943
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
929
944
|
} catch (err) {
|
|
@@ -3,6 +3,29 @@ import type { BunPlugin } from "bun";
|
|
|
3
3
|
|
|
4
4
|
const svelteHash = (s: string) => Bun.hash(s, 5381).toString(36);
|
|
5
5
|
|
|
6
|
+
// Bun's bundler does not chain sourcemaps from `onLoad` results, so the final
|
|
7
|
+
// bundle map points at the compiled svelte output (e.g. `$.next()`) using the
|
|
8
|
+
// .svelte filename — runtime stacks resolve to nonsense line numbers past EOF.
|
|
9
|
+
// We capture each per-file svelte compile map here, keyed by absolute source
|
|
10
|
+
// path; `remapBundleSourcemaps()` reads these after `Bun.build` and rewrites
|
|
11
|
+
// the output `.map` files to chain back to original source positions.
|
|
12
|
+
export const svelteMapCache = new Map<string, unknown>();
|
|
13
|
+
|
|
14
|
+
// Svelte 5 dev compile emits named `function get()` / `function set($$value)`
|
|
15
|
+
// expressions inside `$.bind_*` calls (for nicer `$inspect` stack traces). Bun's
|
|
16
|
+
// bundler destructures `import * as $ from "svelte/internal/client"` into named
|
|
17
|
+
// imports, so `$.get(search)` becomes plain `get(search)` — which collides with
|
|
18
|
+
// the wrapping function name and recurses into itself → RangeError. Prod compile
|
|
19
|
+
// uses anonymous arrow functions and is unaffected.
|
|
20
|
+
//
|
|
21
|
+
// Rename to `$$g` / `$$s` (3 chars — length-preserving so cached svelte source
|
|
22
|
+
// map columns stay accurate). These names aren't present in svelte/internal/client.
|
|
23
|
+
function fixBindShadow(code: string): string {
|
|
24
|
+
return code
|
|
25
|
+
.replace(/\bfunction get\(\)/g, () => "function $$g()")
|
|
26
|
+
.replace(/\bfunction set\(\$\$value\)/g, () => "function $$s($$value)");
|
|
27
|
+
}
|
|
28
|
+
|
|
6
29
|
export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
7
30
|
const generate = target === "browser" ? "client" : "server";
|
|
8
31
|
const dev = process.env.NODE_ENV !== "production";
|
|
@@ -25,7 +48,19 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
|
25
48
|
cssHash: ({ css }) => `svelte-${svelteHash(css)}`,
|
|
26
49
|
filename: args.path,
|
|
27
50
|
});
|
|
28
|
-
|
|
51
|
+
// Only the client target's map is useful to the inspector's runtime
|
|
52
|
+
// resolver — browser-side stack frames are what we need to translate.
|
|
53
|
+
// Server (Bun) compile output has different line numbers and would
|
|
54
|
+
// clobber the client entry under the same cache key.
|
|
55
|
+
if (dev && target === "browser" && result.js.map) {
|
|
56
|
+
const m =
|
|
57
|
+
typeof result.js.map === "string"
|
|
58
|
+
? JSON.parse(result.js.map)
|
|
59
|
+
: result.js.map;
|
|
60
|
+
svelteMapCache.set(args.path, m);
|
|
61
|
+
}
|
|
62
|
+
const contents = dev ? fixBindShadow(result.js.code) : result.js.code;
|
|
63
|
+
return { contents, loader: "ts" };
|
|
29
64
|
});
|
|
30
65
|
|
|
31
66
|
build.onLoad({ filter: /\.svelte\.[tj]s$/ }, async (args) => {
|
|
@@ -38,6 +73,13 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
|
38
73
|
dev,
|
|
39
74
|
filename: args.path,
|
|
40
75
|
});
|
|
76
|
+
if (dev && target === "browser" && result.js.map) {
|
|
77
|
+
const m =
|
|
78
|
+
typeof result.js.map === "string"
|
|
79
|
+
? JSON.parse(result.js.map)
|
|
80
|
+
: result.js.map;
|
|
81
|
+
svelteMapCache.set(args.path, m);
|
|
82
|
+
}
|
|
41
83
|
return { contents: result.js.code, loader: "js" };
|
|
42
84
|
});
|
|
43
85
|
},
|