bosia 0.1.26 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.1.26",
3
+ "version": "0.2.1",
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!`);
@@ -2,9 +2,12 @@ import { hydrate } from "svelte";
2
2
  import App from "./App.svelte";
3
3
  import { router } from "./router.svelte.ts";
4
4
  import { initPrefetch } from "./prefetch.ts";
5
- import { findMatch } from "../matcher.ts";
5
+ import { findMatch, compileRoutes } from "../matcher.ts";
6
6
  import { clientRoutes } from "bosia:routes";
7
7
 
8
+ // Pre-compile route patterns into RegExp at startup (shared by App.svelte and router via module reference)
9
+ compileRoutes(clientRoutes);
10
+
8
11
  // ─── Hydration ────────────────────────────────────────────
9
12
 
10
13
  async function main() {
@@ -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
  }
@@ -9,6 +9,78 @@ import type { RouteMatch } from "./types.ts";
9
9
  // 2. Dynamic match — "/blog/[slug]" matches "/blog/hello"
10
10
  // 3. Catch-all match — "/[...rest]" matches anything
11
11
 
12
+ // ─── Compiled Route Types ────────────────────────────────
13
+
14
+ interface CompiledRoute {
15
+ regex: RegExp;
16
+ paramNames: string[];
17
+ isExact: boolean;
18
+ }
19
+
20
+ // ─── Pattern Compiler ────────────────────────────────────
21
+
22
+ /** Escape regex special chars in a literal string segment. */
23
+ function escapeRegex(s: string): string {
24
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ }
26
+
27
+ /**
28
+ * Pre-compile a route pattern into a RegExp for fast matching.
29
+ */
30
+ function compilePattern(pattern: string): CompiledRoute {
31
+ // No dynamic segments — exact match via ===
32
+ if (!pattern.includes("[")) {
33
+ return { regex: null!, paramNames: [], isExact: true };
34
+ }
35
+
36
+ const paramNames: string[] = [];
37
+
38
+ // Catch-all: /prefix/[...name]
39
+ const catchallMatch = pattern.match(/^(.*?)\/\[\.\.\.(\w+)\]$/);
40
+ if (catchallMatch) {
41
+ const prefix = catchallMatch[1] || "";
42
+ paramNames.push(catchallMatch[2]!);
43
+ const escaped = prefix ? escapeRegex(prefix) : "";
44
+ // Root catch-all /[...rest] must have at least one char after /
45
+ const regex = prefix
46
+ ? new RegExp(`^${escaped}\\/(.+)$`)
47
+ : new RegExp(`^\\/(.+)$`);
48
+ return { regex, paramNames, isExact: false };
49
+ }
50
+
51
+ // Dynamic segments: /blog/[slug]/comments → ^\/blog\/([^/]+)\/comments$
52
+ const segments = pattern.split("/").filter(Boolean);
53
+ let regexStr = "^";
54
+ for (const seg of segments) {
55
+ regexStr += "\\/";
56
+ if (seg.startsWith("[") && seg.endsWith("]")) {
57
+ paramNames.push(seg.slice(1, -1));
58
+ regexStr += "([^/]+)";
59
+ } else {
60
+ regexStr += escapeRegex(seg);
61
+ }
62
+ }
63
+ regexStr += "$";
64
+
65
+ return { regex: new RegExp(regexStr), paramNames, isExact: false };
66
+ }
67
+
68
+ /**
69
+ * Pre-compile all route patterns in-place.
70
+ * Mutates each route by adding a `_compiled` property.
71
+ * Call once at startup — all modules sharing the same route array see the result.
72
+ */
73
+ export function compileRoutes<T extends { pattern: string }>(
74
+ routes: T[],
75
+ ): (T & { _compiled: CompiledRoute })[] {
76
+ for (const route of routes) {
77
+ (route as any)._compiled = compilePattern(route.pattern);
78
+ }
79
+ return routes as (T & { _compiled: CompiledRoute })[];
80
+ }
81
+
82
+ // ─── Legacy Pattern Matcher (fallback for uncompiled routes) ─
83
+
12
84
  /**
13
85
  * Match a URL pathname against a single route pattern.
14
86
  * Returns extracted params if matched, null otherwise.
@@ -60,6 +132,31 @@ function matchPattern(
60
132
  return params;
61
133
  }
62
134
 
135
+ // ─── Route Matching ──────────────────────────────────────
136
+
137
+ /**
138
+ * Match a compiled route against a pathname using regex.
139
+ * Returns extracted params if matched, null otherwise.
140
+ */
141
+ function matchCompiled(
142
+ compiled: CompiledRoute,
143
+ pattern: string,
144
+ pathname: string,
145
+ ): Record<string, string> | null {
146
+ if (compiled.isExact) {
147
+ return pattern === pathname ? {} : null;
148
+ }
149
+
150
+ const m = compiled.regex.exec(pathname);
151
+ if (!m) return null;
152
+
153
+ const params: Record<string, string> = {};
154
+ for (let i = 0; i < compiled.paramNames.length; i++) {
155
+ params[compiled.paramNames[i]!] = m[i + 1]!;
156
+ }
157
+ return params;
158
+ }
159
+
63
160
  /**
64
161
  * Find the first matching route from a list.
65
162
  * Routes must be pre-sorted by priority (exact → dynamic → catch-all).
@@ -75,7 +172,10 @@ export function findMatch<T extends { pattern: string }>(
75
172
  }
76
173
 
77
174
  for (const route of routes) {
78
- const params = matchPattern(route.pattern, pathname);
175
+ const compiled = (route as any)._compiled as CompiledRoute | undefined;
176
+ const params = compiled
177
+ ? matchCompiled(compiled, route.pattern, pathname)
178
+ : matchPattern(route.pattern, pathname);
79
179
  if (params !== null) return { route, params };
80
180
  }
81
181
 
@@ -3,8 +3,12 @@ import { Elysia } from "elysia";
3
3
  import { existsSync } from "fs";
4
4
  import { join, resolve as resolvePath } from "path";
5
5
 
6
- import { findMatch } from "./matcher.ts";
6
+ import { findMatch, compileRoutes } from "./matcher.ts";
7
7
  import { apiRoutes, serverRoutes } from "bosia:routes";
8
+
9
+ // Pre-compile route patterns into RegExp at startup (shared by renderer.ts via module reference)
10
+ compileRoutes(apiRoutes);
11
+ compileRoutes(serverRoutes);
8
12
  import type { Handle, RequestEvent } from "./hooks.ts";
9
13
  import { HttpError, Redirect, ActionFailure } from "./errors.ts";
10
14
  import { CookieJar } from "./cookies.ts";
@@ -15,6 +19,7 @@ import type { CorsConfig } from "./cors.ts";
15
19
  import { isDev, compress, isStaticPath } from "./html.ts";
16
20
  import { loadRouteData, loadMetadata, renderSSRStream, renderErrorPage, renderPageWithFormData } from "./renderer.ts";
17
21
  import { getServerTime } from "../lib/utils.ts";
22
+ import { dedup, dedupKey } from "./dedup.ts";
18
23
 
19
24
  // ─── User Hooks ──────────────────────────────────────────
20
25
  // Load src/hooks.server.ts if present. Uses process.cwd() so
@@ -128,29 +133,30 @@ async function resolve(event: RequestEvent): Promise<Response> {
128
133
  }
129
134
  // Rewrite event.url so logging middleware sees the real page path, not /__bosia/data
130
135
  event.url = routeUrl;
136
+ const dedupKeyStr = dedupKey(routeUrl, request);
131
137
  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
- }
138
+ const result = await dedup(dedupKeyStr, async () => {
139
+ const pageMatch = findMatch(serverRoutes, routeUrl.pathname);
140
+ const data = await loadRouteData(routeUrl, locals, request, cookies);
138
141
 
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
- }
142
+ let metadata = null;
143
+ if (pageMatch) {
144
+ try {
145
+ const meta = await loadMetadata(pageMatch.route, pageMatch.params, routeUrl, locals, cookies, request);
146
+ if (meta) metadata = { title: meta.title, description: meta.description };
147
+ } catch { /* non-fatal */ }
148
+ }
149
+
150
+ return { data, metadata, cookiesAccessed: (cookies as CookieJar).accessed };
151
+ });
147
152
 
148
- const cacheControl = (cookies as CookieJar).accessed
149
- ? "private, no-cache"
150
- : "public, max-age=0, must-revalidate";
151
- const cacheHeaders = { "Cache-Control": cacheControl };
153
+ const cookiesWereAccessed = (cookies as CookieJar).accessed || result.cookiesAccessed;
154
+ const cc = cookiesWereAccessed ? "private, no-cache" : "public, max-age=0, must-revalidate";
152
155
 
153
- return compress(JSON.stringify({ ...data, metadata }), "application/json", request, 200, cacheHeaders);
156
+ if (!result.data) {
157
+ return compress(JSON.stringify({ pageData: {}, layoutData: [] }), "application/json", request, 200, { "Cache-Control": cc });
158
+ }
159
+ return compress(JSON.stringify({ ...result.data, metadata: result.metadata }), "application/json", request, 200, { "Cache-Control": cc });
154
160
  } catch (err) {
155
161
  if (err instanceof Redirect) {
156
162
  return compress(JSON.stringify({ redirect: err.location, status: err.status }), "application/json", request);