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.
- package/README.md +39 -39
- package/package.json +56 -54
- package/src/ambient.d.ts +31 -0
- package/src/cli/add.ts +120 -114
- package/src/cli/build.ts +10 -10
- package/src/cli/create.ts +142 -137
- package/src/cli/dev.ts +7 -9
- package/src/cli/feat.ts +266 -258
- package/src/cli/index.ts +51 -42
- package/src/cli/registry.ts +136 -115
- package/src/cli/start.ts +17 -17
- package/src/cli/test.ts +25 -0
- package/src/core/build.ts +72 -56
- package/src/core/client/App.svelte +177 -156
- package/src/core/client/appState.svelte.ts +33 -31
- package/src/core/client/enhance.ts +83 -78
- package/src/core/client/hydrate.ts +95 -81
- package/src/core/client/prefetch.ts +101 -94
- package/src/core/client/router.svelte.ts +64 -51
- package/src/core/cookies.ts +70 -66
- package/src/core/cors.ts +44 -35
- package/src/core/csrf.ts +38 -38
- package/src/core/dedup.ts +17 -17
- package/src/core/dev.ts +196 -168
- package/src/core/env.ts +160 -148
- package/src/core/envCodegen.ts +73 -73
- package/src/core/errors.ts +48 -49
- package/src/core/hooks.ts +50 -50
- package/src/core/html.ts +184 -145
- package/src/core/matcher.ts +130 -121
- package/src/core/paths.ts +8 -10
- package/src/core/plugin.ts +113 -107
- package/src/core/prerender.ts +191 -122
- package/src/core/renderer.ts +359 -286
- package/src/core/routeFile.ts +140 -127
- package/src/core/routeTypes.ts +144 -83
- package/src/core/scanner.ts +125 -95
- package/src/core/server.ts +538 -424
- package/src/core/types.ts +25 -20
- package/src/lib/index.ts +8 -8
- package/src/lib/utils.ts +44 -30
- package/templates/default/.prettierignore +5 -0
- package/templates/default/.prettierrc.json +9 -0
- package/templates/default/README.md +5 -5
- package/templates/default/package.json +22 -18
- package/templates/default/src/app.css +80 -80
- package/templates/default/src/app.d.ts +3 -3
- package/templates/default/src/routes/+error.svelte +7 -10
- package/templates/default/src/routes/+layout.svelte +2 -2
- package/templates/default/src/routes/+page.svelte +30 -32
- package/templates/default/src/routes/about/+page.svelte +3 -3
- package/templates/default/tsconfig.json +20 -20
- package/templates/demo/.prettierignore +5 -0
- package/templates/demo/.prettierrc.json +9 -0
- package/templates/demo/README.md +9 -9
- package/templates/demo/package.json +22 -17
- package/templates/demo/src/app.css +80 -80
- package/templates/demo/src/app.d.ts +3 -3
- package/templates/demo/src/hooks.server.ts +9 -9
- package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
- package/templates/demo/src/routes/(public)/+page.svelte +96 -67
- package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
- package/templates/demo/src/routes/+error.svelte +10 -7
- package/templates/demo/src/routes/+layout.server.ts +4 -4
- package/templates/demo/src/routes/+layout.svelte +2 -2
- package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
- package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
- package/templates/demo/src/routes/api/hello/+server.ts +25 -25
- package/templates/demo/tsconfig.json +20 -20
- package/templates/todo/.prettierignore +5 -0
- package/templates/todo/.prettierrc.json +9 -0
- package/templates/todo/README.md +9 -9
- package/templates/todo/package.json +22 -17
- package/templates/todo/src/app.css +80 -80
- package/templates/todo/src/app.d.ts +7 -7
- package/templates/todo/src/hooks.server.ts +9 -9
- package/templates/todo/src/routes/+error.svelte +10 -7
- package/templates/todo/src/routes/+layout.server.ts +4 -4
- package/templates/todo/src/routes/+layout.svelte +2 -2
- package/templates/todo/src/routes/+page.svelte +44 -44
- package/templates/todo/template.json +1 -1
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
243
|
+
return GENERATED.some((g) => path.startsWith(g));
|
|
226
244
|
}
|
|
227
245
|
|
|
228
|
-
watch(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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");
|