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
package/src/core/dev.ts CHANGED
@@ -17,17 +17,17 @@ const RAPID_CRASH_WINDOW = 5_000; // 5 seconds
17
17
  // ─── SSE Broadcast ────────────────────────────────────────
18
18
 
19
19
  function broadcastReload() {
20
- const msg = new TextEncoder().encode("event: reload\ndata: ok\n\n");
21
- for (const ctrl of sseClients) {
22
- try {
23
- ctrl.enqueue(msg);
24
- } catch {
25
- sseClients.delete(ctrl);
26
- }
27
- }
28
- if (sseClients.size > 0) {
29
- console.log(`📡 Reload sent to ${sseClients.size} client(s)`);
30
- }
20
+ const msg = new TextEncoder().encode("event: reload\ndata: ok\n\n");
21
+ for (const ctrl of sseClients) {
22
+ try {
23
+ ctrl.enqueue(msg);
24
+ } catch {
25
+ sseClients.delete(ctrl);
26
+ }
27
+ }
28
+ if (sseClients.size > 0) {
29
+ console.log(`📡 Reload sent to ${sseClients.size} client(s)`);
30
+ }
31
31
  }
32
32
 
33
33
  // ─── Build ────────────────────────────────────────────────
@@ -37,13 +37,13 @@ import { BOSIA_NODE_PATH } from "./paths.ts";
37
37
  const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
38
38
 
39
39
  async function runBuild(): Promise<boolean> {
40
- console.log("🏗️ Building...");
41
- const proc = spawn(["bun", "run", BUILD_SCRIPT], {
42
- stdout: "inherit",
43
- stderr: "inherit",
44
- cwd: process.cwd(),
45
- });
46
- return (await proc.exited) === 0;
40
+ console.log("🏗️ Building...");
41
+ const proc = spawn(["bun", "run", BUILD_SCRIPT], {
42
+ stdout: "inherit",
43
+ stderr: "inherit",
44
+ cwd: process.cwd(),
45
+ });
46
+ return (await proc.exited) === 0;
47
47
  }
48
48
 
49
49
  // ─── Ports ────────────────────────────────────────────────
@@ -52,57 +52,59 @@ const DEV_PORT = Number(process.env.PORT) || 9000;
52
52
  const APP_PORT = DEV_PORT + 1; // internal, hidden from user
53
53
 
54
54
  async function startAppServer() {
55
- if (appProcess) {
56
- intentionalKill = true;
57
- appProcess.kill();
58
- await appProcess.exited;
59
- intentionalKill = false;
60
- }
61
-
62
- // Read the server entry filename from the manifest written by build.ts
63
- let serverEntry = "index.js";
64
- try {
65
- const manifest = await Bun.file("./dist/manifest.json").json();
66
- serverEntry = manifest.serverEntry ?? "index.js";
67
- } catch { }
68
-
69
- appProcess = spawn(["bun", "run", `dist/server/${serverEntry}`], {
70
- stdout: "inherit",
71
- stderr: "inherit",
72
- cwd: process.cwd(),
73
- env: {
74
- ...process.env,
75
- NODE_ENV: "development",
76
- // Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
77
- PORT: String(APP_PORT),
78
- // Allow externalized deps (elysia, etc.) to resolve from bosia's node_modules
79
- NODE_PATH: BOSIA_NODE_PATH,
80
- },
81
- });
82
-
83
- // Monitor for unexpected crashes
84
- const proc = appProcess;
85
- proc.exited.then((code) => {
86
- if (proc !== appProcess || intentionalKill) return;
87
- if (code === 0) return; // clean exit
88
-
89
- const now = Date.now();
90
- if (now - lastCrashTime < RAPID_CRASH_WINDOW) {
91
- crashCount++;
92
- } else {
93
- crashCount = 1;
94
- }
95
- lastCrashTime = now;
96
-
97
- if (crashCount >= MAX_RAPID_CRASHES) {
98
- console.error(`\n💥 App crashed ${crashCount} times in ${RAPID_CRASH_WINDOW / 1000}s — waiting for file change to restart\n`);
99
- crashCount = 0;
100
- return;
101
- }
102
-
103
- console.warn(`\n⚠️ App crashed (exit code ${code}). Restarting...\n`);
104
- startAppServer();
105
- });
55
+ if (appProcess) {
56
+ intentionalKill = true;
57
+ appProcess.kill();
58
+ await appProcess.exited;
59
+ intentionalKill = false;
60
+ }
61
+
62
+ // Read the server entry filename from the manifest written by build.ts
63
+ let serverEntry = "index.js";
64
+ try {
65
+ const manifest = await Bun.file("./dist/manifest.json").json();
66
+ serverEntry = manifest.serverEntry ?? "index.js";
67
+ } catch {}
68
+
69
+ appProcess = spawn(["bun", "run", `dist/server/${serverEntry}`], {
70
+ stdout: "inherit",
71
+ stderr: "inherit",
72
+ cwd: process.cwd(),
73
+ env: {
74
+ ...process.env,
75
+ NODE_ENV: "development",
76
+ // Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
77
+ PORT: String(APP_PORT),
78
+ // Allow externalized deps (elysia, etc.) to resolve from bosia's node_modules
79
+ NODE_PATH: BOSIA_NODE_PATH,
80
+ },
81
+ });
82
+
83
+ // Monitor for unexpected crashes
84
+ const proc = appProcess;
85
+ proc.exited.then((code) => {
86
+ if (proc !== appProcess || intentionalKill) return;
87
+ if (code === 0) return; // clean exit
88
+
89
+ const now = Date.now();
90
+ if (now - lastCrashTime < RAPID_CRASH_WINDOW) {
91
+ crashCount++;
92
+ } else {
93
+ crashCount = 1;
94
+ }
95
+ lastCrashTime = now;
96
+
97
+ if (crashCount >= MAX_RAPID_CRASHES) {
98
+ console.error(
99
+ `\n💥 App crashed ${crashCount} times in ${RAPID_CRASH_WINDOW / 1000}s — waiting for file change to restart\n`,
100
+ );
101
+ crashCount = 0;
102
+ return;
103
+ }
104
+
105
+ console.warn(`\n⚠️ App crashed (exit code ${code}). Restarting...\n`);
106
+ startAppServer();
107
+ });
106
108
  }
107
109
 
108
110
  // ─── Build & Restart ──────────────────────────────────────
@@ -112,33 +114,33 @@ let building = false;
112
114
  let buildPending = false;
113
115
 
114
116
  async function buildAndRestart() {
115
- if (building) {
116
- buildPending = true;
117
- return;
118
- }
119
- building = true;
120
- try {
121
- const ok = await runBuild();
122
- if (!ok) {
123
- console.error("❌ Build failed — fix errors and save again");
124
- return;
125
- }
126
- await startAppServer();
127
- // Give the app server a moment to bind its port
128
- await Bun.sleep(200);
129
- broadcastReload();
130
- } finally {
131
- building = false;
132
- }
133
- if (buildPending) {
134
- buildPending = false;
135
- buildAndRestart();
136
- }
117
+ if (building) {
118
+ buildPending = true;
119
+ return;
120
+ }
121
+ building = true;
122
+ try {
123
+ const ok = await runBuild();
124
+ if (!ok) {
125
+ console.error("❌ Build failed — fix errors and save again");
126
+ return;
127
+ }
128
+ await startAppServer();
129
+ // Give the app server a moment to bind its port
130
+ await Bun.sleep(200);
131
+ broadcastReload();
132
+ } finally {
133
+ building = false;
134
+ }
135
+ if (buildPending) {
136
+ buildPending = false;
137
+ buildAndRestart();
138
+ }
137
139
  }
138
140
 
139
141
  function scheduleBuild() {
140
- if (buildTimer) clearTimeout(buildTimer);
141
- buildTimer = setTimeout(buildAndRestart, 300);
142
+ if (buildTimer) clearTimeout(buildTimer);
143
+ buildTimer = setTimeout(buildAndRestart, 300);
142
144
  }
143
145
 
144
146
  // ─── Dev Proxy ────────────────────────────────────────────
@@ -146,65 +148,67 @@ function scheduleBuild() {
146
148
  // All other requests are proxied to the app server.
147
149
 
148
150
  Bun.serve({
149
- port: DEV_PORT,
150
- idleTimeout: 255,
151
- async fetch(req) {
152
- const url = new URL(req.url);
153
-
154
- // SSE endpoint — owned by dev server, not the app
155
- if (url.pathname === "/__bosia/sse") {
156
- return new Response(
157
- new ReadableStream({
158
- start(ctrl) {
159
- sseClients.add(ctrl);
160
- // Initial keepalive so the browser knows the connection is open
161
- ctrl.enqueue(new TextEncoder().encode(":ok\n\n"));
162
-
163
- // Ping every 25s to prevent idle timeout
164
- const ping = setInterval(() => {
165
- try {
166
- ctrl.enqueue(new TextEncoder().encode(":ping\n\n"));
167
- } catch {
168
- clearInterval(ping);
169
- sseClients.delete(ctrl);
170
- }
171
- }, 25_000);
172
-
173
- req.signal.addEventListener("abort", () => {
174
- clearInterval(ping);
175
- sseClients.delete(ctrl);
176
- });
177
- },
178
- }),
179
- {
180
- headers: {
181
- "Content-Type": "text/event-stream; charset=utf-8",
182
- "Cache-Control": "no-cache",
183
- Connection: "keep-alive",
184
- },
185
- },
186
- );
187
- }
188
-
189
- // Proxy everything else to the app server
190
- try {
191
- const target = new URL(req.url);
192
- target.hostname = "localhost";
193
- target.port = String(APP_PORT);
194
-
195
- return await fetch(new Request(target.toString(), {
196
- method: req.method,
197
- headers: req.headers,
198
- body: req.body,
199
- redirect: "manual",
200
- }));
201
- } catch {
202
- return new Response("App server is starting...", {
203
- status: 503,
204
- headers: { "Content-Type": "text/plain", "Retry-After": "1" },
205
- });
206
- }
207
- },
151
+ port: DEV_PORT,
152
+ idleTimeout: 255,
153
+ async fetch(req) {
154
+ const url = new URL(req.url);
155
+
156
+ // SSE endpoint — owned by dev server, not the app
157
+ if (url.pathname === "/__bosia/sse") {
158
+ return new Response(
159
+ new ReadableStream({
160
+ start(ctrl) {
161
+ sseClients.add(ctrl);
162
+ // Initial keepalive so the browser knows the connection is open
163
+ ctrl.enqueue(new TextEncoder().encode(":ok\n\n"));
164
+
165
+ // Ping every 25s to prevent idle timeout
166
+ const ping = setInterval(() => {
167
+ try {
168
+ ctrl.enqueue(new TextEncoder().encode(":ping\n\n"));
169
+ } catch {
170
+ clearInterval(ping);
171
+ sseClients.delete(ctrl);
172
+ }
173
+ }, 25_000);
174
+
175
+ req.signal.addEventListener("abort", () => {
176
+ clearInterval(ping);
177
+ sseClients.delete(ctrl);
178
+ });
179
+ },
180
+ }),
181
+ {
182
+ headers: {
183
+ "Content-Type": "text/event-stream; charset=utf-8",
184
+ "Cache-Control": "no-cache",
185
+ Connection: "keep-alive",
186
+ },
187
+ },
188
+ );
189
+ }
190
+
191
+ // Proxy everything else to the app server
192
+ try {
193
+ const target = new URL(req.url);
194
+ target.hostname = "localhost";
195
+ target.port = String(APP_PORT);
196
+
197
+ return await fetch(
198
+ new Request(target.toString(), {
199
+ method: req.method,
200
+ headers: req.headers,
201
+ body: req.body,
202
+ redirect: "manual",
203
+ }),
204
+ );
205
+ } catch {
206
+ return new Response("App server is starting...", {
207
+ status: 503,
208
+ headers: { "Content-Type": "text/plain", "Retry-After": "1" },
209
+ });
210
+ }
211
+ },
208
212
  });
209
213
 
210
214
  // ─── Initial Build ────────────────────────────────────────
@@ -216,25 +220,18 @@ console.log(`\n🌐 Open http://localhost:${DEV_PORT}\n`);
216
220
  // ─── File Watcher ─────────────────────────────────────────
217
221
  // Watch src/ recursively. Skip generated files to avoid loops.
218
222
 
219
- const GENERATED = [
220
- join(process.cwd(), ".bosia"),
221
- join(process.cwd(), "public", "bosia-tw.css"),
222
- ];
223
+ const GENERATED = [join(process.cwd(), ".bosia"), join(process.cwd(), "public", "bosia-tw.css")];
223
224
 
224
225
  function isGenerated(path: string): boolean {
225
- return GENERATED.some(g => path.startsWith(g));
226
+ return GENERATED.some((g) => path.startsWith(g));
226
227
  }
227
228
 
228
- watch(
229
- join(process.cwd(), "src"),
230
- { recursive: true },
231
- (_event, filename) => {
232
- if (!filename) return;
233
- const abs = join(process.cwd(), "src", filename);
234
- if (isGenerated(abs)) return;
235
- console.log(`[watch] changed: ${filename}`);
236
- scheduleBuild();
237
- },
238
- );
229
+ watch(join(process.cwd(), "src"), { recursive: true }, (_event, filename) => {
230
+ if (!filename) return;
231
+ const abs = join(process.cwd(), "src", filename);
232
+ if (isGenerated(abs)) return;
233
+ console.log(`[watch] changed: ${filename}`);
234
+ scheduleBuild();
235
+ });
239
236
 
240
237
  console.log("👀 Watching src/ for changes...\n");