bosia 0.4.1 → 0.4.3
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/package.json +2 -2
- package/src/cli/create.ts +4 -1
- package/src/core/build.ts +3 -3
- package/src/core/html.ts +4 -1
- package/src/core/renderer.ts +19 -16
- package/src/core/server.ts +9 -12
- package/src/core/svelteCompiler.ts +45 -0
- package/templates/default/.prettierignore +1 -0
- package/templates/default/_gitignore +12 -0
- package/templates/demo/.prettierignore +1 -0
- package/templates/demo/_gitignore +12 -0
- package/templates/todo/.prettierignore +1 -0
- package/templates/todo/_gitignore +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"check": "tsc --noEmit && prettier --check .",
|
|
44
|
+
"check:templates": "bun scripts/check-templates.ts",
|
|
44
45
|
"test": "bun test",
|
|
45
46
|
"test:watch": "bun test --watch"
|
|
46
47
|
},
|
|
@@ -51,7 +52,6 @@
|
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@clack/prompts": "^1.1.0",
|
|
53
54
|
"@tailwindcss/cli": "^4.2.1",
|
|
54
|
-
"bun-plugin-svelte": "^0.0.6",
|
|
55
55
|
"elysia": "^1.4.26",
|
|
56
56
|
"magic-string": "^0.30.0",
|
|
57
57
|
"svelte": "^5.53.6",
|
package/src/cli/create.ts
CHANGED
|
@@ -148,7 +148,10 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
|
|
|
148
148
|
mkdirSync(dest, { recursive: true });
|
|
149
149
|
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
150
150
|
const srcPath = join(src, entry.name);
|
|
151
|
-
|
|
151
|
+
// npm pack strips `.gitignore` from published packages, so templates ship
|
|
152
|
+
// it as `_gitignore` and we restore the dotfile name on copy.
|
|
153
|
+
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
154
|
+
const destPath = join(dest, destName);
|
|
152
155
|
|
|
153
156
|
// Do not copy instructions.txt or template.json to the final project
|
|
154
157
|
if (entry.name === "instructions.txt" || entry.name === "template.json") continue;
|
package/src/core/build.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SveltePlugin } from "bun-plugin-svelte";
|
|
2
1
|
import { writeFileSync, rmSync, mkdirSync } from "fs";
|
|
3
2
|
import { join, relative } from "path";
|
|
4
3
|
|
|
@@ -6,6 +5,7 @@ import { scanRoutes } from "./scanner.ts";
|
|
|
6
5
|
import { generateRoutesFile } from "./routeFile.ts";
|
|
7
6
|
import { generateRouteTypes, ensureRootDirs } from "./routeTypes.ts";
|
|
8
7
|
import { makeBosiaPlugin } from "./plugin.ts";
|
|
8
|
+
import { makeBosiaSvelteCompiler } from "./svelteCompiler.ts";
|
|
9
9
|
import { prerenderStaticRoutes, generateStaticSite } from "./prerender.ts";
|
|
10
10
|
import { loadEnv, classifyEnvVars } from "./env.ts";
|
|
11
11
|
import { generateEnvModules } from "./envCodegen.ts";
|
|
@@ -134,7 +134,7 @@ const clientPromise = Bun.build({
|
|
|
134
134
|
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "development"),
|
|
135
135
|
...staticDefines,
|
|
136
136
|
},
|
|
137
|
-
plugins: [clientPlugin, ...userClientBunPlugins,
|
|
137
|
+
plugins: [clientPlugin, ...userClientBunPlugins, makeBosiaSvelteCompiler("browser")],
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
const serverPromise = Bun.build({
|
|
@@ -145,7 +145,7 @@ const serverPromise = Bun.build({
|
|
|
145
145
|
naming: { entry: "index.[ext]", chunk: "[name]-[hash].[ext]" },
|
|
146
146
|
minify: isProduction,
|
|
147
147
|
external: ["elysia"],
|
|
148
|
-
plugins: [serverPlugin, ...userServerBunPlugins,
|
|
148
|
+
plugins: [serverPlugin, ...userServerBunPlugins, makeBosiaSvelteCompiler("bun")],
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
const [tailwindExitCode, clientResult, serverResult] = await Promise.all([
|
package/src/core/html.ts
CHANGED
|
@@ -43,7 +43,7 @@ export function safeJsonStringify(data: unknown): string {
|
|
|
43
43
|
* Only exposes keys tracked by loadEnv() — never leaks system env vars
|
|
44
44
|
* that happen to start with PUBLIC_.
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
const _publicDynamicEnv: Record<string, string> = (() => {
|
|
47
47
|
const declared = getDeclaredEnvKeys();
|
|
48
48
|
const result: Record<string, string> = {};
|
|
49
49
|
for (const key of declared) {
|
|
@@ -53,6 +53,9 @@ function getPublicDynamicEnv(): Record<string, string> {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
return result;
|
|
56
|
+
})();
|
|
57
|
+
function getPublicDynamicEnv(): Record<string, string> {
|
|
58
|
+
return _publicDynamicEnv;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
// ─── Lang Validation ──────────────────────────────────────
|
package/src/core/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { render } from "svelte/server";
|
|
|
2
2
|
|
|
3
3
|
import { findMatch } from "./matcher.ts";
|
|
4
4
|
import { serverRoutes, errorPage } from "bosia:routes";
|
|
5
|
+
import type { RouteMatch } from "./types.ts";
|
|
5
6
|
import type { Cookies } from "./hooks.ts";
|
|
6
7
|
import { HttpError, Redirect } from "./errors.ts";
|
|
7
8
|
import { pickErrorPage, type ErrorOrigin } from "./errorMatch.ts";
|
|
@@ -166,30 +167,33 @@ export async function loadRouteData(
|
|
|
166
167
|
req: Request,
|
|
167
168
|
cookies: Cookies,
|
|
168
169
|
metadataData: Record<string, any> | null = null,
|
|
170
|
+
match?: RouteMatch<(typeof serverRoutes)[number]> | null,
|
|
169
171
|
) {
|
|
170
|
-
|
|
172
|
+
match ??= findMatch(serverRoutes, url.pathname);
|
|
171
173
|
if (!match) return null;
|
|
172
174
|
|
|
173
175
|
const { route, params } = match;
|
|
174
176
|
const fetch = makeFetch(req, url);
|
|
175
177
|
const layoutData: Record<string, any>[] = [];
|
|
178
|
+
let parentData: Record<string, any> = {};
|
|
176
179
|
|
|
177
180
|
// Run layout server loaders root → leaf, each gets parent() data
|
|
178
181
|
for (const ls of route.layoutServers) {
|
|
179
182
|
try {
|
|
180
183
|
const mod = await ls.loader();
|
|
181
184
|
if (typeof mod.load === "function") {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
layoutData[ls.depth] =
|
|
185
|
+
// Snapshot per layer so loaders cannot mutate the shared accumulator,
|
|
186
|
+
// preserving the same isolation semantics as the previous merge-on-call code.
|
|
187
|
+
const snapshot = { ...parentData };
|
|
188
|
+
const parent = async () => snapshot;
|
|
189
|
+
const result =
|
|
188
190
|
(await withTimeout(
|
|
189
191
|
mod.load({ params, url, locals, cookies, parent, fetch, metadata: null }),
|
|
190
192
|
LOAD_TIMEOUT,
|
|
191
193
|
`layout load (depth=${ls.depth}, ${url.pathname})`,
|
|
192
194
|
)) ?? {};
|
|
195
|
+
layoutData[ls.depth] = result;
|
|
196
|
+
parentData = { ...parentData, ...result };
|
|
193
197
|
}
|
|
194
198
|
} catch (err) {
|
|
195
199
|
if (err instanceof Redirect) throw err;
|
|
@@ -215,11 +219,8 @@ export async function loadRouteData(
|
|
|
215
219
|
if (mod.csr === false) csr = false;
|
|
216
220
|
if (mod.ssr === false) ssr = false;
|
|
217
221
|
if (typeof mod.load === "function") {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
for (const d of layoutData) if (d) Object.assign(merged, d);
|
|
221
|
-
return merged;
|
|
222
|
-
};
|
|
222
|
+
const snapshot = { ...parentData };
|
|
223
|
+
const parent = async () => snapshot;
|
|
223
224
|
pageData =
|
|
224
225
|
(await withTimeout(
|
|
225
226
|
mod.load({
|
|
@@ -289,8 +290,9 @@ export async function renderSSRStream(
|
|
|
289
290
|
locals: Record<string, any>,
|
|
290
291
|
req: Request,
|
|
291
292
|
cookies: Cookies,
|
|
293
|
+
match?: RouteMatch<(typeof serverRoutes)[number]> | null,
|
|
292
294
|
): Promise<Response | null> {
|
|
293
|
-
|
|
295
|
+
match ??= findMatch(serverRoutes, url.pathname);
|
|
294
296
|
if (!match) return null;
|
|
295
297
|
|
|
296
298
|
const { route, params } = match;
|
|
@@ -321,7 +323,7 @@ export async function renderSSRStream(
|
|
|
321
323
|
|
|
322
324
|
try {
|
|
323
325
|
[data, pageMod, layoutMods] = await Promise.all([
|
|
324
|
-
loadRouteData(url, locals, req, cookies, metadataData),
|
|
326
|
+
loadRouteData(url, locals, req, cookies, metadataData, match),
|
|
325
327
|
route.pageModule(),
|
|
326
328
|
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
327
329
|
]);
|
|
@@ -469,15 +471,16 @@ export async function renderPageWithFormData(
|
|
|
469
471
|
cookies: Cookies,
|
|
470
472
|
formData: any,
|
|
471
473
|
status: number,
|
|
474
|
+
match?: RouteMatch<(typeof serverRoutes)[number]> | null,
|
|
472
475
|
): Promise<Response> {
|
|
473
|
-
|
|
476
|
+
match ??= findMatch(serverRoutes, url.pathname);
|
|
474
477
|
if (!match) return renderErrorPage(404, "Not Found", url, req);
|
|
475
478
|
|
|
476
479
|
const { route } = match;
|
|
477
480
|
|
|
478
481
|
// Load components + data in parallel
|
|
479
482
|
const [data, pageMod, layoutMods] = await Promise.all([
|
|
480
|
-
loadRouteData(url, locals, req, cookies),
|
|
483
|
+
loadRouteData(url, locals, req, cookies, null, match),
|
|
481
484
|
route.pageModule(),
|
|
482
485
|
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
483
486
|
]);
|
package/src/core/server.ts
CHANGED
|
@@ -315,12 +315,14 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// Resolve the page route once; reuse for trailing-slash, form-action, and SSR phases.
|
|
319
|
+
const pageMatch = findMatch(serverRoutes, path);
|
|
320
|
+
|
|
318
321
|
// Trailing-slash canonicalization — 308 preserves method (form POSTs included)
|
|
319
|
-
|
|
320
|
-
if (canonicalMatch) {
|
|
322
|
+
if (pageMatch) {
|
|
321
323
|
const canonical = canonicalPathname(
|
|
322
324
|
path,
|
|
323
|
-
(
|
|
325
|
+
(pageMatch.route as any).trailingSlash ?? "never",
|
|
324
326
|
);
|
|
325
327
|
if (canonical !== null) {
|
|
326
328
|
return new Response(null, {
|
|
@@ -332,7 +334,6 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
332
334
|
|
|
333
335
|
// Form actions — POST to page routes with `actions` export
|
|
334
336
|
if (method === "POST") {
|
|
335
|
-
const pageMatch = findMatch(serverRoutes, path);
|
|
336
337
|
if (pageMatch?.route.pageServer) {
|
|
337
338
|
// `use:enhance` sets this header — return JSON instead of re-rendering HTML
|
|
338
339
|
const isEnhanced = request.headers.get("x-bosia-action") === "1";
|
|
@@ -421,6 +422,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
421
422
|
cookies,
|
|
422
423
|
result.data,
|
|
423
424
|
result.status,
|
|
425
|
+
pageMatch,
|
|
424
426
|
);
|
|
425
427
|
}
|
|
426
428
|
|
|
@@ -439,6 +441,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
439
441
|
cookies,
|
|
440
442
|
result ?? null,
|
|
441
443
|
200,
|
|
444
|
+
pageMatch,
|
|
442
445
|
);
|
|
443
446
|
}
|
|
444
447
|
} catch (err) {
|
|
@@ -478,7 +481,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
478
481
|
}
|
|
479
482
|
|
|
480
483
|
// SSR pages (+page.svelte) — streaming by default
|
|
481
|
-
const streamResponse = await renderSSRStream(url, locals, request, cookies);
|
|
484
|
+
const streamResponse = await renderSSRStream(url, locals, request, cookies, pageMatch);
|
|
482
485
|
if (!streamResponse) return renderErrorPage(404, "Not Found", url, request);
|
|
483
486
|
return streamResponse;
|
|
484
487
|
}
|
|
@@ -659,18 +662,12 @@ for (const plugin of plugins) {
|
|
|
659
662
|
|
|
660
663
|
app = app
|
|
661
664
|
// Static files are served by resolve() with path traversal protection and security headers
|
|
662
|
-
// API routes must intercept all HTTP methods before the GET catch-all
|
|
663
|
-
.onBeforeHandle(async ({ request }) => {
|
|
664
|
-
const url = new URL(request.url);
|
|
665
|
-
if (!findMatch(apiRoutes, url.pathname)) return; // not an API route
|
|
666
|
-
return handleRequest(request, url);
|
|
667
|
-
})
|
|
668
665
|
// SSR pages
|
|
669
666
|
.get("*", ({ request }: { request: Request }) => {
|
|
670
667
|
const url = new URL(request.url);
|
|
671
668
|
return handleRequest(request, url);
|
|
672
669
|
})
|
|
673
|
-
// Non-GET catch-alls
|
|
670
|
+
// Non-GET catch-alls route every method through handleRequest()
|
|
674
671
|
.post("*", ({ request }: { request: Request }) => {
|
|
675
672
|
const url = new URL(request.url);
|
|
676
673
|
return handleRequest(request, url);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { compile, compileModule } from "svelte/compiler";
|
|
2
|
+
import type { BunPlugin } from "bun";
|
|
3
|
+
|
|
4
|
+
const svelteHash = (s: string) => Bun.hash(s, 5381).toString(36);
|
|
5
|
+
|
|
6
|
+
export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
7
|
+
const generate = target === "browser" ? "client" : "server";
|
|
8
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
name: "bosia-svelte-compiler",
|
|
12
|
+
setup(build) {
|
|
13
|
+
const ts = new Bun.Transpiler({
|
|
14
|
+
loader: "ts",
|
|
15
|
+
target: target === "browser" ? "browser" : "bun",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
|
|
19
|
+
const source = await Bun.file(args.path).text();
|
|
20
|
+
const result = compile(source, {
|
|
21
|
+
generate,
|
|
22
|
+
css: target === "browser" ? "injected" : "external",
|
|
23
|
+
dev,
|
|
24
|
+
hmr: false,
|
|
25
|
+
cssHash: ({ css }) => `svelte-${svelteHash(css)}`,
|
|
26
|
+
filename: args.path,
|
|
27
|
+
});
|
|
28
|
+
return { contents: result.js.code, loader: "ts" };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
build.onLoad({ filter: /\.svelte\.[tj]s$/ }, async (args) => {
|
|
32
|
+
let source = await Bun.file(args.path).text();
|
|
33
|
+
if (args.path.endsWith(".ts")) {
|
|
34
|
+
source = await ts.transform(source);
|
|
35
|
+
}
|
|
36
|
+
const result = compileModule(source, {
|
|
37
|
+
generate,
|
|
38
|
+
dev,
|
|
39
|
+
filename: args.path,
|
|
40
|
+
});
|
|
41
|
+
return { contents: result.js.code, loader: "js" };
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|