bosia 0.5.7 → 0.5.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
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": [
package/src/core/build.ts CHANGED
@@ -5,7 +5,7 @@ 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";
@@ -187,6 +187,18 @@ if (!serverResult.success) {
187
187
  process.exit(1);
188
188
  }
189
189
 
190
+ // Persist the per-file Svelte compile maps so the inspector's runtime stack
191
+ // resolver can chain bundle-map → svelte-map to land on original source. We
192
+ // cannot chain at bundle time because Bun's bundle maps reference intermediate
193
+ // JS positions that are mostly absent from Svelte's sparse compile map —
194
+ // post-build remapping nukes mappings. Instead we keep both maps separate and
195
+ // do a two-stage lookup with `bias` interpolation in the resolver.
196
+ if (!isProduction && svelteMapCache.size > 0) {
197
+ const entries: Record<string, unknown> = {};
198
+ for (const [k, v] of svelteMapCache) entries[k] = v;
199
+ writeFileSync(`${OUT_DIR}/svelte-maps.json`, JSON.stringify(entries));
200
+ }
201
+
190
202
  // 6. Collect output files for dist/manifest.json
191
203
  const jsFiles: string[] = [];
192
204
  const cssFiles: string[] = [];
@@ -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,7 +136,29 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
120
136
  cssHash: ({ css }) => `svelte-${fnv(css)}`,
121
137
  });
122
138
 
123
- let js = result.js.code;
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;
124
162
  // Skip empty CSS — multiple +page.svelte routes with no <style> would
125
163
  // otherwise each emit an empty CSS chunk, all hashing to the same
126
164
  // content → Bun fails with "Multiple files share the same output path".
@@ -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
  }
@@ -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
- return { contents: result.js.code, loader: "ts" };
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
  },