stx-router 0.2.8

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/dist/index.js ADDED
@@ -0,0 +1,887 @@
1
+ // @bun
2
+ // src/file-router.ts
3
+ import fs3 from "fs";
4
+ import path4 from "path";
5
+
6
+ // src/codegen.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ function generateRouteTypes(routes, outputDir) {
10
+ const lines = [
11
+ "// Auto-generated by STX - do not edit",
12
+ 'declare module "stx/routes" {',
13
+ " interface RouteMap {"
14
+ ];
15
+ for (const route of routes) {
16
+ const paramEntries = [];
17
+ for (const p of route.params) {
18
+ const type = route.pattern.includes(`:${p}*`) ? "string[]" : "string";
19
+ paramEntries.push(`${p}: ${type}`);
20
+ }
21
+ const paramsStr = paramEntries.join("; ");
22
+ lines.push(` '${route.pattern}': { ${paramsStr} }`);
23
+ }
24
+ lines.push(" }", "}", "");
25
+ fs.mkdirSync(outputDir, { recursive: true });
26
+ fs.writeFileSync(path.join(outputDir, "route-types.d.ts"), lines.join(`
27
+ `));
28
+ }
29
+
30
+ // src/matcher.ts
31
+ import path2 from "path";
32
+ function filePathToPattern(filePath, pagesDir) {
33
+ const relativePath = path2.relative(pagesDir, filePath);
34
+ let route = relativePath.replace(/\.(stx|md)$/, "");
35
+ route = route.split(path2.sep).join("/");
36
+ if (route === "index") {
37
+ return "/";
38
+ }
39
+ if (route.endsWith("/index")) {
40
+ route = route.slice(0, -6);
41
+ }
42
+ route = route.replace(/\[\[([^\]]+)\]\]/g, ":$1?");
43
+ route = route.replace(/\[([^\]]+)\]/g, ":$1");
44
+ route = route.replace(/:\.\.\.([^/]+)/g, ":$1*");
45
+ return `/${route}`;
46
+ }
47
+ function patternToRegex(pattern) {
48
+ const params = [];
49
+ let regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
50
+ regexStr = regexStr.replace(/\/:([^/]+)\?/g, (_, name) => {
51
+ params.push(name);
52
+ return "(?:/([^/]+))?";
53
+ });
54
+ regexStr = regexStr.replace(/:([^/]+)\*/g, (_, name) => {
55
+ params.push(name);
56
+ return "(.+)";
57
+ });
58
+ regexStr = regexStr.replace(/:([^/]+)/g, (_, name) => {
59
+ params.push(name);
60
+ return "([^/]+)";
61
+ });
62
+ regexStr = `^${regexStr}$`;
63
+ return { regex: new RegExp(regexStr), params };
64
+ }
65
+ function matchRoute(pathname, routes) {
66
+ const normalizedPath = pathname === "" ? "/" : pathname;
67
+ for (const route of routes) {
68
+ const match = normalizedPath.match(route.regex);
69
+ if (match) {
70
+ const params = {};
71
+ route.params.forEach((name, index) => {
72
+ params[name] = match[index + 1];
73
+ });
74
+ return { route, params };
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ // src/nested-layouts.ts
81
+ import fs2 from "fs";
82
+ import path3 from "path";
83
+ function resolveLayoutChain(routeFilePath, pagesDir) {
84
+ const layouts = [];
85
+ let dir = path3.dirname(routeFilePath);
86
+ while (dir.startsWith(pagesDir) || dir === pagesDir) {
87
+ const layoutFile = path3.join(dir, "_layout.stx");
88
+ if (fs2.existsSync(layoutFile)) {
89
+ layouts.unshift(layoutFile);
90
+ }
91
+ if (dir === pagesDir)
92
+ break;
93
+ dir = path3.dirname(dir);
94
+ }
95
+ return layouts;
96
+ }
97
+
98
+ // src/file-router.ts
99
+ function scanDirectory(dir, extensions) {
100
+ const files = [];
101
+ if (!fs3.existsSync(dir)) {
102
+ return files;
103
+ }
104
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
105
+ for (const entry of entries) {
106
+ const fullPath = path4.join(dir, entry.name);
107
+ if (entry.isDirectory()) {
108
+ if (entry.name.startsWith(".")) {
109
+ continue;
110
+ }
111
+ files.push(...scanDirectory(fullPath, extensions));
112
+ } else if (entry.isFile()) {
113
+ const ext = path4.extname(entry.name);
114
+ if (extensions.includes(ext)) {
115
+ if (entry.name.startsWith("_") && entry.name !== "_layout.stx") {
116
+ continue;
117
+ }
118
+ if (entry.name === "_layout.stx") {
119
+ continue;
120
+ }
121
+ files.push(fullPath);
122
+ }
123
+ }
124
+ }
125
+ return files;
126
+ }
127
+
128
+ class Router {
129
+ routes;
130
+ pagesDir;
131
+ config;
132
+ constructor(baseDir, config = {}) {
133
+ this.config = config;
134
+ this.pagesDir = path4.resolve(baseDir, config.pagesDir || "pages");
135
+ const extensions = config.extensions || [".stx"];
136
+ const files = scanDirectory(this.pagesDir, extensions);
137
+ this.routes = files.map((filePath) => {
138
+ const pattern = filePathToPattern(filePath, this.pagesDir);
139
+ const { regex, params } = patternToRegex(pattern);
140
+ const layouts = config.layouts !== false ? resolveLayoutChain(filePath, this.pagesDir) : [];
141
+ return {
142
+ pattern,
143
+ regex,
144
+ params,
145
+ filePath,
146
+ isDynamic: params.length > 0,
147
+ layout: layouts.length > 0 ? layouts[layouts.length - 1] : undefined
148
+ };
149
+ });
150
+ this.routes.sort((a, b) => {
151
+ if (!a.isDynamic && b.isDynamic)
152
+ return -1;
153
+ if (a.isDynamic && !b.isDynamic)
154
+ return 1;
155
+ const aSegments = a.pattern.split("/").length;
156
+ const bSegments = b.pattern.split("/").length;
157
+ if (aSegments !== bSegments)
158
+ return bSegments - aSegments;
159
+ return a.pattern.localeCompare(b.pattern);
160
+ });
161
+ const stxDir = path4.join(baseDir, ".stx");
162
+ generateRouteTypes(this.routes, stxDir);
163
+ }
164
+ match(pathname) {
165
+ const normalizedPath = pathname === "" ? "/" : pathname;
166
+ for (const route of this.routes) {
167
+ const m = normalizedPath.match(route.regex);
168
+ if (m) {
169
+ const params = {};
170
+ route.params.forEach((name, index) => {
171
+ params[name] = m[index + 1];
172
+ });
173
+ return { route, params };
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ resolve(name, params) {
179
+ const route = this.routes.find((r) => r.pattern === name || r.filePath.includes(name));
180
+ if (!route)
181
+ return "#not-found";
182
+ let resolved = route.pattern;
183
+ if (params) {
184
+ for (const [key, value] of Object.entries(params)) {
185
+ resolved = resolved.replace(`:${key}`, encodeURIComponent(value));
186
+ }
187
+ }
188
+ return resolved;
189
+ }
190
+ addRoute(route) {
191
+ this.routes.push(route);
192
+ this.routes.sort((a, b) => {
193
+ if (!a.isDynamic && b.isDynamic)
194
+ return -1;
195
+ if (a.isDynamic && !b.isDynamic)
196
+ return 1;
197
+ return a.pattern.localeCompare(b.pattern);
198
+ });
199
+ }
200
+ getLayout(route) {
201
+ return route.layout ?? null;
202
+ }
203
+ }
204
+ function createRouter(baseDir, config) {
205
+ return new Router(baseDir, config);
206
+ }
207
+ function findErrorPage(pagesDir, statusCode) {
208
+ const candidates = [`${statusCode}.stx`, "error.stx"];
209
+ for (const candidate of candidates) {
210
+ const filePath = path4.join(pagesDir, candidate);
211
+ if (fs3.existsSync(filePath))
212
+ return filePath;
213
+ }
214
+ return null;
215
+ }
216
+ function formatRoutes(routes, baseDir) {
217
+ return routes.map((route) => {
218
+ const relativePath = path4.relative(baseDir, route.filePath);
219
+ const dynamicLabel = route.isDynamic ? " (dynamic)" : "";
220
+ return `${route.pattern} \u2192 ${relativePath}${dynamicLabel}`;
221
+ });
222
+ }
223
+ // src/named-routes.ts
224
+ var routes = {};
225
+ var appUrl = "";
226
+ function setAppUrl(url) {
227
+ appUrl = url.replace(/\/$/, "");
228
+ }
229
+ function defineRoute(name, routePath, params = {}) {
230
+ routes[name] = { path: routePath, name, params };
231
+ }
232
+ function defineRoutes(routeDefinitions) {
233
+ for (const [name, definition] of Object.entries(routeDefinitions)) {
234
+ if (typeof definition === "string") {
235
+ routes[name] = { path: definition, name };
236
+ } else {
237
+ routes[name] = { ...definition, name };
238
+ }
239
+ }
240
+ }
241
+ function route(name, params = {}, absolute = false) {
242
+ const r = routes[name];
243
+ if (!r) {
244
+ console.warn(`Route [${name}] not defined.`);
245
+ return "#undefined-route";
246
+ }
247
+ const mergedParams = { ...r.params, ...params };
248
+ let routePath = r.path;
249
+ routePath = routePath.replace(/:(\w+)/g, (_, paramName) => {
250
+ const value = mergedParams[paramName];
251
+ if (value === undefined) {
252
+ console.warn(`Missing parameter [${paramName}] for route [${name}].`);
253
+ return `:${paramName}`;
254
+ }
255
+ delete mergedParams[paramName];
256
+ return encodeURIComponent(String(value));
257
+ });
258
+ const remainingParams = Object.entries(mergedParams);
259
+ if (remainingParams.length > 0) {
260
+ const queryString = remainingParams.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
261
+ routePath += (routePath.includes("?") ? "&" : "?") + queryString;
262
+ }
263
+ return absolute && appUrl ? `${appUrl}${routePath}` : routePath;
264
+ }
265
+ function resetRoutes() {
266
+ for (const key of Object.keys(routes)) {
267
+ delete routes[key];
268
+ }
269
+ appUrl = "";
270
+ }
271
+ // src/middleware.ts
272
+ import fs4 from "fs";
273
+ import path5 from "path";
274
+ function navigateTo(navPath, options = {}) {
275
+ return {
276
+ type: "redirect",
277
+ path: navPath,
278
+ options: {
279
+ replace: options.replace ?? false,
280
+ external: options.external ?? false,
281
+ redirectCode: options.redirectCode ?? 302
282
+ }
283
+ };
284
+ }
285
+ function abortNavigation(error) {
286
+ const normalizedError = typeof error === "string" ? { statusCode: 500, message: error } : error;
287
+ return {
288
+ type: "abort",
289
+ error: normalizedError
290
+ };
291
+ }
292
+ function defineMiddleware(handler, options = {}) {
293
+ return {
294
+ handler,
295
+ mode: options.mode ?? "universal"
296
+ };
297
+ }
298
+ function parseCookies(cookieHeader) {
299
+ const cookies = {};
300
+ if (!cookieHeader)
301
+ return cookies;
302
+ const pairs = cookieHeader.split(";");
303
+ for (const pair of pairs) {
304
+ const [name, ...rest] = pair.split("=");
305
+ if (name) {
306
+ const key = name.trim();
307
+ const value = rest.join("=").trim();
308
+ if (key) {
309
+ cookies[key] = decodeURIComponent(value);
310
+ }
311
+ }
312
+ }
313
+ return cookies;
314
+ }
315
+ function serializeCookie(name, value, options = {}) {
316
+ let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
317
+ if (options.expires) {
318
+ const expires = typeof options.expires === "number" ? new Date(Date.now() + options.expires * 1000) : options.expires;
319
+ cookie += `; Expires=${expires.toUTCString()}`;
320
+ }
321
+ if (options.path)
322
+ cookie += `; Path=${options.path}`;
323
+ if (options.domain)
324
+ cookie += `; Domain=${options.domain}`;
325
+ if (options.secure)
326
+ cookie += "; Secure";
327
+ if (options.httpOnly)
328
+ cookie += "; HttpOnly";
329
+ if (options.sameSite)
330
+ cookie += `; SameSite=${options.sameSite}`;
331
+ return cookie;
332
+ }
333
+ function createServerCookieManager(request, responseHeaders) {
334
+ const cookieHeader = request.headers.get("cookie") || "";
335
+ const cookies = parseCookies(cookieHeader);
336
+ return {
337
+ get(name) {
338
+ return cookies[name];
339
+ },
340
+ set(name, value, options = {}) {
341
+ cookies[name] = value;
342
+ responseHeaders.append("Set-Cookie", serializeCookie(name, value, options));
343
+ },
344
+ delete(name) {
345
+ delete cookies[name];
346
+ responseHeaders.append("Set-Cookie", serializeCookie(name, "", { expires: new Date(0), path: "/" }));
347
+ },
348
+ getAll() {
349
+ return { ...cookies };
350
+ }
351
+ };
352
+ }
353
+ function createClientCookieManager() {
354
+ return {
355
+ get(name) {
356
+ if (typeof document === "undefined")
357
+ return;
358
+ const cookies = parseCookies(document.cookie);
359
+ return cookies[name];
360
+ },
361
+ set(name, value, options = {}) {
362
+ if (typeof document === "undefined")
363
+ return;
364
+ document.cookie = serializeCookie(name, value, options);
365
+ },
366
+ delete(name) {
367
+ if (typeof document === "undefined")
368
+ return;
369
+ document.cookie = serializeCookie(name, "", { expires: new Date(0), path: "/" });
370
+ },
371
+ getAll() {
372
+ if (typeof document === "undefined")
373
+ return {};
374
+ return parseCookies(document.cookie);
375
+ }
376
+ };
377
+ }
378
+ function createStorageManager() {
379
+ const available = typeof window !== "undefined" && typeof window.localStorage !== "undefined";
380
+ return {
381
+ get(key) {
382
+ if (!available)
383
+ return null;
384
+ try {
385
+ return window.localStorage.getItem(key);
386
+ } catch {
387
+ return null;
388
+ }
389
+ },
390
+ set(key, value) {
391
+ if (!available)
392
+ return;
393
+ try {
394
+ window.localStorage.setItem(key, value);
395
+ } catch {}
396
+ },
397
+ remove(key) {
398
+ if (!available)
399
+ return;
400
+ try {
401
+ window.localStorage.removeItem(key);
402
+ } catch {}
403
+ },
404
+ isAvailable() {
405
+ return available;
406
+ }
407
+ };
408
+ }
409
+ var middlewareRegistry = new Map;
410
+ function registerMiddleware(name, middleware) {
411
+ middlewareRegistry.set(name, middleware);
412
+ }
413
+ function getMiddleware(name) {
414
+ return middlewareRegistry.get(name);
415
+ }
416
+ function hasMiddleware(name) {
417
+ return middlewareRegistry.has(name);
418
+ }
419
+ function clearMiddleware() {
420
+ middlewareRegistry.clear();
421
+ }
422
+ function getMiddlewareNames() {
423
+ return Array.from(middlewareRegistry.keys());
424
+ }
425
+ async function loadMiddlewareFromDirectory(baseDir, middlewareDir = "middleware") {
426
+ const fullPath = path5.resolve(baseDir, middlewareDir);
427
+ if (!fs4.existsSync(fullPath)) {
428
+ return;
429
+ }
430
+ const files = fs4.readdirSync(fullPath);
431
+ for (const file of files) {
432
+ if (!file.endsWith(".ts") && !file.endsWith(".js"))
433
+ continue;
434
+ if (file.includes(".test.") || file.includes(".spec."))
435
+ continue;
436
+ const name = file.replace(/\.(ts|js)$/, "");
437
+ const filePath = path5.join(fullPath, file);
438
+ try {
439
+ const mod = await import(filePath);
440
+ const mw = mod.default;
441
+ if (mw && typeof mw.handler === "function") {
442
+ registerMiddleware(name, mw);
443
+ } else if (typeof mw === "function") {
444
+ registerMiddleware(name, { handler: mw, mode: "universal" });
445
+ }
446
+ } catch (error) {
447
+ console.warn(`Failed to load middleware '${name}' from ${filePath}:`, error);
448
+ }
449
+ }
450
+ }
451
+ function parseQuery(search) {
452
+ const query = {};
453
+ if (!search)
454
+ return query;
455
+ const params = new URLSearchParams(search);
456
+ for (const [key, value] of params) {
457
+ query[key] = value;
458
+ }
459
+ return query;
460
+ }
461
+ function createRouteLocation(pathname, params, meta, search = "") {
462
+ return {
463
+ path: pathname,
464
+ params,
465
+ query: parseQuery(search),
466
+ fullPath: search ? `${pathname}?${search}` : pathname,
467
+ meta
468
+ };
469
+ }
470
+ function createMiddlewareContext(to, from, request) {
471
+ const isServer = typeof window === "undefined";
472
+ const isClient = !isServer;
473
+ const responseHeaders = new Headers;
474
+ return {
475
+ to,
476
+ from,
477
+ isClient,
478
+ isServer,
479
+ cookies: isServer && request ? createServerCookieManager(request, responseHeaders) : createClientCookieManager(),
480
+ storage: createStorageManager(),
481
+ state: {},
482
+ request,
483
+ responseHeaders
484
+ };
485
+ }
486
+ async function runMiddleware(middlewareNames, context) {
487
+ const names = Array.isArray(middlewareNames) ? middlewareNames : [middlewareNames];
488
+ const isServer = context.isServer;
489
+ for (const name of names) {
490
+ const middleware = getMiddleware(name);
491
+ if (!middleware) {
492
+ console.warn(`Middleware '${name}' not found`);
493
+ continue;
494
+ }
495
+ if (middleware.mode === "server" && !isServer)
496
+ continue;
497
+ if (middleware.mode === "client" && isServer)
498
+ continue;
499
+ try {
500
+ const result = await middleware.handler(context);
501
+ if (result) {
502
+ if (result.type === "redirect") {
503
+ return {
504
+ passed: false,
505
+ redirect: result,
506
+ state: context.state,
507
+ responseHeaders: context.responseHeaders
508
+ };
509
+ }
510
+ if (result.type === "abort") {
511
+ return {
512
+ passed: false,
513
+ abort: result,
514
+ state: context.state,
515
+ responseHeaders: context.responseHeaders
516
+ };
517
+ }
518
+ }
519
+ } catch (error) {
520
+ console.error(`Error in middleware '${name}':`, error);
521
+ return {
522
+ passed: false,
523
+ abort: {
524
+ type: "abort",
525
+ error: {
526
+ statusCode: 500,
527
+ message: error instanceof Error ? error.message : "Middleware error"
528
+ }
529
+ },
530
+ state: context.state,
531
+ responseHeaders: context.responseHeaders
532
+ };
533
+ }
534
+ }
535
+ return {
536
+ passed: true,
537
+ state: context.state,
538
+ responseHeaders: context.responseHeaders
539
+ };
540
+ }
541
+ // src/client.ts
542
+ function getRouterScript() {
543
+ return `
544
+ ;(function(){
545
+ 'use strict';
546
+ if(window.__stxRouter)return;
547
+
548
+ // \u2500\u2500 Configuration \u2500\u2500
549
+ var defaults={container:'main',loadingClass:'stx-navigating',viewTransitions:true,cache:true,scrollToTop:true,prefetch:true};
550
+ var o=Object.assign({},defaults,window.__stxRouterConfig||{},window.STX_ROUTER_OPTIONS||{});
551
+ var containerSel=o.container;
552
+
553
+ var cache={};
554
+ var prefetching={};
555
+ var isNavigating=false;
556
+
557
+ function getContainer(){
558
+ return document.querySelector(containerSel)||document.querySelector('[data-stx-content]')||document.querySelector('main');
559
+ }
560
+
561
+ // \u2500\u2500 Navigation \u2500\u2500
562
+ function navigate(url,pushState){
563
+ if(isNavigating)return;
564
+ var t=new URL(url,location.origin);
565
+
566
+ // Different origin \u2192 full navigation
567
+ if(t.origin!==location.origin){location.href=url;return}
568
+
569
+ // Hash-only navigation on same page
570
+ if(t.pathname===location.pathname&&t.hash){
571
+ if(pushState!==false)history.pushState({},'',t.href);
572
+ var el=document.querySelector(t.hash);
573
+ if(el)el.scrollIntoView({behavior:'smooth'});
574
+ return;
575
+ }
576
+
577
+ // Same page, no hash \u2192 skip
578
+ if(t.href===location.href&&!t.hash)return;
579
+
580
+ isNavigating=true;
581
+ document.body.classList.add(o.loadingClass);
582
+
583
+ var targetHref=t.href;
584
+ var targetPath=t.pathname;
585
+ var targetHash=t.hash;
586
+
587
+ function done(){isNavigating=false;document.body.classList.remove(o.loadingClass)}
588
+
589
+ if(o.cache&&cache[targetPath]){
590
+ swap(cache[targetPath],targetPath,pushState,targetHash);
591
+ done();
592
+ }else{
593
+ fetch(url,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){
594
+ if(!r.ok)throw new Error(r.status);
595
+ return r.text();
596
+ }).then(function(html){
597
+ if(o.cache)cache[targetPath]=html;
598
+ swap(html,targetPath,pushState,targetHash);
599
+ }).catch(function(){
600
+ location.href=url;
601
+ }).finally(done);
602
+ }
603
+ }
604
+
605
+ function swap(html,url,pushState,hash){
606
+ var parser=new DOMParser();
607
+ var doc=parser.parseFromString(html,'text/html');
608
+ var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
609
+ var currentContent=getContainer();
610
+ if(!newContent||!currentContent){location.href=url;return}
611
+
612
+ // Clean up existing signals/effects
613
+ if(window.stx&&window.stx._cleanupContainer){
614
+ window.stx._cleanupContainer(currentContent);
615
+ }
616
+
617
+ function doSwap(){
618
+ // \u2500\u2500 Swap <head> styles \u2500\u2500
619
+ // Inject new styles FIRST, then remove old to prevent unstyled flash
620
+ var keepIds={'stx-view-transitions':1,'stx-r-css':1};
621
+ var curStyles=document.querySelectorAll('head style');
622
+ var newStyles=doc.querySelectorAll('head style');
623
+
624
+ var incoming=[];
625
+ newStyles.forEach(function(s){
626
+ if(!keepIds[s.id]){
627
+ var ns=document.createElement('style');
628
+ ns.textContent=s.textContent;
629
+ ns.setAttribute('data-stx-incoming','');
630
+ if(s.getAttribute('data-crosswind'))ns.setAttribute('data-crosswind',s.getAttribute('data-crosswind'));
631
+ document.head.appendChild(ns);
632
+ incoming.push(ns);
633
+ }
634
+ });
635
+
636
+ // Remove old styles (except persistent ones and incoming)
637
+ curStyles.forEach(function(s){
638
+ if(!keepIds[s.id]&&!s.hasAttribute('data-stx-incoming'))s.remove();
639
+ });
640
+
641
+ incoming.forEach(function(s){s.removeAttribute('data-stx-incoming')});
642
+
643
+ // \u2500\u2500 Swap main content \u2500\u2500
644
+ currentContent.innerHTML=newContent.innerHTML;
645
+
646
+ // \u2500\u2500 Load new external <head> scripts \u2500\u2500
647
+ var loadedSrcs={};
648
+ document.querySelectorAll('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
649
+ var extPromises=[];
650
+ doc.querySelectorAll('head script[src]').forEach(function(s){
651
+ var src=new URL(s.getAttribute('src'),location.origin).href;
652
+ if(loadedSrcs[src])return;
653
+ loadedSrcs[src]=1;
654
+ extPromises.push(new Promise(function(resolve,reject){
655
+ var ns=document.createElement('script');
656
+ ns.src=src;
657
+ ns.onload=resolve;
658
+ ns.onerror=reject;
659
+ document.head.appendChild(ns);
660
+ }));
661
+ });
662
+
663
+ // \u2500\u2500 Script re-execution \u2500\u2500
664
+ // Remove previously injected page scripts
665
+ document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
666
+
667
+ var scripts=[];
668
+ doc.querySelectorAll('script').forEach(function(s){
669
+ var text=s.textContent||'';
670
+ // Skip the signals runtime
671
+ if(text.indexOf('let activeEffect')!==-1)return;
672
+ // Skip the router
673
+ if(text.indexOf('__stxRouter')!==-1)return;
674
+ // Skip layout shell scripts
675
+ if(text.indexOf('__stxDashboardInit')!==-1||text.indexOf('__stxLayoutInit')!==-1)return;
676
+ // Skip external scripts
677
+ if(s.hasAttribute('src'))return;
678
+ // Skip empty
679
+ if(!text.trim())return;
680
+ scripts.push(text);
681
+ });
682
+
683
+ function execScripts(){
684
+ scripts.forEach(function(text){
685
+ var ns=document.createElement('script');
686
+ ns.textContent=text;
687
+ ns.setAttribute('data-stx-page','');
688
+ document.body.appendChild(ns);
689
+ });
690
+ }
691
+
692
+ if(extPromises.length>0){
693
+ Promise.all(extPromises).then(execScripts).catch(execScripts);
694
+ }else{
695
+ execScripts();
696
+ }
697
+
698
+ // Update active nav links
699
+ updateNav(url);
700
+ updateActiveLinks();
701
+
702
+ // Scroll
703
+ if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
704
+ else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
705
+
706
+ // Update title
707
+ var newTitle=doc.querySelector('title');
708
+ if(newTitle)document.title=newTitle.textContent;
709
+
710
+ // Push history state
711
+ if(pushState!==false)history.pushState({},'',url+(hash||''));
712
+
713
+ // Dispatch navigation events
714
+ window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
715
+ window.dispatchEvent(new Event('stx:load'));
716
+ }
717
+
718
+ if(o.viewTransitions&&document.startViewTransition){
719
+ document.startViewTransition(doSwap);
720
+ }else{
721
+ // Fallback fade for browsers without View Transitions API
722
+ currentContent.style.transition='opacity 0.12s ease-out';
723
+ currentContent.style.opacity='0';
724
+ setTimeout(function(){
725
+ doSwap();
726
+ currentContent.style.opacity='1';
727
+ setTimeout(function(){currentContent.style.transition=''},150);
728
+ },120);
729
+ }
730
+ }
731
+
732
+ // \u2500\u2500 Link interception \u2500\u2500
733
+ function shouldIntercept(link){
734
+ if(!link)return false;
735
+ var href=link.getAttribute('href');
736
+ if(!href)return false;
737
+ if(href.startsWith('http')||href.startsWith('#')||href.startsWith('mailto:')||href.startsWith('tel:')||href.startsWith('javascript:'))return false;
738
+ if(link.target==='_blank')return false;
739
+ if(link.hasAttribute('data-stx-no-router')||link.hasAttribute('data-no-router')||link.hasAttribute('download'))return false;
740
+ if(href===location.pathname)return false;
741
+ if(!getContainer())return false;
742
+ return true;
743
+ }
744
+
745
+ document.addEventListener('click',function(e){
746
+ if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0)return;
747
+ if(!e.target||!e.target.closest)return;
748
+ var link=e.target.closest('a[href]');
749
+ if(!shouldIntercept(link))return;
750
+ e.preventDefault();
751
+ e.stopPropagation();
752
+ navigate(link.getAttribute('href'));
753
+ },true);
754
+
755
+ // \u2500\u2500 Back/forward \u2500\u2500
756
+ window.addEventListener('popstate',function(){
757
+ navigate(location.pathname+location.search+location.hash,false);
758
+ });
759
+
760
+ // \u2500\u2500 Prefetch on hover \u2500\u2500
761
+ if(o.prefetch){
762
+ document.addEventListener('mouseover',function(e){
763
+ if(!e.target||!e.target.closest)return;
764
+ var link=e.target.closest('a[href]');
765
+ if(!shouldIntercept(link))return;
766
+ // Only prefetch links with data-stx-prefetch or all internal links
767
+ var href=link.getAttribute('href');
768
+ if(cache[href]||prefetching[href])return;
769
+ prefetching[href]=true;
770
+ fetch(href,{headers:{'X-STX-Router':'true','Accept':'text/html'}}).then(function(r){return r.text()}).then(function(html){
771
+ if(o.cache)cache[href]=html;
772
+ }).catch(function(){}).finally(function(){delete prefetching[href]});
773
+ },true);
774
+ }
775
+
776
+ // \u2500\u2500 Active link management \u2500\u2500
777
+ function updateNav(url){
778
+ document.querySelectorAll('nav a[href], #mobileNav a[href], [data-stx-nav] a[href]').forEach(function(a){
779
+ var href=a.getAttribute('href');
780
+ if(!href||href.startsWith('#')||href.startsWith('http'))return;
781
+ var isActive=(href===url)||(href==='/'&&url==='/');
782
+ a.style.background=isActive?'var(--nav-active-bg, rgba(0,0,0,0.06))':'';
783
+ a.style.color=isActive?'var(--text-primary, inherit)':'var(--text-nav, inherit)';
784
+ a.style.fontWeight=isActive?'500':'';
785
+ if(a.hasAttribute('data-stx-link')){
786
+ var ac=a.getAttribute('data-stx-active-class')||'active';
787
+ if(isActive)a.classList.add(ac);else a.classList.remove(ac);
788
+ }
789
+ });
790
+ }
791
+
792
+ function updateActiveLinks(){
793
+ var links=document.querySelectorAll('[data-stx-link]');
794
+ var cur=location.pathname;
795
+ links.forEach(function(link){
796
+ var href=link.getAttribute('href')||'';
797
+ var ac=link.getAttribute('data-stx-active-class')||'active';
798
+ var eac=link.getAttribute('data-stx-exact-active-class')||'exact-active';
799
+ ac.split(' ').forEach(function(cls){if(cls)link.classList.remove(cls)});
800
+ eac.split(' ').forEach(function(cls){if(cls)link.classList.remove(cls)});
801
+ var isExact=cur===href;
802
+ var isActive=href!=='/'?cur.startsWith(href):cur==='/';
803
+ if(isExact)eac.split(' ').forEach(function(cls){if(cls)link.classList.add(cls)});
804
+ if(isActive)ac.split(' ').forEach(function(cls){if(cls)link.classList.add(cls)});
805
+ });
806
+ }
807
+
808
+ // \u2500\u2500 Loading bar + View Transitions CSS \u2500\u2500
809
+ function injectStyles(){
810
+ if(document.getElementById('stx-r-css'))return;
811
+ var s=document.createElement('style');s.id='stx-r-css';
812
+ s.textContent='.stx-navigating{cursor:wait}.stx-navigating *{pointer-events:none}@keyframes stx-l{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}.stx-navigating::before{content:\\'\\';position:fixed;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,#78dce8,transparent);animation:stx-l 1s ease-in-out infinite;z-index:99999}';
813
+ document.head.appendChild(s);
814
+ }
815
+
816
+ function injectViewTransitionCSS(){
817
+ if(document.getElementById('stx-view-transitions'))return;
818
+ var s=document.createElement('style');s.id='stx-view-transitions';
819
+ s.textContent='::view-transition-group(root){animation:none}::view-transition-old(root){animation:none}::view-transition-new(root){animation:none}#app-content,[data-stx-content]{view-transition-name:stx-content}::view-transition-old(stx-content){animation:stx-fade-out .15s ease-out both}::view-transition-new(stx-content){animation:stx-fade-in .15s ease-in .1s both}@keyframes stx-fade-out{from{opacity:1}to{opacity:0}}@keyframes stx-fade-in{from{opacity:0}to{opacity:1}}::view-transition{background:transparent}::view-transition-group(stx-content){background:inherit;overflow:hidden}';
820
+ (document.head||document.documentElement).appendChild(s);
821
+ }
822
+
823
+ // \u2500\u2500 Public API \u2500\u2500
824
+ var router={
825
+ navigate:navigate,
826
+ navigateTo:navigate,
827
+ prefetch:function(url){
828
+ if(!cache[url]){
829
+ fetch(url,{headers:{'X-STX-Router':'true'}}).then(function(r){return r.text()}).then(function(html){cache[url]=html}).catch(function(){});
830
+ }
831
+ },
832
+ clearCache:function(){for(var k in cache)delete cache[k]},
833
+ cache:cache,
834
+ swap:swap,
835
+ updateNav:updateNav
836
+ };
837
+
838
+ window.__stxRouter=router;
839
+ window.stxRouter=router;
840
+ if(window.stx)window.stx.router=router;
841
+
842
+ // \u2500\u2500 Initialize \u2500\u2500
843
+ function init(){
844
+ injectStyles();
845
+ injectViewTransitionCSS();
846
+ updateActiveLinks();
847
+ updateNav(location.pathname);
848
+ }
849
+
850
+ if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
851
+ })();
852
+ `;
853
+ }
854
+ export {
855
+ setAppUrl,
856
+ runMiddleware,
857
+ route,
858
+ resolveLayoutChain,
859
+ resetRoutes,
860
+ registerMiddleware,
861
+ patternToRegex,
862
+ navigateTo,
863
+ matchRoute,
864
+ loadMiddlewareFromDirectory,
865
+ hasMiddleware,
866
+ getRouterScript,
867
+ getMiddlewareNames,
868
+ getMiddleware,
869
+ generateRouteTypes,
870
+ formatRoutes,
871
+ findErrorPage,
872
+ filePathToPattern,
873
+ defineRoutes,
874
+ defineRoute,
875
+ defineMiddleware,
876
+ createStorageManager,
877
+ createServerCookieManager,
878
+ createRouter,
879
+ createRouteLocation,
880
+ createMiddlewareContext,
881
+ createClientCookieManager,
882
+ clearMiddleware,
883
+ abortNavigation,
884
+ Router
885
+ };
886
+
887
+ //# debugId=3C52557F9211FAA564756E2164756E21