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 +1 -1
- package/src/core/client/hydrate.ts +4 -1
- package/src/core/matcher.ts +101 -1
- package/src/core/server.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.2.
|
|
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() {
|
package/src/core/matcher.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/core/server.ts
CHANGED
|
@@ -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";
|