bosia 0.1.26 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.1.26",
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. Build Tailwind CSS
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 tailwindResult = spawnSync(
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
- if (tailwindResult.exitCode !== 0) {
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 bundle
88
- console.log("\n📦 Building client bundle...");
89
- const clientResult = await Bun.build({
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
- if (!clientResult.success) {
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("\n🎉 Build complete!");
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
- function apply(i: number, e: RequestEvent): MaybePromise<Response> {
89
- if (i >= handlers.length) return resolve(e);
90
- return handlers[i]!({ event: e, resolve: (next) => apply(i + 1, next) });
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 apply(0, event);
94
+ return next(event);
93
95
  };
94
96
  }
@@ -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 pageMatch = findMatch(serverRoutes, routeUrl.pathname);
133
- const data = await loadRouteData(routeUrl, locals, request, cookies);
134
- if (!data) {
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
- // Include metadata for client-side title/description updates
140
- let metadata = null;
141
- if (pageMatch) {
142
- try {
143
- const meta = await loadMetadata(pageMatch.route, pageMatch.params, routeUrl, locals, cookies, request);
144
- if (meta) metadata = { title: meta.title, description: meta.description };
145
- } catch { /* non-fatal */ }
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
- const cacheControl = (cookies as CookieJar).accessed
149
- ? "private, no-cache"
150
- : "public, max-age=0, must-revalidate";
151
- const cacheHeaders = { "Cache-Control": cacheControl };
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
- return compress(JSON.stringify({ ...data, metadata }), "application/json", request, 200, cacheHeaders);
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);