@zenithbuild/router 0.6.17 → 0.7.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/template.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { renderRouterCoreSource } from './template-core.js';
2
+ import { renderRouterLifecycleSource } from './template-lifecycle.js';
3
+ import { renderRouterNavigationSource } from './template-navigation.js';
4
+
1
5
  function normalizeManifestJson(manifestJson) {
2
6
  return manifestJson.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
3
7
  }
@@ -16,7 +20,6 @@ export function renderRouterModule(opts) {
16
20
  }
17
21
 
18
22
  const { manifestJson, runtimeImport, coreImport } = opts;
19
-
20
23
  if (typeof manifestJson !== 'string' || manifestJson.length === 0) {
21
24
  throw new Error('renderRouterModule(opts) requires opts.manifestJson string');
22
25
  }
@@ -31,415 +34,5 @@ export function renderRouterModule(opts) {
31
34
  const runtimeSpec = sanitizeImportSpecifier(runtimeImport);
32
35
  const coreSpec = sanitizeImportSpecifier(coreImport);
33
36
 
34
- const lines = [
35
- `import { hydrate as __zenithHydrate } from '${runtimeSpec}';`,
36
- `import { zenOnMount as __zenithOnMount } from '${coreSpec}';`,
37
- '',
38
- 'void __zenithHydrate;',
39
- 'void __zenithOnMount;',
40
- '',
41
- `const __ZENITH_MANIFEST__ = ${manifest};`,
42
- '',
43
- 'let activeCleanup = null;',
44
- 'let navigationToken = 0;',
45
- '',
46
- 'function splitPath(path) {',
47
- " return path.split('/').filter(Boolean);",
48
- '}',
49
- '',
50
- 'function normalizeCatchAll(segments) {',
51
- " return segments.filter(Boolean).join('/');",
52
- '}',
53
- '',
54
- 'function requiresServerReload(route) {',
55
- ' const routes = __ZENITH_MANIFEST__.server_routes || __ZENITH_MANIFEST__.serverRoutes || [];',
56
- " return Array.isArray(routes) && routes.includes(route);",
57
- '}',
58
- '',
59
- 'const __ZENITH_ROUTE_POLICY_KEY = "__zenith_route_protection_policy";',
60
- 'const __ZENITH_ROUTE_EVENT_KEY = "__zenith_route_event_listeners";',
61
- 'const __ZENITH_ROUTE_EVENT_NAMES = ["guard:start", "guard:end", "route-check:start", "route-check:end", "route-check:error", "route:deny", "route:redirect"];',
62
- '',
63
- 'function __zenithEnsureRouteProtectionState() {',
64
- ' const scope = typeof globalThis === "object" && globalThis ? globalThis : window;',
65
- ' let policy = scope[__ZENITH_ROUTE_POLICY_KEY];',
66
- ' if (!policy || typeof policy !== "object") {',
67
- ' policy = {};',
68
- ' scope[__ZENITH_ROUTE_POLICY_KEY] = policy;',
69
- ' }',
70
- ' let listeners = scope[__ZENITH_ROUTE_EVENT_KEY];',
71
- ' if (!listeners || typeof listeners !== "object") {',
72
- ' listeners = Object.create(null);',
73
- ' scope[__ZENITH_ROUTE_EVENT_KEY] = listeners;',
74
- ' }',
75
- ' for (let i = 0; i < __ZENITH_ROUTE_EVENT_NAMES.length; i++) {',
76
- ' const name = __ZENITH_ROUTE_EVENT_NAMES[i];',
77
- ' if (!(listeners[name] instanceof Set)) {',
78
- ' listeners[name] = new Set();',
79
- ' }',
80
- ' }',
81
- ' return { policy, listeners };',
82
- '}',
83
- '',
84
- 'function __zenithGetRouteProtectionPolicy() {',
85
- ' return __zenithEnsureRouteProtectionState().policy;',
86
- '}',
87
- '',
88
- 'function __zenithDispatchRouteEvent(eventName, payload) {',
89
- ' const listeners = __zenithEnsureRouteProtectionState().listeners[eventName];',
90
- ' if (!(listeners instanceof Set)) return;',
91
- ' for (const handler of listeners) {',
92
- ' try {',
93
- ' handler(payload);',
94
- ' } catch (error) {',
95
- ' console.error("[Zenith Router] route event handler failed", error);',
96
- ' }',
97
- ' }',
98
- '}',
99
- '',
100
- 'function segmentWeight(segment) {',
101
- ' if (!segment) return 0;',
102
- " if (segment.startsWith('*')) return 1;",
103
- " if (segment.startsWith(':')) return 2;",
104
- ' return 3;',
105
- '}',
106
- '',
107
- 'function routeClass(segments) {',
108
- ' let hasParam = false;',
109
- ' let hasCatchAll = false;',
110
- ' for (let i = 0; i < segments.length; i++) {',
111
- ' const segment = segments[i];',
112
- " if (segment.startsWith('*')) {",
113
- ' hasCatchAll = true;',
114
- " } else if (segment.startsWith(':')) {",
115
- ' hasParam = true;',
116
- ' }',
117
- ' }',
118
- ' if (!hasParam && !hasCatchAll) return 3;',
119
- ' if (hasCatchAll) return 1;',
120
- ' return 2;',
121
- '}',
122
- '',
123
- 'function compareRouteSpecificity(a, b) {',
124
- " if (a === '/' && b !== '/') return -1;",
125
- " if (b === '/' && a !== '/') return 1;",
126
- ' const aSegs = splitPath(a);',
127
- ' const bSegs = splitPath(b);',
128
- ' const aClass = routeClass(aSegs);',
129
- ' const bClass = routeClass(bSegs);',
130
- ' if (aClass !== bClass) {',
131
- ' return bClass - aClass;',
132
- ' }',
133
- ' const max = Math.min(aSegs.length, bSegs.length);',
134
- ' for (let i = 0; i < max; i++) {',
135
- ' const aWeight = segmentWeight(aSegs[i]);',
136
- ' const bWeight = segmentWeight(bSegs[i]);',
137
- ' if (aWeight !== bWeight) {',
138
- ' return bWeight - aWeight;',
139
- ' }',
140
- ' }',
141
- ' if (aSegs.length !== bSegs.length) {',
142
- ' return bSegs.length - aSegs.length;',
143
- ' }',
144
- ' return a.localeCompare(b);',
145
- '}',
146
- '',
147
- 'function resolveRoute(pathname) {',
148
- ' if (__ZENITH_MANIFEST__.chunks[pathname]) {',
149
- ' return { route: pathname, params: {} };',
150
- ' }',
151
- '',
152
- ' const pathnameSegments = splitPath(pathname);',
153
- ' const routes = Object.keys(__ZENITH_MANIFEST__.chunks).sort(compareRouteSpecificity);',
154
- '',
155
- ' for (let i = 0; i < routes.length; i++) {',
156
- ' const route = routes[i];',
157
- ' const routeSegments = splitPath(route);',
158
- ' const params = Object.create(null);',
159
- ' let routeIndex = 0;',
160
- ' let pathIndex = 0;',
161
- ' let matched = true;',
162
- '',
163
- ' while (routeIndex < routeSegments.length) {',
164
- ' const routeSegment = routeSegments[routeIndex];',
165
- " if (routeSegment.startsWith('*')) {",
166
- " const optionalCatchAll = routeSegment.endsWith('?');",
167
- " const key = optionalCatchAll ? routeSegment.slice(1, -1) : routeSegment.slice(1);",
168
- ' if (routeIndex !== routeSegments.length - 1) {',
169
- ' matched = false;',
170
- ' break;',
171
- ' }',
172
- ' const rest = pathnameSegments.slice(pathIndex);',
173
- ' const rootRequiredCatchAll = !optionalCatchAll && routeSegments.length === 1;',
174
- ' if (rest.length === 0 && !optionalCatchAll && !rootRequiredCatchAll) {',
175
- ' matched = false;',
176
- ' break;',
177
- ' }',
178
- ' params[key] = normalizeCatchAll(rest);',
179
- ' pathIndex = pathnameSegments.length;',
180
- ' routeIndex = routeSegments.length;',
181
- ' continue;',
182
- ' }',
183
- ' if (pathIndex >= pathnameSegments.length) {',
184
- ' matched = false;',
185
- ' break;',
186
- ' }',
187
- ' const pathnameSegment = pathnameSegments[pathIndex];',
188
- " if (routeSegment.startsWith(':')) {",
189
- ' params[routeSegment.slice(1)] = pathnameSegment;',
190
- ' } else if (routeSegment !== pathnameSegment) {',
191
- ' matched = false;',
192
- ' break;',
193
- ' }',
194
- ' routeIndex += 1;',
195
- ' pathIndex += 1;',
196
- ' }',
197
- '',
198
- ' if (matched && routeIndex === routeSegments.length && pathIndex === pathnameSegments.length) {',
199
- ' return { route, params: { ...params } };',
200
- ' }',
201
- ' }',
202
- '',
203
- ' return null;',
204
- '}',
205
- '',
206
- 'function teardownRuntime() {',
207
- " if (typeof activeCleanup === 'function') {",
208
- ' activeCleanup();',
209
- ' activeCleanup = null;',
210
- ' }',
211
- '}',
212
- '',
213
- 'async function mountRoute(route, params, token) {',
214
- ' if (token !== navigationToken) return;',
215
- ' teardownRuntime();',
216
- ' if (token !== navigationToken) return;',
217
- '',
218
- ' const pageModule = await import(__ZENITH_MANIFEST__.chunks[route]);',
219
- ' if (token !== navigationToken) return;',
220
- '',
221
- ' const mountFn = pageModule.__zenith_mount || pageModule.default;',
222
- " if (typeof mountFn === 'function') {",
223
- ' const cleanup = mountFn(document, params);',
224
- " activeCleanup = typeof cleanup === 'function' ? cleanup : null;",
225
- ' }',
226
- '}',
227
- '',
228
- 'async function navigate(pathname, url) {',
229
- ' const next = resolveRoute(pathname);',
230
- ' if (!next) return false;',
231
- '',
232
- ' navigationToken += 1;',
233
- ' const token = navigationToken;',
234
- '',
235
- ' await mountRoute(next.route, next.params, token);',
236
- ' if (token !== navigationToken) return false;',
237
- " if (typeof window.scrollTo === 'function') {",
238
- ' window.scrollTo(0, 0);',
239
- ' }',
240
- ' return true;',
241
- '}',
242
- '',
243
- 'function handleNavigationFailure(error, url) {',
244
- " console.error('[Zenith Router] navigation failed', error);",
245
- " if (url && typeof url.href === 'string') {",
246
- ' window.location.assign(url.href);',
247
- ' }',
248
- '}',
249
- '',
250
- 'function isInternalLink(anchor) {',
251
- " if (!anchor || anchor.target || anchor.hasAttribute('download')) return false;",
252
- " const href = anchor.getAttribute('href');",
253
- " if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return false;",
254
- ' const url = new URL(anchor.href, window.location.href);',
255
- ' return url.origin === window.location.origin;',
256
- '}',
257
- '',
258
- 'function start() {',
259
- " if (typeof history === 'object' && history && 'scrollRestoration' in history) {",
260
- " history.scrollRestoration = 'manual';",
261
- ' }',
262
- '',
263
- " document.addEventListener('click', function (event) {",
264
- " if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;",
265
- " const target = event.target && event.target.closest ? event.target.closest('a[data-zen-link]') : null;",
266
- ' if (!isInternalLink(target)) return;',
267
- '',
268
- ' const url = new URL(target.href, window.location.href);',
269
- ' const hashOnlyNavigation =',
270
- ' url.hash &&',
271
- ' url.pathname === window.location.pathname &&',
272
- ' url.search === window.location.search;',
273
- ' if (hashOnlyNavigation) return;',
274
- ' const nextPath = url.pathname;',
275
- " if (nextPath === window.location.pathname && url.search === window.location.search && url.hash === window.location.hash) return;",
276
- ' const resolved = resolveRoute(nextPath);',
277
- ' if (!resolved) return;',
278
- '',
279
- ' function evaluateServerGuard(path, targetUrl, onSuccess) {',
280
- ' if (!requiresServerReload(resolved.route)) {',
281
- ' onSuccess();',
282
- ' return;',
283
- ' }',
284
- ' __zenithDispatchRouteEvent("route-check:start", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route });',
285
- ' fetch("/__zenith/route-check?path=" + encodeURIComponent(path), { headers: { "x-zenith-route-check": "1" }, credentials: "include" }).then(function(res) {',
286
- ' return res.json().then(function(data) {',
287
- ' if (!res.ok) throw new Error("route-check failed");',
288
- ' return data;',
289
- ' });',
290
- ' }).then(function(checkResult) {',
291
- ' if (checkResult && checkResult.result) {',
292
- ' const result = checkResult.result;',
293
- ' __zenithDispatchRouteEvent("route-check:end", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, result });',
294
- ' const policy = __zenithGetRouteProtectionPolicy();',
295
- ' if (result.kind === "redirect") {',
296
- ' __zenithDispatchRouteEvent("route:redirect", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, result });',
297
- ' window.location.assign(result.location || "/");',
298
- ' return;',
299
- ' }',
300
- ' if (result.kind === "deny") {',
301
- ' __zenithDispatchRouteEvent("route:deny", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, result });',
302
- ' console.warn("[Zenith] Route denied:", result.message);',
303
- ' if (result.status === 401 && policy.deny401RedirectToLogin !== false) {',
304
- ' const loginPath = policy.defaultLoginPath || "/login";',
305
- ' window.location.assign(loginPath + "?next=" + encodeURIComponent(targetUrl.pathname + targetUrl.search + targetUrl.hash));',
306
- ' return;',
307
- ' }',
308
- ' if (result.status === 403 && (policy.onDeny === "navigate" || policy.onDeny === "render403" || policy.forbiddenPath)) {',
309
- ' window.location.assign(policy.forbiddenPath || "/403");',
310
- ' return;',
311
- ' }',
312
- ' if (policy.onDeny === "redirect") {',
313
- ' window.location.assign(policy.defaultLoginPath || "/login");',
314
- ' return;',
315
- ' }',
316
- ' if (typeof policy.onDeny === "function") {',
317
- ' policy.onDeny(result);',
318
- ' return;',
319
- ' }',
320
- ' // Restore history state if this was a popstate (to prevent URL bar flash)',
321
- ' if (window._zenith_is_popstate_nav) {',
322
- ' history.pushState(null, "", window.location.href);',
323
- ' }',
324
- ' return; // No-flash block',
325
- ' }',
326
- ' onSuccess();',
327
- ' } else {',
328
- ' window.location.assign(targetUrl.href);',
329
- ' }',
330
- ' }).catch(function(e) {',
331
- ' __zenithDispatchRouteEvent("route-check:error", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, error: e });',
332
- ' console.error("[Zenith Router] fallback route-check failed", e);',
333
- ' window.location.assign(targetUrl.href);',
334
- ' });',
335
- ' }',
336
- '',
337
- ' evaluateServerGuard(nextPath, url, function() {',
338
- ' try {',
339
- ' event.preventDefault();',
340
- ' navigate(nextPath, url).catch(function (error) {',
341
- " console.error('[Zenith Router] click navigation failed', error);",
342
- ' window.location.assign(url.href);',
343
- ' });',
344
- ' } catch (e) {',
345
- " console.error('[Zenith Router] click navigation generated sync error', e);",
346
- ' window.location.assign(url.href);',
347
- ' }',
348
- ' });',
349
- ' });',
350
- '',
351
- " window.addEventListener('popstate', function (event) {",
352
- ' const url = new URL(window.location.href);',
353
- ' const nextPath = url.pathname;',
354
- ' const resolved = resolveRoute(nextPath);',
355
- ' if (!resolved) return;',
356
- '',
357
- ' window._zenith_is_popstate_nav = true;',
358
- ' // Re-use evaluateServerGuard but attach different success handler since popstate already changes URL',
359
- ' function popstateEvaluateGuard(path, targetUrl, onSuccess) {',
360
- ' if (!requiresServerReload(resolved.route)) {',
361
- ' onSuccess();',
362
- ' return;',
363
- ' }',
364
- ' __zenithDispatchRouteEvent("route-check:start", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route });',
365
- ' fetch("/__zenith/route-check?path=" + encodeURIComponent(path), { headers: { "x-zenith-route-check": "1" }, credentials: "include" }).then(function(res) {',
366
- ' return res.json().then(function(data) {',
367
- ' if (!res.ok) throw new Error("route-check failed");',
368
- ' return data;',
369
- ' });',
370
- ' }).then(function(checkResult) {',
371
- ' if (checkResult && checkResult.result) {',
372
- ' const result = checkResult.result;',
373
- ' __zenithDispatchRouteEvent("route-check:end", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, result });',
374
- ' const policy = __zenithGetRouteProtectionPolicy();',
375
- ' if (result.kind === "redirect") {',
376
- ' __zenithDispatchRouteEvent("route:redirect", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, result });',
377
- ' window._zenith_is_popstate_nav = false;',
378
- ' window.location.assign(result.location || "/");',
379
- ' return;',
380
- ' }',
381
- ' if (result.kind === "deny") {',
382
- ' __zenithDispatchRouteEvent("route:deny", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, result });',
383
- ' console.warn("[Zenith] Route denied:", result.message);',
384
- ' if (result.status === 401 && policy.deny401RedirectToLogin !== false) {',
385
- ' const loginPath = policy.defaultLoginPath || "/login";',
386
- ' window._zenith_is_popstate_nav = false;',
387
- ' window.location.assign(loginPath + "?next=" + encodeURIComponent(targetUrl.pathname + targetUrl.search + targetUrl.hash));',
388
- ' return;',
389
- ' }',
390
- ' if (result.status === 403 && (policy.onDeny === "navigate" || policy.onDeny === "render403" || policy.forbiddenPath)) {',
391
- ' window._zenith_is_popstate_nav = false;',
392
- ' window.location.assign(policy.forbiddenPath || "/403");',
393
- ' return;',
394
- ' }',
395
- ' if (policy.onDeny === "redirect") {',
396
- ' window._zenith_is_popstate_nav = false;',
397
- ' window.location.assign(policy.defaultLoginPath || "/login");',
398
- ' return;',
399
- ' }',
400
- ' if (typeof policy.onDeny === "function") {',
401
- ' policy.onDeny(result);',
402
- ' window._zenith_is_popstate_nav = false;',
403
- ' return;',
404
- ' }',
405
- ' window._zenith_is_popstate_nav = false;',
406
- ' history.back(); // Revert the popstate in history to align url bar with DOM',
407
- ' return; // No-flash block',
408
- ' }',
409
- ' onSuccess();',
410
- ' } else {',
411
- ' window._zenith_is_popstate_nav = false;',
412
- ' window.location.assign(targetUrl.href);',
413
- ' }',
414
- ' }).catch(function(e) {',
415
- ' __zenithDispatchRouteEvent("route-check:error", { to: targetUrl, from: new URL(window.location.href), routeId: resolved.route, error: e });',
416
- ' console.error("[Zenith Router] fallback route-check failed", e);',
417
- ' window._zenith_is_popstate_nav = false;',
418
- ' window.location.assign(targetUrl.href);',
419
- ' });',
420
- ' }',
421
- '',
422
- ' popstateEvaluateGuard(nextPath, url, function() {',
423
- ' navigate(nextPath, url).catch(function (error) {',
424
- " console.error('[Zenith Router] popstate navigation failed', error);",
425
- ' window.location.assign(url.href);',
426
- ' });',
427
- ' window._zenith_is_popstate_nav = false;',
428
- ' });',
429
- ' });',
430
- '',
431
- '',
432
- ' navigate(window.location.pathname, null).catch(function (error) {',
433
- " console.error('[Zenith Router] initial navigation failed', error);",
434
- ' });',
435
- '}',
436
- '',
437
- "if (document.readyState === 'loading') {",
438
- " document.addEventListener('DOMContentLoaded', start, { once: true });",
439
- '} else {',
440
- ' start();',
441
- '}'
442
- ];
443
-
444
- return `${lines.join('\n')}\n`;
37
+ return `${renderRouterCoreSource({ manifest, runtimeSpec, coreSpec })}\n\n${renderRouterLifecycleSource()}\n\n${renderRouterNavigationSource()}\n`;
445
38
  }