@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 ADDED
@@ -0,0 +1,127 @@
1
+ # @zenithbuild/router
2
+
3
+ File-based SPA router for Zenith framework with **deterministic, compile-time route resolution**.
4
+
5
+ ## Features
6
+
7
+ - 📁 **File-based routing** — Pages in `pages/` directory become routes automatically
8
+ - ⚡ **Compile-time resolution** — Route manifest generated at build time, not runtime
9
+ - 🔗 **ZenLink component** — Declarative navigation with prefetching
10
+ - 🧭 **Programmatic navigation** — `navigate()`, `prefetch()`, `isActive()` APIs
11
+ - 🎯 **Type-safe** — Full TypeScript support with route parameter inference
12
+ - 🚀 **Hydration-safe** — No runtime hacks, works seamlessly with SSR/SSG
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ bun add @zenithbuild/router
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Programmatic Navigation
23
+
24
+ ```ts
25
+ import { navigate, prefetch, isActive, getRoute } from '@zenithbuild/router'
26
+
27
+ // Navigate to a route
28
+ navigate('/about')
29
+
30
+ // Navigate with replace (no history entry)
31
+ navigate('/dashboard', { replace: true })
32
+
33
+ // Prefetch a route for faster navigation
34
+ prefetch('/blog')
35
+
36
+ // Check if a route is active
37
+ if (isActive('/blog')) {
38
+ console.log('Currently on blog section')
39
+ }
40
+
41
+ // Get current route state
42
+ const { path, params, query } = getRoute()
43
+ ```
44
+
45
+ ### ZenLink Component (in .zen files)
46
+
47
+ ```html
48
+ <ZenLink href="/about">About Us</ZenLink>
49
+
50
+ <!-- With prefetching on hover -->
51
+ <ZenLink href="/blog" preload>Blog</ZenLink>
52
+
53
+ <!-- External links automatically open in new tab -->
54
+ <ZenLink href="https://github.com">GitHub</ZenLink>
55
+ ```
56
+
57
+ ### Build-time Route Manifest
58
+
59
+ The router generates a route manifest at compile time:
60
+
61
+ ```ts
62
+ import { generateRouteManifest, discoverPages } from '@zenithbuild/router/manifest'
63
+
64
+ const pagesDir = './src/pages'
65
+ const manifest = generateRouteManifest(pagesDir)
66
+
67
+ // manifest contains:
68
+ // - path: Route pattern (e.g., /blog/:id)
69
+ // - regex: Compiled RegExp for matching
70
+ // - paramNames: Dynamic segment names
71
+ // - score: Priority for deterministic matching
72
+ ```
73
+
74
+ ## Route Patterns
75
+
76
+ | File Path | Route Pattern |
77
+ |-----------|---------------|
78
+ | `pages/index.zen` | `/` |
79
+ | `pages/about.zen` | `/about` |
80
+ | `pages/blog/index.zen` | `/blog` |
81
+ | `pages/blog/[id].zen` | `/blog/:id` |
82
+ | `pages/posts/[...slug].zen` | `/posts/*slug` |
83
+ | `pages/[[...all]].zen` | `/*all?` (optional) |
84
+
85
+ ## Architecture
86
+
87
+ ```
88
+ @zenithbuild/router
89
+ ├── src/
90
+ │ ├── index.ts # Main exports
91
+ │ ├── types.ts # Core types
92
+ │ ├── manifest.ts # Build-time manifest generation
93
+ │ ├── runtime.ts # Client-side SPA router
94
+ │ └── navigation/
95
+ │ ├── index.ts # Navigation exports
96
+ │ ├── zen-link.ts # Navigation API
97
+ │ └── ZenLink.zen # Declarative component
98
+ ```
99
+
100
+ ## API Reference
101
+
102
+ ### Navigation Functions
103
+
104
+ - `navigate(path, options?)` — Navigate to a path
105
+ - `prefetch(path)` — Prefetch a route for faster navigation
106
+ - `isActive(path, exact?)` — Check if path is currently active
107
+ - `getRoute()` — Get current route state
108
+ - `back()`, `forward()`, `go(delta)` — History navigation
109
+
110
+ ### Manifest Generation
111
+
112
+ - `discoverPages(pagesDir)` — Find all .zen files in pages directory
113
+ - `generateRouteManifest(pagesDir)` — Generate complete route manifest
114
+ - `filePathToRoutePath(filePath, pagesDir)` — Convert file path to route
115
+ - `routePathToRegex(routePath)` — Compile route to RegExp
116
+
117
+ ### Types
118
+
119
+ - `RouteState` — Current route state (path, params, query)
120
+ - `RouteRecord` — Compiled route definition
121
+ - `NavigateOptions` — Options for navigation
122
+ - `ZenLinkProps` — Props for ZenLink component
123
+
124
+ ## License
125
+
126
+ MIT
127
+ # zenith-router
package/index.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ export declare function generateRouteManifestNative(pagesDir: string): Array<RouteRecord>
7
+ export declare function renderRouteNative(inputJson: string): string
8
+ export declare function resolveRouteNative(path: string, routes: Array<RouteRecord>): RouteState | null
9
+ export declare function generateRuntimeRouterNative(): string
10
+ export const enum SegmentType {
11
+ Static = 0,
12
+ Dynamic = 1,
13
+ CatchAll = 2,
14
+ OptionalCatchAll = 3
15
+ }
16
+ export interface ParsedSegment {
17
+ segmentType: SegmentType
18
+ paramName?: string
19
+ raw: string
20
+ }
21
+ export interface RouteRecord {
22
+ path: string
23
+ regex: string
24
+ paramNames: Array<string>
25
+ score: number
26
+ filePath: string
27
+ }
28
+ export interface RouteState {
29
+ path: string
30
+ params: Record<string, string>
31
+ query: Record<string, string>
32
+ matched?: RouteRecord
33
+ }
34
+ export interface RouteManifest {
35
+ routes: Array<RouteRecord>
36
+ generatedAt: number
37
+ }
38
+ export declare function routerBridge(): string
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createRequire } from 'module';
2
+ const require = createRequire(import.meta.url);
3
+ const native = require('./zenith-router.node');
4
+ export const { generateRouteManifestNative, renderRouteNative, resolveRouteNative, generateRuntimeRouterNative, SegmentType, routerBridge } = native;
5
+ export default native;
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@zenithbuild/router",
3
+ "version": "1.0.1",
4
+ "description": "File-based SPA router for Zenith framework with deterministic, compile-time route resolution",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "files": [
9
+ "src",
10
+ "dist",
11
+ "tsconfig.json",
12
+ "index.js",
13
+ "index.d.ts"
14
+ ],
15
+ "napi": {
16
+ "name": "zenith-router",
17
+ "package": {
18
+ "name": "@zenithbuild/router"
19
+ }
20
+ },
21
+ "exports": {
22
+ ".": "./src/index.ts",
23
+ "./manifest": "./src/manifest.ts",
24
+ "./runtime": "./src/runtime.ts",
25
+ "./navigation": "./src/navigation/index.ts",
26
+ "./types": "./src/types.ts"
27
+ },
28
+ "keywords": [
29
+ "zenith",
30
+ "router",
31
+ "spa",
32
+ "file-based-routing",
33
+ "compile-time"
34
+ ],
35
+ "author": "Zenith Team",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+ssh://git@github.com/zenithbuild/zenith-router.git"
39
+ },
40
+ "scripts": {
41
+ "format": "prettier --write \"**/*.ts\"",
42
+ "format:check": "prettier --check \"**/*.ts\"",
43
+ "typecheck": "tsc --noEmit",
44
+ "build": "napi build --release",
45
+ "build:debug": "napi build",
46
+ "release": "bun run scripts/release.ts",
47
+ "release:dry": "bun run scripts/release.ts --dry-run",
48
+ "release:patch": "bun run scripts/release.ts --bump=patch",
49
+ "release:minor": "bun run scripts/release.ts --bump=minor",
50
+ "release:major": "bun run scripts/release.ts --bump=major"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "private": false,
56
+ "devDependencies": {
57
+ "@napi-rs/cli": "^2.18.4",
58
+ "@types/bun": "latest",
59
+ "@types/node": "latest",
60
+ "prettier": "^3.7.4"
61
+ },
62
+ "peerDependencies": {
63
+ "typescript": "^5"
64
+ }
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @zenithbuild/router
3
+ *
4
+ * File-based SPA router for Zenith framework.
5
+ * Includes routing, navigation, and ZenLink components.
6
+ *
7
+ * Features:
8
+ * - Deterministic, compile-time route resolution
9
+ * - File-based routing (pages/ directory → routes)
10
+ * - SPA navigation with prefetching
11
+ * - ZenLink component for declarative links
12
+ * - Type-safe route parameters
13
+ * - Hydration-safe, no runtime hacks
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { navigate, isActive, prefetch } from '@zenithbuild/router'
18
+ *
19
+ * // Navigate programmatically
20
+ * navigate('/about')
21
+ *
22
+ * // Check active state
23
+ * if (isActive('/blog')) {
24
+ * console.log('On blog section')
25
+ * }
26
+ * ```
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * // Build-time manifest generation
31
+ * import { generateRouteManifest, discoverPages } from '@zenithbuild/router/manifest'
32
+ *
33
+ * const manifest = generateRouteManifest('./src/pages')
34
+ * ```
35
+ */
36
+
37
+ // ============================================
38
+ // Core Types
39
+ // ============================================
40
+
41
+ export * from "./types"
42
+
43
+ // ============================================
44
+ // Build-time Manifest Generation
45
+ // ============================================
46
+
47
+ export {
48
+ generateRouteManifest,
49
+ generateRouteManifestCode
50
+ } from "./manifest"
51
+
52
+ // ============================================
53
+ // Runtime Router
54
+ // ============================================
55
+
56
+ export {
57
+ initRouter,
58
+ resolveRoute,
59
+ navigate,
60
+ getRoute,
61
+ onRouteChange,
62
+ isActive,
63
+ prefetch,
64
+ isPrefetched,
65
+ generateRuntimeRouterCode
66
+ } from "./runtime"
67
+
68
+ // ============================================
69
+ // Navigation Utilities
70
+ // ============================================
71
+
72
+ export {
73
+ // Navigation API (zen* prefixed names)
74
+ zenNavigate,
75
+ zenBack,
76
+ zenForward,
77
+ zenGo,
78
+ zenIsActive,
79
+ zenPrefetch,
80
+ zenIsPrefetched,
81
+ zenGetRoute,
82
+ zenGetParam,
83
+ zenGetQuery,
84
+ createZenLink,
85
+ zenLink,
86
+ // Additional navigation utilities
87
+ back,
88
+ forward,
89
+ go,
90
+ getParam,
91
+ getQuery,
92
+ isExternalUrl,
93
+ shouldUseSPANavigation,
94
+ normalizePath,
95
+ setGlobalTransition,
96
+ getGlobalTransition,
97
+ createTransitionContext
98
+ } from "./navigation/index"
99
+
100
+ // ============================================
101
+ // Navigation Types
102
+ // ============================================
103
+
104
+ export type {
105
+ ZenLinkProps,
106
+ TransitionContext,
107
+ TransitionHandler
108
+ } from "./navigation/index"
package/src/lib.rs ADDED
@@ -0,0 +1,18 @@
1
+ use napi_derive::napi;
2
+
3
+ pub mod manifest;
4
+ pub mod render;
5
+ pub mod resolve;
6
+ pub mod runtime_gen;
7
+ pub mod types;
8
+
9
+ pub use manifest::*;
10
+ pub use render::*;
11
+ pub use resolve::*;
12
+ pub use runtime_gen::*;
13
+ pub use types::*;
14
+
15
+ #[napi]
16
+ pub fn router_bridge() -> String {
17
+ "Zenith Router Native Bridge Connected".to_string()
18
+ }
@@ -0,0 +1,189 @@
1
+ use crate::types::{ParsedSegment, RouteRecord, SegmentType};
2
+ use napi_derive::napi;
3
+ use std::path::Path;
4
+ use walkdir::WalkDir;
5
+
6
+ const STATIC_SCORE: i32 = 10;
7
+ const DYNAMIC_SCORE: i32 = 5;
8
+ const CATCH_ALL_SCORE: i32 = 1;
9
+ const OPTIONAL_CATCH_ALL_SCORE: i32 = 0;
10
+
11
+ pub fn discover_pages(pages_dir: &str) -> Vec<String> {
12
+ let mut pages = Vec::new();
13
+ for entry in WalkDir::new(pages_dir)
14
+ .into_iter()
15
+ .filter_map(|e| e.ok())
16
+ .filter(|e| {
17
+ e.file_type().is_file() && e.path().extension().map_or(false, |ext| ext == "zen")
18
+ })
19
+ {
20
+ pages.push(entry.path().to_string_lossy().to_string());
21
+ }
22
+ pages
23
+ }
24
+
25
+ pub fn file_path_to_route_path(file_path: &str, pages_dir: &str) -> String {
26
+ let base = Path::new(pages_dir);
27
+ let path = Path::new(file_path);
28
+ let relative = path.strip_prefix(base).unwrap_or(path);
29
+
30
+ let without_ext = relative.with_extension("");
31
+ let components: Vec<String> = without_ext
32
+ .components()
33
+ .map(|c| c.as_os_str().to_string_lossy().to_string())
34
+ .collect();
35
+
36
+ let mut route_segments = Vec::new();
37
+ for segment in components {
38
+ if segment == "index" {
39
+ continue;
40
+ }
41
+
42
+ if segment.starts_with("[[...") && segment.ends_with("]]") {
43
+ let param = &segment[5..segment.len() - 2];
44
+ route_segments.push(format!("*{}?", param));
45
+ continue;
46
+ }
47
+
48
+ if segment.starts_with("[...") && segment.ends_with("]") {
49
+ let param = &segment[4..segment.len() - 1];
50
+ route_segments.push(format!("*{}", param));
51
+ continue;
52
+ }
53
+
54
+ if segment.starts_with("[") && segment.ends_with("]") {
55
+ let param = &segment[1..segment.len() - 1];
56
+ route_segments.push(format!(":{}", param));
57
+ continue;
58
+ }
59
+
60
+ route_segments.push(segment);
61
+ }
62
+
63
+ let mut route_path = format!("/{}", route_segments.join("/"));
64
+ if route_path.len() > 1 && route_path.ends_with("/") {
65
+ route_path.pop();
66
+ }
67
+ route_path
68
+ }
69
+
70
+ pub fn parse_route_segments(route_path: &str) -> Vec<ParsedSegment> {
71
+ if route_path == "/" {
72
+ return Vec::new();
73
+ }
74
+
75
+ let segments = route_path[1..].split('/');
76
+ let mut parsed = Vec::new();
77
+
78
+ for segment in segments {
79
+ if segment.is_empty() {
80
+ continue;
81
+ }
82
+
83
+ if segment.starts_with('*') && segment.ends_with('?') {
84
+ parsed.push(ParsedSegment {
85
+ segment_type: SegmentType::OptionalCatchAll,
86
+ param_name: Some(segment[1..segment.len() - 1].to_string()),
87
+ raw: segment.to_string(),
88
+ });
89
+ } else if segment.starts_with('*') {
90
+ parsed.push(ParsedSegment {
91
+ segment_type: SegmentType::CatchAll,
92
+ param_name: Some(segment[1..].to_string()),
93
+ raw: segment.to_string(),
94
+ });
95
+ } else if segment.starts_with(':') {
96
+ parsed.push(ParsedSegment {
97
+ segment_type: SegmentType::Dynamic,
98
+ param_name: Some(segment[1..].to_string()),
99
+ raw: segment.to_string(),
100
+ });
101
+ } else {
102
+ parsed.push(ParsedSegment {
103
+ segment_type: SegmentType::Static,
104
+ param_name: None,
105
+ raw: segment.to_string(),
106
+ });
107
+ }
108
+ }
109
+ parsed
110
+ }
111
+
112
+ pub fn calculate_route_score(segments: &[ParsedSegment]) -> i32 {
113
+ if segments.is_empty() {
114
+ return 100;
115
+ }
116
+
117
+ let mut score = 0;
118
+ let mut static_count = 0;
119
+
120
+ for segment in segments {
121
+ score += match segment.segment_type {
122
+ SegmentType::Static => {
123
+ static_count += 1;
124
+ STATIC_SCORE
125
+ }
126
+ SegmentType::Dynamic => DYNAMIC_SCORE,
127
+ SegmentType::CatchAll => CATCH_ALL_SCORE,
128
+ SegmentType::OptionalCatchAll => OPTIONAL_CATCH_ALL_SCORE,
129
+ };
130
+ }
131
+
132
+ score += static_count * 2;
133
+ score
134
+ }
135
+
136
+ pub fn route_path_to_regex_pattern(route_path: &str) -> String {
137
+ if route_path == "/" {
138
+ return r"^\/$".to_string();
139
+ }
140
+
141
+ let segments: Vec<&str> = route_path[1..]
142
+ .split('/')
143
+ .filter(|s| !s.is_empty())
144
+ .collect();
145
+ let mut regex_parts = Vec::new();
146
+
147
+ for segment in segments {
148
+ if segment.starts_with('*') && segment.ends_with('?') {
149
+ regex_parts.push(r"(?:\/(.*))?".to_string());
150
+ } else if segment.starts_with('*') {
151
+ regex_parts.push(r"\/(.+)".to_string());
152
+ } else if segment.starts_with(':') {
153
+ regex_parts.push(r"\/([^/]+)".to_string());
154
+ } else {
155
+ let escaped = regex::escape(segment);
156
+ regex_parts.push(format!(r"\/{}", escaped));
157
+ }
158
+ }
159
+
160
+ format!(r"^{}\/?$", regex_parts.join(""))
161
+ }
162
+
163
+ #[napi]
164
+ pub fn generate_route_manifest_native(pages_dir: String) -> Vec<RouteRecord> {
165
+ let pages = discover_pages(&pages_dir);
166
+ let mut definitions = Vec::new();
167
+
168
+ for file_path in pages {
169
+ let route_path = file_path_to_route_path(&file_path, &pages_dir);
170
+ let segments = parse_route_segments(&route_path);
171
+ let param_names = segments
172
+ .iter()
173
+ .filter_map(|s| s.param_name.clone())
174
+ .collect();
175
+ let score = calculate_route_score(&segments);
176
+ let regex = route_path_to_regex_pattern(&route_path);
177
+
178
+ definitions.push(RouteRecord {
179
+ path: route_path,
180
+ regex,
181
+ param_names,
182
+ score,
183
+ file_path,
184
+ });
185
+ }
186
+
187
+ definitions.sort_by(|a, b| b.score.cmp(&a.score));
188
+ definitions
189
+ }