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
@@ -8,106 +8,115 @@ import type { RouteManifest } from "./types.ts";
8
8
  // apiRoutes — used by API handler
9
9
 
10
10
  function routePriority(pattern: string): number {
11
- if (!pattern.includes("[")) return 0; // exact
12
- if (!pattern.includes("[...")) return 1; // dynamic
13
- return 2; // catch-all
11
+ if (!pattern.includes("[")) return 0; // exact
12
+ if (!pattern.includes("[...")) return 1; // dynamic
13
+ return 2; // catch-all
14
14
  }
15
15
 
16
16
  function sortRoutes<T extends { pattern: string }>(routes: T[]): T[] {
17
- return [...routes].sort((a, b) => {
18
- const pa = routePriority(a.pattern);
19
- const pb = routePriority(b.pattern);
20
- if (pa !== pb) return pa - pb;
21
- // same tier: more segments first, then alphabetical
22
- const sa = a.pattern.split("/").length;
23
- const sb = b.pattern.split("/").length;
24
- if (sa !== sb) return sb - sa;
25
- return a.pattern.localeCompare(b.pattern);
26
- });
17
+ return [...routes].sort((a, b) => {
18
+ const pa = routePriority(a.pattern);
19
+ const pb = routePriority(b.pattern);
20
+ if (pa !== pb) return pa - pb;
21
+ // same tier: more segments first, then alphabetical
22
+ const sa = a.pattern.split("/").length;
23
+ const sb = b.pattern.split("/").length;
24
+ if (sa !== sb) return sb - sa;
25
+ return a.pattern.localeCompare(b.pattern);
26
+ });
27
27
  }
28
28
 
29
29
  export function generateRoutesFile(manifest: RouteManifest): void {
30
- const lines: string[] = [
31
- "// AUTO-GENERATED by bosia build — do not edit\n",
32
- ];
33
-
34
- const pages = sortRoutes(manifest.pages);
35
- const apis = sortRoutes(manifest.apis);
36
-
37
- // clientRoutes
38
- lines.push("export const clientRoutes: Array<{");
39
- lines.push(" pattern: string;");
40
- lines.push(" page: () => Promise<any>;");
41
- lines.push(" layouts: (() => Promise<any>)[];");
42
- lines.push(" hasServerData: boolean;");
43
- lines.push("}> = [");
44
- for (const r of pages) {
45
- const layoutImports = r.layouts
46
- .map(l => `() => import(${JSON.stringify(toImportPath(l))})`)
47
- .join(", ");
48
- const hasServerData = !!(r.pageServer || r.layoutServers.length > 0);
49
- lines.push(" {");
50
- lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
51
- lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
52
- lines.push(` layouts: [${layoutImports}],`);
53
- lines.push(` hasServerData: ${hasServerData},`);
54
- lines.push(" },");
55
- }
56
- lines.push("];\n");
57
-
58
- // serverRoutes
59
- lines.push("export const serverRoutes: Array<{");
60
- lines.push(" pattern: string;");
61
- lines.push(" pageModule: () => Promise<any>;");
62
- lines.push(" layoutModules: (() => Promise<any>)[];");
63
- lines.push(" pageServer: (() => Promise<any>) | null;");
64
- lines.push(" layoutServers: { loader: () => Promise<any>; depth: number }[];");
65
- lines.push("}> = [");
66
- for (const r of pages) {
67
- const layoutImports = r.layouts
68
- .map(l => `() => import(${JSON.stringify(toImportPath(l))})`)
69
- .join(", ");
70
- const layoutServerImports = r.layoutServers
71
- .map(ls => `{ loader: () => import(${JSON.stringify(toImportPath(ls.path))}), depth: ${ls.depth} }`)
72
- .join(", ");
73
- lines.push(" {");
74
- lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
75
- lines.push(` pageModule: () => import(${JSON.stringify(toImportPath(r.page))}),`);
76
- lines.push(` layoutModules: [${layoutImports}],`);
77
- lines.push(` pageServer: ${r.pageServer ? `() => import(${JSON.stringify(toImportPath(r.pageServer))})` : "null"},`);
78
- lines.push(` layoutServers: [${layoutServerImports}],`);
79
- lines.push(" },");
80
- }
81
- lines.push("];\n");
82
-
83
- // apiRoutes
84
- lines.push("export const apiRoutes: Array<{");
85
- lines.push(" pattern: string;");
86
- lines.push(" module: () => Promise<any>;");
87
- lines.push("}> = [");
88
- for (const r of apis) {
89
- lines.push(" {");
90
- lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
91
- lines.push(` module: () => import(${JSON.stringify(toImportPath(r.server))}),`);
92
- lines.push(" },");
93
- }
94
- lines.push("];\n");
95
-
96
- // errorPage
97
- const ep = manifest.errorPage;
98
- lines.push(`export const errorPage: (() => Promise<any>) | null = ${
99
- ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
100
- };\n`);
101
-
102
- mkdirSync(".bosia", { recursive: true });
103
- writeFileSync(".bosia/routes.ts", lines.join("\n"));
104
-
105
- // Generate client-only routes file (no server imports that could pull in Node builtins)
106
- generateClientRoutesFile(pages, manifest.errorPage);
107
-
108
- const pagePatterns = pages.map(p => p.pattern).join(", ") || "(none)";
109
- console.log(`✅ Routes generated: .bosia/routes.ts`);
110
- console.log(` Found ${pages.length} page route(s): ${pagePatterns}`);
30
+ const lines: string[] = ["// AUTO-GENERATED by bosia build — do not edit\n"];
31
+
32
+ const pages = sortRoutes(manifest.pages);
33
+ const apis = sortRoutes(manifest.apis);
34
+
35
+ // clientRoutes
36
+ lines.push("export const clientRoutes: Array<{");
37
+ lines.push(" pattern: string;");
38
+ lines.push(" page: () => Promise<any>;");
39
+ lines.push(" layouts: (() => Promise<any>)[];");
40
+ lines.push(" hasServerData: boolean;");
41
+ lines.push(' trailingSlash: "never" | "always" | "ignore";');
42
+ lines.push("}> = [");
43
+ for (const r of pages) {
44
+ const layoutImports = r.layouts
45
+ .map((l) => `() => import(${JSON.stringify(toImportPath(l))})`)
46
+ .join(", ");
47
+ const hasServerData = !!(r.pageServer || r.layoutServers.length > 0);
48
+ lines.push(" {");
49
+ lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
50
+ lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
51
+ lines.push(` layouts: [${layoutImports}],`);
52
+ lines.push(` hasServerData: ${hasServerData},`);
53
+ lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
54
+ lines.push(" },");
55
+ }
56
+ lines.push("];\n");
57
+
58
+ // serverRoutes
59
+ lines.push("export const serverRoutes: Array<{");
60
+ lines.push(" pattern: string;");
61
+ lines.push(" pageModule: () => Promise<any>;");
62
+ lines.push(" layoutModules: (() => Promise<any>)[];");
63
+ lines.push(" pageServer: (() => Promise<any>) | null;");
64
+ lines.push(" layoutServers: { loader: () => Promise<any>; depth: number }[];");
65
+ lines.push(' trailingSlash: "never" | "always" | "ignore";');
66
+ lines.push("}> = [");
67
+ for (const r of pages) {
68
+ const layoutImports = r.layouts
69
+ .map((l) => `() => import(${JSON.stringify(toImportPath(l))})`)
70
+ .join(", ");
71
+ const layoutServerImports = r.layoutServers
72
+ .map(
73
+ (ls) =>
74
+ `{ loader: () => import(${JSON.stringify(toImportPath(ls.path))}), depth: ${ls.depth} }`,
75
+ )
76
+ .join(", ");
77
+ lines.push(" {");
78
+ lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
79
+ lines.push(` pageModule: () => import(${JSON.stringify(toImportPath(r.page))}),`);
80
+ lines.push(` layoutModules: [${layoutImports}],`);
81
+ lines.push(
82
+ ` pageServer: ${r.pageServer ? `() => import(${JSON.stringify(toImportPath(r.pageServer))})` : "null"},`,
83
+ );
84
+ lines.push(` layoutServers: [${layoutServerImports}],`);
85
+ lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
86
+ lines.push(" },");
87
+ }
88
+ lines.push("];\n");
89
+
90
+ // apiRoutes
91
+ lines.push("export const apiRoutes: Array<{");
92
+ lines.push(" pattern: string;");
93
+ lines.push(" module: () => Promise<any>;");
94
+ lines.push("}> = [");
95
+ for (const r of apis) {
96
+ lines.push(" {");
97
+ lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
98
+ lines.push(` module: () => import(${JSON.stringify(toImportPath(r.server))}),`);
99
+ lines.push(" },");
100
+ }
101
+ lines.push("];\n");
102
+
103
+ // errorPage
104
+ const ep = manifest.errorPage;
105
+ lines.push(
106
+ `export const errorPage: (() => Promise<any>) | null = ${
107
+ ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
108
+ };\n`,
109
+ );
110
+
111
+ mkdirSync(".bosia", { recursive: true });
112
+ writeFileSync(".bosia/routes.ts", lines.join("\n"));
113
+
114
+ // Generate client-only routes file (no server imports that could pull in Node builtins)
115
+ generateClientRoutesFile(pages, manifest.errorPage);
116
+
117
+ const pagePatterns = pages.map((p) => p.pattern).join(", ") || "(none)";
118
+ console.log(`✅ Routes generated: .bosia/routes.ts`);
119
+ console.log(` Found ${pages.length} page route(s): ${pagePatterns}`);
111
120
  }
112
121
 
113
122
  // ─── Client-only routes file ─────────────────────────────
@@ -116,42 +125,46 @@ export function generateRoutesFile(manifest: RouteManifest): void {
116
125
  // (e.g. +page.server.ts → drizzle-orm → postgres → Node builtins).
117
126
 
118
127
  function generateClientRoutesFile(
119
- pages: RouteManifest["pages"],
120
- errorPage: RouteManifest["errorPage"],
128
+ pages: RouteManifest["pages"],
129
+ errorPage: RouteManifest["errorPage"],
121
130
  ): void {
122
- const lines: string[] = [
123
- "// AUTO-GENERATED by bosia build — client-only routes (no server imports)\n",
124
- ];
125
-
126
- lines.push("export const clientRoutes: Array<{");
127
- lines.push(" pattern: string;");
128
- lines.push(" page: () => Promise<any>;");
129
- lines.push(" layouts: (() => Promise<any>)[];");
130
- lines.push(" hasServerData: boolean;");
131
- lines.push("}> = [");
132
- for (const r of pages) {
133
- const layoutImports = r.layouts
134
- .map(l => `() => import(${JSON.stringify(toImportPath(l))})`)
135
- .join(", ");
136
- const hasServerData = !!(r.pageServer || r.layoutServers.length > 0);
137
- lines.push(" {");
138
- lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
139
- lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
140
- lines.push(` layouts: [${layoutImports}],`);
141
- lines.push(` hasServerData: ${hasServerData},`);
142
- lines.push(" },");
143
- }
144
- lines.push("];\n");
145
-
146
- const ep = errorPage;
147
- lines.push(`export const errorPage: (() => Promise<any>) | null = ${
148
- ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
149
- };\n`);
150
-
151
- writeFileSync(".bosia/routes.client.ts", lines.join("\n"));
131
+ const lines: string[] = [
132
+ "// AUTO-GENERATED by bosia build — client-only routes (no server imports)\n",
133
+ ];
134
+
135
+ lines.push("export const clientRoutes: Array<{");
136
+ lines.push(" pattern: string;");
137
+ lines.push(" page: () => Promise<any>;");
138
+ lines.push(" layouts: (() => Promise<any>)[];");
139
+ lines.push(" hasServerData: boolean;");
140
+ lines.push(' trailingSlash: "never" | "always" | "ignore";');
141
+ lines.push("}> = [");
142
+ for (const r of pages) {
143
+ const layoutImports = r.layouts
144
+ .map((l) => `() => import(${JSON.stringify(toImportPath(l))})`)
145
+ .join(", ");
146
+ const hasServerData = !!(r.pageServer || r.layoutServers.length > 0);
147
+ lines.push(" {");
148
+ lines.push(` pattern: ${JSON.stringify(r.pattern)},`);
149
+ lines.push(` page: () => import(${JSON.stringify(toImportPath(r.page))}),`);
150
+ lines.push(` layouts: [${layoutImports}],`);
151
+ lines.push(` hasServerData: ${hasServerData},`);
152
+ lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
153
+ lines.push(" },");
154
+ }
155
+ lines.push("];\n");
156
+
157
+ const ep = errorPage;
158
+ lines.push(
159
+ `export const errorPage: (() => Promise<any>) | null = ${
160
+ ep ? `() => import(${JSON.stringify(toImportPath(ep))})` : "null"
161
+ };\n`,
162
+ );
163
+
164
+ writeFileSync(".bosia/routes.client.ts", lines.join("\n"));
152
165
  }
153
166
 
154
167
  // Import path from .bosia/routes.ts to src/routes/<routePath>
155
168
  function toImportPath(routePath: string): string {
156
- return "../src/routes/" + routePath.replace(/\\/g, "/");
169
+ return "../src/routes/" + routePath.replace(/\\/g, "/");
157
170
  }
@@ -9,71 +9,127 @@ import type { RouteManifest } from "./types.ts";
9
9
  // work in +page.svelte files — identical to SvelteKit's API.
10
10
 
11
11
  function routeDirOf(filePath: string): string {
12
- const parts = filePath.replace(/\\/g, "/").split("/");
13
- parts.pop();
14
- return parts.join("/") || ".";
12
+ const parts = filePath.replace(/\\/g, "/").split("/");
13
+ parts.pop();
14
+ return parts.join("/") || ".";
15
+ }
16
+
17
+ function paramsForDir(dir: string): string[] {
18
+ if (dir === ".") return [];
19
+ const segs = dir
20
+ .split("/")
21
+ .filter(Boolean)
22
+ .filter((s) => !/^\(.*\)$/.test(s));
23
+ const out: string[] = [];
24
+ for (const seg of segs) {
25
+ const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/);
26
+ if (catchAll) {
27
+ out.push(catchAll[1]!);
28
+ continue;
29
+ }
30
+ const dyn = seg.match(/^\[(\w+)\]$/);
31
+ if (dyn) out.push(dyn[1]!);
32
+ }
33
+ return out;
15
34
  }
16
35
 
17
36
  export function generateRouteTypes(manifest: RouteManifest): void {
18
- // Collect { dir → { pageServer?, layoutServer? } }
19
- const dirs = new Map<string, { pageServer?: string; layoutServer?: string }>();
20
-
21
- for (const route of manifest.pages) {
22
- const pageDir = routeDirOf(route.page);
23
- if (!dirs.has(pageDir)) dirs.set(pageDir, {});
24
- if (route.pageServer) {
25
- dirs.get(pageDir)!.pageServer = route.pageServer;
26
- }
27
- for (const ls of route.layoutServers) {
28
- const lsDir = routeDirOf(ls.path);
29
- if (!dirs.has(lsDir)) dirs.set(lsDir, {});
30
- dirs.get(lsDir)!.layoutServer = ls.path;
31
- }
32
- }
33
-
34
- for (const [dir, info] of dirs) {
35
- // Path segments of the route dir (empty array for root ".")
36
- const segments = dir === "." ? [] : dir.split("/").filter(Boolean);
37
-
38
- // Depth of the generated file from project root:
39
- // .bosia/ + types/ + src/ + routes/ + ...segments
40
- const depth = 4 + segments.length;
41
- const up = "../".repeat(depth);
42
- const srcBase = `${up}src/routes/${segments.length ? segments.join("/") + "/" : ""}`;
43
-
44
- const lines: string[] = ["// AUTO-GENERATED by bosia — do not edit\n"];
45
-
46
- if (info.pageServer) {
47
- lines.push(`import type { load as _pageLoad } from '${srcBase}+page.server.ts';`);
48
- lines.push(`export type PageData = Awaited<ReturnType<typeof _pageLoad>> & { params: Record<string, string> };`);
49
- } else {
50
- lines.push(`export type PageData = { params: Record<string, string> };`);
51
- }
52
- lines.push(`export type PageProps = { data: PageData };`);
53
-
54
- // ActionData — union of all action return types, unwrapping ActionFailure
55
- if (info.pageServer) {
56
- lines.push(``);
57
- lines.push(`import type { actions as _actions } from '${srcBase}+page.server.ts';`);
58
- lines.push(`type _ActionReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;`);
59
- lines.push(`type _UnwrapFailure<T> = T extends { status: number; data: infer D } ? D : T;`);
60
- lines.push(`export type ActionData = _actions extends Record<string, (...args: any[]) => any>`);
61
- lines.push(` ? _UnwrapFailure<_ActionReturn<_actions[keyof _actions]>> | null`);
62
- lines.push(` : null;`);
63
- }
64
-
65
- if (info.layoutServer) {
66
- lines.push(`\nimport type { load as _layoutLoad } from '${srcBase}+layout.server.ts';`);
67
- lines.push(`export type LayoutData = Awaited<ReturnType<typeof _layoutLoad>> & { params: Record<string, string> };`);
68
- lines.push(`export type LayoutProps = { data: LayoutData; children: any };`);
69
- }
70
-
71
- const outDir = join(process.cwd(), ".bosia", "types", "src", "routes", ...segments);
72
- mkdirSync(outDir, { recursive: true });
73
- writeFileSync(join(outDir, "$types.d.ts"), lines.join("\n") + "\n");
74
- }
75
-
76
- console.log(`✅ Route types generated: .bosia/types/ (${dirs.size} route director${dirs.size === 1 ? "y" : "ies"})`);
37
+ // Collect { dir → { pageServer?, layoutServer? } }
38
+ const dirs = new Map<string, { pageServer?: string; layoutServer?: string }>();
39
+
40
+ for (const route of manifest.pages) {
41
+ const pageDir = routeDirOf(route.page);
42
+ if (!dirs.has(pageDir)) dirs.set(pageDir, {});
43
+ if (route.pageServer) {
44
+ dirs.get(pageDir)!.pageServer = route.pageServer;
45
+ }
46
+ for (const ls of route.layoutServers) {
47
+ const lsDir = routeDirOf(ls.path);
48
+ if (!dirs.has(lsDir)) dirs.set(lsDir, {});
49
+ dirs.get(lsDir)!.layoutServer = ls.path;
50
+ }
51
+ }
52
+
53
+ for (const [dir, info] of dirs) {
54
+ // Path segments of the route dir (empty array for root ".")
55
+ const segments = dir === "." ? [] : dir.split("/").filter(Boolean);
56
+
57
+ // Depth of the generated file from project root:
58
+ // .bosia/ + types/ + src/ + routes/ + ...segments
59
+ const depth = 4 + segments.length;
60
+ const up = "../".repeat(depth);
61
+ const srcBase = `${up}src/routes/${segments.length ? segments.join("/") + "/" : ""}`;
62
+
63
+ const params = paramsForDir(dir);
64
+ const paramsType =
65
+ params.length === 0 ? `{}` : `{ ${params.map((p) => `${p}: string`).join("; ")} }`;
66
+
67
+ const lines: string[] = ["// AUTO-GENERATED by bosia do not edit\n"];
68
+ lines.push(
69
+ `import type { LoadEvent, MetadataEvent, RequestEvent, Metadata } from 'bosia';`,
70
+ );
71
+ lines.push(``);
72
+ lines.push(`export type Params = ${paramsType};`);
73
+ lines.push(``);
74
+ lines.push(`type _LoadEvent = Omit<LoadEvent, 'params'> & { params: Params };`);
75
+ lines.push(`type _MetadataEvent = Omit<MetadataEvent, 'params'> & { params: Params };`);
76
+ lines.push(`type _RequestEvent = Omit<RequestEvent, 'params'> & { params: Params };`);
77
+
78
+ if (info.pageServer) {
79
+ lines.push(``);
80
+ lines.push(`import type { load as _pageLoad } from '${srcBase}+page.server.ts';`);
81
+ lines.push(`export type PageServerLoad = (event: _LoadEvent) => any;`);
82
+ lines.push(
83
+ `export type PageMetadataLoad = (event: _MetadataEvent) => Metadata | Promise<Metadata>;`,
84
+ );
85
+ lines.push(`export type Action = (event: _RequestEvent) => any;`);
86
+ lines.push(
87
+ `export type PageData = Awaited<ReturnType<typeof _pageLoad>> & { params: Params };`,
88
+ );
89
+ } else {
90
+ lines.push(``);
91
+ lines.push(
92
+ `export type PageMetadataLoad = (event: _MetadataEvent) => Metadata | Promise<Metadata>;`,
93
+ );
94
+ lines.push(`export type Action = (event: _RequestEvent) => any;`);
95
+ lines.push(`export type PageData = { params: Params };`);
96
+ }
97
+ lines.push(`export type PageProps = { data: PageData };`);
98
+
99
+ // ActionData — union of all action return types, unwrapping ActionFailure
100
+ if (info.pageServer) {
101
+ lines.push(``);
102
+ lines.push(`import type { actions as _actions } from '${srcBase}+page.server.ts';`);
103
+ lines.push(
104
+ `type _ActionReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;`,
105
+ );
106
+ lines.push(
107
+ `type _UnwrapFailure<T> = T extends { status: number; data: infer D } ? D : T;`,
108
+ );
109
+ lines.push(
110
+ `export type ActionData = _actions extends Record<string, (...args: any[]) => any>`,
111
+ );
112
+ lines.push(` ? _UnwrapFailure<_ActionReturn<_actions[keyof _actions]>> | null`);
113
+ lines.push(` : null;`);
114
+ }
115
+
116
+ if (info.layoutServer) {
117
+ lines.push(`\nimport type { load as _layoutLoad } from '${srcBase}+layout.server.ts';`);
118
+ lines.push(`export type LayoutServerLoad = (event: _LoadEvent) => any;`);
119
+ lines.push(
120
+ `export type LayoutData = Awaited<ReturnType<typeof _layoutLoad>> & { params: Params };`,
121
+ );
122
+ lines.push(`export type LayoutProps = { data: LayoutData; children: any };`);
123
+ }
124
+
125
+ const outDir = join(process.cwd(), ".bosia", "types", "src", "routes", ...segments);
126
+ mkdirSync(outDir, { recursive: true });
127
+ writeFileSync(join(outDir, "$types.d.ts"), lines.join("\n") + "\n");
128
+ }
129
+
130
+ console.log(
131
+ `✅ Route types generated: .bosia/types/ (${dirs.size} route director${dirs.size === 1 ? "y" : "ies"})`,
132
+ );
77
133
  }
78
134
 
79
135
  // ─── Ensure tsconfig rootDirs ─────────────────────────────
@@ -82,25 +138,30 @@ export function generateRouteTypes(manifest: RouteManifest): void {
82
138
  // Only patches the file if rootDirs is not already set.
83
139
 
84
140
  export function ensureRootDirs(): void {
85
- const tsconfigPath = join(process.cwd(), "tsconfig.json");
86
- if (!existsSync(tsconfigPath)) return;
87
-
88
- let tsconfig: any;
89
- try {
90
- tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
91
- } catch {
92
- console.warn("⚠️ Could not parse tsconfig.json — add rootDirs manually:\n" +
93
- ' "rootDirs": [".", ".bosia/types"]');
94
- return;
95
- }
96
-
97
- const rootDirs: string[] = tsconfig.compilerOptions?.rootDirs ?? [];
98
- if (rootDirs.includes(".bosia/types")) return;
99
-
100
- tsconfig.compilerOptions ??= {};
101
- tsconfig.compilerOptions.rootDirs = [".", ".bosia/types",
102
- ...rootDirs.filter((d: string) => d !== ".")];
103
-
104
- writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
105
- console.log("✅ tsconfig.json: added .bosia/types to rootDirs");
141
+ const tsconfigPath = join(process.cwd(), "tsconfig.json");
142
+ if (!existsSync(tsconfigPath)) return;
143
+
144
+ let tsconfig: any;
145
+ try {
146
+ tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
147
+ } catch {
148
+ console.warn(
149
+ "⚠️ Could not parse tsconfig.json — add rootDirs manually:\n" +
150
+ ' "rootDirs": [".", ".bosia/types"]',
151
+ );
152
+ return;
153
+ }
154
+
155
+ const rootDirs: string[] = tsconfig.compilerOptions?.rootDirs ?? [];
156
+ if (rootDirs.includes(".bosia/types")) return;
157
+
158
+ tsconfig.compilerOptions ??= {};
159
+ tsconfig.compilerOptions.rootDirs = [
160
+ ".",
161
+ ".bosia/types",
162
+ ...rootDirs.filter((d: string) => d !== "."),
163
+ ];
164
+
165
+ writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
166
+ console.log("✅ tsconfig.json: added .bosia/types to rootDirs");
106
167
  }