@zenithbuild/router 0.7.3 → 0.7.5
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 +21 -0
- package/README.md +2 -1
- package/dist/history.js +31 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/navigate.d.ts +2 -0
- package/dist/navigate.js +16 -0
- package/dist/navigation-shell.d.ts +38 -0
- package/dist/navigation-shell.js +392 -0
- package/dist/router.js +9 -2
- package/index.d.ts +46 -4
- package/package.json +3 -2
- package/template-core.js +10 -1
- package/template-document.js +49 -0
- package/template-form.js +174 -0
- package/template-navigation.js +130 -151
- package/template.js +5 -2
package/index.d.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
export type RouteDownloadBody = string | Uint8Array | ArrayBuffer;
|
|
2
|
+
|
|
3
|
+
export interface RouteDownloadOptions {
|
|
4
|
+
filename: string;
|
|
5
|
+
contentType?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export type RouteResult =
|
|
2
9
|
| { kind: "allow" }
|
|
3
10
|
| { kind: "redirect"; location: string; status?: number }
|
|
4
11
|
| { kind: "deny"; status: 401 | 403 | 404; message?: string }
|
|
5
|
-
| { kind: "data"; data: any }
|
|
12
|
+
| { kind: "data"; data: any }
|
|
13
|
+
| { kind: "invalid"; data: any; status: 400 | 422 }
|
|
14
|
+
| { kind: "json"; data: any; status?: number }
|
|
15
|
+
| { kind: "text"; body: string; status?: number }
|
|
16
|
+
| { kind: "download"; filename: string; contentType: string; status?: 200 };
|
|
6
17
|
|
|
7
18
|
export type GuardResult = Extract<RouteResult, { kind: "allow" | "redirect" | "deny" }>;
|
|
8
19
|
export type LoadResult = Extract<RouteResult, { kind: "data" | "redirect" | "deny" }>;
|
|
20
|
+
export type RouteSession = Record<string, unknown>;
|
|
21
|
+
export type RequireSessionOptions =
|
|
22
|
+
| { redirectTo: string; status?: 302 | 303 | 307 }
|
|
23
|
+
| { deny: 401 | 403 | 404; message?: string };
|
|
9
24
|
|
|
10
25
|
export interface RouteContext {
|
|
11
26
|
params: Record<string, string>;
|
|
@@ -17,17 +32,24 @@ export interface RouteContext {
|
|
|
17
32
|
route: { id: string; pattern: string; file: string };
|
|
18
33
|
env: Record<string, string>;
|
|
19
34
|
auth: {
|
|
20
|
-
getSession(
|
|
21
|
-
requireSession(
|
|
35
|
+
getSession(): Promise<RouteSession | null>;
|
|
36
|
+
requireSession(options: RequireSessionOptions): Promise<RouteSession>;
|
|
37
|
+
signIn(sessionObject: RouteSession): Promise<void>;
|
|
38
|
+
signOut(): Promise<void>;
|
|
22
39
|
};
|
|
23
40
|
allow(): { kind: "allow" };
|
|
24
41
|
redirect(location: string, status?: number): { kind: "redirect"; location: string; status: number };
|
|
25
42
|
deny(status: 401 | 403 | 404, message?: string): { kind: "deny"; status: 401 | 403 | 404; message?: string };
|
|
26
43
|
data(payload: any): { kind: "data"; data: any };
|
|
44
|
+
invalid(payload: any, status?: 400 | 422): { kind: "invalid"; data: any; status: 400 | 422 };
|
|
45
|
+
json(payload: any, status?: number): { kind: "json"; data: any; status: number };
|
|
46
|
+
text(body: string, status?: number): { kind: "text"; body: string; status: number };
|
|
47
|
+
download(body: RouteDownloadBody, options: RouteDownloadOptions): { kind: "download"; filename: string; contentType: string; status: 200 };
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
export declare function createRouter(config: { routes: any[]; container: HTMLElement }): { start: () => Promise<void>; destroy: () => void; };
|
|
30
51
|
export declare function navigate(path: string): Promise<void>;
|
|
52
|
+
export declare function refreshCurrentRoute(): Promise<void>;
|
|
31
53
|
export declare function back(): void;
|
|
32
54
|
export declare function forward(): void;
|
|
33
55
|
export declare function getCurrentPath(): string;
|
|
@@ -41,7 +63,26 @@ export interface RouteProtectionPolicy {
|
|
|
41
63
|
forbiddenPath?: string;
|
|
42
64
|
}
|
|
43
65
|
|
|
44
|
-
export type NavigationType = "push" | "pop";
|
|
66
|
+
export type NavigationType = "push" | "pop" | "refresh";
|
|
67
|
+
export type NavigationShellPhase = "idle" | "leaving" | "swapping" | "entering";
|
|
68
|
+
|
|
69
|
+
export interface NavigationShellState {
|
|
70
|
+
phase: NavigationShellPhase;
|
|
71
|
+
navigationId: number | null;
|
|
72
|
+
navigationType: NavigationType | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface NavigationShellOptions {
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
onStateChange?: (state: NavigationShellState, context: { previousState: NavigationShellState }) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface NavigationShellController {
|
|
81
|
+
mount(): () => void;
|
|
82
|
+
destroy(): void;
|
|
83
|
+
getPhase(): NavigationShellPhase;
|
|
84
|
+
getState(): NavigationShellState;
|
|
85
|
+
}
|
|
45
86
|
|
|
46
87
|
export interface NavigationLifecyclePayload {
|
|
47
88
|
navigationId: number;
|
|
@@ -95,3 +136,4 @@ export type RouteEventName =
|
|
|
95
136
|
export declare function setRouteProtectionPolicy(policy: RouteProtectionPolicy): void;
|
|
96
137
|
export declare function on(eventName: RouteEventName, handler: RouteEventHandler): void;
|
|
97
138
|
export declare function off(eventName: RouteEventName, handler: RouteEventHandler): void;
|
|
139
|
+
export declare function zenNavigationShell(ref: { current?: Element | null }, options?: NavigationShellOptions | null): NavigationShellController;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/router",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "File-based SPA router for Zenith framework with deterministic, compile-time route resolution",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
"dist",
|
|
10
10
|
"template.js",
|
|
11
11
|
"template-core.js",
|
|
12
|
+
"template-document.js",
|
|
13
|
+
"template-form.js",
|
|
12
14
|
"template-lifecycle.js",
|
|
13
15
|
"template-navigation.js",
|
|
14
|
-
"index.js",
|
|
15
16
|
"index.d.ts",
|
|
16
17
|
"README.md",
|
|
17
18
|
"dist/**",
|
package/template-core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function renderRouterCoreSource({ manifest, runtimeSpec, coreSpec }) {
|
|
1
|
+
export function renderRouterCoreSource({ manifest, runtimeSpec, coreSpec, routeCheck = false }) {
|
|
2
2
|
return `import { hydrate as __zenithHydrate } from '${runtimeSpec}';
|
|
3
3
|
import { zenOnMount as __zenithOnMount } from '${coreSpec}';
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ void __zenithHydrate;
|
|
|
6
6
|
void __zenithOnMount;
|
|
7
7
|
|
|
8
8
|
const __ZENITH_MANIFEST__ = ${manifest};
|
|
9
|
+
const __ZENITH_ROUTE_CHECK_ENABLED__ = ${routeCheck ? 'true' : 'false'};
|
|
9
10
|
const __ZENITH_BASE_PATH__ = normalizeBasePath(
|
|
10
11
|
typeof __ZENITH_MANIFEST__.base_path === "string" ? __ZENITH_MANIFEST__.base_path : "/"
|
|
11
12
|
);
|
|
@@ -436,6 +437,14 @@ function resolveScrollTarget(targetUrl, historyMode, popstateState) {
|
|
|
436
437
|
const saved = readStoredScroll(popstateState);
|
|
437
438
|
return { mode: "restore", x: saved.x, y: saved.y, focusTarget: null };
|
|
438
439
|
}
|
|
440
|
+
if (historyMode === "refresh") {
|
|
441
|
+
return {
|
|
442
|
+
mode: "restore",
|
|
443
|
+
x: window.scrollX || window.pageXOffset || 0,
|
|
444
|
+
y: window.scrollY || window.pageYOffset || 0,
|
|
445
|
+
focusTarget: null
|
|
446
|
+
};
|
|
447
|
+
}
|
|
439
448
|
return { mode: "top", x: 0, y: 0, focusTarget: null };
|
|
440
449
|
}
|
|
441
450
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function renderRouterDocumentSource() {
|
|
2
|
+
return `function extractSsrData(parsed) {
|
|
3
|
+
if (!parsed || typeof parsed.getElementById !== "function") return {};
|
|
4
|
+
const ssrScript = parsed.getElementById("zenith-ssr-data");
|
|
5
|
+
if (!ssrScript) return {};
|
|
6
|
+
const source = typeof ssrScript.textContent === "string" ? ssrScript.textContent : "";
|
|
7
|
+
const marker = "window.__zenith_ssr_data =";
|
|
8
|
+
const markerIndex = source.indexOf(marker);
|
|
9
|
+
if (markerIndex === -1) return {};
|
|
10
|
+
const jsonText = source.slice(markerIndex + marker.length).trim().replace(/;$/, "");
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(jsonText);
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseDocumentPayload(html) {
|
|
19
|
+
if (typeof DOMParser === "undefined") return null;
|
|
20
|
+
const parsed = new DOMParser().parseFromString(html, "text/html");
|
|
21
|
+
return {
|
|
22
|
+
html,
|
|
23
|
+
title: parsed.title || "",
|
|
24
|
+
ssrData: extractSsrData(parsed)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isHtmlResponse(response) {
|
|
29
|
+
const contentType = String(response.headers.get("content-type") || "");
|
|
30
|
+
return /text\\/html|application\\/xhtml\\+xml/i.test(contentType);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createDocumentDetail(payload, response) {
|
|
34
|
+
return {
|
|
35
|
+
title: payload && typeof payload.title === "string" ? payload.title : "",
|
|
36
|
+
hasSsrData: !!(payload && payload.ssrData && typeof payload.ssrData === "object"),
|
|
37
|
+
status: response && typeof response.status === "number" ? response.status : 200
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createScrollDetail(targetUrl, scrollTarget) {
|
|
42
|
+
return {
|
|
43
|
+
mode: scrollTarget.mode,
|
|
44
|
+
x: scrollTarget.x,
|
|
45
|
+
y: scrollTarget.y,
|
|
46
|
+
hash: targetUrl.hash || ""
|
|
47
|
+
};
|
|
48
|
+
}`;
|
|
49
|
+
}
|
package/template-form.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export function renderRouterFormSource() {
|
|
2
|
+
return `function normalizeFormMethod(method) {
|
|
3
|
+
const value = typeof method === "string" ? method.trim().toUpperCase() : "";
|
|
4
|
+
return value || "GET";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function readSubmitOverride(submitter, attributeName, propertyName) {
|
|
8
|
+
if (!submitter) return "";
|
|
9
|
+
if (typeof submitter.getAttribute === "function") {
|
|
10
|
+
const attrValue = submitter.getAttribute(attributeName);
|
|
11
|
+
if (typeof attrValue === "string" && attrValue.length > 0) {
|
|
12
|
+
return attrValue;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const propertyValue = submitter[propertyName];
|
|
16
|
+
return typeof propertyValue === "string" ? propertyValue : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveFormTargetUrl(form, submitter) {
|
|
20
|
+
const action =
|
|
21
|
+
readSubmitOverride(submitter, "formaction", "formAction") ||
|
|
22
|
+
form.getAttribute("action") ||
|
|
23
|
+
form.action ||
|
|
24
|
+
window.location.href;
|
|
25
|
+
return new URL(action, window.location.href);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveFormTargetValue(form, submitter) {
|
|
29
|
+
const target = readSubmitOverride(submitter, "formtarget", "formTarget") || form.target || "";
|
|
30
|
+
return String(target || "").trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveFormMethod(form, submitter) {
|
|
34
|
+
return normalizeFormMethod(
|
|
35
|
+
readSubmitOverride(submitter, "formmethod", "formMethod") || form.getAttribute("method") || form.method || "GET"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createFormSubmissionPayload(form, submitter) {
|
|
40
|
+
try {
|
|
41
|
+
return submitter ? new FormData(form, submitter) : new FormData(form);
|
|
42
|
+
} catch {
|
|
43
|
+
const formData = new FormData(form);
|
|
44
|
+
if (submitter && submitter.name) {
|
|
45
|
+
formData.append(submitter.name, submitter.value || "");
|
|
46
|
+
}
|
|
47
|
+
return formData;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldEnhanceForm(form, submitter) {
|
|
52
|
+
if (!form || typeof form.getAttribute !== "function") return false;
|
|
53
|
+
if (!form.hasAttribute("data-zen-form")) return false;
|
|
54
|
+
const target = resolveFormTargetValue(form, submitter);
|
|
55
|
+
if (target && target !== "_self") return false;
|
|
56
|
+
if (resolveFormMethod(form, submitter) !== "POST") return false;
|
|
57
|
+
|
|
58
|
+
const targetUrl = resolveFormTargetUrl(form, submitter);
|
|
59
|
+
if (targetUrl.origin !== window.location.origin) return false;
|
|
60
|
+
const resolved = resolveRoute(targetUrl.pathname);
|
|
61
|
+
return !!resolved && requiresServerReload(resolved.route);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function performEnhancedFormSubmission(form, submitter) {
|
|
65
|
+
const targetUrl = resolveFormTargetUrl(form, submitter);
|
|
66
|
+
const resolved = resolveRoute(targetUrl.pathname);
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
navigateViaBrowser(targetUrl, false);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const context = beginNavigation(targetUrl, resolved, "push");
|
|
73
|
+
let historyCommitted = false;
|
|
74
|
+
let documentDetail = null;
|
|
75
|
+
try {
|
|
76
|
+
dispatchRouteEvent("navigation:request", buildNavigationPayload(context));
|
|
77
|
+
context.stage = "fetch";
|
|
78
|
+
const response = await fetch(targetUrl.href, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: createFormSubmissionPayload(form, submitter),
|
|
81
|
+
credentials: "include",
|
|
82
|
+
headers: { Accept: "text/html,application/xhtml+xml" },
|
|
83
|
+
redirect: "manual",
|
|
84
|
+
signal: context.signal
|
|
85
|
+
});
|
|
86
|
+
if (!ensureCurrentNavigation(context)) return false;
|
|
87
|
+
|
|
88
|
+
if (response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400)) {
|
|
89
|
+
const redirectUrl = resolveRedirectUrl(response.headers.get("location"), targetUrl);
|
|
90
|
+
dispatchNavigationFallback(context, {
|
|
91
|
+
reason: "server-redirect",
|
|
92
|
+
location: redirectUrl.href,
|
|
93
|
+
status: response.status
|
|
94
|
+
});
|
|
95
|
+
navigateViaBrowser(redirectUrl, false);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isHtmlResponse(response) || (response.status !== 200 && response.status !== 400 && response.status !== 422)) {
|
|
100
|
+
dispatchNavigationFallback(context, {
|
|
101
|
+
reason: "http-status",
|
|
102
|
+
status: response.status
|
|
103
|
+
});
|
|
104
|
+
navigateViaBrowser(targetUrl, false);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const html = await response.text();
|
|
109
|
+
if (!ensureCurrentNavigation(context)) return false;
|
|
110
|
+
const payload = parseDocumentPayload(html);
|
|
111
|
+
if (!payload) {
|
|
112
|
+
dispatchNavigationFallback(context, {
|
|
113
|
+
reason: "document-parse"
|
|
114
|
+
});
|
|
115
|
+
navigateViaBrowser(targetUrl, false);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const committed = await commitNavigationDocument(
|
|
120
|
+
context,
|
|
121
|
+
resolved,
|
|
122
|
+
targetUrl,
|
|
123
|
+
"push",
|
|
124
|
+
null,
|
|
125
|
+
payload,
|
|
126
|
+
response
|
|
127
|
+
);
|
|
128
|
+
documentDetail = committed.documentDetail;
|
|
129
|
+
historyCommitted = committed.historyCommitted;
|
|
130
|
+
if (!committed.committed) return false;
|
|
131
|
+
return true;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!isAbortError(error)) {
|
|
134
|
+
emitNavigationError(context, {
|
|
135
|
+
reason: "runtime-failure",
|
|
136
|
+
error,
|
|
137
|
+
historyCommitted,
|
|
138
|
+
document: documentDetail
|
|
139
|
+
});
|
|
140
|
+
console.error("[Zenith Router] form submission failed", error);
|
|
141
|
+
dispatchNavigationFallback(context, {
|
|
142
|
+
reason: "runtime-failure",
|
|
143
|
+
historyCommitted
|
|
144
|
+
});
|
|
145
|
+
navigateViaBrowser(targetUrl, false);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
dispatchNavigationFallback(context, context.abortReason || {
|
|
149
|
+
reason: "superseded",
|
|
150
|
+
abortedStage: context.stage
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
} finally {
|
|
154
|
+
completeNavigation(context);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function installEnhancedFormHandling() {
|
|
159
|
+
document.addEventListener("submit", function(event) {
|
|
160
|
+
if (event.defaultPrevented) return;
|
|
161
|
+
const form = event.target;
|
|
162
|
+
const submitter = event.submitter || null;
|
|
163
|
+
if (!shouldEnhanceForm(form, submitter)) return;
|
|
164
|
+
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
performEnhancedFormSubmission(form, submitter).catch(function(error) {
|
|
167
|
+
if (!isAbortError(error)) {
|
|
168
|
+
console.error("[Zenith Router] enhanced form submission failed", error);
|
|
169
|
+
navigateViaBrowser(resolveFormTargetUrl(form, submitter), false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}`;
|
|
174
|
+
}
|