@zenithbuild/core 0.6.2 → 1.1.0

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/router/runtime.ts DELETED
@@ -1,458 +0,0 @@
1
- /**
2
- * Zenith Runtime Router
3
- *
4
- * SPA-style client-side router that handles:
5
- * - URL resolution and route matching
6
- * - Browser history management (pushState/popstate)
7
- * - Reactive route state
8
- * - Page component mounting/unmounting
9
- *
10
- * Extension points for future ZenLink:
11
- * - navigate() API
12
- * - beforeEach/afterEach guards
13
- * - Active link state
14
- */
15
-
16
- import type {
17
- RouteState,
18
- NavigateOptions,
19
- RouteRecord,
20
- PageModule
21
- } from "./types"
22
-
23
- /**
24
- * Runtime route record (with load function bound)
25
- */
26
- interface RuntimeRouteRecord {
27
- path: string
28
- regex: RegExp
29
- paramNames: string[]
30
- score: number
31
- filePath: string
32
- /** Page module or loader function */
33
- module?: PageModule
34
- load?: () => PageModule
35
- }
36
-
37
- /**
38
- * Global route state - reactive and accessible from page components
39
- */
40
- let currentRoute: RouteState = {
41
- path: "/",
42
- params: {},
43
- query: {}
44
- }
45
-
46
- /**
47
- * Route change listeners
48
- */
49
- type RouteListener = (route: RouteState, prevRoute: RouteState) => void
50
- const routeListeners: Set<RouteListener> = new Set()
51
-
52
- /**
53
- * Route manifest (populated at build time)
54
- */
55
- let routeManifest: RuntimeRouteRecord[] = []
56
-
57
- /**
58
- * Current page module
59
- */
60
- let currentPageModule: PageModule | null = null
61
-
62
- /**
63
- * Router outlet element
64
- */
65
- let routerOutlet: HTMLElement | null = null
66
-
67
- /**
68
- * Initialize the router with the route manifest
69
- */
70
- export function initRouter(
71
- manifest: RuntimeRouteRecord[],
72
- outlet?: HTMLElement | string
73
- ): void {
74
- routeManifest = manifest
75
-
76
- // Set router outlet
77
- if (outlet) {
78
- routerOutlet = typeof outlet === "string"
79
- ? document.querySelector(outlet)
80
- : outlet
81
- }
82
-
83
- // Listen for popstate (back/forward navigation)
84
- window.addEventListener("popstate", handlePopState)
85
-
86
- // Resolve initial route
87
- const initialPath = window.location.pathname
88
- const initialQuery = parseQueryString(window.location.search)
89
-
90
- resolveAndRender(initialPath, initialQuery, false)
91
- }
92
-
93
- /**
94
- * Parse query string into object
95
- */
96
- function parseQueryString(search: string): Record<string, string> {
97
- const query: Record<string, string> = {}
98
-
99
- if (!search || search === "?") {
100
- return query
101
- }
102
-
103
- const params = new URLSearchParams(search)
104
- params.forEach((value, key) => {
105
- query[key] = value
106
- })
107
-
108
- return query
109
- }
110
-
111
- /**
112
- * Handle browser back/forward navigation
113
- */
114
- function handlePopState(_event: PopStateEvent): void {
115
- const path = window.location.pathname
116
- const query = parseQueryString(window.location.search)
117
-
118
- // Don't update history on popstate - browser already changed it
119
- resolveAndRender(path, query, false, false)
120
- }
121
-
122
- /**
123
- * Resolve route from path
124
- */
125
- export function resolveRoute(
126
- pathname: string
127
- ): { record: RuntimeRouteRecord; params: Record<string, string> } | null {
128
- // Normalize pathname
129
- const normalizedPath = pathname === "" ? "/" : pathname
130
-
131
- for (const route of routeManifest) {
132
- const match = route.regex.exec(normalizedPath)
133
-
134
- if (match) {
135
- // Extract params from capture groups
136
- const params: Record<string, string> = {}
137
-
138
- for (let i = 0; i < route.paramNames.length; i++) {
139
- const paramName = route.paramNames[i]
140
- const paramValue = match[i + 1] // +1 because match[0] is full match
141
-
142
- if (paramName && paramValue !== undefined) {
143
- params[paramName] = decodeURIComponent(paramValue)
144
- }
145
- }
146
-
147
- return { record: route, params }
148
- }
149
- }
150
-
151
- return null
152
- }
153
-
154
- /**
155
- * Resolve route and render page
156
- */
157
- async function resolveAndRender(
158
- path: string,
159
- query: Record<string, string>,
160
- updateHistory: boolean = true,
161
- replace: boolean = false
162
- ): Promise<void> {
163
- const prevRoute = { ...currentRoute }
164
-
165
- const resolved = resolveRoute(path)
166
-
167
- if (resolved) {
168
- // Update route state
169
- currentRoute = {
170
- path,
171
- params: resolved.params,
172
- query,
173
- matched: resolved.record as unknown as RouteRecord
174
- }
175
-
176
- // Load and render page
177
- const pageModule = resolved.record.module ||
178
- (resolved.record.load ? resolved.record.load() : null)
179
-
180
- if (pageModule) {
181
- await renderPage(pageModule)
182
- }
183
- } else {
184
- // No route matched - could render 404
185
- currentRoute = {
186
- path,
187
- params: {},
188
- query,
189
- matched: undefined
190
- }
191
-
192
- console.warn(`[Zenith Router] No route matched for path: ${path}`)
193
- }
194
-
195
- // Update browser history
196
- if (updateHistory) {
197
- const url = path + (Object.keys(query).length > 0
198
- ? "?" + new URLSearchParams(query).toString()
199
- : "")
200
-
201
- if (replace) {
202
- window.history.replaceState(null, "", url)
203
- } else {
204
- window.history.pushState(null, "", url)
205
- }
206
- }
207
-
208
- // Notify listeners
209
- notifyListeners(currentRoute, prevRoute)
210
-
211
- // Expose route to window for component access
212
- ;(window as any).__zenith_route = currentRoute
213
- }
214
-
215
- /**
216
- * Render a page module to the router outlet
217
- */
218
- async function renderPage(pageModule: PageModule): Promise<void> {
219
- if (!routerOutlet) {
220
- console.warn("[Zenith Router] No router outlet configured")
221
- return
222
- }
223
-
224
- // Clear previous page scripts from window
225
- cleanupPreviousPage()
226
-
227
- currentPageModule = pageModule
228
-
229
- // Render HTML to outlet
230
- routerOutlet.innerHTML = pageModule.html
231
-
232
- // Inject styles
233
- injectStyles(pageModule.styles)
234
-
235
- // Execute scripts
236
- executeScripts(pageModule.scripts)
237
- }
238
-
239
- /**
240
- * Clean up previous page (remove event listeners, etc.)
241
- */
242
- function cleanupPreviousPage(): void {
243
- // Remove previous page styles
244
- const prevStyles = document.querySelectorAll("style[data-zen-page-style]")
245
- prevStyles.forEach(style => style.remove())
246
-
247
- // Note: Script cleanup is handled by the state management system
248
- // State variables and event handlers will be overwritten by new page
249
- }
250
-
251
- /**
252
- * Inject page styles into document head
253
- */
254
- function injectStyles(styles: string[]): void {
255
- styles.forEach((styleContent, index) => {
256
- const styleEl = document.createElement("style")
257
- styleEl.setAttribute("data-zen-page-style", String(index))
258
- styleEl.textContent = styleContent
259
- document.head.appendChild(styleEl)
260
- })
261
- }
262
-
263
- /**
264
- * Execute page scripts
265
- */
266
- function executeScripts(scripts: string[]): void {
267
- scripts.forEach(scriptContent => {
268
- try {
269
- // Create a function and execute it
270
- const scriptFn = new Function(scriptContent)
271
- scriptFn()
272
- } catch (error) {
273
- console.error("[Zenith Router] Error executing page script:", error)
274
- }
275
- })
276
- }
277
-
278
- /**
279
- * Notify route change listeners
280
- */
281
- function notifyListeners(route: RouteState, prevRoute: RouteState): void {
282
- routeListeners.forEach(listener => {
283
- try {
284
- listener(route, prevRoute)
285
- } catch (error) {
286
- console.error("[Zenith Router] Error in route listener:", error)
287
- }
288
- })
289
- }
290
-
291
- /**
292
- * Navigate to a new URL (SPA navigation)
293
- *
294
- * This is the main API for programmatic navigation.
295
- * ZenLink will use this internally.
296
- *
297
- * @param to - The target URL path
298
- * @param options - Navigation options
299
- */
300
- export async function navigate(
301
- to: string,
302
- options: NavigateOptions = {}
303
- ): Promise<void> {
304
- // Parse the URL
305
- let path: string
306
- let query: Record<string, string> = {}
307
-
308
- if (to.includes("?")) {
309
- const [pathname, search] = to.split("?")
310
- path = pathname || "/"
311
- query = parseQueryString("?" + (search || ""))
312
- } else {
313
- path = to
314
- }
315
-
316
- // Normalize path
317
- if (!path.startsWith("/")) {
318
- // Relative path - resolve against current path
319
- const currentDir = currentRoute.path.split("/").slice(0, -1).join("/")
320
- path = currentDir + "/" + path
321
- }
322
-
323
- // Normalize path for comparison (ensure trailing slash consistency)
324
- const normalizedPath = path === "" ? "/" : path
325
- const currentPath = currentRoute.path === "" ? "/" : currentRoute.path
326
-
327
- // Check if we're already on this path
328
- const isSamePath = normalizedPath === currentPath
329
-
330
- // If same path and same query, don't navigate (idempotent)
331
- if (isSamePath && JSON.stringify(query) === JSON.stringify(currentRoute.query)) {
332
- return
333
- }
334
-
335
- // Resolve and render with replace option if specified
336
- await resolveAndRender(path, query, true, options.replace || false)
337
- }
338
-
339
- /**
340
- * Get current route state
341
- */
342
- export function getRoute(): RouteState {
343
- return { ...currentRoute }
344
- }
345
-
346
- /**
347
- * Subscribe to route changes
348
- */
349
- export function onRouteChange(listener: RouteListener): () => void {
350
- routeListeners.add(listener)
351
-
352
- // Return unsubscribe function
353
- return () => {
354
- routeListeners.delete(listener)
355
- }
356
- }
357
-
358
- /**
359
- * FUTURE EXTENSION POINTS
360
- *
361
- * These are placeholders for features ZenLink and other extensions will use.
362
- * They are not implemented yet but define the API surface.
363
- */
364
-
365
- /**
366
- * Navigation guards (future extension)
367
- */
368
- type NavigationGuard = (
369
- to: RouteState,
370
- from: RouteState
371
- ) => boolean | string | Promise<boolean | string>
372
-
373
- const beforeGuards: NavigationGuard[] = []
374
-
375
- /**
376
- * Register a navigation guard (future extension)
377
- */
378
- export function beforeEach(guard: NavigationGuard): () => void {
379
- beforeGuards.push(guard)
380
- return () => {
381
- const index = beforeGuards.indexOf(guard)
382
- if (index > -1) beforeGuards.splice(index, 1)
383
- }
384
- }
385
-
386
- /**
387
- * After navigation hooks (future extension)
388
- */
389
- type AfterHook = (to: RouteState, from: RouteState) => void | Promise<void>
390
-
391
- const afterHooks: AfterHook[] = []
392
-
393
- /**
394
- * Register an after-navigation hook (future extension)
395
- */
396
- export function afterEach(hook: AfterHook): () => void {
397
- afterHooks.push(hook)
398
- return () => {
399
- const index = afterHooks.indexOf(hook)
400
- if (index > -1) afterHooks.splice(index, 1)
401
- }
402
- }
403
-
404
- /**
405
- * Check if a path is active (for ZenLink active state)
406
- */
407
- export function isActive(path: string, exact: boolean = false): boolean {
408
- if (exact) {
409
- return currentRoute.path === path
410
- }
411
- return currentRoute.path.startsWith(path)
412
- }
413
-
414
- /**
415
- * Prefetch a route for faster navigation
416
- *
417
- * This preloads the page module into the route manifest cache,
418
- * so when the user navigates to it, there's no loading delay.
419
- */
420
- const prefetchedRoutes = new Set<string>()
421
-
422
- export async function prefetch(path: string): Promise<void> {
423
- // Normalize path
424
- const normalizedPath = path === "" ? "/" : path
425
-
426
- // Don't prefetch if already done
427
- if (prefetchedRoutes.has(normalizedPath)) {
428
- return
429
- }
430
-
431
- // Find matching route
432
- const resolved = resolveRoute(normalizedPath)
433
-
434
- if (!resolved) {
435
- console.warn(`[Zenith Router] Cannot prefetch: no route matches ${path}`)
436
- return
437
- }
438
-
439
- // Mark as prefetched
440
- prefetchedRoutes.add(normalizedPath)
441
-
442
- // If route has a load function, call it to preload the module
443
- if (resolved.record.load && !resolved.record.module) {
444
- try {
445
- resolved.record.module = resolved.record.load()
446
- } catch (error) {
447
- console.warn(`[Zenith Router] Error prefetching ${path}:`, error)
448
- }
449
- }
450
- }
451
-
452
- /**
453
- * Check if a route has been prefetched
454
- */
455
- export function isPrefetched(path: string): boolean {
456
- return prefetchedRoutes.has(path === "" ? "/" : path)
457
- }
458
-
package/router/types.ts DELETED
@@ -1,168 +0,0 @@
1
- /**
2
- * Zenith Router Types
3
- *
4
- * File-based routing system types for build-time manifest generation
5
- * and runtime route resolution.
6
- */
7
-
8
- /**
9
- * A compiled route record used for runtime matching
10
- */
11
- export interface RouteRecord {
12
- /** The route pattern (e.g., /blog/:id, /posts/*slug) */
13
- path: string
14
- /** Compiled regex for matching URLs */
15
- regex: RegExp
16
- /** Parameter names extracted from the route pattern */
17
- paramNames: string[]
18
- /** Dynamic import function for the page module */
19
- load: () => Promise<PageModule>
20
- /** Route priority score for deterministic matching */
21
- score: number
22
- /** Original file path (for debugging) */
23
- filePath: string
24
- }
25
-
26
- /**
27
- * A compiled page module containing the page's compiled code
28
- */
29
- export interface PageModule {
30
- /** The compiled HTML template */
31
- html: string
32
- /** Array of compiled script contents */
33
- scripts: string[]
34
- /** Array of compiled style contents */
35
- styles: string[]
36
- /** Page metadata (title, etc.) */
37
- meta?: PageMeta
38
- }
39
-
40
- /**
41
- * Page metadata for head management
42
- */
43
- export interface PageMeta {
44
- title?: string
45
- description?: string
46
- [key: string]: string | undefined
47
- }
48
-
49
- /**
50
- * The reactive route state exposed to components
51
- */
52
- export interface RouteState {
53
- /** Current pathname (e.g., /blog/123) */
54
- path: string
55
- /** Extracted route parameters (e.g., { id: '123' }) */
56
- params: Record<string, string>
57
- /** Parsed query string parameters */
58
- query: Record<string, string>
59
- /** The matched route record (if any) */
60
- matched?: RouteRecord
61
- }
62
-
63
- /**
64
- * Navigation options for programmatic navigation
65
- */
66
- export interface NavigateOptions {
67
- /** Replace current history entry instead of pushing */
68
- replace?: boolean
69
- }
70
-
71
- /**
72
- * Route segment types for scoring
73
- */
74
- export enum SegmentType {
75
- /** Static segment (e.g., "blog") - highest priority */
76
- STATIC = 'static',
77
- /** Dynamic parameter (e.g., "[id]") - medium priority */
78
- DYNAMIC = 'dynamic',
79
- /** Required catch-all (e.g., "[...slug]") - low priority */
80
- CATCH_ALL = 'catch_all',
81
- /** Optional catch-all (e.g., "[[...slug]]") - lowest priority */
82
- OPTIONAL_CATCH_ALL = 'optional_catch_all'
83
- }
84
-
85
- /**
86
- * Parsed segment information
87
- */
88
- export interface ParsedSegment {
89
- /** The segment type */
90
- type: SegmentType
91
- /** The parameter name (for dynamic/catch-all segments) */
92
- paramName?: string
93
- /** The raw segment string */
94
- raw: string
95
- }
96
-
97
- /**
98
- * Build-time route definition before regex compilation
99
- */
100
- export interface RouteDefinition {
101
- /** The route pattern */
102
- path: string
103
- /** Parsed segments */
104
- segments: ParsedSegment[]
105
- /** Parameter names */
106
- paramNames: string[]
107
- /** Route score */
108
- score: number
109
- /** Source file path */
110
- filePath: string
111
- }
112
-
113
- /**
114
- * Route manifest generated at build time
115
- */
116
- export interface RouteManifest {
117
- /** Array of route records, sorted by score (highest first) */
118
- routes: RouteRecord[]
119
- /** Timestamp of manifest generation */
120
- generatedAt: number
121
- }
122
-
123
- /**
124
- * Router instance interface (for future ZenLink extension)
125
- */
126
- export interface Router {
127
- /** Current reactive route state */
128
- readonly route: RouteState
129
-
130
- /** Navigate to a new URL */
131
- navigate(to: string, options?: NavigateOptions): Promise<void>
132
-
133
- /** Resolve a route without navigating */
134
- resolve(path: string): { record: RouteRecord; params: Record<string, string> } | null
135
-
136
- /** Add a navigation guard (future extension point) */
137
- beforeEach?(guard: NavigationGuard): () => void
138
-
139
- /** Add an after-navigation hook (future extension point) */
140
- afterEach?(hook: NavigationHook): () => void
141
- }
142
-
143
- /**
144
- * Navigation guard for route protection (future extension)
145
- */
146
- export type NavigationGuard = (
147
- to: RouteState,
148
- from: RouteState
149
- ) => boolean | string | Promise<boolean | string>
150
-
151
- /**
152
- * Navigation hook for post-navigation actions (future extension)
153
- */
154
- export type NavigationHook = (
155
- to: RouteState,
156
- from: RouteState
157
- ) => void | Promise<void>
158
-
159
- /**
160
- * Router view mount options
161
- */
162
- export interface RouterViewOptions {
163
- /** Container element or selector */
164
- container: HTMLElement | string
165
- /** Whether to preserve layout between routes */
166
- preserveLayout?: boolean
167
- }
168
-