bosia 0.2.2 → 0.3.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.
Files changed (87) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -53
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +8 -8
  8. package/src/cli/feat.ts +291 -132
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -153
  15. package/src/core/client/appState.svelte.ts +57 -0
  16. package/src/core/client/enhance.ts +112 -0
  17. package/src/core/client/hydrate.ts +97 -65
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +165 -168
  25. package/src/core/env.ts +155 -128
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +192 -139
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -118
  34. package/src/core/renderer.ts +359 -265
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +543 -370
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/client.ts +12 -0
  41. package/src/lib/index.ts +8 -8
  42. package/src/lib/utils.ts +44 -30
  43. package/templates/default/.prettierignore +5 -0
  44. package/templates/default/.prettierrc.json +9 -0
  45. package/templates/default/README.md +5 -5
  46. package/templates/default/package.json +22 -18
  47. package/templates/default/src/app.css +80 -80
  48. package/templates/default/src/app.d.ts +3 -3
  49. package/templates/default/src/routes/+error.svelte +7 -10
  50. package/templates/default/src/routes/+layout.svelte +2 -2
  51. package/templates/default/src/routes/+page.svelte +31 -29
  52. package/templates/default/src/routes/about/+page.svelte +3 -3
  53. package/templates/default/tsconfig.json +20 -20
  54. package/templates/demo/.prettierignore +5 -0
  55. package/templates/demo/.prettierrc.json +9 -0
  56. package/templates/demo/README.md +9 -9
  57. package/templates/demo/package.json +22 -17
  58. package/templates/demo/src/app.css +80 -80
  59. package/templates/demo/src/app.d.ts +3 -3
  60. package/templates/demo/src/hooks.server.ts +9 -9
  61. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  62. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  63. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  64. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  65. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  67. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  68. package/templates/demo/src/routes/+error.svelte +10 -7
  69. package/templates/demo/src/routes/+layout.server.ts +4 -4
  70. package/templates/demo/src/routes/+layout.svelte +2 -2
  71. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  72. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  73. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  74. package/templates/demo/tsconfig.json +20 -20
  75. package/templates/todo/.prettierignore +5 -0
  76. package/templates/todo/.prettierrc.json +9 -0
  77. package/templates/todo/README.md +9 -9
  78. package/templates/todo/package.json +22 -17
  79. package/templates/todo/src/app.css +80 -80
  80. package/templates/todo/src/app.d.ts +7 -7
  81. package/templates/todo/src/hooks.server.ts +9 -9
  82. package/templates/todo/src/routes/+error.svelte +10 -7
  83. package/templates/todo/src/routes/+layout.server.ts +4 -4
  84. package/templates/todo/src/routes/+layout.svelte +2 -2
  85. package/templates/todo/src/routes/+page.svelte +44 -44
  86. package/templates/todo/template.json +1 -1
  87. package/templates/todo/tsconfig.json +20 -20
@@ -1,4 +1,4 @@
1
- import type { RouteMatch } from "./types.ts";
1
+ import type { RouteMatch, TrailingSlash } from "./types.ts";
2
2
 
3
3
  // ─── Route Matcher ───────────────────────────────────────
4
4
  // Single shared matcher used by both client and server at runtime.
@@ -12,57 +12,55 @@ import type { RouteMatch } from "./types.ts";
12
12
  // ─── Compiled Route Types ────────────────────────────────
13
13
 
14
14
  interface CompiledRoute {
15
- regex: RegExp;
16
- paramNames: string[];
17
- isExact: boolean;
15
+ regex: RegExp;
16
+ paramNames: string[];
17
+ isExact: boolean;
18
18
  }
19
19
 
20
20
  // ─── Pattern Compiler ────────────────────────────────────
21
21
 
22
22
  /** Escape regex special chars in a literal string segment. */
23
23
  function escapeRegex(s: string): string {
24
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
25
  }
26
26
 
27
27
  /**
28
28
  * Pre-compile a route pattern into a RegExp for fast matching.
29
29
  */
30
30
  function compilePattern(pattern: string): CompiledRoute {
31
- // No dynamic segments — exact match via ===
32
- if (!pattern.includes("[")) {
33
- return { regex: null!, paramNames: [], isExact: true };
34
- }
35
-
36
- const paramNames: string[] = [];
37
-
38
- // Catch-all: /prefix/[...name]
39
- const catchallMatch = pattern.match(/^(.*?)\/\[\.\.\.(\w+)\]$/);
40
- if (catchallMatch) {
41
- const prefix = catchallMatch[1] || "";
42
- paramNames.push(catchallMatch[2]!);
43
- const escaped = prefix ? escapeRegex(prefix) : "";
44
- // Root catch-all /[...rest] must have at least one char after /
45
- const regex = prefix
46
- ? new RegExp(`^${escaped}\\/(.+)$`)
47
- : new RegExp(`^\\/(.+)$`);
48
- return { regex, paramNames, isExact: false };
49
- }
50
-
51
- // Dynamic segments: /blog/[slug]/comments → ^\/blog\/([^/]+)\/comments$
52
- const segments = pattern.split("/").filter(Boolean);
53
- let regexStr = "^";
54
- for (const seg of segments) {
55
- regexStr += "\\/";
56
- if (seg.startsWith("[") && seg.endsWith("]")) {
57
- paramNames.push(seg.slice(1, -1));
58
- regexStr += "([^/]+)";
59
- } else {
60
- regexStr += escapeRegex(seg);
61
- }
62
- }
63
- regexStr += "$";
64
-
65
- return { regex: new RegExp(regexStr), paramNames, isExact: false };
31
+ // No dynamic segments — exact match via ===
32
+ if (!pattern.includes("[")) {
33
+ return { regex: null!, paramNames: [], isExact: true };
34
+ }
35
+
36
+ const paramNames: string[] = [];
37
+
38
+ // Catch-all: /prefix/[...name]
39
+ const catchallMatch = pattern.match(/^(.*?)\/\[\.\.\.(\w+)\]$/);
40
+ if (catchallMatch) {
41
+ const prefix = catchallMatch[1] || "";
42
+ paramNames.push(catchallMatch[2]!);
43
+ const escaped = prefix ? escapeRegex(prefix) : "";
44
+ // Root catch-all /[...rest] must have at least one char after /
45
+ const regex = prefix ? new RegExp(`^${escaped}\\/(.+)$`) : new RegExp(`^\\/(.+)$`);
46
+ return { regex, paramNames, isExact: false };
47
+ }
48
+
49
+ // Dynamic segments: /blog/[slug]/comments → ^\/blog\/([^/]+)\/comments$
50
+ const segments = pattern.split("/").filter(Boolean);
51
+ let regexStr = "^";
52
+ for (const seg of segments) {
53
+ regexStr += "\\/";
54
+ if (seg.startsWith("[") && seg.endsWith("]")) {
55
+ paramNames.push(seg.slice(1, -1));
56
+ regexStr += "([^/]+)";
57
+ } else {
58
+ regexStr += escapeRegex(seg);
59
+ }
60
+ }
61
+ regexStr += "$";
62
+
63
+ return { regex: new RegExp(regexStr), paramNames, isExact: false };
66
64
  }
67
65
 
68
66
  /**
@@ -71,12 +69,12 @@ function compilePattern(pattern: string): CompiledRoute {
71
69
  * Call once at startup — all modules sharing the same route array see the result.
72
70
  */
73
71
  export function compileRoutes<T extends { pattern: string }>(
74
- routes: T[],
72
+ routes: T[],
75
73
  ): (T & { _compiled: CompiledRoute })[] {
76
- for (const route of routes) {
77
- (route as any)._compiled = compilePattern(route.pattern);
78
- }
79
- return routes as (T & { _compiled: CompiledRoute })[];
74
+ for (const route of routes) {
75
+ (route as any)._compiled = compilePattern(route.pattern);
76
+ }
77
+ return routes as (T & { _compiled: CompiledRoute })[];
80
78
  }
81
79
 
82
80
  // ─── Legacy Pattern Matcher (fallback for uncompiled routes) ─
@@ -85,51 +83,48 @@ export function compileRoutes<T extends { pattern: string }>(
85
83
  * Match a URL pathname against a single route pattern.
86
84
  * Returns extracted params if matched, null otherwise.
87
85
  */
88
- function matchPattern(
89
- pattern: string,
90
- pathname: string,
91
- ): Record<string, string> | null {
92
- // Strip trailing slash (but keep "/" as-is)
93
- if (pathname.length > 1 && pathname.endsWith("/")) {
94
- pathname = pathname.slice(0, -1);
95
- }
96
-
97
- // Exact match
98
- if (pattern === pathname) return {};
99
-
100
- // Catch-all pattern: /[...name] or /prefix/[...name]
101
- const catchallMatch = pattern.match(/^(.*?)\/\[\.\.\.(\w+)\]$/);
102
- if (catchallMatch) {
103
- const prefix = catchallMatch[1] || "";
104
- const paramName = catchallMatch[2]!;
105
- if (prefix === "" || pathname.startsWith(prefix + "/") || pathname === prefix) {
106
- const rest = prefix ? pathname.slice(prefix.length + 1) : pathname.slice(1);
107
- // Don't let a root catch-all match "/" with an empty slug.
108
- // If you want the catch-all to also serve "/", add an explicit +page.svelte at the root.
109
- if (!prefix && rest === "") return null;
110
- return { [paramName]: rest };
111
- }
112
- return null;
113
- }
114
-
115
- // Dynamic segments: must have same segment count
116
- if (!pattern.includes("[")) return null;
117
-
118
- const patParts = pattern.split("/").filter(Boolean);
119
- const pathParts = pathname.split("/").filter(Boolean);
120
- if (patParts.length !== pathParts.length) return null;
121
-
122
- const params: Record<string, string> = {};
123
- for (let i = 0; i < patParts.length; i++) {
124
- const pp = patParts[i]!;
125
- const val = pathParts[i]!;
126
- if (pp.startsWith("[") && pp.endsWith("]")) {
127
- params[pp.slice(1, -1)] = val;
128
- } else if (pp !== val) {
129
- return null;
130
- }
131
- }
132
- return params;
86
+ function matchPattern(pattern: string, pathname: string): Record<string, string> | null {
87
+ // Strip trailing slash (but keep "/" as-is)
88
+ if (pathname.length > 1 && pathname.endsWith("/")) {
89
+ pathname = pathname.slice(0, -1);
90
+ }
91
+
92
+ // Exact match
93
+ if (pattern === pathname) return {};
94
+
95
+ // Catch-all pattern: /[...name] or /prefix/[...name]
96
+ const catchallMatch = pattern.match(/^(.*?)\/\[\.\.\.(\w+)\]$/);
97
+ if (catchallMatch) {
98
+ const prefix = catchallMatch[1] || "";
99
+ const paramName = catchallMatch[2]!;
100
+ if (prefix === "" || pathname.startsWith(prefix + "/") || pathname === prefix) {
101
+ const rest = prefix ? pathname.slice(prefix.length + 1) : pathname.slice(1);
102
+ // Don't let a root catch-all match "/" with an empty slug.
103
+ // If you want the catch-all to also serve "/", add an explicit +page.svelte at the root.
104
+ if (!prefix && rest === "") return null;
105
+ return { [paramName]: rest };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // Dynamic segments: must have same segment count
111
+ if (!pattern.includes("[")) return null;
112
+
113
+ const patParts = pattern.split("/").filter(Boolean);
114
+ const pathParts = pathname.split("/").filter(Boolean);
115
+ if (patParts.length !== pathParts.length) return null;
116
+
117
+ const params: Record<string, string> = {};
118
+ for (let i = 0; i < patParts.length; i++) {
119
+ const pp = patParts[i]!;
120
+ const val = pathParts[i]!;
121
+ if (pp.startsWith("[") && pp.endsWith("]")) {
122
+ params[pp.slice(1, -1)] = val;
123
+ } else if (pp !== val) {
124
+ return null;
125
+ }
126
+ }
127
+ return params;
133
128
  }
134
129
 
135
130
  // ─── Route Matching ──────────────────────────────────────
@@ -139,22 +134,22 @@ function matchPattern(
139
134
  * Returns extracted params if matched, null otherwise.
140
135
  */
141
136
  function matchCompiled(
142
- compiled: CompiledRoute,
143
- pattern: string,
144
- pathname: string,
137
+ compiled: CompiledRoute,
138
+ pattern: string,
139
+ pathname: string,
145
140
  ): Record<string, string> | null {
146
- if (compiled.isExact) {
147
- return pattern === pathname ? {} : null;
148
- }
149
-
150
- const m = compiled.regex.exec(pathname);
151
- if (!m) return null;
152
-
153
- const params: Record<string, string> = {};
154
- for (let i = 0; i < compiled.paramNames.length; i++) {
155
- params[compiled.paramNames[i]!] = m[i + 1]!;
156
- }
157
- return params;
141
+ if (compiled.isExact) {
142
+ return pattern === pathname ? {} : null;
143
+ }
144
+
145
+ const m = compiled.regex.exec(pathname);
146
+ if (!m) return null;
147
+
148
+ const params: Record<string, string> = {};
149
+ for (let i = 0; i < compiled.paramNames.length; i++) {
150
+ params[compiled.paramNames[i]!] = m[i + 1]!;
151
+ }
152
+ return params;
158
153
  }
159
154
 
160
155
  /**
@@ -163,21 +158,35 @@ function matchCompiled(
163
158
  * Single pass — first match wins.
164
159
  */
165
160
  export function findMatch<T extends { pattern: string }>(
166
- routes: T[],
167
- pathname: string,
161
+ routes: T[],
162
+ pathname: string,
168
163
  ): RouteMatch<T> | null {
169
- // Strip trailing slash (but keep "/" as-is)
170
- if (pathname.length > 1 && pathname.endsWith("/")) {
171
- pathname = pathname.slice(0, -1);
172
- }
173
-
174
- for (const route of routes) {
175
- const compiled = (route as any)._compiled as CompiledRoute | undefined;
176
- const params = compiled
177
- ? matchCompiled(compiled, route.pattern, pathname)
178
- : matchPattern(route.pattern, pathname);
179
- if (params !== null) return { route, params };
180
- }
181
-
182
- return null;
164
+ // Strip trailing slash (but keep "/" as-is)
165
+ if (pathname.length > 1 && pathname.endsWith("/")) {
166
+ pathname = pathname.slice(0, -1);
167
+ }
168
+
169
+ for (const route of routes) {
170
+ const compiled = (route as any)._compiled as CompiledRoute | undefined;
171
+ const params = compiled
172
+ ? matchCompiled(compiled, route.pattern, pathname)
173
+ : matchPattern(route.pattern, pathname);
174
+ if (params !== null) return { route, params };
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ // ─── Trailing-Slash Canonicalization ──────────────────────
181
+ // Returns the canonical pathname for a given mode, or null if pathname is
182
+ // already canonical. Root "/" is never modified. Caller decides whether to
183
+ // 308-redirect (server) or replaceState (client).
184
+
185
+ export function canonicalPathname(pathname: string, mode: TrailingSlash): string | null {
186
+ if (mode === "ignore") return null;
187
+ if (pathname === "/") return null;
188
+ const endsWithSlash = pathname.endsWith("/");
189
+ if (mode === "never" && endsWithSlash) return pathname.slice(0, -1);
190
+ if (mode === "always" && !endsWithSlash) return pathname + "/";
191
+ return null;
183
192
  }
package/src/core/paths.ts CHANGED
@@ -16,17 +16,15 @@ const isInstalledAsDep = parentDir.endsWith("node_modules");
16
16
  const HOISTED_NM = isInstalledAsDep ? parentDir : null;
17
17
 
18
18
  /** NODE_PATH value covering both nested and hoisted dependency locations */
19
- export const BOSIA_NODE_PATH = HOISTED_NM
20
- ? [NESTED_NM, HOISTED_NM].join(":")
21
- : NESTED_NM;
19
+ export const BOSIA_NODE_PATH = HOISTED_NM ? [NESTED_NM, HOISTED_NM].join(":") : NESTED_NM;
22
20
 
23
21
  /** Find a binary from bosia's dependencies (handles hoisting) */
24
22
  export function resolveBosiaBin(name: string): string {
25
- const nested = join(NESTED_NM, ".bin", name);
26
- if (existsSync(nested)) return nested;
27
- if (HOISTED_NM) {
28
- const hoisted = join(HOISTED_NM, ".bin", name);
29
- if (existsSync(hoisted)) return hoisted;
30
- }
31
- return nested; // fallback — will produce a clear ENOENT
23
+ const nested = join(NESTED_NM, ".bin", name);
24
+ if (existsSync(nested)) return nested;
25
+ if (HOISTED_NM) {
26
+ const hoisted = join(HOISTED_NM, ".bin", name);
27
+ if (existsSync(hoisted)) return hoisted;
28
+ }
29
+ return nested; // fallback — will produce a clear ENOENT
32
30
  }
@@ -8,126 +8,132 @@ import { join, dirname } from "path";
8
8
 
9
9
  let cachedTsconfigPaths: Record<string, string[]> | null = null;
10
10
  async function getTsconfigPaths() {
11
- if (cachedTsconfigPaths !== null) return cachedTsconfigPaths;
12
- try {
13
- const tsconfig = await Bun.file(join(process.cwd(), "tsconfig.json")).json();
14
- cachedTsconfigPaths = tsconfig?.compilerOptions?.paths || {};
15
- } catch {
16
- cachedTsconfigPaths = {};
17
- }
18
- return cachedTsconfigPaths!;
11
+ if (cachedTsconfigPaths !== null) return cachedTsconfigPaths;
12
+ try {
13
+ const tsconfig = await Bun.file(join(process.cwd(), "tsconfig.json")).json();
14
+ cachedTsconfigPaths = tsconfig?.compilerOptions?.paths || {};
15
+ } catch {
16
+ cachedTsconfigPaths = {};
17
+ }
18
+ return cachedTsconfigPaths!;
19
19
  }
20
20
 
21
21
  export function makeBosiaPlugin(target: "browser" | "bun" = "bun") {
22
- return {
23
- name: "bosia-resolver",
24
- setup(build: import("bun").PluginBuilder) {
25
- // bosia:routes → .bosia/routes.client.ts (browser) or .bosia/routes.ts (server)
26
- // Client-only file excludes serverRoutes/apiRoutes to prevent the browser
27
- // bundler from following server-side dynamic imports into Node builtins.
28
- build.onResolve({ filter: /^bosia:routes$/ }, () => ({
29
- path: join(process.cwd(), ".bosia", target === "browser" ? "routes.client.ts" : "routes.ts"),
30
- }));
22
+ return {
23
+ name: "bosia-resolver",
24
+ setup(build: import("bun").PluginBuilder) {
25
+ // bosia:routes → .bosia/routes.client.ts (browser) or .bosia/routes.ts (server)
26
+ // Client-only file excludes serverRoutes/apiRoutes to prevent the browser
27
+ // bundler from following server-side dynamic imports into Node builtins.
28
+ build.onResolve({ filter: /^bosia:routes$/ }, () => ({
29
+ path: join(
30
+ process.cwd(),
31
+ ".bosia",
32
+ target === "browser" ? "routes.client.ts" : "routes.ts",
33
+ ),
34
+ }));
31
35
 
32
- // $env → .bosia/env.client.ts (browser) or .bosia/env.server.ts (bun)
33
- build.onResolve({ filter: /^\$env$/ }, () => ({
34
- path: join(
35
- process.cwd(),
36
- ".bosia",
37
- target === "browser" ? "env.client.ts" : "env.server.ts",
38
- ),
39
- }));
36
+ // $env → .bosia/env.client.ts (browser) or .bosia/env.server.ts (bun)
37
+ build.onResolve({ filter: /^\$env$/ }, () => ({
38
+ path: join(
39
+ process.cwd(),
40
+ ".bosia",
41
+ target === "browser" ? "env.client.ts" : "env.server.ts",
42
+ ),
43
+ }));
40
44
 
41
- // Handle all $ aliases using tsconfig.json paths (e.g. $lib, $registry)
42
- build.onResolve({ filter: /^\$/ }, async (args) => {
43
- if (args.path === "$env") return undefined; // Handled above
45
+ // Handle all $ aliases using tsconfig.json paths (e.g. $lib, $registry)
46
+ build.onResolve({ filter: /^\$/ }, async (args) => {
47
+ if (args.path === "$env") return undefined; // Handled above
44
48
 
45
- const paths = await getTsconfigPaths();
46
- let longestMatch = "";
47
- let targetPattern = "";
49
+ const paths = await getTsconfigPaths();
50
+ let longestMatch = "";
51
+ let targetPattern = "";
48
52
 
49
- for (const [pattern, targets] of Object.entries(paths)) {
50
- const prefix = pattern.replace(/\*$/, "");
51
- if (args.path.startsWith(prefix) && prefix.length > longestMatch.length) {
52
- longestMatch = prefix;
53
- targetPattern = (targets as string[])[0];
54
- }
55
- }
53
+ for (const [pattern, targets] of Object.entries(paths)) {
54
+ const prefix = pattern.replace(/\*$/, "");
55
+ if (args.path.startsWith(prefix) && prefix.length > longestMatch.length) {
56
+ longestMatch = prefix;
57
+ targetPattern = (targets as string[])[0];
58
+ }
59
+ }
56
60
 
57
- if (longestMatch && targetPattern) {
58
- const suffix = args.path.slice(longestMatch.length);
59
- const targetDir = targetPattern.replace(/\*$/, "");
60
- const resolved = join(process.cwd(), targetDir, suffix);
61
- return { path: await resolveWithExts(resolved) };
62
- }
61
+ if (longestMatch && targetPattern) {
62
+ const suffix = args.path.slice(longestMatch.length);
63
+ const targetDir = targetPattern.replace(/\*$/, "");
64
+ const resolved = join(process.cwd(), targetDir, suffix);
65
+ return { path: await resolveWithExts(resolved) };
66
+ }
63
67
 
64
- // Fallback for $lib/* if not in tsconfig
65
- if (args.path.startsWith("$lib/")) {
66
- const rel = args.path.slice(5);
67
- const base = join(process.cwd(), "src", "lib", rel);
68
- return { path: await resolveWithExts(base) };
69
- }
68
+ // Fallback for $lib/* if not in tsconfig
69
+ if (args.path.startsWith("$lib/")) {
70
+ const rel = args.path.slice(5);
71
+ const base = join(process.cwd(), "src", "lib", rel);
72
+ return { path: await resolveWithExts(base) };
73
+ }
70
74
 
71
- return undefined;
72
- });
75
+ return undefined;
76
+ });
73
77
 
74
- // Force svelte imports to resolve from the app's node_modules.
75
- // Without this, when bosia is symlinked (bun link / workspace),
76
- // hydrate.ts resolves "svelte" from the framework's location while
77
- // compiled components resolve "svelte/internal/client" from the app's.
78
- // Two different Svelte copies = duplicate runtime state = broken hydration.
79
- //
80
- // require.resolve uses the "default" export condition, which for
81
- // bare "svelte" returns index-server.js. For browser builds we need
82
- // index-client.js, so we read the "browser" condition from package.json.
83
- const appDir = process.cwd();
84
- let svelteBrowserEntry: string | null = null;
85
- if (target === "browser") {
86
- try {
87
- const svelteDir = dirname(require.resolve("svelte/package.json", { paths: [appDir] }));
88
- const pkg = require(join(svelteDir, "package.json"));
89
- const dotExport = pkg.exports?.["."];
90
- const browserPath = typeof dotExport === "object" ? dotExport.browser : null;
91
- if (browserPath) {
92
- svelteBrowserEntry = join(svelteDir, browserPath);
93
- }
94
- } catch { }
95
- }
96
- build.onResolve({ filter: /^svelte(\/.*)?$/ }, (args) => {
97
- try {
98
- // Bare "svelte" in browser build: use the "browser" export condition
99
- if (args.path === "svelte" && svelteBrowserEntry) {
100
- return { path: svelteBrowserEntry };
101
- }
102
- return { path: require.resolve(args.path, { paths: [appDir] }) };
103
- } catch {
104
- return undefined; // fall through to default resolution
105
- }
106
- });
78
+ // Force svelte imports to resolve from the app's node_modules.
79
+ // Without this, when bosia is symlinked (bun link / workspace),
80
+ // hydrate.ts resolves "svelte" from the framework's location while
81
+ // compiled components resolve "svelte/internal/client" from the app's.
82
+ // Two different Svelte copies = duplicate runtime state = broken hydration.
83
+ //
84
+ // require.resolve uses the "default" export condition, which for
85
+ // bare "svelte" returns index-server.js. For browser builds we need
86
+ // index-client.js, so we read the "browser" condition from package.json.
87
+ const appDir = process.cwd();
88
+ let svelteBrowserEntry: string | null = null;
89
+ if (target === "browser") {
90
+ try {
91
+ const svelteDir = dirname(
92
+ require.resolve("svelte/package.json", { paths: [appDir] }),
93
+ );
94
+ const pkg = require(join(svelteDir, "package.json"));
95
+ const dotExport = pkg.exports?.["."];
96
+ const browserPath = typeof dotExport === "object" ? dotExport.browser : null;
97
+ if (browserPath) {
98
+ svelteBrowserEntry = join(svelteDir, browserPath);
99
+ }
100
+ } catch {}
101
+ }
102
+ build.onResolve({ filter: /^svelte(\/.*)?$/ }, (args) => {
103
+ try {
104
+ // Bare "svelte" in browser build: use the "browser" export condition
105
+ if (args.path === "svelte" && svelteBrowserEntry) {
106
+ return { path: svelteBrowserEntry };
107
+ }
108
+ return { path: require.resolve(args.path, { paths: [appDir] }) };
109
+ } catch {
110
+ return undefined; // fall through to default resolution
111
+ }
112
+ });
107
113
 
108
- // "tailwindcss" inside app.css is a Tailwind CLI directive —
109
- // it's already compiled to public/bosia-tw.css by the CLI step.
110
- // Return an empty CSS module so Bun's CSS bundler doesn't choke on it.
111
- build.onResolve({ filter: /^tailwindcss$/ }, () => ({
112
- path: "tailwindcss",
113
- namespace: "bosia-empty-css",
114
- }));
115
- build.onLoad({ filter: /.*/, namespace: "bosia-empty-css" }, () => ({
116
- contents: "",
117
- loader: "css",
118
- }));
119
- },
120
- };
114
+ // "tailwindcss" inside app.css is a Tailwind CLI directive —
115
+ // it's already compiled to public/bosia-tw.css by the CLI step.
116
+ // Return an empty CSS module so Bun's CSS bundler doesn't choke on it.
117
+ build.onResolve({ filter: /^tailwindcss$/ }, () => ({
118
+ path: "tailwindcss",
119
+ namespace: "bosia-empty-css",
120
+ }));
121
+ build.onLoad({ filter: /.*/, namespace: "bosia-empty-css" }, () => ({
122
+ contents: "",
123
+ loader: "css",
124
+ }));
125
+ },
126
+ };
121
127
  }
122
128
 
123
129
  async function resolveWithExts(base: string): Promise<string> {
124
- if (await Bun.file(base).exists()) return base;
125
- for (const ext of [".ts", ".svelte", ".js"]) {
126
- if (await Bun.file(base + ext).exists()) return base + ext;
127
- }
128
- for (const idx of ["index.ts", "index.svelte", "index.js"]) {
129
- const p = join(base, idx);
130
- if (await Bun.file(p).exists()) return p;
131
- }
132
- return base;
130
+ if (await Bun.file(base).exists()) return base;
131
+ for (const ext of [".ts", ".svelte", ".js"]) {
132
+ if (await Bun.file(base + ext).exists()) return base + ext;
133
+ }
134
+ for (const idx of ["index.ts", "index.svelte", "index.js"]) {
135
+ const p = join(base, idx);
136
+ if (await Bun.file(p).exists()) return p;
137
+ }
138
+ return base;
133
139
  }