bosia 0.1.25 → 0.2.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.
- package/package.json +1 -1
- package/src/core/build.ts +38 -31
- package/src/core/dedup.ts +32 -0
- package/src/core/hooks.ts +6 -4
- package/src/core/server.ts +21 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": [
|
package/src/core/build.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { SveltePlugin } from "bun-plugin-svelte";
|
|
2
2
|
import { writeFileSync, rmSync, mkdirSync } from "fs";
|
|
3
3
|
import { join, relative } from "path";
|
|
4
|
-
import { spawnSync } from "bun";
|
|
5
4
|
|
|
6
5
|
import { scanRoutes } from "./scanner.ts";
|
|
7
6
|
import { generateRoutesFile } from "./routeFile.ts";
|
|
@@ -19,6 +18,7 @@ const CORE_DIR = import.meta.dir;
|
|
|
19
18
|
|
|
20
19
|
const isProduction = process.env.NODE_ENV === "production";
|
|
21
20
|
|
|
21
|
+
const buildStart = performance.now();
|
|
22
22
|
console.log("🏗️ Starting Bosia build...\n");
|
|
23
23
|
|
|
24
24
|
// 0. Load .env files (before cleaning .bosia so loadEnv can set process.env early)
|
|
@@ -55,21 +55,17 @@ ensureRootDirs();
|
|
|
55
55
|
// 2d. Generate .bosia/env.server.ts, .bosia/env.client.ts, .bosia/types/env.d.ts
|
|
56
56
|
generateEnvModules(classifiedEnv);
|
|
57
57
|
|
|
58
|
-
// 3.
|
|
59
|
-
console.log("\n🎨 Building Tailwind CSS...");
|
|
58
|
+
// 3. Start Tailwind CSS (async — runs concurrently with client+server builds)
|
|
60
59
|
const tailwindBin = resolveBosiaBin("tailwindcss");
|
|
61
|
-
const
|
|
60
|
+
const tailwindProc = Bun.spawn(
|
|
62
61
|
[tailwindBin, "-i", "./src/app.css", "-o", "./public/bosia-tw.css", ...(isProduction ? ["--minify"] : [])],
|
|
63
62
|
{
|
|
64
63
|
cwd: process.cwd(),
|
|
65
64
|
env: { ...process.env, NODE_PATH: BOSIA_NODE_PATH },
|
|
65
|
+
stderr: "pipe",
|
|
66
66
|
},
|
|
67
67
|
);
|
|
68
|
-
|
|
69
|
-
console.error("❌ Tailwind CSS build failed:\n" + tailwindResult.stderr.toString());
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
console.log("✅ Tailwind CSS built: public/bosia-tw.css");
|
|
68
|
+
const tailwindPromise = tailwindProc.exited;
|
|
73
69
|
|
|
74
70
|
// Separate plugin instances per build target ($env resolves differently)
|
|
75
71
|
const clientPlugin = makeBosiaPlugin("browser");
|
|
@@ -84,9 +80,9 @@ for (const [key, value] of Object.entries(classifiedEnv.privateStatic)) {
|
|
|
84
80
|
staticDefines[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
// 5. Build client
|
|
88
|
-
console.log("\n📦 Building client
|
|
89
|
-
const
|
|
83
|
+
// 5. Build Tailwind + client + server bundles in parallel
|
|
84
|
+
console.log("\n📦 Building Tailwind + client + server...");
|
|
85
|
+
const clientPromise = Bun.build({
|
|
90
86
|
entrypoints: [join(CORE_DIR, "client", "hydrate.ts")],
|
|
91
87
|
outdir: "./dist/client",
|
|
92
88
|
target: "browser",
|
|
@@ -100,24 +96,7 @@ const clientResult = await Bun.build({
|
|
|
100
96
|
plugins: [clientPlugin, SveltePlugin()],
|
|
101
97
|
});
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
console.error("❌ Client build failed:");
|
|
105
|
-
for (const msg of clientResult.logs) console.error(msg);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 6. Collect output files for dist/manifest.json
|
|
110
|
-
const jsFiles: string[] = [];
|
|
111
|
-
const cssFiles: string[] = [];
|
|
112
|
-
for (const output of clientResult.outputs) {
|
|
113
|
-
const rel = relative("./dist/client", output.path);
|
|
114
|
-
if (output.path.endsWith(".js")) jsFiles.push(rel);
|
|
115
|
-
if (output.path.endsWith(".css")) cssFiles.push(rel);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 7. Build server bundle (before writing manifest so we can record the entry)
|
|
119
|
-
console.log("\n📦 Building server bundle...");
|
|
120
|
-
const serverResult = await Bun.build({
|
|
99
|
+
const serverPromise = Bun.build({
|
|
121
100
|
entrypoints: [join(CORE_DIR, "server.ts")],
|
|
122
101
|
outdir: "./dist/server",
|
|
123
102
|
target: "bun",
|
|
@@ -128,12 +107,40 @@ const serverResult = await Bun.build({
|
|
|
128
107
|
plugins: [serverPlugin, SveltePlugin()],
|
|
129
108
|
});
|
|
130
109
|
|
|
110
|
+
const [tailwindExitCode, clientResult, serverResult] = await Promise.all([
|
|
111
|
+
tailwindPromise,
|
|
112
|
+
clientPromise,
|
|
113
|
+
serverPromise,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
if (tailwindExitCode !== 0) {
|
|
117
|
+
const stderr = await new Response(tailwindProc.stderr).text();
|
|
118
|
+
console.error("❌ Tailwind CSS build failed:\n" + stderr);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
console.log("✅ Tailwind CSS built: public/bosia-tw.css");
|
|
122
|
+
|
|
123
|
+
if (!clientResult.success) {
|
|
124
|
+
console.error("❌ Client build failed:");
|
|
125
|
+
for (const msg of clientResult.logs) console.error(msg);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
131
129
|
if (!serverResult.success) {
|
|
132
130
|
console.error("❌ Server build failed:");
|
|
133
131
|
for (const msg of serverResult.logs) console.error(msg);
|
|
134
132
|
process.exit(1);
|
|
135
133
|
}
|
|
136
134
|
|
|
135
|
+
// 6. Collect output files for dist/manifest.json
|
|
136
|
+
const jsFiles: string[] = [];
|
|
137
|
+
const cssFiles: string[] = [];
|
|
138
|
+
for (const output of clientResult.outputs) {
|
|
139
|
+
const rel = relative("./dist/client", output.path);
|
|
140
|
+
if (output.path.endsWith(".js")) jsFiles.push(rel);
|
|
141
|
+
if (output.path.endsWith(".css")) cssFiles.push(rel);
|
|
142
|
+
}
|
|
143
|
+
|
|
137
144
|
// Entry is always "index.js" due to naming: { entry: "index.[ext]" }
|
|
138
145
|
const serverEntry = serverResult.outputs
|
|
139
146
|
.find(o => o.path.endsWith("index.js"))
|
|
@@ -157,4 +164,4 @@ await prerenderStaticRoutes(manifest);
|
|
|
157
164
|
// 10. Generate static site output (HTML + client assets + public → dist/static/)
|
|
158
165
|
generateStaticSite();
|
|
159
166
|
|
|
160
|
-
console.log(
|
|
167
|
+
console.log(`\n🎉 Build complete in ${Math.round(performance.now() - buildStart)}ms!`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// packages/bosia/src/core/dedup.ts
|
|
2
|
+
// Request deduplication for concurrent identical GET requests to /__bosia/data/
|
|
3
|
+
|
|
4
|
+
const inflight = new Map<string, Promise<any>>();
|
|
5
|
+
|
|
6
|
+
const AUTH_COOKIE_RE = /(?:^|;\s*)authorization=([^;]*)/i;
|
|
7
|
+
|
|
8
|
+
/** Build dedup key from route URL + request identity. Sort search params for consistency. */
|
|
9
|
+
export function dedupKey(url: URL, request: Request): string {
|
|
10
|
+
let path = url.pathname;
|
|
11
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
12
|
+
const sorted = new URLSearchParams([...url.searchParams.entries()].sort());
|
|
13
|
+
const search = sorted.toString();
|
|
14
|
+
const base = search ? `${path}?${search}` : path;
|
|
15
|
+
|
|
16
|
+
const authHeader = request.headers.get("authorization") ?? "";
|
|
17
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
18
|
+
const match = cookieHeader.match(AUTH_COOKIE_RE);
|
|
19
|
+
const authCookie = match?.[1] ?? "";
|
|
20
|
+
const identity = authHeader || authCookie;
|
|
21
|
+
if (!identity) return base;
|
|
22
|
+
return `${base}|${Bun.hash(identity).toString(36)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Run `fn` with dedup. Concurrent calls with same key share the in-flight promise. */
|
|
26
|
+
export function dedup<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
27
|
+
const existing = inflight.get(key);
|
|
28
|
+
if (existing) return existing;
|
|
29
|
+
const promise = fn().finally(() => inflight.delete(key));
|
|
30
|
+
inflight.set(key, promise);
|
|
31
|
+
return promise;
|
|
32
|
+
}
|
package/src/core/hooks.ts
CHANGED
|
@@ -85,10 +85,12 @@ type MaybePromise<T> = T | Promise<T>;
|
|
|
85
85
|
*/
|
|
86
86
|
export function sequence(...handlers: Handle[]): Handle {
|
|
87
87
|
return ({ event, resolve }) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
let next = resolve;
|
|
89
|
+
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
90
|
+
const handler = handlers[i]!;
|
|
91
|
+
const prev = next;
|
|
92
|
+
next = (e: RequestEvent) => handler({ event: e, resolve: prev });
|
|
91
93
|
}
|
|
92
|
-
return
|
|
94
|
+
return next(event);
|
|
93
95
|
};
|
|
94
96
|
}
|
package/src/core/server.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { CorsConfig } from "./cors.ts";
|
|
|
15
15
|
import { isDev, compress, isStaticPath } from "./html.ts";
|
|
16
16
|
import { loadRouteData, loadMetadata, renderSSRStream, renderErrorPage, renderPageWithFormData } from "./renderer.ts";
|
|
17
17
|
import { getServerTime } from "../lib/utils.ts";
|
|
18
|
+
import { dedup, dedupKey } from "./dedup.ts";
|
|
18
19
|
|
|
19
20
|
// ─── User Hooks ──────────────────────────────────────────
|
|
20
21
|
// Load src/hooks.server.ts if present. Uses process.cwd() so
|
|
@@ -128,29 +129,30 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
128
129
|
}
|
|
129
130
|
// Rewrite event.url so logging middleware sees the real page path, not /__bosia/data
|
|
130
131
|
event.url = routeUrl;
|
|
132
|
+
const dedupKeyStr = dedupKey(routeUrl, request);
|
|
131
133
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const cc = (cookies as CookieJar).accessed ? "private, no-cache" : "public, max-age=0, must-revalidate";
|
|
136
|
-
return compress(JSON.stringify({ pageData: {}, layoutData: [] }), "application/json", request, 200, { "Cache-Control": cc });
|
|
137
|
-
}
|
|
134
|
+
const result = await dedup(dedupKeyStr, async () => {
|
|
135
|
+
const pageMatch = findMatch(serverRoutes, routeUrl.pathname);
|
|
136
|
+
const data = await loadRouteData(routeUrl, locals, request, cookies);
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
}
|
|
138
|
+
let metadata = null;
|
|
139
|
+
if (pageMatch) {
|
|
140
|
+
try {
|
|
141
|
+
const meta = await loadMetadata(pageMatch.route, pageMatch.params, routeUrl, locals, cookies, request);
|
|
142
|
+
if (meta) metadata = { title: meta.title, description: meta.description };
|
|
143
|
+
} catch { /* non-fatal */ }
|
|
144
|
+
}
|
|
147
145
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
146
|
+
return { data, metadata, cookiesAccessed: (cookies as CookieJar).accessed };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const cookiesWereAccessed = (cookies as CookieJar).accessed || result.cookiesAccessed;
|
|
150
|
+
const cc = cookiesWereAccessed ? "private, no-cache" : "public, max-age=0, must-revalidate";
|
|
152
151
|
|
|
153
|
-
|
|
152
|
+
if (!result.data) {
|
|
153
|
+
return compress(JSON.stringify({ pageData: {}, layoutData: [] }), "application/json", request, 200, { "Cache-Control": cc });
|
|
154
|
+
}
|
|
155
|
+
return compress(JSON.stringify({ ...result.data, metadata: result.metadata }), "application/json", request, 200, { "Cache-Control": cc });
|
|
154
156
|
} catch (err) {
|
|
155
157
|
if (err instanceof Redirect) {
|
|
156
158
|
return compress(JSON.stringify({ redirect: err.location, status: err.status }), "application/json", request);
|