bosia 0.2.0 → 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.2.0",
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": [
@@ -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() {
@@ -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";