bosia 0.2.3 → 0.3.1

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 (86) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -54
  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 +7 -9
  8. package/src/cli/feat.ts +266 -258
  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 -156
  15. package/src/core/client/appState.svelte.ts +33 -31
  16. package/src/core/client/enhance.ts +83 -78
  17. package/src/core/client/hydrate.ts +95 -81
  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 +196 -168
  25. package/src/core/env.ts +160 -148
  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 +184 -145
  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 -122
  34. package/src/core/renderer.ts +359 -286
  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 +538 -424
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/index.ts +8 -8
  41. package/src/lib/utils.ts +44 -30
  42. package/templates/default/.prettierignore +5 -0
  43. package/templates/default/.prettierrc.json +9 -0
  44. package/templates/default/README.md +5 -5
  45. package/templates/default/package.json +22 -18
  46. package/templates/default/src/app.css +80 -80
  47. package/templates/default/src/app.d.ts +3 -3
  48. package/templates/default/src/routes/+error.svelte +7 -10
  49. package/templates/default/src/routes/+layout.svelte +2 -2
  50. package/templates/default/src/routes/+page.svelte +30 -32
  51. package/templates/default/src/routes/about/+page.svelte +3 -3
  52. package/templates/default/tsconfig.json +20 -20
  53. package/templates/demo/.prettierignore +5 -0
  54. package/templates/demo/.prettierrc.json +9 -0
  55. package/templates/demo/README.md +9 -9
  56. package/templates/demo/package.json +22 -17
  57. package/templates/demo/src/app.css +80 -80
  58. package/templates/demo/src/app.d.ts +3 -3
  59. package/templates/demo/src/hooks.server.ts +9 -9
  60. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  61. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  62. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  63. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  64. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  65. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  67. package/templates/demo/src/routes/+error.svelte +10 -7
  68. package/templates/demo/src/routes/+layout.server.ts +4 -4
  69. package/templates/demo/src/routes/+layout.svelte +2 -2
  70. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  71. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  72. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  73. package/templates/demo/tsconfig.json +20 -20
  74. package/templates/todo/.prettierignore +5 -0
  75. package/templates/todo/.prettierrc.json +9 -0
  76. package/templates/todo/README.md +9 -9
  77. package/templates/todo/package.json +22 -17
  78. package/templates/todo/src/app.css +80 -80
  79. package/templates/todo/src/app.d.ts +7 -7
  80. package/templates/todo/src/hooks.server.ts +9 -9
  81. package/templates/todo/src/routes/+error.svelte +10 -7
  82. package/templates/todo/src/routes/+layout.server.ts +4 -4
  83. package/templates/todo/src/routes/+layout.svelte +2 -2
  84. package/templates/todo/src/routes/+page.svelte +44 -44
  85. package/templates/todo/template.json +1 -1
  86. package/templates/todo/tsconfig.json +20 -20
package/src/core/dev.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  import { spawn, type Subprocess } from "bun";
2
2
  import { watch } from "fs";
3
3
  import { join } from "path";
4
+ import { loadEnv, resetDeclaredKeys } from "./env.ts";
5
+
6
+ // Snapshot pure shell env BEFORE any loadEnv call pollutes process.env.
7
+ // On `.env*` change we restore from this snapshot, then re-run loadEnv,
8
+ // so removed/renamed keys no longer linger in the dev process.
9
+ const SHELL_ENV_SNAPSHOT: Record<string, string | undefined> = { ...process.env };
10
+
11
+ loadEnv("development");
12
+
13
+ function reloadEnv() {
14
+ for (const k of Object.keys(process.env)) delete process.env[k];
15
+ for (const [k, v] of Object.entries(SHELL_ENV_SNAPSHOT)) {
16
+ if (v !== undefined) process.env[k] = v;
17
+ }
18
+ resetDeclaredKeys();
19
+ loadEnv("development");
20
+ }
4
21
 
5
22
  console.log("⬡ Bosia dev server starting...\n");
6
23
 
@@ -17,17 +34,17 @@ const RAPID_CRASH_WINDOW = 5_000; // 5 seconds
17
34
  // ─── SSE Broadcast ────────────────────────────────────────
18
35
 
19
36
  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
- }
37
+ const msg = new TextEncoder().encode("event: reload\ndata: ok\n\n");
38
+ for (const ctrl of sseClients) {
39
+ try {
40
+ ctrl.enqueue(msg);
41
+ } catch {
42
+ sseClients.delete(ctrl);
43
+ }
44
+ }
45
+ if (sseClients.size > 0) {
46
+ console.log(`📡 Reload sent to ${sseClients.size} client(s)`);
47
+ }
31
48
  }
32
49
 
33
50
  // ─── Build ────────────────────────────────────────────────
@@ -37,13 +54,13 @@ import { BOSIA_NODE_PATH } from "./paths.ts";
37
54
  const BUILD_SCRIPT = join(import.meta.dir, "build.ts");
38
55
 
39
56
  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;
57
+ console.log("🏗️ Building...");
58
+ const proc = spawn(["bun", "run", BUILD_SCRIPT], {
59
+ stdout: "inherit",
60
+ stderr: "inherit",
61
+ cwd: process.cwd(),
62
+ });
63
+ return (await proc.exited) === 0;
47
64
  }
48
65
 
49
66
  // ─── Ports ────────────────────────────────────────────────
@@ -52,57 +69,59 @@ const DEV_PORT = Number(process.env.PORT) || 9000;
52
69
  const APP_PORT = DEV_PORT + 1; // internal, hidden from user
53
70
 
54
71
  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
- });
72
+ if (appProcess) {
73
+ intentionalKill = true;
74
+ appProcess.kill();
75
+ await appProcess.exited;
76
+ intentionalKill = false;
77
+ }
78
+
79
+ // Read the server entry filename from the manifest written by build.ts
80
+ let serverEntry = "index.js";
81
+ try {
82
+ const manifest = await Bun.file("./dist/manifest.json").json();
83
+ serverEntry = manifest.serverEntry ?? "index.js";
84
+ } catch {}
85
+
86
+ appProcess = spawn(["bun", "run", `dist/server/${serverEntry}`], {
87
+ stdout: "inherit",
88
+ stderr: "inherit",
89
+ cwd: process.cwd(),
90
+ env: {
91
+ ...process.env,
92
+ NODE_ENV: "development",
93
+ // Force app server to APP_PORT — prevents PORT from .env conflicting with the dev proxy
94
+ PORT: String(APP_PORT),
95
+ // Allow externalized deps (elysia, etc.) to resolve from bosia's node_modules
96
+ NODE_PATH: BOSIA_NODE_PATH,
97
+ },
98
+ });
99
+
100
+ // Monitor for unexpected crashes
101
+ const proc = appProcess;
102
+ proc.exited.then((code) => {
103
+ if (proc !== appProcess || intentionalKill) return;
104
+ if (code === 0) return; // clean exit
105
+
106
+ const now = Date.now();
107
+ if (now - lastCrashTime < RAPID_CRASH_WINDOW) {
108
+ crashCount++;
109
+ } else {
110
+ crashCount = 1;
111
+ }
112
+ lastCrashTime = now;
113
+
114
+ if (crashCount >= MAX_RAPID_CRASHES) {
115
+ console.error(
116
+ `\n💥 App crashed ${crashCount} times in ${RAPID_CRASH_WINDOW / 1000}s — waiting for file change to restart\n`,
117
+ );
118
+ crashCount = 0;
119
+ return;
120
+ }
121
+
122
+ console.warn(`\n⚠️ App crashed (exit code ${code}). Restarting...\n`);
123
+ startAppServer();
124
+ });
106
125
  }
107
126
 
108
127
  // ─── Build & Restart ──────────────────────────────────────
@@ -112,33 +131,33 @@ let building = false;
112
131
  let buildPending = false;
113
132
 
114
133
  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
- }
134
+ if (building) {
135
+ buildPending = true;
136
+ return;
137
+ }
138
+ building = true;
139
+ try {
140
+ const ok = await runBuild();
141
+ if (!ok) {
142
+ console.error("❌ Build failed — fix errors and save again");
143
+ return;
144
+ }
145
+ await startAppServer();
146
+ // Give the app server a moment to bind its port
147
+ await Bun.sleep(200);
148
+ broadcastReload();
149
+ } finally {
150
+ building = false;
151
+ }
152
+ if (buildPending) {
153
+ buildPending = false;
154
+ buildAndRestart();
155
+ }
137
156
  }
138
157
 
139
158
  function scheduleBuild() {
140
- if (buildTimer) clearTimeout(buildTimer);
141
- buildTimer = setTimeout(buildAndRestart, 300);
159
+ if (buildTimer) clearTimeout(buildTimer);
160
+ buildTimer = setTimeout(buildAndRestart, 300);
142
161
  }
143
162
 
144
163
  // ─── Dev Proxy ────────────────────────────────────────────
@@ -146,65 +165,67 @@ function scheduleBuild() {
146
165
  // All other requests are proxied to the app server.
147
166
 
148
167
  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
- },
168
+ port: DEV_PORT,
169
+ idleTimeout: 255,
170
+ async fetch(req) {
171
+ const url = new URL(req.url);
172
+
173
+ // SSE endpoint — owned by dev server, not the app
174
+ if (url.pathname === "/__bosia/sse") {
175
+ return new Response(
176
+ new ReadableStream({
177
+ start(ctrl) {
178
+ sseClients.add(ctrl);
179
+ // Initial keepalive so the browser knows the connection is open
180
+ ctrl.enqueue(new TextEncoder().encode(":ok\n\n"));
181
+
182
+ // Ping every 25s to prevent idle timeout
183
+ const ping = setInterval(() => {
184
+ try {
185
+ ctrl.enqueue(new TextEncoder().encode(":ping\n\n"));
186
+ } catch {
187
+ clearInterval(ping);
188
+ sseClients.delete(ctrl);
189
+ }
190
+ }, 25_000);
191
+
192
+ req.signal.addEventListener("abort", () => {
193
+ clearInterval(ping);
194
+ sseClients.delete(ctrl);
195
+ });
196
+ },
197
+ }),
198
+ {
199
+ headers: {
200
+ "Content-Type": "text/event-stream; charset=utf-8",
201
+ "Cache-Control": "no-cache",
202
+ Connection: "keep-alive",
203
+ },
204
+ },
205
+ );
206
+ }
207
+
208
+ // Proxy everything else to the app server
209
+ try {
210
+ const target = new URL(req.url);
211
+ target.hostname = "localhost";
212
+ target.port = String(APP_PORT);
213
+
214
+ return await fetch(
215
+ new Request(target.toString(), {
216
+ method: req.method,
217
+ headers: req.headers,
218
+ body: req.body,
219
+ redirect: "manual",
220
+ }),
221
+ );
222
+ } catch {
223
+ return new Response("App server is starting...", {
224
+ status: 503,
225
+ headers: { "Content-Type": "text/plain", "Retry-After": "1" },
226
+ });
227
+ }
228
+ },
208
229
  });
209
230
 
210
231
  // ─── Initial Build ────────────────────────────────────────
@@ -216,25 +237,32 @@ console.log(`\n🌐 Open http://localhost:${DEV_PORT}\n`);
216
237
  // ─── File Watcher ─────────────────────────────────────────
217
238
  // Watch src/ recursively. Skip generated files to avoid loops.
218
239
 
219
- const GENERATED = [
220
- join(process.cwd(), ".bosia"),
221
- join(process.cwd(), "public", "bosia-tw.css"),
222
- ];
240
+ const GENERATED = [join(process.cwd(), ".bosia"), join(process.cwd(), "public", "bosia-tw.css")];
223
241
 
224
242
  function isGenerated(path: string): boolean {
225
- return GENERATED.some(g => path.startsWith(g));
243
+ return GENERATED.some((g) => path.startsWith(g));
226
244
  }
227
245
 
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
- );
246
+ watch(join(process.cwd(), "src"), { recursive: true }, (_event, filename) => {
247
+ if (!filename) return;
248
+ const abs = join(process.cwd(), "src", filename);
249
+ if (isGenerated(abs)) return;
250
+ console.log(`[watch] changed: ${filename}`);
251
+ scheduleBuild();
252
+ });
253
+
254
+ // ─── .env Watcher ─────────────────────────────────────────
255
+ // Reset to shell-env snapshot and re-run loadEnv so removed/renamed
256
+ // keys don't linger across hot-reloads. The respawn at startAppServer
257
+ // already spreads `...process.env`, so the child picks up the fresh state.
258
+
259
+ const ENV_FILES = new Set([".env", ".env.local", ".env.development", ".env.development.local"]);
260
+
261
+ watch(process.cwd(), { recursive: false }, (_event, filename) => {
262
+ if (!filename || !ENV_FILES.has(filename)) return;
263
+ console.log(`[watch] env changed: ${filename}`);
264
+ reloadEnv();
265
+ scheduleBuild();
266
+ });
239
267
 
240
268
  console.log("👀 Watching src/ for changes...\n");