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/LICENSE.md +21 -0
- package/README.md +233 -0
- package/dist/client.d.ts +19 -0
- package/dist/codegen.d.ts +2 -0
- package/dist/file-router.d.ts +12 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +887 -0
- package/dist/index.js.map +16 -0
- package/dist/matcher.d.ts +3 -0
- package/dist/middleware.d.ts +28 -0
- package/dist/named-routes.d.ts +10 -0
- package/dist/nested-layouts.d.ts +1 -0
- package/dist/types.d.ts +97 -0
- package/package.json +50 -0
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
|