@zenithbuild/router 1.0.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/README.md +127 -0
- package/index.d.ts +38 -0
- package/index.js +5 -0
- package/package.json +65 -0
- package/src/index.ts +108 -0
- package/src/lib.rs +18 -0
- package/src/manifest.rs +189 -0
- package/src/manifest.ts +208 -0
- package/src/navigation/ZenLink.zen +231 -0
- package/src/navigation/index.ts +79 -0
- package/src/navigation/zen-link.ts +585 -0
- package/src/render.rs +8 -0
- package/src/resolve.rs +53 -0
- package/src/runtime.ts +163 -0
- package/src/runtime_gen.rs +14 -0
- package/src/types.rs +46 -0
- package/src/types.ts +63 -0
- package/tsconfig.json +30 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Runtime Router (Native Bridge)
|
|
3
|
+
*
|
|
4
|
+
* SPA-style client-side router with SSR support via native resolution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import native from "../index.js"
|
|
9
|
+
import type {
|
|
10
|
+
RouteState,
|
|
11
|
+
NavigateOptions,
|
|
12
|
+
RouteRecord,
|
|
13
|
+
PageModule,
|
|
14
|
+
RuntimeRouteRecord,
|
|
15
|
+
RouteListener
|
|
16
|
+
} from "./types"
|
|
17
|
+
|
|
18
|
+
const { resolveRouteNative, generateRuntimeRouterNative } = native
|
|
19
|
+
|
|
20
|
+
let currentRoute: RouteState = {
|
|
21
|
+
path: "/",
|
|
22
|
+
params: {},
|
|
23
|
+
query: {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const routeListeners: Set<RouteListener> = new Set()
|
|
27
|
+
let routeManifest: RuntimeRouteRecord[] = []
|
|
28
|
+
let routerOutlet: HTMLElement | null = null
|
|
29
|
+
|
|
30
|
+
export function initRouter(
|
|
31
|
+
manifest: RuntimeRouteRecord[],
|
|
32
|
+
outlet?: HTMLElement | string
|
|
33
|
+
): void {
|
|
34
|
+
routeManifest = manifest
|
|
35
|
+
if (outlet) {
|
|
36
|
+
routerOutlet = typeof outlet === "string" ? document.querySelector(outlet) : outlet
|
|
37
|
+
}
|
|
38
|
+
window.addEventListener("popstate", handlePopState)
|
|
39
|
+
resolveAndRender(window.location.pathname, parseQueryString(window.location.search), false)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseQueryString(search: string): Record<string, string> {
|
|
43
|
+
const query: Record<string, string> = {}
|
|
44
|
+
if (!search || search === "?") return query
|
|
45
|
+
const params = new URLSearchParams(search)
|
|
46
|
+
params.forEach((value, key) => { query[key] = value; })
|
|
47
|
+
return query
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handlePopState(_event: PopStateEvent): void {
|
|
51
|
+
resolveAndRender(window.location.pathname, parseQueryString(window.location.search), false, false)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve route - uses native implementation if available (Node.js/SSR)
|
|
56
|
+
* or falls back to JS implementation for the browser.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveRoute(
|
|
59
|
+
pathname: string
|
|
60
|
+
): { record: RuntimeRouteRecord; params: Record<string, string> } | null {
|
|
61
|
+
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
|
62
|
+
// SSR / Node environment
|
|
63
|
+
const resolved = resolveRouteNative(pathname, routeManifest as any)
|
|
64
|
+
if (!resolved) return null
|
|
65
|
+
return {
|
|
66
|
+
record: resolved.matched as unknown as RuntimeRouteRecord,
|
|
67
|
+
params: resolved.params
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Client-side fallback implementation
|
|
72
|
+
const normalizedPath = pathname === "" ? "/" : pathname
|
|
73
|
+
for (const route of routeManifest) {
|
|
74
|
+
const match = route.regex.exec(normalizedPath)
|
|
75
|
+
if (match) {
|
|
76
|
+
const params: Record<string, string> = {}
|
|
77
|
+
for (let i = 0; i < route.paramNames.length; i++) {
|
|
78
|
+
const paramName = route.paramNames[i]
|
|
79
|
+
const paramValue = match[i + 1]
|
|
80
|
+
if (paramName && paramValue !== undefined) {
|
|
81
|
+
params[paramName] = decodeURIComponent(paramValue)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { record: route, params }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function resolveAndRender(
|
|
91
|
+
path: string,
|
|
92
|
+
query: Record<string, string>,
|
|
93
|
+
updateHistory: boolean = true,
|
|
94
|
+
replace: boolean = false
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const prevRoute = { ...currentRoute }
|
|
97
|
+
const resolved = resolveRoute(path)
|
|
98
|
+
|
|
99
|
+
if (resolved) {
|
|
100
|
+
currentRoute = {
|
|
101
|
+
path,
|
|
102
|
+
params: resolved.params,
|
|
103
|
+
query,
|
|
104
|
+
matched: resolved.record as unknown as RouteRecord
|
|
105
|
+
}
|
|
106
|
+
const pageModule = resolved.record.module || (resolved.record.load ? await resolved.record.load() : null)
|
|
107
|
+
if (pageModule) await renderPage(pageModule)
|
|
108
|
+
} else {
|
|
109
|
+
currentRoute = { path, params: {}, query, matched: undefined }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (updateHistory) {
|
|
113
|
+
const url = path + (Object.keys(query).length > 0 ? "?" + new URLSearchParams(query).toString() : "")
|
|
114
|
+
if (replace) window.history.replaceState(null, "", url)
|
|
115
|
+
else window.history.pushState(null, "", url)
|
|
116
|
+
}
|
|
117
|
+
notifyListeners(currentRoute, prevRoute)
|
|
118
|
+
; (window as any).__zenith_route = currentRoute
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function renderPage(pageModule: PageModule): Promise<void> {
|
|
122
|
+
if (!routerOutlet) return
|
|
123
|
+
routerOutlet.innerHTML = pageModule.html
|
|
124
|
+
// ... logic for styles and scripts
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function notifyListeners(route: RouteState, prevRoute: RouteState): void {
|
|
128
|
+
routeListeners.forEach(listener => { listener(route, prevRoute) })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function navigate(to: string, options: NavigateOptions = {}): Promise<void> {
|
|
132
|
+
let path, query: Record<string, string> = {}
|
|
133
|
+
if (to.includes("?")) {
|
|
134
|
+
const [pathname, search] = to.split("?")
|
|
135
|
+
path = pathname || "/"
|
|
136
|
+
query = parseQueryString("?" + (search || ""))
|
|
137
|
+
} else path = to
|
|
138
|
+
|
|
139
|
+
await resolveAndRender(path, query, true, options.replace || false)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getRoute(): RouteState { return { ...currentRoute } }
|
|
143
|
+
export function onRouteChange(listener: RouteListener): () => void {
|
|
144
|
+
routeListeners.add(listener)
|
|
145
|
+
return () => { routeListeners.delete(listener) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isActive(path: string, exact: boolean = false): boolean {
|
|
149
|
+
return exact ? currentRoute.path === path : currentRoute.path.startsWith(path)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function prefetch(path: string): Promise<void> {
|
|
153
|
+
const resolved = resolveRoute(path)
|
|
154
|
+
if (resolved && resolved.record.load && !resolved.record.module) {
|
|
155
|
+
resolved.record.module = await resolved.record.load()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isPrefetched(_path: string): boolean { return false }
|
|
160
|
+
|
|
161
|
+
export function generateRuntimeRouterCode(): string {
|
|
162
|
+
return generateRuntimeRouterNative()
|
|
163
|
+
}
|
package/src/types.rs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
use napi_derive::napi;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
|
|
5
|
+
#[napi]
|
|
6
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
7
|
+
pub enum SegmentType {
|
|
8
|
+
Static,
|
|
9
|
+
Dynamic,
|
|
10
|
+
CatchAll,
|
|
11
|
+
OptionalCatchAll,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
15
|
+
#[napi(object)]
|
|
16
|
+
pub struct ParsedSegment {
|
|
17
|
+
pub segment_type: SegmentType,
|
|
18
|
+
pub param_name: Option<String>,
|
|
19
|
+
pub raw: String,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
23
|
+
#[napi(object)]
|
|
24
|
+
pub struct RouteRecord {
|
|
25
|
+
pub path: String,
|
|
26
|
+
pub regex: String,
|
|
27
|
+
pub param_names: Vec<String>,
|
|
28
|
+
pub score: i32,
|
|
29
|
+
pub file_path: String,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
33
|
+
#[napi(object)]
|
|
34
|
+
pub struct RouteState {
|
|
35
|
+
pub path: String,
|
|
36
|
+
pub params: HashMap<String, String>,
|
|
37
|
+
pub query: HashMap<String, String>,
|
|
38
|
+
pub matched: Option<RouteRecord>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
42
|
+
#[napi(object)]
|
|
43
|
+
pub struct RouteManifest {
|
|
44
|
+
pub routes: Vec<RouteRecord>,
|
|
45
|
+
pub generated_at: i64,
|
|
46
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zenith Router Types
|
|
3
|
+
*
|
|
4
|
+
* Re-exports types from the native Rust implementation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
RouteRecord as NativeRouteRecord,
|
|
9
|
+
RouteState as NativeRouteState,
|
|
10
|
+
RouteManifest as NativeRouteManifest,
|
|
11
|
+
SegmentType as NativeSegmentType,
|
|
12
|
+
ParsedSegment as NativeParsedSegment
|
|
13
|
+
} from "../index.js"
|
|
14
|
+
|
|
15
|
+
export { NativeSegmentType as SegmentType }
|
|
16
|
+
|
|
17
|
+
export interface RouteRecord extends Omit<NativeRouteRecord, 'regex'> {
|
|
18
|
+
regex: RegExp
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RouteState extends Omit<NativeRouteState, 'matched'> {
|
|
22
|
+
matched?: RouteRecord
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RouteManifest extends Omit<NativeRouteManifest, 'routes'> {
|
|
26
|
+
routes: RouteRecord[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ParsedSegment extends NativeParsedSegment { }
|
|
30
|
+
|
|
31
|
+
export interface PageModule {
|
|
32
|
+
html: string
|
|
33
|
+
scripts: string[]
|
|
34
|
+
styles: string[]
|
|
35
|
+
meta?: PageMeta
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PageMeta {
|
|
39
|
+
title?: string
|
|
40
|
+
description?: string
|
|
41
|
+
[key: string]: string | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NavigateOptions {
|
|
45
|
+
replace?: boolean
|
|
46
|
+
scrollToTop?: boolean
|
|
47
|
+
state?: Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RouteDefinition extends RouteRecord { }
|
|
51
|
+
|
|
52
|
+
export interface RuntimeRouteRecord extends RouteRecord {
|
|
53
|
+
module?: PageModule | null;
|
|
54
|
+
load?: () => Promise<PageModule>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type RouteListener = (route: RouteState, prevRoute: RouteState) => void;
|
|
58
|
+
|
|
59
|
+
export interface Router {
|
|
60
|
+
readonly route: RouteState
|
|
61
|
+
navigate(to: string, options?: NavigateOptions): Promise<void>
|
|
62
|
+
resolve(path: string): { record: RouteRecord; params: Record<string, string> } | null
|
|
63
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./src",
|
|
13
|
+
"lib": [
|
|
14
|
+
"ES2022",
|
|
15
|
+
"DOM",
|
|
16
|
+
"DOM.Iterable"
|
|
17
|
+
],
|
|
18
|
+
"types": [
|
|
19
|
+
"bun-types",
|
|
20
|
+
"node"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src/**/*"
|
|
25
|
+
],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"node_modules",
|
|
28
|
+
"dist"
|
|
29
|
+
]
|
|
30
|
+
}
|