@hypen-space/core 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -11
- package/dist/src/app.js +470 -44
- package/dist/src/app.js.map +7 -5
- package/dist/src/components/builtin.js +470 -44
- package/dist/src/components/builtin.js.map +7 -5
- package/dist/src/discovery.js +559 -65
- package/dist/src/discovery.js.map +8 -6
- package/dist/src/engine.browser.js +2 -2
- package/dist/src/engine.browser.js.map +2 -2
- package/dist/src/engine.js +18 -9
- package/dist/src/engine.js.map +3 -3
- package/dist/src/index.browser.js +863 -82
- package/dist/src/index.browser.js.map +11 -7
- package/dist/src/index.js +1591 -125
- package/dist/src/index.js.map +17 -10
- package/dist/src/remote/client.js +525 -35
- package/dist/src/remote/client.js.map +7 -4
- package/dist/src/remote/index.js +1796 -35
- package/dist/src/remote/index.js.map +13 -4
- package/dist/src/router.js +55 -29
- package/dist/src/router.js.map +3 -3
- package/dist/src/state.js +57 -29
- package/dist/src/state.js.map +3 -3
- package/package.json +8 -2
- package/src/app.ts +292 -13
- package/src/discovery.ts +123 -18
- package/src/disposable.ts +281 -0
- package/src/engine.browser.ts +1 -1
- package/src/engine.ts +29 -10
- package/src/hypen.ts +209 -0
- package/src/index.ts +147 -11
- package/src/logger.ts +338 -0
- package/src/remote/client.ts +263 -56
- package/src/remote/index.ts +25 -1
- package/src/remote/server.ts +652 -0
- package/src/remote/session.ts +256 -0
- package/src/remote/types.ts +68 -1
- package/src/result.ts +260 -0
- package/src/retry.ts +306 -0
- package/src/state.ts +103 -45
- package/wasm-browser/README.md +4 -0
- package/wasm-browser/hypen_engine_bg.wasm +0 -0
- package/wasm-browser/package.json +1 -1
- package/wasm-node/README.md +4 -0
- package/wasm-node/hypen_engine_bg.wasm +0 -0
- package/wasm-node/package.json +1 -1
- package/wasm-browser/hypen_engine_bg.js +0 -736
- package/wasm-node/hypen_engine_bg.js +0 -736
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disposable Pattern for Resource Management
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent way to manage and clean up resources like
|
|
5
|
+
* event listeners, timers, WebSocket connections, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface for objects that can be disposed
|
|
10
|
+
*/
|
|
11
|
+
export interface Disposable {
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if an object is Disposable
|
|
17
|
+
*/
|
|
18
|
+
export function isDisposable(obj: unknown): obj is Disposable {
|
|
19
|
+
return (
|
|
20
|
+
obj !== null &&
|
|
21
|
+
typeof obj === 'object' &&
|
|
22
|
+
'dispose' in obj &&
|
|
23
|
+
typeof (obj as Disposable).dispose === 'function'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A stack of disposables that are disposed in LIFO order
|
|
29
|
+
*/
|
|
30
|
+
export class DisposableStack implements Disposable {
|
|
31
|
+
private stack: Disposable[] = [];
|
|
32
|
+
private disposed = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Add a disposable to the stack and return it
|
|
36
|
+
*/
|
|
37
|
+
add<T extends Disposable>(disposable: T): T {
|
|
38
|
+
if (this.disposed) {
|
|
39
|
+
// If already disposed, immediately dispose the new item
|
|
40
|
+
disposable.dispose();
|
|
41
|
+
return disposable;
|
|
42
|
+
}
|
|
43
|
+
this.stack.push(disposable);
|
|
44
|
+
return disposable;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add a cleanup callback to the stack
|
|
49
|
+
*/
|
|
50
|
+
addCallback(callback: () => void): void {
|
|
51
|
+
this.add({ dispose: callback });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Add a value with a custom dispose function
|
|
56
|
+
*/
|
|
57
|
+
addValue<T>(value: T, dispose: (value: T) => void): T {
|
|
58
|
+
this.add({ dispose: () => dispose(value) });
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dispose all items in reverse order (LIFO)
|
|
64
|
+
*/
|
|
65
|
+
dispose(): void {
|
|
66
|
+
if (this.disposed) return;
|
|
67
|
+
this.disposed = true;
|
|
68
|
+
|
|
69
|
+
while (this.stack.length > 0) {
|
|
70
|
+
const item = this.stack.pop()!;
|
|
71
|
+
try {
|
|
72
|
+
item.dispose();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Log but continue disposing other items
|
|
75
|
+
console.error('[DisposableStack] Error during dispose:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if this stack has been disposed
|
|
82
|
+
*/
|
|
83
|
+
get isDisposed(): boolean {
|
|
84
|
+
return this.disposed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the number of items in the stack
|
|
89
|
+
*/
|
|
90
|
+
get size(): number {
|
|
91
|
+
return this.stack.length;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a disposable from an event listener
|
|
97
|
+
*/
|
|
98
|
+
export function disposableListener(
|
|
99
|
+
target: EventTarget,
|
|
100
|
+
event: string,
|
|
101
|
+
handler: EventListenerOrEventListenerObject,
|
|
102
|
+
options?: AddEventListenerOptions
|
|
103
|
+
): Disposable {
|
|
104
|
+
target.addEventListener(event, handler, options);
|
|
105
|
+
return {
|
|
106
|
+
dispose: () => target.removeEventListener(event, handler, options),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a disposable from a timeout
|
|
112
|
+
*/
|
|
113
|
+
export function disposableTimeout(
|
|
114
|
+
callback: () => void,
|
|
115
|
+
ms: number
|
|
116
|
+
): Disposable & { id: ReturnType<typeof setTimeout> } {
|
|
117
|
+
const id = setTimeout(callback, ms);
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
dispose: () => clearTimeout(id),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a disposable from an interval
|
|
126
|
+
*/
|
|
127
|
+
export function disposableInterval(
|
|
128
|
+
callback: () => void,
|
|
129
|
+
ms: number
|
|
130
|
+
): Disposable & { id: ReturnType<typeof setInterval> } {
|
|
131
|
+
const id = setInterval(callback, ms);
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
dispose: () => clearInterval(id),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a disposable from a WebSocket
|
|
140
|
+
*/
|
|
141
|
+
export function disposableWebSocket(ws: WebSocket): Disposable {
|
|
142
|
+
return {
|
|
143
|
+
dispose: () => {
|
|
144
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
145
|
+
ws.close();
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a disposable from an AbortController
|
|
153
|
+
*/
|
|
154
|
+
export function disposableAbortController(): Disposable & { controller: AbortController; signal: AbortSignal } {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
return {
|
|
157
|
+
controller,
|
|
158
|
+
signal: controller.signal,
|
|
159
|
+
dispose: () => controller.abort(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a disposable subscription (for event emitters, observables, etc.)
|
|
165
|
+
*/
|
|
166
|
+
export function disposableSubscription(unsubscribe: () => void): Disposable {
|
|
167
|
+
return { dispose: unsubscribe };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Symbol used to store disposables on DOM elements
|
|
172
|
+
*/
|
|
173
|
+
const ELEMENT_DISPOSABLES = Symbol('hypen.disposables');
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get or create a DisposableStack for an HTML element
|
|
177
|
+
*/
|
|
178
|
+
export function getElementDisposables(element: HTMLElement): DisposableStack {
|
|
179
|
+
const existing = (element as any)[ELEMENT_DISPOSABLES];
|
|
180
|
+
if (existing instanceof DisposableStack) {
|
|
181
|
+
return existing;
|
|
182
|
+
}
|
|
183
|
+
const stack = new DisposableStack();
|
|
184
|
+
(element as any)[ELEMENT_DISPOSABLES] = stack;
|
|
185
|
+
return stack;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Dispose all disposables attached to an element
|
|
190
|
+
*/
|
|
191
|
+
export function disposeElement(element: HTMLElement): void {
|
|
192
|
+
const stack = (element as any)[ELEMENT_DISPOSABLES];
|
|
193
|
+
if (stack instanceof DisposableStack) {
|
|
194
|
+
stack.dispose();
|
|
195
|
+
delete (element as any)[ELEMENT_DISPOSABLES];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if an element has disposables
|
|
201
|
+
*/
|
|
202
|
+
export function hasElementDisposables(element: HTMLElement): boolean {
|
|
203
|
+
return (element as any)[ELEMENT_DISPOSABLES] instanceof DisposableStack;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Decorator/helper to make a class disposable
|
|
208
|
+
* Tracks all resources and disposes them when dispose() is called
|
|
209
|
+
*/
|
|
210
|
+
export class DisposableMixin {
|
|
211
|
+
protected disposables = new DisposableStack();
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Register a disposable to be cleaned up
|
|
215
|
+
*/
|
|
216
|
+
protected track<T extends Disposable>(disposable: T): T {
|
|
217
|
+
return this.disposables.add(disposable);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Register a cleanup callback
|
|
222
|
+
*/
|
|
223
|
+
protected onDispose(callback: () => void): void {
|
|
224
|
+
this.disposables.addCallback(callback);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Dispose all tracked resources
|
|
229
|
+
*/
|
|
230
|
+
dispose(): void {
|
|
231
|
+
this.disposables.dispose();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a composite disposable that disposes multiple items together
|
|
237
|
+
*/
|
|
238
|
+
export function compositeDisposable(...disposables: Disposable[]): Disposable {
|
|
239
|
+
return {
|
|
240
|
+
dispose: () => {
|
|
241
|
+
for (const d of disposables) {
|
|
242
|
+
try {
|
|
243
|
+
d.dispose();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('[compositeDisposable] Error during dispose:', error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Run a function with automatic cleanup on exit
|
|
254
|
+
* Similar to Python's context managers or C#'s using statement
|
|
255
|
+
*/
|
|
256
|
+
export async function using<T extends Disposable, R>(
|
|
257
|
+
resource: T | (() => T),
|
|
258
|
+
fn: (resource: T) => R | Promise<R>
|
|
259
|
+
): Promise<R> {
|
|
260
|
+
const r = typeof resource === 'function' ? resource() : resource;
|
|
261
|
+
try {
|
|
262
|
+
return await fn(r);
|
|
263
|
+
} finally {
|
|
264
|
+
r.dispose();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Synchronous version of using()
|
|
270
|
+
*/
|
|
271
|
+
export function usingSync<T extends Disposable, R>(
|
|
272
|
+
resource: T | (() => T),
|
|
273
|
+
fn: (resource: T) => R
|
|
274
|
+
): R {
|
|
275
|
+
const r = typeof resource === 'function' ? resource() : resource;
|
|
276
|
+
try {
|
|
277
|
+
return fn(r);
|
|
278
|
+
} finally {
|
|
279
|
+
r.dispose();
|
|
280
|
+
}
|
|
281
|
+
}
|
package/src/engine.browser.ts
CHANGED
|
@@ -94,7 +94,7 @@ export class Engine {
|
|
|
94
94
|
if (this.initialized) return;
|
|
95
95
|
|
|
96
96
|
// Default to CDN for zero-config experience
|
|
97
|
-
const cdnBase = "https://unpkg.com/@hypen-space/core@0.2.
|
|
97
|
+
const cdnBase = "https://unpkg.com/@hypen-space/core@0.2.12/wasm-browser";
|
|
98
98
|
const jsUrl = options.jsUrl ?? `${cdnBase}/hypen_engine.js`;
|
|
99
99
|
const wasmUrl = options.wasmUrl ?? `${cdnBase}/hypen_engine_bg.wasm`;
|
|
100
100
|
|
package/src/engine.ts
CHANGED
|
@@ -6,6 +6,31 @@
|
|
|
6
6
|
// WASM module types
|
|
7
7
|
import { WasmEngine } from "../wasm-node/hypen_engine.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Unwrap proxy objects to plain values for WASM serialization.
|
|
11
|
+
* Uses structuredClone when available (Node 17+, Bun, modern browsers),
|
|
12
|
+
* falls back to JSON round-trip for proxy objects or older environments.
|
|
13
|
+
*/
|
|
14
|
+
function unwrapForWasm<T>(value: T): T {
|
|
15
|
+
// Fast path: primitives don't need cloning
|
|
16
|
+
if (value === null || typeof value !== 'object') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if the object has a snapshot method (our proxy convention)
|
|
21
|
+
if (typeof (value as any).__getSnapshot === 'function') {
|
|
22
|
+
return (value as any).__getSnapshot() as T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try structuredClone first (fastest for plain objects)
|
|
26
|
+
try {
|
|
27
|
+
return structuredClone(value);
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback for proxy objects or unsupported types
|
|
30
|
+
return JSON.parse(JSON.stringify(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
export type Patch = {
|
|
10
35
|
type: "create" | "setProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent";
|
|
11
36
|
id?: string;
|
|
@@ -108,8 +133,7 @@ export class Engine {
|
|
|
108
133
|
*/
|
|
109
134
|
renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
|
|
110
135
|
const engine = this.ensureInitialized();
|
|
111
|
-
|
|
112
|
-
engine.renderInto(source, parentNodeId, safeState);
|
|
136
|
+
engine.renderInto(source, parentNodeId, unwrapForWasm(state));
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
/**
|
|
@@ -122,9 +146,7 @@ export class Engine {
|
|
|
122
146
|
return;
|
|
123
147
|
}
|
|
124
148
|
|
|
125
|
-
|
|
126
|
-
const plainValues = JSON.parse(JSON.stringify(values));
|
|
127
|
-
engine.updateStateSparse(paths, plainValues);
|
|
149
|
+
engine.updateStateSparse(paths, unwrapForWasm(values));
|
|
128
150
|
console.debug("[Hypen] State changed (sparse):", paths);
|
|
129
151
|
}
|
|
130
152
|
|
|
@@ -134,9 +156,7 @@ export class Engine {
|
|
|
134
156
|
*/
|
|
135
157
|
notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
|
|
136
158
|
const engine = this.ensureInitialized();
|
|
137
|
-
|
|
138
|
-
const plainObject = JSON.parse(JSON.stringify(currentState));
|
|
139
|
-
engine.updateState(plainObject);
|
|
159
|
+
engine.updateState(unwrapForWasm(currentState));
|
|
140
160
|
|
|
141
161
|
if (paths.length > 0) {
|
|
142
162
|
console.debug("[Hypen] State changed (full):", paths);
|
|
@@ -149,8 +169,7 @@ export class Engine {
|
|
|
149
169
|
*/
|
|
150
170
|
updateState(statePatch: Record<string, any>): void {
|
|
151
171
|
const engine = this.ensureInitialized();
|
|
152
|
-
|
|
153
|
-
engine.updateState(plainObject);
|
|
172
|
+
engine.updateState(unwrapForWasm(statePatch));
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
/**
|
package/src/hypen.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hypen Tagged Template Literal
|
|
3
|
+
*
|
|
4
|
+
* Enables single-file components with inline UI templates.
|
|
5
|
+
*
|
|
6
|
+
* The `hypen` tagged template preserves ${state.x} and ${item.x} bindings
|
|
7
|
+
* instead of interpolating them, allowing you to write:
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { app, hypen, state } from "@hypen-space/core";
|
|
12
|
+
*
|
|
13
|
+
* export default app
|
|
14
|
+
* .defineState({ count: 0 })
|
|
15
|
+
* .onAction("increment", ({ state }) => {
|
|
16
|
+
* state.count += 1;
|
|
17
|
+
* })
|
|
18
|
+
* .ui(hypen`
|
|
19
|
+
* Column {
|
|
20
|
+
* Text("Count: ${state.count}")
|
|
21
|
+
* Button { Text("+") }
|
|
22
|
+
* .onClick("@actions.increment")
|
|
23
|
+
* }
|
|
24
|
+
* `);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a proxy that captures property access as a binding path string.
|
|
30
|
+
*
|
|
31
|
+
* When used in a template literal, the proxy's toString() returns the
|
|
32
|
+
* full binding expression (e.g., "${state.user.name}").
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const state = createBindingProxy('state');
|
|
37
|
+
* `${state.user.name}` // Returns: "${state.user.name}"
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
function createBindingProxy(root: string): any {
|
|
41
|
+
const handler: ProxyHandler<object> = {
|
|
42
|
+
get(_, prop: string | symbol): any {
|
|
43
|
+
// Handle Symbol.toPrimitive, toString, and valueOf for string conversion
|
|
44
|
+
if (
|
|
45
|
+
prop === Symbol.toPrimitive ||
|
|
46
|
+
prop === "toString" ||
|
|
47
|
+
prop === "valueOf"
|
|
48
|
+
) {
|
|
49
|
+
return () => `\${${root}}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle other Symbol properties (e.g., Symbol.toStringTag)
|
|
53
|
+
if (typeof prop === "symbol") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle JSON.stringify
|
|
58
|
+
if (prop === "toJSON") {
|
|
59
|
+
return () => `\${${root}}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Chain to nested property: state.user -> state.user.name
|
|
63
|
+
return createBindingProxy(`${root}.${prop}`);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Support for `in` operator
|
|
67
|
+
has() {
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Support for Object.keys() - return empty to avoid enumeration issues
|
|
72
|
+
ownKeys() {
|
|
73
|
+
return [];
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
getOwnPropertyDescriptor() {
|
|
77
|
+
return {
|
|
78
|
+
configurable: true,
|
|
79
|
+
enumerable: true,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return new Proxy({} as any, handler);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Proxy for state bindings.
|
|
89
|
+
*
|
|
90
|
+
* Use in hypen templates to create reactive state bindings:
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* hypen`Text("Hello, ${state.user.name}")`
|
|
95
|
+
* // Produces: Text("Hello, ${state.user.name}")
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export const state: any = createBindingProxy("state");
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Proxy for item bindings in list iteration.
|
|
102
|
+
*
|
|
103
|
+
* Use inside List components to reference the current item:
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* hypen`
|
|
108
|
+
* List(@state.items) {
|
|
109
|
+
* Text("${item.name}: ${item.price}")
|
|
110
|
+
* }
|
|
111
|
+
* `
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export const item: any = createBindingProxy("item");
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Proxy for index in list iteration.
|
|
118
|
+
*
|
|
119
|
+
* Use inside List components to reference the current index:
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* hypen`
|
|
124
|
+
* List(@state.items) {
|
|
125
|
+
* Text("Item #${index}: ${item.name}")
|
|
126
|
+
* }
|
|
127
|
+
* `
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export const index: any = {
|
|
131
|
+
[Symbol.toPrimitive]: () => "${index}",
|
|
132
|
+
toString: () => "${index}",
|
|
133
|
+
valueOf: () => "${index}",
|
|
134
|
+
toJSON: () => "${index}",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Tagged template literal for Hypen DSL templates.
|
|
139
|
+
*
|
|
140
|
+
* Works with the `state`, `item`, and `index` binding proxies to preserve
|
|
141
|
+
* binding syntax in the output string.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```typescript
|
|
145
|
+
* import { hypen, state, item } from "@hypen-space/core";
|
|
146
|
+
*
|
|
147
|
+
* // Simple state binding
|
|
148
|
+
* const t1 = hypen`Text("Count: ${state.count}")`;
|
|
149
|
+
* // Result: 'Text("Count: ${state.count}")'
|
|
150
|
+
*
|
|
151
|
+
* // Nested state binding
|
|
152
|
+
* const t2 = hypen`Text("Hello, ${state.user.profile.name}")`;
|
|
153
|
+
* // Result: 'Text("Hello, ${state.user.profile.name}")'
|
|
154
|
+
*
|
|
155
|
+
* // List with item binding
|
|
156
|
+
* const t3 = hypen`
|
|
157
|
+
* List(@state.products) {
|
|
158
|
+
* Text("${item.name} - $${item.price}")
|
|
159
|
+
* }
|
|
160
|
+
* `;
|
|
161
|
+
*
|
|
162
|
+
* // Complex expressions (use regular JS interpolation for static values)
|
|
163
|
+
* const title = "My App";
|
|
164
|
+
* const t4 = hypen`Text("${title}: ${state.count}")`;
|
|
165
|
+
* // Result: 'Text("My App: ${state.count}")'
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @param strings - Template literal string parts
|
|
169
|
+
* @param expressions - Interpolated expressions (proxies return binding strings)
|
|
170
|
+
* @returns The template string with bindings preserved
|
|
171
|
+
*/
|
|
172
|
+
export function hypen(
|
|
173
|
+
strings: TemplateStringsArray,
|
|
174
|
+
...expressions: unknown[]
|
|
175
|
+
): string {
|
|
176
|
+
let result = strings[0];
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < expressions.length; i++) {
|
|
179
|
+
const expr = expressions[i];
|
|
180
|
+
|
|
181
|
+
// Convert expression to string
|
|
182
|
+
// Binding proxies will return "${state.x}" via their toString()
|
|
183
|
+
result += String(expr);
|
|
184
|
+
result += strings[i + 1]!;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result!.trim();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Type helper for defining state shape.
|
|
192
|
+
* Use with state proxy for better IDE support in complex scenarios.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* type MyState = { user: { name: string; age: number } };
|
|
197
|
+
* const typedState = state as StateProxy<MyState>;
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export type StateProxy<T> = {
|
|
201
|
+
[K in keyof T]: T[K] extends object
|
|
202
|
+
? StateProxy<T[K]> & { toString(): string }
|
|
203
|
+
: { toString(): string };
|
|
204
|
+
} & { toString(): string };
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Type helper for item proxy in lists.
|
|
208
|
+
*/
|
|
209
|
+
export type ItemProxy<T> = StateProxy<T>;
|