@hypen-space/core 0.4.980 → 0.5.3
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/README.md +5 -8
- package/dist/a11y.d.ts +82 -0
- package/dist/app.js +13 -1168
- package/dist/app.js.map +3 -9
- package/dist/chunk-00zt3w6e.js +58 -0
- package/dist/chunk-00zt3w6e.js.map +10 -0
- package/dist/chunk-3zvyth50.js +87 -0
- package/dist/chunk-3zvyth50.js.map +10 -0
- package/dist/chunk-5gtx3pza.js +11 -0
- package/dist/chunk-5gtx3pza.js.map +9 -0
- package/dist/chunk-7222131a.js +191 -0
- package/dist/chunk-7222131a.js.map +10 -0
- package/dist/chunk-avchpwhf.js +200 -0
- package/dist/chunk-avchpwhf.js.map +10 -0
- package/dist/chunk-ec0tgy9a.js +154 -0
- package/dist/chunk-ec0tgy9a.js.map +10 -0
- package/dist/chunk-fnss4dm2.js +479 -0
- package/dist/chunk-fnss4dm2.js.map +10 -0
- package/dist/chunk-g2gz9pgg.js +133 -0
- package/dist/chunk-g2gz9pgg.js.map +10 -0
- package/dist/chunk-g72v6mmf.js +341 -0
- package/dist/chunk-g72v6mmf.js.map +10 -0
- package/dist/chunk-g7g3jxty.js +351 -0
- package/dist/chunk-g7g3jxty.js.map +10 -0
- package/dist/chunk-g830re2c.js +50 -0
- package/dist/chunk-g830re2c.js.map +10 -0
- package/dist/chunk-gtwh1sg7.js +127 -0
- package/dist/chunk-gtwh1sg7.js.map +10 -0
- package/dist/chunk-k12cwfst.js +72 -0
- package/dist/chunk-k12cwfst.js.map +10 -0
- package/dist/chunk-mbqqsz6r.js +128 -0
- package/dist/chunk-mbqqsz6r.js.map +10 -0
- package/dist/chunk-ne632swm.js +209 -0
- package/dist/chunk-ne632swm.js.map +10 -0
- package/dist/chunk-p3pj1bax.js +206 -0
- package/dist/chunk-p3pj1bax.js.map +10 -0
- package/dist/chunk-vshdk2pw.js +56 -0
- package/dist/chunk-vshdk2pw.js.map +10 -0
- package/dist/chunk-wp2ebn2m.js +51 -0
- package/dist/chunk-wp2ebn2m.js.map +10 -0
- package/dist/chunk-x4qv4jm9.js +88 -0
- package/dist/chunk-x4qv4jm9.js.map +10 -0
- package/dist/chunk-ycxp4drd.js +211 -0
- package/dist/chunk-ycxp4drd.js.map +10 -0
- package/dist/components/builtin.js +14 -1246
- package/dist/components/builtin.js.map +3 -10
- package/dist/context.js +7 -362
- package/dist/context.js.map +3 -6
- package/dist/datasource.js +5 -76
- package/dist/datasource.js.map +3 -4
- package/dist/disposable.js +20 -367
- package/dist/disposable.js.map +3 -5
- package/dist/engine-base.d.ts +38 -2
- package/dist/engine-base.js +45 -412
- package/dist/engine-base.js.map +4 -6
- package/dist/events.js +7 -285
- package/dist/events.js.map +3 -5
- package/dist/hypen.js +8 -71
- package/dist/hypen.js.map +3 -4
- package/dist/index.browser.js +58 -2152
- package/dist/index.browser.js.map +3 -16
- package/dist/index.d.ts +4 -0
- package/dist/index.js +175 -3057
- package/dist/index.js.map +5 -22
- package/dist/logger.js +16 -220
- package/dist/logger.js.map +3 -4
- package/dist/managed-router.js +12 -1367
- package/dist/managed-router.js.map +3 -10
- package/dist/portable.js +6 -70
- package/dist/portable.js.map +3 -4
- package/dist/remote/client.js +9 -1010
- package/dist/remote/client.js.map +3 -8
- package/dist/remote/index.d.ts +2 -0
- package/dist/remote/index.js +511 -1066
- package/dist/remote/index.js.map +4 -9
- package/dist/remote/remote-session.d.ts +245 -0
- package/dist/remote/session.js +5 -147
- package/dist/remote/session.js.map +3 -4
- package/dist/renderer.js +7 -271
- package/dist/renderer.js.map +3 -5
- package/dist/resolver.js +5 -484
- package/dist/resolver.js.map +3 -5
- package/dist/result.js +25 -211
- package/dist/result.js.map +3 -4
- package/dist/retry.js +10 -336
- package/dist/retry.js.map +3 -5
- package/dist/router.js +9 -807
- package/dist/router.js.map +3 -8
- package/dist/state.js +10 -274
- package/dist/state.js.map +3 -5
- package/dist/types.d.ts +129 -1
- package/dist/validate.d.ts +19 -0
- package/package.json +13 -1
- package/src/a11y.ts +127 -0
- package/src/engine-base.ts +87 -4
- package/src/index.ts +10 -0
- package/src/remote/index.ts +16 -0
- package/src/remote/remote-session.ts +998 -0
- package/src/types.ts +131 -1
- package/src/validate.ts +37 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
disposableListener
|
|
3
|
+
} from "./chunk-ec0tgy9a.js";
|
|
4
|
+
import {
|
|
5
|
+
createObservableState,
|
|
6
|
+
getStateSnapshot
|
|
7
|
+
} from "./chunk-ycxp4drd.js";
|
|
8
|
+
import {
|
|
9
|
+
portable
|
|
10
|
+
} from "./chunk-g830re2c.js";
|
|
11
|
+
import {
|
|
12
|
+
frameworkLoggers
|
|
13
|
+
} from "./chunk-avchpwhf.js";
|
|
14
|
+
|
|
15
|
+
// src/router.ts
|
|
16
|
+
var log = frameworkLoggers.router;
|
|
17
|
+
|
|
18
|
+
class HypenRouter {
|
|
19
|
+
state;
|
|
20
|
+
subscribers = new Set;
|
|
21
|
+
isInitialized = false;
|
|
22
|
+
isUpdating = false;
|
|
23
|
+
browserListeners = [];
|
|
24
|
+
constructor() {
|
|
25
|
+
this.state = createObservableState({
|
|
26
|
+
currentPath: "/",
|
|
27
|
+
params: {},
|
|
28
|
+
query: {},
|
|
29
|
+
previousPath: null
|
|
30
|
+
}, {
|
|
31
|
+
onChange: () => {
|
|
32
|
+
this.notifySubscribers();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (typeof window !== "undefined") {
|
|
36
|
+
this.initializeBrowserSync();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
initializeBrowserSync() {
|
|
40
|
+
const initialPath = this.getPathFromBrowser();
|
|
41
|
+
this.state.currentPath = initialPath;
|
|
42
|
+
this.state.params = {};
|
|
43
|
+
this.state.query = this.parseQuery();
|
|
44
|
+
this.browserListeners.push(disposableListener(window, "popstate", () => {
|
|
45
|
+
const newPath = this.getPathFromBrowser();
|
|
46
|
+
this.updatePath(newPath, false);
|
|
47
|
+
}));
|
|
48
|
+
this.browserListeners.push(disposableListener(window, "hashchange", () => {
|
|
49
|
+
if (this.isUpdating)
|
|
50
|
+
return;
|
|
51
|
+
const newPath = this.getPathFromBrowser();
|
|
52
|
+
this.updatePath(newPath, false);
|
|
53
|
+
}));
|
|
54
|
+
this.isInitialized = true;
|
|
55
|
+
log.debug("Router initialized at:", initialPath);
|
|
56
|
+
}
|
|
57
|
+
getPathFromBrowser() {
|
|
58
|
+
if (typeof window === "undefined")
|
|
59
|
+
return "/";
|
|
60
|
+
const hash = window.location.hash.slice(1);
|
|
61
|
+
if (hash)
|
|
62
|
+
return hash;
|
|
63
|
+
return window.location.pathname;
|
|
64
|
+
}
|
|
65
|
+
parseQuery() {
|
|
66
|
+
if (typeof window === "undefined")
|
|
67
|
+
return {};
|
|
68
|
+
const query = {};
|
|
69
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
70
|
+
searchParams.forEach((value, key) => {
|
|
71
|
+
query[key] = value;
|
|
72
|
+
});
|
|
73
|
+
return query;
|
|
74
|
+
}
|
|
75
|
+
push(path) {
|
|
76
|
+
log.debug("push:", path);
|
|
77
|
+
this.updatePath(path, true);
|
|
78
|
+
}
|
|
79
|
+
replace(path) {
|
|
80
|
+
log.debug("replace:", path);
|
|
81
|
+
this.updatePath(path, true, true);
|
|
82
|
+
}
|
|
83
|
+
back() {
|
|
84
|
+
log.debug("back");
|
|
85
|
+
if (typeof window !== "undefined") {
|
|
86
|
+
window.history.back();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
forward() {
|
|
90
|
+
log.debug("forward");
|
|
91
|
+
if (typeof window !== "undefined") {
|
|
92
|
+
window.history.forward();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
updatePath(path, updateBrowser, replace = false) {
|
|
96
|
+
if (this.isUpdating)
|
|
97
|
+
return;
|
|
98
|
+
this.isUpdating = true;
|
|
99
|
+
try {
|
|
100
|
+
const oldPath = this.state.currentPath;
|
|
101
|
+
this.state.previousPath = oldPath;
|
|
102
|
+
this.state.currentPath = path;
|
|
103
|
+
this.state.query = this.parseQuery();
|
|
104
|
+
this.notifySubscribers();
|
|
105
|
+
if (updateBrowser && typeof window !== "undefined") {
|
|
106
|
+
const url = "#" + path;
|
|
107
|
+
if (replace) {
|
|
108
|
+
window.history.replaceState(null, "", url);
|
|
109
|
+
} else {
|
|
110
|
+
window.history.pushState(null, "", url);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const hashChangeEvent = new HashChangeEvent("hashchange", {
|
|
114
|
+
oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
|
|
115
|
+
newURL: window.location.href
|
|
116
|
+
});
|
|
117
|
+
window.dispatchEvent(hashChangeEvent);
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
this.isUpdating = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
getCurrentPath() {
|
|
125
|
+
return this.state.currentPath;
|
|
126
|
+
}
|
|
127
|
+
getParams() {
|
|
128
|
+
return { ...this.state.params };
|
|
129
|
+
}
|
|
130
|
+
getQuery() {
|
|
131
|
+
return { ...this.state.query };
|
|
132
|
+
}
|
|
133
|
+
getState() {
|
|
134
|
+
return getStateSnapshot(this.state);
|
|
135
|
+
}
|
|
136
|
+
matchPath(pattern, path) {
|
|
137
|
+
if (!pattern || typeof pattern !== "string")
|
|
138
|
+
return null;
|
|
139
|
+
if (!path || typeof path !== "string")
|
|
140
|
+
return null;
|
|
141
|
+
const cleanPath = path.split("?")[0] ?? path;
|
|
142
|
+
const result = portable.matchPath(pattern, cleanPath);
|
|
143
|
+
if (!result)
|
|
144
|
+
return null;
|
|
145
|
+
const params = {};
|
|
146
|
+
for (const [name, raw] of Object.entries(result.params)) {
|
|
147
|
+
try {
|
|
148
|
+
params[name] = decodeURIComponent(raw);
|
|
149
|
+
} catch {
|
|
150
|
+
params[name] = raw;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
params,
|
|
155
|
+
query: this.state.query,
|
|
156
|
+
path
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
onNavigate(callback) {
|
|
160
|
+
this.subscribers.add(callback);
|
|
161
|
+
try {
|
|
162
|
+
callback(this.getState());
|
|
163
|
+
} catch (error) {
|
|
164
|
+
log.error("Error in route subscriber:", error);
|
|
165
|
+
}
|
|
166
|
+
return () => {
|
|
167
|
+
this.subscribers.delete(callback);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
notifySubscribers() {
|
|
171
|
+
const routeState = this.getState();
|
|
172
|
+
this.subscribers.forEach((callback) => {
|
|
173
|
+
try {
|
|
174
|
+
callback(routeState);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
log.error("Error in route subscriber:", error);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
isActive(pattern) {
|
|
181
|
+
return this.matchPath(pattern, this.state.currentPath) !== null;
|
|
182
|
+
}
|
|
183
|
+
buildUrl(path, query) {
|
|
184
|
+
if (!query || Object.keys(query).length === 0) {
|
|
185
|
+
return path;
|
|
186
|
+
}
|
|
187
|
+
const queryString = new URLSearchParams(query).toString();
|
|
188
|
+
return `${path}?${queryString}`;
|
|
189
|
+
}
|
|
190
|
+
dispose() {
|
|
191
|
+
for (const listener of this.browserListeners) {
|
|
192
|
+
listener.dispose();
|
|
193
|
+
}
|
|
194
|
+
this.browserListeners = [];
|
|
195
|
+
this.subscribers.clear();
|
|
196
|
+
this.isInitialized = false;
|
|
197
|
+
log.debug("Router disposed");
|
|
198
|
+
}
|
|
199
|
+
[Symbol.dispose]() {
|
|
200
|
+
this.dispose();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { HypenRouter };
|
|
205
|
+
|
|
206
|
+
//# debugId=8596642936BDD80A64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/router.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Hypen Router - Declarative routing system\n * Integrated with Hypen's reactive state management\n */\n\nimport { createObservableState, getStateSnapshot } from \"./state.js\";\nimport { disposableListener, type Disposable } from \"./disposable.js\";\nimport { frameworkLoggers } from \"./logger.js\";\nimport { portable } from \"./portable.js\";\n\nconst log = frameworkLoggers.router;\n\nexport type RouteMatch = {\n params: Record<string, string>;\n query: Record<string, string>;\n path: string;\n};\n\nexport type RouteState = {\n currentPath: string;\n params: Record<string, string>;\n query: Record<string, string>;\n previousPath: string | null;\n};\n\nexport type RouteChangeCallback = (route: RouteState) => void;\n\n/**\n * Hypen Router - Manages application routing with pattern matching\n */\nexport class HypenRouter {\n private state: RouteState;\n private subscribers = new Set<RouteChangeCallback>();\n private isInitialized = false;\n private isUpdating = false;\n private browserListeners: Disposable[] = [];\n\n constructor() {\n // Create observable state for reactivity\n this.state = createObservableState<RouteState>(\n {\n currentPath: \"/\",\n params: {},\n query: {},\n previousPath: null,\n },\n {\n onChange: () => {\n this.notifySubscribers();\n },\n }\n );\n\n // Initialize from browser if available\n if (typeof window !== \"undefined\") {\n this.initializeBrowserSync();\n }\n }\n\n /**\n * Initialize browser history sync\n */\n private initializeBrowserSync() {\n // Get initial path from hash or pathname\n const initialPath = this.getPathFromBrowser();\n this.state.currentPath = initialPath;\n this.state.params = {};\n this.state.query = this.parseQuery();\n\n // Listen for browser back/forward\n this.browserListeners.push(\n disposableListener(window, \"popstate\", () => {\n const newPath = this.getPathFromBrowser();\n this.updatePath(newPath, false); // Don't push to history again\n })\n );\n\n // Listen for hash changes\n this.browserListeners.push(\n disposableListener(window, \"hashchange\", () => {\n // Don't respond to hashchange events we triggered ourselves\n if (this.isUpdating) return;\n const newPath = this.getPathFromBrowser();\n this.updatePath(newPath, false);\n })\n );\n\n this.isInitialized = true;\n log.debug(\"Router initialized at:\", initialPath);\n }\n\n /**\n * Get path from browser URL (supports both hash and pathname)\n */\n private getPathFromBrowser(): string {\n if (typeof window === \"undefined\") return \"/\";\n\n // Prefer hash-based routing for simplicity\n const hash = window.location.hash.slice(1);\n if (hash) return hash;\n\n // Fallback to pathname\n return window.location.pathname;\n }\n\n /**\n * Parse query string from URL\n */\n private parseQuery(): Record<string, string> {\n if (typeof window === \"undefined\") return {};\n\n const query: Record<string, string> = {};\n const searchParams = new URLSearchParams(window.location.search);\n\n searchParams.forEach((value, key) => {\n query[key] = value;\n });\n\n return query;\n }\n\n /**\n * Navigate to a new path\n */\n push(path: string) {\n log.debug(\"push:\", path);\n this.updatePath(path, true);\n }\n\n /**\n * Replace current path without adding to history\n */\n replace(path: string) {\n log.debug(\"replace:\", path);\n this.updatePath(path, true, true);\n }\n\n /**\n * Go back in history\n */\n back() {\n log.debug(\"back\");\n if (typeof window !== \"undefined\") {\n window.history.back();\n }\n }\n\n /**\n * Go forward in history\n */\n forward() {\n log.debug(\"forward\");\n if (typeof window !== \"undefined\") {\n window.history.forward();\n }\n }\n\n /**\n * Update the current path\n */\n private updatePath(\n path: string,\n updateBrowser: boolean,\n replace: boolean = false\n ) {\n // Prevent re-entrant updates\n if (this.isUpdating) return;\n\n this.isUpdating = true;\n try {\n const oldPath = this.state.currentPath;\n this.state.previousPath = oldPath;\n this.state.currentPath = path;\n this.state.query = this.parseQuery();\n\n // Notify subscribers synchronously before any browser events\n this.notifySubscribers();\n\n // Update browser URL if needed\n if (updateBrowser && typeof window !== \"undefined\") {\n const url = \"#\" + path;\n if (replace) {\n window.history.replaceState(null, \"\", url);\n } else {\n window.history.pushState(null, \"\", url);\n }\n\n // Manually trigger hashchange event. Wrap in try-catch because\n // in test environments the global HashChangeEvent class may come\n // from a different realm than the window, causing dispatchEvent to\n // reject it with \"parameter 1 is not of type 'Event'\".\n try {\n const hashChangeEvent = new HashChangeEvent(\"hashchange\", {\n oldURL: window.location.href.replace(window.location.hash, \"#\" + oldPath),\n newURL: window.location.href,\n });\n window.dispatchEvent(hashChangeEvent);\n } catch {\n // Ignore cross-realm Event dispatch errors\n }\n }\n } finally {\n this.isUpdating = false;\n }\n }\n\n /**\n * Get current path\n */\n getCurrentPath(): string {\n return this.state.currentPath;\n }\n\n /**\n * Get current route params\n */\n getParams(): Record<string, string> {\n return { ...this.state.params };\n }\n\n /**\n * Get current query params\n */\n getQuery(): Record<string, string> {\n return { ...this.state.query };\n }\n\n /**\n * Get full route state snapshot\n */\n getState(): RouteState {\n return getStateSnapshot(this.state);\n }\n\n /**\n * Match a pattern against a path.\n *\n * Thin wrapper over [`portable.matchPath`] — the three-case matcher\n * (exact / `/prefix/*` wildcard / `:param`) lives in the Rust engine\n * at `hypen-engine-rs/src/portable/route.rs` and is reached through\n * the WASM installed by server / web-engine, with a TS fallback for\n * standalone core use.\n */\n matchPath(pattern: string, path: string): RouteMatch | null {\n if (!pattern || typeof pattern !== \"string\") return null;\n if (!path || typeof path !== \"string\") return null;\n\n // Strip any query portion before matching; the engine matcher\n // operates on the clean path and we track `query` ourselves.\n // `noUncheckedIndexedAccess` makes `[0]` `string | undefined`; fall\n // back to the full path when split yields an empty array (which\n // only happens for empty input — already ruled out above).\n const cleanPath = path.split(\"?\")[0] ?? path;\n const result = portable.matchPath(pattern, cleanPath);\n if (!result) return null;\n\n const params: Record<string, string> = {};\n for (const [name, raw] of Object.entries(result.params)) {\n try {\n params[name] = decodeURIComponent(raw);\n } catch {\n params[name] = raw;\n }\n }\n\n return {\n params,\n query: this.state.query,\n path,\n };\n }\n\n /**\n * Subscribe to route changes\n */\n onNavigate(callback: RouteChangeCallback): () => void {\n this.subscribers.add(callback);\n\n // Call immediately with current state\n try {\n callback(this.getState());\n } catch (error) {\n log.error(\"Error in route subscriber:\", error);\n }\n\n // Return unsubscribe function\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n /**\n * Notify all subscribers of route change\n */\n private notifySubscribers() {\n const routeState = this.getState();\n this.subscribers.forEach((callback) => {\n try {\n callback(routeState);\n } catch (error) {\n log.error(\"Error in route subscriber:\", error);\n }\n });\n }\n\n /**\n * Check if a path matches the current route\n */\n isActive(pattern: string): boolean {\n return this.matchPath(pattern, this.state.currentPath) !== null;\n }\n\n /**\n * Get a URL with query params\n */\n buildUrl(path: string, query?: Record<string, string>): string {\n if (!query || Object.keys(query).length === 0) {\n return path;\n }\n\n const queryString = new URLSearchParams(query).toString();\n return `${path}?${queryString}`;\n }\n\n /**\n * Clean up browser event listeners and subscriptions\n */\n dispose(): void {\n for (const listener of this.browserListeners) {\n listener.dispose();\n }\n this.browserListeners = [];\n this.subscribers.clear();\n this.isInitialized = false;\n log.debug(\"Router disposed\");\n }\n\n /**\n * Support `using` syntax for automatic cleanup\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "8596642936BDD80A64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/datasource.ts
|
|
2
|
+
class DataSourceManager {
|
|
3
|
+
plugins = new Map;
|
|
4
|
+
configs = new Map;
|
|
5
|
+
state = new Map;
|
|
6
|
+
engine;
|
|
7
|
+
constructor(engine) {
|
|
8
|
+
this.engine = engine;
|
|
9
|
+
}
|
|
10
|
+
async use(plugin, config) {
|
|
11
|
+
const { name } = plugin;
|
|
12
|
+
this.state.set(name, {});
|
|
13
|
+
this.plugins.set(name, plugin);
|
|
14
|
+
this.configs.set(name, config);
|
|
15
|
+
await plugin.connect(config, (change) => {
|
|
16
|
+
const current = this.state.get(name) ?? {};
|
|
17
|
+
for (const path of change.paths) {
|
|
18
|
+
if (path in change.values) {
|
|
19
|
+
current[path] = change.values[path];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
this.state.set(name, current);
|
|
23
|
+
this.engine.setContext(name, current);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
get(name) {
|
|
27
|
+
return this.plugins.get(name);
|
|
28
|
+
}
|
|
29
|
+
has(name) {
|
|
30
|
+
return this.plugins.has(name);
|
|
31
|
+
}
|
|
32
|
+
getNames() {
|
|
33
|
+
return Array.from(this.plugins.keys());
|
|
34
|
+
}
|
|
35
|
+
getStatus(name) {
|
|
36
|
+
return this.plugins.get(name)?.status;
|
|
37
|
+
}
|
|
38
|
+
async remove(name) {
|
|
39
|
+
const plugin = this.plugins.get(name);
|
|
40
|
+
if (plugin) {
|
|
41
|
+
await plugin.disconnect();
|
|
42
|
+
this.plugins.delete(name);
|
|
43
|
+
this.configs.delete(name);
|
|
44
|
+
this.state.delete(name);
|
|
45
|
+
this.engine.removeContext(name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async disconnectAll() {
|
|
49
|
+
const names = Array.from(this.plugins.keys());
|
|
50
|
+
await Promise.all(names.map((name) => this.remove(name)));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { DataSourceManager };
|
|
55
|
+
|
|
56
|
+
//# debugId=1CFD72C4B3AA1F6464756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/datasource.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Data Source Plugin System for Hypen\n *\n * Enables live subscription databases (SpacetimeDB, Firebase, Convex, Electric SQL)\n * to push data into the Hypen reactive engine via a unified plugin interface.\n *\n * UI bindings use the `@provider.path` syntax:\n * - `@spacetime.messages` → SpacetimeDB messages table\n * - `@firebase.user.profile` → Firebase user profile\n * - `@convex.tasks` → Convex tasks table\n *\n * @module\n */\n\n// ---------------------------------------------------------------------------\n// Core Types\n// ---------------------------------------------------------------------------\n\n/** Connection status for a data source provider */\nexport type DataSourceStatus =\n | \"disconnected\"\n | \"connecting\"\n | \"connected\"\n | \"reconnecting\"\n | \"error\";\n\n/** Describes a subscription to a table or query */\nexport interface DataSourceQuery {\n /** Table or collection name (e.g., \"messages\", \"users\") */\n table: string;\n /** Optional filter criteria */\n filter?: Record<string, unknown>;\n /** Optional raw query string (e.g., SQL for SpacetimeDB) */\n sql?: string;\n}\n\n/** Handle returned by subscribe() — call unsubscribe() to stop receiving updates */\nexport interface DataSourceSubscription {\n /** Stop receiving updates for this subscription */\n unsubscribe(): void;\n /** Current subscription status */\n status: \"pending\" | \"applied\" | \"error\";\n /** Error details if status is \"error\" */\n error?: Error;\n}\n\n/** Describes a batch of changes pushed by a plugin */\nexport interface DataSourceChange {\n /** Changed paths relative to the provider root (e.g., [\"messages\", \"users\"]) */\n paths: string[];\n /** New values at those paths */\n values: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Plugin Interface\n// ---------------------------------------------------------------------------\n\n/**\n * Interface that all data source plugins must implement.\n *\n * A plugin manages the connection lifecycle and translates database-specific\n * events (row inserts, updates, deletes) into `DataSourceChange` objects that\n * the Hypen engine can consume.\n *\n * @typeParam TConfig - Plugin-specific configuration type\n *\n * @example\n * ```typescript\n * class SpacetimeDBPlugin implements DataSourcePlugin<SpacetimeDBConfig> {\n * readonly name = \"spacetime\";\n * status: DataSourceStatus = \"disconnected\";\n *\n * async connect(config: SpacetimeDBConfig, onChange: (change: DataSourceChange) => void) {\n * // Connect to SpacetimeDB, subscribe to tables, push changes via onChange\n * }\n * // ...\n * }\n * ```\n */\nexport interface DataSourcePlugin<TConfig = unknown> {\n /** Unique provider name (e.g., \"spacetime\", \"firebase\", \"convex\") */\n readonly name: string;\n\n /** Current connection status */\n status: DataSourceStatus;\n\n /**\n * Connect to the data source and begin pushing changes.\n *\n * @param config - Plugin-specific configuration\n * @param onChange - Callback to push data changes into the engine.\n * The plugin MUST call this whenever subscribed data changes.\n */\n connect(config: TConfig, onChange: (change: DataSourceChange) => void): Promise<void>;\n\n /**\n * Subscribe to a specific table or query.\n * Returns a handle to unsubscribe later.\n */\n subscribe(query: DataSourceQuery): DataSourceSubscription;\n\n /**\n * Call a remote procedure / reducer / mutation.\n *\n * @param method - Method name (e.g., \"sendMessage\" for SpacetimeDB reducer)\n * @param args - Arguments to pass\n * @returns The result from the remote call\n */\n call(method: string, ...args: unknown[]): Promise<unknown>;\n\n /**\n * Disconnect from the data source and clean up all subscriptions.\n */\n disconnect(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Data Source Manager\n// ---------------------------------------------------------------------------\n\n/** Interface for the engine's data source operations (maps to WASM setContext/removeContext) */\nexport interface IDataSourceEngine {\n /**\n * Set (or replace) a named data source context.\n * Registers the provider, stores the data, and re-renders bound nodes.\n * Sparse merging should happen at the SDK layer before calling this.\n */\n setContext(name: string, data: Record<string, unknown>): void;\n /**\n * Remove a data source context entirely.\n * Drops the provider's state and re-renders bound nodes (they resolve to null).\n */\n removeContext(name: string): void;\n}\n\n/**\n * Manages data source plugin lifecycles and bridges them to the Hypen engine.\n *\n * The manager:\n * 1. Registers plugins with the engine (creates state slots)\n * 2. Connects plugins and wires their onChange callbacks to engine updates\n * 3. Manages subscription lifecycles\n * 4. Handles cleanup on disconnect\n *\n * @example\n * ```typescript\n * const manager = new DataSourceManager(engine);\n *\n * // Register and connect a SpacetimeDB plugin\n * await manager.use(new SpacetimeDBPlugin(), {\n * uri: \"ws://localhost:3000\",\n * moduleName: \"chat\",\n * tables: [\"user\", \"message\"],\n * });\n *\n * // Access plugin for mutations\n * await manager.get(\"spacetime\").call(\"sendMessage\", \"Hello!\");\n *\n * // Clean up\n * await manager.disconnectAll();\n * ```\n */\nexport class DataSourceManager {\n private plugins = new Map<string, DataSourcePlugin>();\n private configs = new Map<string, unknown>();\n /** Accumulated state per provider — sparse merging happens here before setContext */\n private state = new Map<string, Record<string, unknown>>();\n private engine: IDataSourceEngine;\n\n constructor(engine: IDataSourceEngine) {\n this.engine = engine;\n }\n\n /**\n * Register, connect, and start a data source plugin.\n *\n * @param plugin - The plugin instance\n * @param config - Plugin-specific configuration\n */\n async use<C>(plugin: DataSourcePlugin<C>, config: C): Promise<void> {\n const { name } = plugin;\n\n // Initialize state slot\n this.state.set(name, {});\n\n // Store references\n this.plugins.set(name, plugin as DataSourcePlugin);\n this.configs.set(name, config);\n\n // Connect the plugin with an onChange callback that merges sparse data\n // and pushes the full state via setContext\n await plugin.connect(config, (change: DataSourceChange) => {\n // Merge changed paths into the accumulated state\n const current = this.state.get(name) ?? {};\n for (const path of change.paths) {\n if (path in change.values) {\n current[path] = change.values[path];\n }\n }\n this.state.set(name, current);\n\n // Push the full merged state to the engine\n this.engine.setContext(name, current);\n });\n }\n\n /**\n * Get a registered plugin by name.\n * Useful for calling mutations: `manager.get(\"spacetime\").call(\"sendMessage\", text)`\n */\n get<T extends DataSourcePlugin = DataSourcePlugin>(name: string): T | undefined {\n return this.plugins.get(name) as T | undefined;\n }\n\n /** Check if a plugin is registered */\n has(name: string): boolean {\n return this.plugins.has(name);\n }\n\n /** Get all registered provider names */\n getNames(): string[] {\n return Array.from(this.plugins.keys());\n }\n\n /** Get the status of a specific plugin */\n getStatus(name: string): DataSourceStatus | undefined {\n return this.plugins.get(name)?.status;\n }\n\n /**\n * Disconnect and remove a specific plugin.\n */\n async remove(name: string): Promise<void> {\n const plugin = this.plugins.get(name);\n if (plugin) {\n await plugin.disconnect();\n this.plugins.delete(name);\n this.configs.delete(name);\n this.state.delete(name);\n this.engine.removeContext(name);\n }\n }\n\n /**\n * Disconnect all plugins and clean up.\n */\n async disconnectAll(): Promise<void> {\n const names = Array.from(this.plugins.keys());\n await Promise.all(names.map((name) => this.remove(name)));\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAmKO,MAAM,kBAAkB;AAAA,EACrB,UAAU,IAAI;AAAA,EACd,UAAU,IAAI;AAAA,EAEd,QAAQ,IAAI;AAAA,EACZ;AAAA,EAER,WAAW,CAAC,QAA2B;AAAA,IACrC,KAAK,SAAS;AAAA;AAAA,OASV,IAAM,CAAC,QAA6B,QAA0B;AAAA,IAClE,QAAQ,SAAS;AAAA,IAGjB,KAAK,MAAM,IAAI,MAAM,CAAC,CAAC;AAAA,IAGvB,KAAK,QAAQ,IAAI,MAAM,MAA0B;AAAA,IACjD,KAAK,QAAQ,IAAI,MAAM,MAAM;AAAA,IAI7B,MAAM,OAAO,QAAQ,QAAQ,CAAC,WAA6B;AAAA,MAEzD,MAAM,UAAU,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC;AAAA,MACzC,WAAW,QAAQ,OAAO,OAAO;AAAA,QAC/B,IAAI,QAAQ,OAAO,QAAQ;AAAA,UACzB,QAAQ,QAAQ,OAAO,OAAO;AAAA,QAChC;AAAA,MACF;AAAA,MACA,KAAK,MAAM,IAAI,MAAM,OAAO;AAAA,MAG5B,KAAK,OAAO,WAAW,MAAM,OAAO;AAAA,KACrC;AAAA;AAAA,EAOH,GAAkD,CAAC,MAA6B;AAAA,IAC9E,OAAO,KAAK,QAAQ,IAAI,IAAI;AAAA;AAAA,EAI9B,GAAG,CAAC,MAAuB;AAAA,IACzB,OAAO,KAAK,QAAQ,IAAI,IAAI;AAAA;AAAA,EAI9B,QAAQ,GAAa;AAAA,IACnB,OAAO,MAAM,KAAK,KAAK,QAAQ,KAAK,CAAC;AAAA;AAAA,EAIvC,SAAS,CAAC,MAA4C;AAAA,IACpD,OAAO,KAAK,QAAQ,IAAI,IAAI,GAAG;AAAA;AAAA,OAM3B,OAAM,CAAC,MAA6B;AAAA,IACxC,MAAM,SAAS,KAAK,QAAQ,IAAI,IAAI;AAAA,IACpC,IAAI,QAAQ;AAAA,MACV,MAAM,OAAO,WAAW;AAAA,MACxB,KAAK,QAAQ,OAAO,IAAI;AAAA,MACxB,KAAK,QAAQ,OAAO,IAAI;AAAA,MACxB,KAAK,MAAM,OAAO,IAAI;AAAA,MACtB,KAAK,OAAO,cAAc,IAAI;AAAA,IAChC;AAAA;AAAA,OAMI,cAAa,GAAkB;AAAA,IACnC,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,KAAK,CAAC;AAAA,IAC5C,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC,CAAC;AAAA;AAE5D;",
|
|
8
|
+
"debugId": "1CFD72C4B3AA1F6464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/hypen.ts
|
|
2
|
+
function createBindingProxy(root) {
|
|
3
|
+
const handler = {
|
|
4
|
+
get(_, prop) {
|
|
5
|
+
if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf") {
|
|
6
|
+
return () => `@{${root}}`;
|
|
7
|
+
}
|
|
8
|
+
if (typeof prop === "symbol") {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (prop === "toJSON") {
|
|
12
|
+
return () => `@{${root}}`;
|
|
13
|
+
}
|
|
14
|
+
return createBindingProxy(`${root}.${prop}`);
|
|
15
|
+
},
|
|
16
|
+
has() {
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
ownKeys() {
|
|
20
|
+
return [];
|
|
21
|
+
},
|
|
22
|
+
getOwnPropertyDescriptor() {
|
|
23
|
+
return {
|
|
24
|
+
configurable: true,
|
|
25
|
+
enumerable: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
return new Proxy({}, handler);
|
|
30
|
+
}
|
|
31
|
+
var state = createBindingProxy("state");
|
|
32
|
+
var item = createBindingProxy("item");
|
|
33
|
+
var index = {
|
|
34
|
+
[Symbol.toPrimitive]: () => "@{index}",
|
|
35
|
+
toString: () => "@{index}",
|
|
36
|
+
valueOf: () => "@{index}",
|
|
37
|
+
toJSON: () => "@{index}"
|
|
38
|
+
};
|
|
39
|
+
function hypen(strings, ...expressions) {
|
|
40
|
+
let result = strings[0];
|
|
41
|
+
for (let i = 0;i < expressions.length; i++) {
|
|
42
|
+
const expr = expressions[i];
|
|
43
|
+
result += String(expr);
|
|
44
|
+
result += strings[i + 1];
|
|
45
|
+
}
|
|
46
|
+
return result.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { state, item, index, hypen };
|
|
50
|
+
|
|
51
|
+
//# debugId=D49BE0F14E5AC09C64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/hypen.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Hypen Tagged Template Literal\n *\n * Enables single-file components with inline UI templates.\n *\n * The `hypen` tagged template preserves @{state.x} and @{item.x} bindings\n * instead of interpolating them, allowing you to write:\n *\n * @example\n * ```typescript\n * import { app, hypen, state } from \"@hypen-space/core\";\n *\n * export default app\n * .defineState({ count: 0 })\n * .onAction(\"increment\", ({ state }) => {\n * state.count += 1;\n * })\n * .ui(hypen`\n * Column {\n * Text(\"Count: @{state.count}\")\n * Button { Text(\"+\") }\n * .onClick(\"@actions.increment\")\n * }\n * `);\n * ```\n */\n\n/**\n * Creates a proxy that captures property access as a binding path string.\n *\n * When used in a template literal, the proxy's toString() returns the\n * full binding expression (e.g., \"@{state.user.name}\"). This lets you\n * interpolate JS variables into hypen templates without losing type safety.\n *\n * @example\n * ```typescript\n * const state = createBindingProxy('state');\n * String(state.user.name) // Returns: \"@{state.user.name}\"\n * ```\n */\nfunction createBindingProxy(root: string): any {\n const handler: ProxyHandler<object> = {\n get(_, prop: string | symbol): any {\n // Handle Symbol.toPrimitive, toString, and valueOf for string conversion\n if (\n prop === Symbol.toPrimitive ||\n prop === \"toString\" ||\n prop === \"valueOf\"\n ) {\n return () => `@{${root}}`;\n }\n\n // Handle other Symbol properties (e.g., Symbol.toStringTag)\n if (typeof prop === \"symbol\") {\n return undefined;\n }\n\n // Handle JSON.stringify\n if (prop === \"toJSON\") {\n return () => `@{${root}}`;\n }\n\n // Chain to nested property: state.user -> state.user.name\n return createBindingProxy(`${root}.${prop}`);\n },\n\n // Support for `in` operator\n has() {\n return true;\n },\n\n // Support for Object.keys() - return empty to avoid enumeration issues\n ownKeys() {\n return [];\n },\n\n getOwnPropertyDescriptor() {\n return {\n configurable: true,\n enumerable: true,\n };\n },\n };\n\n return new Proxy({} as any, handler);\n}\n\n/**\n * Proxy for state bindings.\n *\n * In hypen templates, prefer the direct `@{state.x}` syntax. This proxy\n * exists for cases where you want to interpolate JS values into templates\n * — e.g., `hypen\\`Text(\"${state.user.name}\")\\`` produces the same string\n * as `hypen\\`Text(\"@{state.user.name}\")\\``.\n *\n * @example\n * ```typescript\n * // Preferred: direct syntax\n * hypen`Text(\"Hello, @{state.user.name}\")`\n *\n * // Also works: proxy interpolation\n * hypen`Text(\"Hello, ${state.user.name}\")`\n * ```\n */\nexport const state: any = createBindingProxy(\"state\");\n\n/**\n * Proxy for item bindings in list iteration.\n *\n * Use inside List components to reference the current item:\n *\n * @example\n * ```typescript\n * hypen`\n * List(@state.items) {\n * Text(\"@{item.name}: @{item.price}\")\n * }\n * `\n * ```\n */\nexport const item: any = createBindingProxy(\"item\");\n\n/**\n * Proxy for index in list iteration.\n *\n * Use inside List components to reference the current index:\n *\n * @example\n * ```typescript\n * hypen`\n * List(@state.items) {\n * Text(\"Item #@{index}: @{item.name}\")\n * }\n * `\n * ```\n */\nexport const index: any = {\n [Symbol.toPrimitive]: () => \"@{index}\",\n toString: () => \"@{index}\",\n valueOf: () => \"@{index}\",\n toJSON: () => \"@{index}\",\n};\n\n/**\n * Tagged template literal for Hypen DSL templates.\n *\n * Prefer the direct `@{state.x}` / `@{item.x}` syntax. The `state`, `item`,\n * and `index` proxies are available for interpolating JS variables or\n * mixing dynamic content with bindings.\n *\n * @example\n * ```typescript\n * import { hypen, state, item } from \"@hypen-space/core\";\n *\n * // Preferred: direct binding syntax\n * const t1 = hypen`Text(\"Count: @{state.count}\")`;\n *\n * // Nested paths work the same way\n * const t2 = hypen`Text(\"Hello, @{state.user.profile.name}\")`;\n *\n * // List with item binding\n * const t3 = hypen`\n * List(@state.products) {\n * Text(\"@{item.name} - $@{item.price}\")\n * }\n * `;\n *\n * // Interpolate JS variables alongside bindings\n * const title = \"My App\";\n * const t4 = hypen`Text(\"${title}: @{state.count}\")`;\n * // Result: 'Text(\"My App: @{state.count}\")'\n * ```\n *\n * @param strings - Template literal string parts\n * @param expressions - Interpolated JS values or binding proxies\n * @returns The template string with bindings preserved\n */\nexport function hypen(\n strings: TemplateStringsArray,\n ...expressions: unknown[]\n): string {\n let result = strings[0];\n\n for (let i = 0; i < expressions.length; i++) {\n const expr = expressions[i];\n\n // Convert expression to string\n // Binding proxies will return \"@{state.x}\" via their toString()\n result += String(expr);\n result += strings[i + 1]!;\n }\n\n return result!.trim();\n}\n\n/**\n * Type helper for defining state shape.\n * Use with state proxy for better IDE support in complex scenarios.\n *\n * @example\n * ```typescript\n * type MyState = { user: { name: string; age: number } };\n * const typedState = state as StateProxy<MyState>;\n * ```\n */\nexport type StateProxy<T> = {\n [K in keyof T]: T[K] extends object\n ? StateProxy<T[K]> & { toString(): string }\n : { toString(): string };\n} & { toString(): string };\n\n/**\n * Type helper for item proxy in lists.\n */\nexport type ItemProxy<T> = StateProxy<T>;\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAwCA,SAAS,kBAAkB,CAAC,MAAmB;AAAA,EAC7C,MAAM,UAAgC;AAAA,IACpC,GAAG,CAAC,GAAG,MAA4B;AAAA,MAEjC,IACE,SAAS,OAAO,eAChB,SAAS,cACT,SAAS,WACT;AAAA,QACA,OAAO,MAAM,KAAK;AAAA,MACpB;AAAA,MAGA,IAAI,OAAO,SAAS,UAAU;AAAA,QAC5B;AAAA,MACF;AAAA,MAGA,IAAI,SAAS,UAAU;AAAA,QACrB,OAAO,MAAM,KAAK;AAAA,MACpB;AAAA,MAGA,OAAO,mBAAmB,GAAG,QAAQ,MAAM;AAAA;AAAA,IAI7C,GAAG,GAAG;AAAA,MACJ,OAAO;AAAA;AAAA,IAIT,OAAO,GAAG;AAAA,MACR,OAAO,CAAC;AAAA;AAAA,IAGV,wBAAwB,GAAG;AAAA,MACzB,OAAO;AAAA,QACL,cAAc;AAAA,QACd,YAAY;AAAA,MACd;AAAA;AAAA,EAEJ;AAAA,EAEA,OAAO,IAAI,MAAM,CAAC,GAAU,OAAO;AAAA;AAoB9B,IAAM,QAAa,mBAAmB,OAAO;AAgB7C,IAAM,OAAY,mBAAmB,MAAM;AAgB3C,IAAM,QAAa;AAAA,GACvB,OAAO,cAAc,MAAM;AAAA,EAC5B,UAAU,MAAM;AAAA,EAChB,SAAS,MAAM;AAAA,EACf,QAAQ,MAAM;AAChB;AAoCO,SAAS,KAAK,CACnB,YACG,aACK;AAAA,EACR,IAAI,SAAS,QAAQ;AAAA,EAErB,SAAS,IAAI,EAAG,IAAI,YAAY,QAAQ,KAAK;AAAA,IAC3C,MAAM,OAAO,YAAY;AAAA,IAIzB,UAAU,OAAO,IAAI;AAAA,IACrB,UAAU,QAAQ,IAAI;AAAA,EACxB;AAAA,EAEA,OAAO,OAAQ,KAAK;AAAA;",
|
|
8
|
+
"debugId": "D49BE0F14E5AC09C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
app
|
|
3
|
+
} from "./chunk-fnss4dm2.js";
|
|
4
|
+
import {
|
|
5
|
+
frameworkLoggers
|
|
6
|
+
} from "./chunk-avchpwhf.js";
|
|
7
|
+
|
|
8
|
+
// src/components/builtin.ts
|
|
9
|
+
var log = frameworkLoggers.router;
|
|
10
|
+
async function updateRouteVisibility(currentPath, engine, doc) {
|
|
11
|
+
const targetDoc = doc ?? document;
|
|
12
|
+
const routeElements = targetDoc.querySelectorAll('[data-hypen-type="route"]');
|
|
13
|
+
let matchFound = false;
|
|
14
|
+
for (let index = 0;index < routeElements.length; index++) {
|
|
15
|
+
const routeEl = routeElements[index];
|
|
16
|
+
const htmlEl = routeEl;
|
|
17
|
+
const routePath = htmlEl.dataset.routePath || "/";
|
|
18
|
+
const isMatch = routePath === currentPath;
|
|
19
|
+
htmlEl.style.display = isMatch ? "flex" : "none";
|
|
20
|
+
if (isMatch) {
|
|
21
|
+
matchFound = true;
|
|
22
|
+
const componentName = htmlEl.dataset.routeComponent;
|
|
23
|
+
const isLazy = htmlEl.dataset.routeLazy === "true";
|
|
24
|
+
const hasContent = htmlEl.children.length > 0;
|
|
25
|
+
const shouldRender = componentName && engine && (isLazy || !hasContent);
|
|
26
|
+
if (shouldRender) {
|
|
27
|
+
try {
|
|
28
|
+
await engine.renderLazyRoute(routePath, componentName, htmlEl);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
log.error(`Failed to render route ${routePath}:`, err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!matchFound) {
|
|
36
|
+
log.warn(`No route matched path: ${currentPath}. Available routes:`, Array.from(routeElements).map((el) => el.dataset.routePath));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
var Router = app.defineState({
|
|
40
|
+
currentPath: "/",
|
|
41
|
+
matchedRoute: null,
|
|
42
|
+
routeParams: {}
|
|
43
|
+
}, { name: "__Router" }).onCreated((state, context) => {
|
|
44
|
+
if (!context) {
|
|
45
|
+
log.error("Requires global context");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const router = context.router;
|
|
49
|
+
if (!router) {
|
|
50
|
+
log.error("Router not found in context");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const hypenEngine = context.__hypenEngine;
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
updateRouteVisibility(state.currentPath, hypenEngine);
|
|
56
|
+
}, 100);
|
|
57
|
+
router.onNavigate((routeState) => {
|
|
58
|
+
state.currentPath = routeState.currentPath;
|
|
59
|
+
state.routeParams = routeState.params;
|
|
60
|
+
updateRouteVisibility(routeState.currentPath, hypenEngine);
|
|
61
|
+
});
|
|
62
|
+
}).build();
|
|
63
|
+
var Route = app.defineState({}, { name: "__Route" }).build();
|
|
64
|
+
var Link = app.defineState({
|
|
65
|
+
to: "/",
|
|
66
|
+
isActive: false
|
|
67
|
+
}, { name: "__Link" }).onAction("navigate", ({ state, context }) => {
|
|
68
|
+
const router = context?.router;
|
|
69
|
+
if (!router) {
|
|
70
|
+
log.error("Link requires router context");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const targetPath = state.to;
|
|
74
|
+
router.push(targetPath);
|
|
75
|
+
}).onCreated((state, context) => {
|
|
76
|
+
if (!context)
|
|
77
|
+
return;
|
|
78
|
+
const router = context.router;
|
|
79
|
+
if (router) {
|
|
80
|
+
router.onNavigate((routeState) => {
|
|
81
|
+
state.isActive = routeState.currentPath === state.to;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}).build();
|
|
85
|
+
|
|
86
|
+
export { updateRouteVisibility, Router, Route, Link };
|
|
87
|
+
|
|
88
|
+
//# debugId=3BF292EEACD8652064756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/components/builtin.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Built-in Hypen Components\n * Framework-provided components like Router and Route\n */\n\nimport { app } from \"../app.js\";\nimport { frameworkLoggers } from \"../logger.js\";\n\nconst log = frameworkLoggers.router;\n\n/**\n * Interface for the engine methods needed by route visibility updates\n */\nexport interface RouteEngine {\n renderLazyRoute(path: string, component: string, element: HTMLElement): Promise<void>;\n getState(): Record<string, unknown>;\n}\n\n/**\n * Update route visibility based on the current path.\n * Shows the matching route element and hides all others.\n * For lazy routes (or routes without content), triggers rendering via the engine.\n *\n * @param currentPath - The path to match against route elements\n * @param engine - Optional engine for lazy route rendering\n * @param doc - Document to query for route elements (defaults to global document)\n */\nexport async function updateRouteVisibility(\n currentPath: string,\n engine?: RouteEngine,\n doc?: Document\n): Promise<void> {\n const targetDoc = doc ?? document;\n const routeElements = targetDoc.querySelectorAll(\n '[data-hypen-type=\"route\"]'\n );\n\n let matchFound = false;\n\n for (let index = 0; index < routeElements.length; index++) {\n const routeEl = routeElements[index];\n const htmlEl = routeEl as HTMLElement;\n const routePath = htmlEl.dataset.routePath || \"/\";\n\n // Simple path matching (exact match for now)\n const isMatch = routePath === currentPath;\n\n // Only show the matching route, hide all others\n htmlEl.style.display = isMatch ? \"flex\" : \"none\";\n\n if (isMatch) {\n matchFound = true;\n\n // Check if this route needs to be rendered\n const componentName = htmlEl.dataset.routeComponent;\n const isLazy = htmlEl.dataset.routeLazy === \"true\";\n const hasContent = htmlEl.children.length > 0;\n\n // Always re-render lazy routes; render non-lazy only if they have no content\n const shouldRender = componentName && engine && (isLazy || !hasContent);\n\n if (shouldRender) {\n try {\n await engine.renderLazyRoute(\n routePath,\n componentName,\n htmlEl\n );\n } catch (err) {\n log.error(`Failed to render route ${routePath}:`, err);\n }\n }\n }\n }\n\n if (!matchFound) {\n log.warn(\n `No route matched path: ${currentPath}. Available routes:`,\n Array.from(routeElements).map(\n (el: Element) => (el as HTMLElement).dataset.routePath\n )\n );\n }\n}\n\n/**\n * Router Component\n * Automatically matches routes and renders the appropriate child Route\n *\n * Usage in Hypen DSL:\n * Router {\n * Route(path: \"/\") { HomePage }\n * Route(path: \"/products\") { ProductList }\n * Route(path: \"/users/:id\") { UserProfile }\n * }\n */\nexport const Router = app\n .defineState(\n {\n currentPath: \"/\",\n matchedRoute: null as any,\n routeParams: {} as Record<string, string>,\n },\n { name: \"__Router\" }\n )\n .onCreated((state, context) => {\n if (!context) {\n log.error(\"Requires global context\");\n return;\n }\n\n // Subscribe to router changes\n const router = context.router;\n if (!router) {\n log.error(\"Router not found in context\");\n return;\n }\n\n // Get access to the Hypen engine for lazy rendering\n const hypenEngine = (context as any).__hypenEngine as RouteEngine | undefined;\n\n // Initial route visibility (after DOM is ready)\n setTimeout(() => {\n updateRouteVisibility(state.currentPath, hypenEngine);\n }, 100);\n\n router.onNavigate((routeState) => {\n state.currentPath = routeState.currentPath;\n state.routeParams = routeState.params;\n\n // Update route visibility when path changes\n updateRouteVisibility(routeState.currentPath, hypenEngine);\n });\n })\n .build();\n\n/**\n * Route Component\n * Defines a route pattern and its content\n * This is just a marker component - Router processes it\n *\n * Props:\n * - path: string - Route pattern (e.g., \"/\", \"/users/:id\", \"/dashboard/*\")\n *\n * Usage:\n * Route(path: \"/products\") {\n * ProductList\n * }\n */\nexport const Route = app.defineState({}, { name: \"__Route\" }).build();\n\n/**\n * Navigation Link Component\n * Navigates to a route when clicked\n *\n * Props:\n * - to: string - Target path\n *\n * Usage:\n * Link(to: \"/products\") {\n * Text(\"View Products\")\n * }\n */\nexport const Link = app\n .defineState(\n {\n to: \"/\",\n isActive: false,\n },\n { name: \"__Link\" }\n )\n .onAction(\"navigate\", ({ state, context }) => {\n const router = context?.router;\n if (!router) {\n log.error(\"Link requires router context\");\n return;\n }\n\n const targetPath = state.to;\n router.push(targetPath);\n })\n .onCreated((state, context) => {\n if (!context) return;\n\n // Check if current path matches this link's target\n const router = context.router;\n if (router) {\n router.onNavigate((routeState) => {\n state.isActive = routeState.currentPath === state.to;\n });\n }\n })\n .build();\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;AAQA,IAAM,MAAM,iBAAiB;AAmB7B,eAAsB,qBAAqB,CACzC,aACA,QACA,KACe;AAAA,EACf,MAAM,YAAY,OAAO;AAAA,EACzB,MAAM,gBAAgB,UAAU,iBAC9B,2BACF;AAAA,EAEA,IAAI,aAAa;AAAA,EAEjB,SAAS,QAAQ,EAAG,QAAQ,cAAc,QAAQ,SAAS;AAAA,IACzD,MAAM,UAAU,cAAc;AAAA,IAC9B,MAAM,SAAS;AAAA,IACf,MAAM,YAAY,OAAO,QAAQ,aAAa;AAAA,IAG9C,MAAM,UAAU,cAAc;AAAA,IAG9B,OAAO,MAAM,UAAU,UAAU,SAAS;AAAA,IAE1C,IAAI,SAAS;AAAA,MACX,aAAa;AAAA,MAGb,MAAM,gBAAgB,OAAO,QAAQ;AAAA,MACrC,MAAM,SAAS,OAAO,QAAQ,cAAc;AAAA,MAC5C,MAAM,aAAa,OAAO,SAAS,SAAS;AAAA,MAG5C,MAAM,eAAe,iBAAiB,WAAW,UAAU,CAAC;AAAA,MAE5D,IAAI,cAAc;AAAA,QAChB,IAAI;AAAA,UACF,MAAM,OAAO,gBACX,WACA,eACA,MACF;AAAA,UACA,OAAO,KAAK;AAAA,UACZ,IAAI,MAAM,0BAA0B,cAAc,GAAG;AAAA;AAAA,MAEzD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,CAAC,YAAY;AAAA,IACf,IAAI,KACF,0BAA0B,kCAC1B,MAAM,KAAK,aAAa,EAAE,IACxB,CAAC,OAAiB,GAAmB,QAAQ,SAC/C,CACF;AAAA,EACF;AAAA;AAcK,IAAM,SAAS,IACnB,YACC;AAAA,EACE,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa,CAAC;AAChB,GACA,EAAE,MAAM,WAAW,CACrB,EACC,UAAU,CAAC,OAAO,YAAY;AAAA,EAC7B,IAAI,CAAC,SAAS;AAAA,IACZ,IAAI,MAAM,yBAAyB;AAAA,IACnC;AAAA,EACF;AAAA,EAGA,MAAM,SAAS,QAAQ;AAAA,EACvB,IAAI,CAAC,QAAQ;AAAA,IACX,IAAI,MAAM,6BAA6B;AAAA,IACvC;AAAA,EACF;AAAA,EAGA,MAAM,cAAe,QAAgB;AAAA,EAGrC,WAAW,MAAM;AAAA,IACf,sBAAsB,MAAM,aAAa,WAAW;AAAA,KACnD,GAAG;AAAA,EAEN,OAAO,WAAW,CAAC,eAAe;AAAA,IAChC,MAAM,cAAc,WAAW;AAAA,IAC/B,MAAM,cAAc,WAAW;AAAA,IAG/B,sBAAsB,WAAW,aAAa,WAAW;AAAA,GAC1D;AAAA,CACF,EACA,MAAM;AAeF,IAAM,QAAQ,IAAI,YAAY,CAAC,GAAG,EAAE,MAAM,UAAU,CAAC,EAAE,MAAM;AAc7D,IAAM,OAAO,IACjB,YACC;AAAA,EACE,IAAI;AAAA,EACJ,UAAU;AACZ,GACA,EAAE,MAAM,SAAS,CACnB,EACC,SAAS,YAAY,GAAG,OAAO,cAAc;AAAA,EAC5C,MAAM,SAAS,SAAS;AAAA,EACxB,IAAI,CAAC,QAAQ;AAAA,IACX,IAAI,MAAM,8BAA8B;AAAA,IACxC;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,MAAM;AAAA,EACzB,OAAO,KAAK,UAAU;AAAA,CACvB,EACA,UAAU,CAAC,OAAO,YAAY;AAAA,EAC7B,IAAI,CAAC;AAAA,IAAS;AAAA,EAGd,MAAM,SAAS,QAAQ;AAAA,EACvB,IAAI,QAAQ;AAAA,IACV,OAAO,WAAW,CAAC,eAAe;AAAA,MAChC,MAAM,WAAW,WAAW,gBAAgB,MAAM;AAAA,KACnD;AAAA,EACH;AAAA,CACD,EACA,MAAM;",
|
|
8
|
+
"debugId": "3BF292EEACD8652064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|