ziex 0.1.0-dev.460 → 0.1.0-dev.522
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 +6 -5
- package/index.js +1 -1
- package/package.json +3 -3
- package/react/dom.d.ts +72 -45
- package/react/index.d.ts +1 -1
- package/react/index.js +93 -8
- package/react/types.d.ts +11 -14
- package/wasm/index.d.ts +41 -27
- package/wasm/index.js +126 -60
package/README.md
CHANGED
|
@@ -89,9 +89,10 @@ const zx = @import("zx");
|
|
|
89
89
|
- [x] Virtual DOM and diffing
|
|
90
90
|
- [x] Rendering only changed nodes
|
|
91
91
|
- [x] `on`event handler
|
|
92
|
-
- [
|
|
93
|
-
- [
|
|
92
|
+
- [x] State managment
|
|
93
|
+
- [x] Hydration
|
|
94
94
|
- [ ] Lifecycle hook
|
|
95
|
+
- [ ] Server Actions
|
|
95
96
|
- [x] Client Side Rendering (CSR) via React
|
|
96
97
|
- [x] Routing
|
|
97
98
|
- [x] File-system Routing
|
|
@@ -116,7 +117,7 @@ const zx = @import("zx");
|
|
|
116
117
|
- [x] HTML (optimized by default)
|
|
117
118
|
- [ ] Middleware (_cancalled_)
|
|
118
119
|
- [ ] Caching (configurable)
|
|
119
|
-
- [
|
|
120
|
+
- [x] Component
|
|
120
121
|
- [ ] Layout
|
|
121
122
|
- [x] Page
|
|
122
123
|
- [ ] Assets
|
|
@@ -144,8 +145,8 @@ const zx = @import("zx");
|
|
|
144
145
|
- [x] `update` Update the version of ZX dependency
|
|
145
146
|
- [x] `upgrade` Upgrade the version of ZX CLI
|
|
146
147
|
- [ ] Platform
|
|
147
|
-
- [x]
|
|
148
|
-
- [x]
|
|
148
|
+
- [x] Server
|
|
149
|
+
- [x] Browser
|
|
149
150
|
- [ ] iOS
|
|
150
151
|
- [ ] Android
|
|
151
152
|
- [ ] macOS
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ziex",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.522",
|
|
4
4
|
"description": "ZX is a framework for building web applications with Zig.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"homepage": "https://github.com/nurulhudaapon/zx",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "
|
|
15
|
+
"url": "https://github.com/nurulhudaapon/zx.git"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"zx",
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"module": "index.js",
|
|
32
32
|
"types": "index.d.ts"
|
|
33
|
-
}
|
|
33
|
+
}
|
package/react/dom.d.ts
CHANGED
|
@@ -8,14 +8,15 @@ export type PreparedComponent = {
|
|
|
8
8
|
/**
|
|
9
9
|
* The HTML element where the component should be rendered.
|
|
10
10
|
*
|
|
11
|
-
* This is
|
|
12
|
-
*
|
|
13
|
-
* React will hydrate this element, replacing its contents with the interactive component.
|
|
11
|
+
* This is a container element created between the comment markers. The server-rendered
|
|
12
|
+
* content is moved into this container, and React will hydrate it with the interactive component.
|
|
14
13
|
*
|
|
15
14
|
* @example
|
|
16
15
|
* ```tsx
|
|
17
|
-
* //
|
|
18
|
-
* //
|
|
16
|
+
* // Server-rendered HTML with comment markers:
|
|
17
|
+
* // <!--$zx-abc123-0 CounterComponent {"max_count":10}-->
|
|
18
|
+
* // <button>0</button>
|
|
19
|
+
* // <!--/$zx-abc123-0-->
|
|
19
20
|
*
|
|
20
21
|
* const { domNode } = await prepareComponent(component);
|
|
21
22
|
* createRoot(domNode).render(<Component {...props} />);
|
|
@@ -23,42 +24,28 @@ export type PreparedComponent = {
|
|
|
23
24
|
*/
|
|
24
25
|
domNode: HTMLElement;
|
|
25
26
|
/**
|
|
26
|
-
* Component props parsed from the
|
|
27
|
+
* Component props parsed from the comment marker.
|
|
27
28
|
*
|
|
28
|
-
* Props are extracted from the
|
|
29
|
-
*
|
|
30
|
-
* `dangerouslySetInnerHTML` for React compatibility.
|
|
29
|
+
* Props are extracted from the start comment marker content. The comment format is:
|
|
30
|
+
* `<!--$id name props-->` where props is JSON-encoded.
|
|
31
31
|
*
|
|
32
32
|
* @example
|
|
33
33
|
* ```tsx
|
|
34
34
|
* // Server-rendered HTML:
|
|
35
|
-
* //
|
|
35
|
+
* // <!--$zx-abc123-0 CounterComponent {"max_count":10,"label":"Counter"}-->
|
|
36
|
+
* // <button>0</button>
|
|
37
|
+
* // <!--/$zx-abc123-0-->
|
|
36
38
|
*
|
|
37
39
|
* const { props } = await prepareComponent(component);
|
|
38
|
-
* // props = {
|
|
39
|
-
* // max_count: 10,
|
|
40
|
-
* // label: "Counter",
|
|
41
|
-
* // dangerouslySetInnerHTML: { __html: "<span>0</span>" }
|
|
42
|
-
* // }
|
|
40
|
+
* // props = { max_count: 10, label: "Counter" }
|
|
43
41
|
* ```
|
|
44
42
|
*/
|
|
45
43
|
props: Record<string, any> & {
|
|
46
44
|
/**
|
|
47
45
|
* React's special prop for setting inner HTML directly.
|
|
48
46
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```tsx
|
|
54
|
-
* // In ZX file:
|
|
55
|
-
* <MyComponent @rendering={.react}>
|
|
56
|
-
* <p>Child content</p>
|
|
57
|
-
* </MyComponent>
|
|
58
|
-
*
|
|
59
|
-
* // Results in:
|
|
60
|
-
* // props.dangerouslySetInnerHTML = { __html: "<p>Child content</p>" }
|
|
61
|
-
* ```
|
|
47
|
+
* May be used when the component has server-rendered children that should
|
|
48
|
+
* be preserved during hydration.
|
|
62
49
|
*/
|
|
63
50
|
dangerouslySetInnerHTML?: {
|
|
64
51
|
__html: string;
|
|
@@ -85,12 +72,12 @@ export type PreparedComponent = {
|
|
|
85
72
|
Component: (props: any) => React.ReactElement;
|
|
86
73
|
};
|
|
87
74
|
/**
|
|
88
|
-
* Prepares a client-side component for hydration by locating its
|
|
89
|
-
* props
|
|
75
|
+
* Prepares a client-side component for hydration by locating its comment markers, extracting
|
|
76
|
+
* props from the marker content, and lazy-loading the component module.
|
|
90
77
|
*
|
|
91
78
|
* This function bridges server-rendered HTML (from ZX's Zig transpiler) and client-side React
|
|
92
|
-
* components. It
|
|
93
|
-
*
|
|
79
|
+
* components. It searches for comment markers in the format `<!--$id name props-->...<!--/$id-->`
|
|
80
|
+
* and extracts the component data from the marker content.
|
|
94
81
|
*
|
|
95
82
|
* @param component - The component metadata containing ID, import function, and other metadata
|
|
96
83
|
* needed to locate and load the component
|
|
@@ -98,8 +85,8 @@ export type PreparedComponent = {
|
|
|
98
85
|
* @returns A Promise that resolves to a `PreparedComponent` object containing the DOM node,
|
|
99
86
|
* parsed props, and the loaded React component function
|
|
100
87
|
*
|
|
101
|
-
* @throws {Error} If the component's
|
|
102
|
-
* happens if the component ID doesn't match any
|
|
88
|
+
* @throws {Error} If the component's comment markers cannot be found in the DOM. This typically
|
|
89
|
+
* happens if the component ID doesn't match any marker, the script runs before
|
|
103
90
|
* the HTML is loaded, or there's a mismatch between server and client metadata
|
|
104
91
|
*
|
|
105
92
|
* @example
|
|
@@ -118,18 +105,58 @@ export type PreparedComponent = {
|
|
|
118
105
|
*
|
|
119
106
|
* @example
|
|
120
107
|
* ```tsx
|
|
121
|
-
* //
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* createRoot(domNode).render(<Component {...props} />);
|
|
126
|
-
* } catch (error) {
|
|
127
|
-
* console.error(`Failed to hydrate ${component.name}:`, error);
|
|
128
|
-
* }
|
|
129
|
-
* }
|
|
130
|
-
*
|
|
131
|
-
* Promise.all(components.map(hydrateComponent));
|
|
108
|
+
* // Server-rendered HTML with comment markers:
|
|
109
|
+
* // <!--$zx-abc123-0 CounterComponent {"max_count":10}-->
|
|
110
|
+
* // <button>0</button>
|
|
111
|
+
* // <!--/$zx-abc123-0-->
|
|
132
112
|
* ```
|
|
133
113
|
*/
|
|
134
114
|
export declare function prepareComponent(component: ComponentMetadata): Promise<PreparedComponent>;
|
|
135
115
|
export declare function filterComponents(components: ComponentMetadata[]): ComponentMetadata[];
|
|
116
|
+
/**
|
|
117
|
+
* Discovered component from DOM traversal.
|
|
118
|
+
* Contains all metadata needed to hydrate the component.
|
|
119
|
+
*/
|
|
120
|
+
export type DiscoveredComponent = {
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
props: Record<string, any>;
|
|
124
|
+
container: HTMLElement;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Finds all React component markers in the DOM and returns their metadata.
|
|
128
|
+
*
|
|
129
|
+
* This is a DOM-first approach that:
|
|
130
|
+
* 1. Walks the DOM once to find all `<!--$id-->` markers
|
|
131
|
+
* 2. Reads metadata from companion `<script data-zx="id">` elements
|
|
132
|
+
* 3. Creates containers for React to render into
|
|
133
|
+
*
|
|
134
|
+
* @returns Array of discovered components with their containers and props
|
|
135
|
+
*/
|
|
136
|
+
export declare function discoverComponents(): DiscoveredComponent[];
|
|
137
|
+
/**
|
|
138
|
+
* Component registry mapping component names to their import functions.
|
|
139
|
+
*/
|
|
140
|
+
export type ComponentRegistry = Record<string, () => Promise<(props: any) => React.ReactElement>>;
|
|
141
|
+
/**
|
|
142
|
+
* Hydrates all React components found in the DOM.
|
|
143
|
+
*
|
|
144
|
+
* This is the simplest way to hydrate React islands - it automatically:
|
|
145
|
+
* 1. Discovers all component markers in the DOM
|
|
146
|
+
* 2. Looks up components by name in the registry
|
|
147
|
+
* 3. Renders each component into its container
|
|
148
|
+
*
|
|
149
|
+
* @param registry - Map of component names to import functions
|
|
150
|
+
* @param render - Function to render a component (e.g., `(el, Component, props) => createRoot(el).render(<Component {...props} />)`)
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { hydrateAll } from "ziex/react";
|
|
155
|
+
*
|
|
156
|
+
* hydrateAll({
|
|
157
|
+
* CounterComponent: () => import("./Counter"),
|
|
158
|
+
* ToggleComponent: () => import("./Toggle"),
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export declare function hydrateAll(registry: ComponentRegistry, render: (container: HTMLElement, Component: (props: any) => React.ReactElement, props: Record<string, any>) => void): Promise<void>;
|
package/react/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { prepareComponent, filterComponents, type PreparedComponent } from "./dom";
|
|
1
|
+
export { prepareComponent, filterComponents, discoverComponents, hydrateAll, type PreparedComponent, type DiscoveredComponent, type ComponentRegistry, } from "./dom";
|
|
2
2
|
export type { ComponentMetadata } from "./types";
|
package/react/index.js
CHANGED
|
@@ -1,13 +1,56 @@
|
|
|
1
1
|
// src/react/dom.ts
|
|
2
|
+
function findCommentMarker(id) {
|
|
3
|
+
const startMarker = `$${id}`;
|
|
4
|
+
const endMarker = `/$${id}`;
|
|
5
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null);
|
|
6
|
+
let startComment = null;
|
|
7
|
+
let endComment = null;
|
|
8
|
+
let node;
|
|
9
|
+
while (node = walker.nextNode()) {
|
|
10
|
+
const text = node.textContent?.trim() || "";
|
|
11
|
+
if (text === startMarker) {
|
|
12
|
+
startComment = node;
|
|
13
|
+
}
|
|
14
|
+
if (text === endMarker) {
|
|
15
|
+
endComment = node;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (startComment && endComment) {
|
|
20
|
+
return { startComment, endComment };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function getComponentMetadata(id) {
|
|
25
|
+
const script = document.querySelector(`script[data-zx="${id}"]`);
|
|
26
|
+
if (script?.textContent) {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(script.textContent);
|
|
29
|
+
return { name: data.name || "", props: data.props || {} };
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
return { name: "", props: {} };
|
|
33
|
+
}
|
|
34
|
+
function createContainerBetweenMarkers(startComment, endComment) {
|
|
35
|
+
const container = document.createElement("div");
|
|
36
|
+
container.style.display = "contents";
|
|
37
|
+
let current = startComment.nextSibling;
|
|
38
|
+
while (current && current !== endComment) {
|
|
39
|
+
const next = current.nextSibling;
|
|
40
|
+
container.appendChild(current);
|
|
41
|
+
current = next;
|
|
42
|
+
}
|
|
43
|
+
endComment.parentNode?.insertBefore(container, endComment);
|
|
44
|
+
return container;
|
|
45
|
+
}
|
|
2
46
|
async function prepareComponent(component) {
|
|
3
|
-
const
|
|
4
|
-
if (!
|
|
5
|
-
throw new Error(`
|
|
6
|
-
const props = JSON.parse(domNode.getAttribute("data-props") || "{}");
|
|
7
|
-
const htmlChildren = domNode.getAttribute("data-children") ?? undefined;
|
|
8
|
-
if (htmlChildren) {
|
|
9
|
-
props.dangerouslySetInnerHTML = { __html: htmlChildren };
|
|
47
|
+
const marker = findCommentMarker(component.id);
|
|
48
|
+
if (!marker) {
|
|
49
|
+
throw new Error(`Comment marker for ${component.id} not found`, { cause: component });
|
|
10
50
|
}
|
|
51
|
+
const metadata = getComponentMetadata(component.id);
|
|
52
|
+
const props = metadata.props;
|
|
53
|
+
const domNode = createContainerBetweenMarkers(marker.startComment, marker.endComment);
|
|
11
54
|
const Component = await component.import();
|
|
12
55
|
return { domNode, props, Component };
|
|
13
56
|
}
|
|
@@ -15,7 +58,49 @@ function filterComponents(components) {
|
|
|
15
58
|
const currentPath = window.location.pathname;
|
|
16
59
|
return components.filter((component) => component.route === currentPath || !component.route);
|
|
17
60
|
}
|
|
61
|
+
function discoverComponents() {
|
|
62
|
+
const components = [];
|
|
63
|
+
const scripts = Array.from(document.querySelectorAll("script[data-zx]"));
|
|
64
|
+
for (const script of scripts) {
|
|
65
|
+
const id = script.getAttribute("data-zx");
|
|
66
|
+
if (!id)
|
|
67
|
+
continue;
|
|
68
|
+
let name = "";
|
|
69
|
+
let props = {};
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(script.textContent || "{}");
|
|
72
|
+
name = data.name || "";
|
|
73
|
+
props = data.props || {};
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const marker = findCommentMarker(id);
|
|
78
|
+
if (!marker)
|
|
79
|
+
continue;
|
|
80
|
+
const container = createContainerBetweenMarkers(marker.startComment, marker.endComment);
|
|
81
|
+
components.push({ id, name, props, container });
|
|
82
|
+
}
|
|
83
|
+
return components;
|
|
84
|
+
}
|
|
85
|
+
async function hydrateAll(registry, render) {
|
|
86
|
+
const components = discoverComponents();
|
|
87
|
+
await Promise.all(components.map(async ({ name, props, container }) => {
|
|
88
|
+
const importer = registry[name];
|
|
89
|
+
if (!importer) {
|
|
90
|
+
console.warn(`Component "${name}" not found in registry`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const Component = await importer();
|
|
95
|
+
render(container, Component, props);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`Failed to hydrate "${name}":`, error);
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
18
101
|
export {
|
|
19
102
|
prepareComponent,
|
|
20
|
-
|
|
103
|
+
hydrateAll,
|
|
104
|
+
filterComponents,
|
|
105
|
+
discoverComponents
|
|
21
106
|
};
|
package/react/types.d.ts
CHANGED
|
@@ -85,17 +85,16 @@ export type ComponentMetadata = {
|
|
|
85
85
|
*/
|
|
86
86
|
route: string | null;
|
|
87
87
|
/**
|
|
88
|
-
* A unique
|
|
88
|
+
* A unique identifier for the component's hydration boundary.
|
|
89
89
|
*
|
|
90
90
|
* This ID is generated by hashing the component's path and name using MD5, then formatting it
|
|
91
|
-
* as a hex string with the "zx-" prefix. The ID is used to locate the
|
|
92
|
-
*
|
|
91
|
+
* as a hex string with the "zx-" prefix and a counter suffix. The ID is used to locate the
|
|
92
|
+
* component's comment markers in the DOM during client-side hydration.
|
|
93
93
|
*
|
|
94
|
-
* The ID format is: `zx-{32 hex characters}` (e.g., `zx-dcde04c415da9d1b15ca2690d8b497ae`)
|
|
94
|
+
* The ID format is: `zx-{32 hex characters}-{counter}` (e.g., `zx-dcde04c415da9d1b15ca2690d8b497ae-0`)
|
|
95
95
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* and `data-children` attributes to hydrate the component with the correct props and children.
|
|
96
|
+
* The ZX runtime renders components with comment markers in the format:
|
|
97
|
+
* `<!--$id name props-->...<!--/$id-->`
|
|
99
98
|
*
|
|
100
99
|
* @example
|
|
101
100
|
* ```tsx
|
|
@@ -103,15 +102,13 @@ export type ComponentMetadata = {
|
|
|
103
102
|
* {
|
|
104
103
|
* name: "CounterComponent",
|
|
105
104
|
* path: "./components/Counter.tsx",
|
|
106
|
-
* id: "zx-dcde04c415da9d1b15ca2690d8b497ae"
|
|
105
|
+
* id: "zx-dcde04c415da9d1b15ca2690d8b497ae-0"
|
|
107
106
|
* }
|
|
108
107
|
*
|
|
109
|
-
* // Generated HTML:
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* <!-- Server-rendered content -->
|
|
114
|
-
* </div>
|
|
108
|
+
* // Generated HTML with comment markers:
|
|
109
|
+
* <!--$zx-dcde04c415da9d1b15ca2690d8b497ae-0 CounterComponent {"max_count":10}-->
|
|
110
|
+
* <button>0</button>
|
|
111
|
+
* <!--/$zx-dcde04c415da9d1b15ca2690d8b497ae-0-->
|
|
115
112
|
* ```
|
|
116
113
|
*/
|
|
117
114
|
id: string;
|
package/wasm/index.d.ts
CHANGED
|
@@ -1,38 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
import { ZigJS } from "../../../../vendor/jsz/js/src";
|
|
2
|
+
/**
|
|
3
|
+
* ZX Client Bridge - Unified JS↔WASM communication layer
|
|
4
|
+
* Handles events, fetch, timers, and other async callbacks using jsz
|
|
5
|
+
*/
|
|
6
|
+
export declare const CallbackType: {
|
|
7
|
+
readonly Event: 0;
|
|
8
|
+
readonly FetchSuccess: 1;
|
|
9
|
+
readonly FetchError: 2;
|
|
10
|
+
readonly Timeout: 3;
|
|
11
|
+
readonly Interval: 4;
|
|
12
|
+
};
|
|
13
|
+
export type CallbackTypeValue = typeof CallbackType[keyof typeof CallbackType];
|
|
14
|
+
export declare const jsz: ZigJS;
|
|
15
|
+
/** Store a value using jsz.storeValue and get the 64-bit reference. */
|
|
16
|
+
export declare function storeValueGetRef(val: any): bigint;
|
|
17
|
+
/** ZX Bridge - provides JS APIs that callback into WASM */
|
|
18
|
+
export declare class ZxBridge {
|
|
2
19
|
#private;
|
|
3
|
-
exports: WebAssembly.Exports;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
constructor(exports: WebAssembly.Exports);
|
|
21
|
+
/** Fetch a URL and callback with the response */
|
|
22
|
+
fetch(urlPtr: number, urlLen: number, callbackId: bigint): void;
|
|
23
|
+
/** Set a timeout and callback when it fires */
|
|
24
|
+
setTimeout(callbackId: bigint, delayMs: number): void;
|
|
25
|
+
/** Set an interval and callback each time it fires */
|
|
26
|
+
setInterval(callbackId: bigint, intervalMs: number): void;
|
|
27
|
+
/** Clear an interval */
|
|
28
|
+
clearInterval(callbackId: bigint): void;
|
|
29
|
+
/** Handle a DOM event (called by event delegation) */
|
|
30
|
+
eventbridge(velementId: bigint, eventTypeId: number, event: Event): void;
|
|
31
|
+
/** Create the import object for WASM instantiation */
|
|
32
|
+
static createImportObject(bridgeRef: {
|
|
33
|
+
current: ZxBridge | null;
|
|
34
|
+
}): WebAssembly.Imports;
|
|
15
35
|
}
|
|
16
|
-
|
|
36
|
+
/** Initialize event delegation */
|
|
37
|
+
export declare function initEventDelegation(bridge: ZxBridge, rootSelector?: string): void;
|
|
17
38
|
export type InitOptions = {
|
|
18
|
-
/** URL to the WASM file (default: /assets/main.wasm) */
|
|
19
39
|
url?: string;
|
|
20
|
-
/** CSS selector for the event delegation root element (default: 'body') */
|
|
21
40
|
eventDelegationRoot?: string;
|
|
41
|
+
importObject?: WebAssembly.Imports;
|
|
22
42
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
/** Initialize WASM with the ZX Bridge */
|
|
44
|
+
export declare function init(options?: InitOptions): Promise<{
|
|
45
|
+
source: WebAssembly.WebAssemblyInstantiatedSource;
|
|
46
|
+
bridge: ZxBridge;
|
|
47
|
+
}>;
|
|
27
48
|
declare global {
|
|
28
|
-
interface Window {
|
|
29
|
-
_zx: ZXInstance;
|
|
30
|
-
}
|
|
31
49
|
interface HTMLElement {
|
|
32
|
-
/**
|
|
33
|
-
* The VElement ID of the element
|
|
34
|
-
*/
|
|
35
50
|
__zx_ref?: number;
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
|
-
export {};
|
package/wasm/index.js
CHANGED
|
@@ -168,8 +168,102 @@ class ZigJS {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
// src/wasm/index.ts
|
|
171
|
-
var
|
|
172
|
-
|
|
171
|
+
var CallbackType = {
|
|
172
|
+
Event: 0,
|
|
173
|
+
FetchSuccess: 1,
|
|
174
|
+
FetchError: 2,
|
|
175
|
+
Timeout: 3,
|
|
176
|
+
Interval: 4
|
|
177
|
+
};
|
|
178
|
+
var jsz = new ZigJS;
|
|
179
|
+
var tempRefBuffer = new ArrayBuffer(8);
|
|
180
|
+
var tempRefView = new DataView(tempRefBuffer);
|
|
181
|
+
function storeValueGetRef(val) {
|
|
182
|
+
const originalMemory = jsz.memory;
|
|
183
|
+
jsz.memory = { buffer: tempRefBuffer };
|
|
184
|
+
jsz.storeValue(0, val);
|
|
185
|
+
jsz.memory = originalMemory;
|
|
186
|
+
return tempRefView.getBigUint64(0, true);
|
|
187
|
+
}
|
|
188
|
+
function readString(ptr, len) {
|
|
189
|
+
const memory = new Uint8Array(jsz.memory.buffer);
|
|
190
|
+
return new TextDecoder().decode(memory.slice(ptr, ptr + len));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class ZxBridge {
|
|
194
|
+
#exports;
|
|
195
|
+
#nextCallbackId = BigInt(1);
|
|
196
|
+
#intervals = new Map;
|
|
197
|
+
constructor(exports) {
|
|
198
|
+
this.#exports = exports;
|
|
199
|
+
}
|
|
200
|
+
get #handler() {
|
|
201
|
+
return this.#exports.__zx_cb;
|
|
202
|
+
}
|
|
203
|
+
#getNextId() {
|
|
204
|
+
return this.#nextCallbackId++;
|
|
205
|
+
}
|
|
206
|
+
#invoke(type, id, data) {
|
|
207
|
+
const handler = this.#handler;
|
|
208
|
+
if (!handler) {
|
|
209
|
+
console.warn("__zx_cb not exported from WASM");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const dataRef = storeValueGetRef(data);
|
|
213
|
+
handler(type, id, dataRef);
|
|
214
|
+
}
|
|
215
|
+
fetch(urlPtr, urlLen, callbackId) {
|
|
216
|
+
const url = readString(urlPtr, urlLen);
|
|
217
|
+
fetch(url).then((response) => response.text()).then((text) => {
|
|
218
|
+
this.#invoke(CallbackType.FetchSuccess, callbackId, text);
|
|
219
|
+
}).catch((error) => {
|
|
220
|
+
this.#invoke(CallbackType.FetchError, callbackId, error.message ?? "Fetch failed");
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
setTimeout(callbackId, delayMs) {
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
this.#invoke(CallbackType.Timeout, callbackId, null);
|
|
226
|
+
}, delayMs);
|
|
227
|
+
}
|
|
228
|
+
setInterval(callbackId, intervalMs) {
|
|
229
|
+
const handle = setInterval(() => {
|
|
230
|
+
this.#invoke(CallbackType.Interval, callbackId, null);
|
|
231
|
+
}, intervalMs);
|
|
232
|
+
this.#intervals.set(callbackId, handle);
|
|
233
|
+
}
|
|
234
|
+
clearInterval(callbackId) {
|
|
235
|
+
const handle = this.#intervals.get(callbackId);
|
|
236
|
+
if (handle !== undefined) {
|
|
237
|
+
clearInterval(handle);
|
|
238
|
+
this.#intervals.delete(callbackId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
eventbridge(velementId, eventTypeId, event) {
|
|
242
|
+
const eventRef = storeValueGetRef(event);
|
|
243
|
+
const eventbridge = this.#exports.__zx_eventbridge;
|
|
244
|
+
if (eventbridge)
|
|
245
|
+
eventbridge(velementId, eventTypeId, eventRef);
|
|
246
|
+
}
|
|
247
|
+
static createImportObject(bridgeRef) {
|
|
248
|
+
return {
|
|
249
|
+
...jsz.importObject(),
|
|
250
|
+
__zx: {
|
|
251
|
+
_fetch: (urlPtr, urlLen, callbackId) => {
|
|
252
|
+
bridgeRef.current?.fetch(urlPtr, urlLen, callbackId);
|
|
253
|
+
},
|
|
254
|
+
_setTimeout: (callbackId, delayMs) => {
|
|
255
|
+
bridgeRef.current?.setTimeout(callbackId, delayMs);
|
|
256
|
+
},
|
|
257
|
+
_setInterval: (callbackId, intervalMs) => {
|
|
258
|
+
bridgeRef.current?.setInterval(callbackId, intervalMs);
|
|
259
|
+
},
|
|
260
|
+
_clearInterval: (callbackId) => {
|
|
261
|
+
bridgeRef.current?.clearInterval(callbackId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
173
267
|
var DELEGATED_EVENTS = [
|
|
174
268
|
"click",
|
|
175
269
|
"dblclick",
|
|
@@ -212,73 +306,45 @@ var EVENT_TYPE_MAP = {
|
|
|
212
306
|
touchmove: 17,
|
|
213
307
|
scroll: 18
|
|
214
308
|
};
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
this.exports = exports;
|
|
228
|
-
this.events = events;
|
|
229
|
-
}
|
|
230
|
-
addEvent(event) {
|
|
231
|
-
if (this.events.length >= MAX_EVENTS)
|
|
232
|
-
this.events.length = 0;
|
|
233
|
-
const idx = this.events.push(event);
|
|
234
|
-
return idx - 1;
|
|
235
|
-
}
|
|
236
|
-
initEventDelegation(rootSelector = "body") {
|
|
237
|
-
if (this.#eventDelegationInitialized)
|
|
238
|
-
return;
|
|
239
|
-
const root = document.querySelector(rootSelector);
|
|
240
|
-
if (!root) {
|
|
241
|
-
console.warn(`[ZX] Event delegation root "${rootSelector}" not found`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
for (const eventType of DELEGATED_EVENTS) {
|
|
245
|
-
root.addEventListener(eventType, (event) => {
|
|
246
|
-
this.#handleDelegatedEvent(eventType, event);
|
|
247
|
-
}, { passive: eventType.startsWith("touch") || eventType === "scroll" });
|
|
248
|
-
}
|
|
249
|
-
this.#eventDelegationInitialized = true;
|
|
250
|
-
console.debug("[ZX] Event delegation initialized on", rootSelector);
|
|
251
|
-
}
|
|
252
|
-
#handleDelegatedEvent(eventType, event) {
|
|
253
|
-
let target = event.target;
|
|
254
|
-
while (target && target !== document.body) {
|
|
255
|
-
const zxRef = target.__zx_ref;
|
|
256
|
-
if (zxRef !== undefined) {
|
|
257
|
-
const eventId = this.addEvent(event);
|
|
258
|
-
const handleEvent = this.exports.handleEvent;
|
|
259
|
-
if (typeof handleEvent === "function") {
|
|
260
|
-
const eventTypeId = EVENT_TYPE_MAP[eventType] ?? 0;
|
|
261
|
-
handleEvent(BigInt(zxRef), eventTypeId, BigInt(eventId));
|
|
309
|
+
function initEventDelegation(bridge, rootSelector = "body") {
|
|
310
|
+
const root = document.querySelector(rootSelector);
|
|
311
|
+
if (!root)
|
|
312
|
+
return;
|
|
313
|
+
for (const eventType of DELEGATED_EVENTS) {
|
|
314
|
+
root.addEventListener(eventType, (event) => {
|
|
315
|
+
let target = event.target;
|
|
316
|
+
while (target && target !== document.body) {
|
|
317
|
+
const zxRef = target.__zx_ref;
|
|
318
|
+
if (zxRef !== undefined) {
|
|
319
|
+
bridge.eventbridge(BigInt(zxRef), EVENT_TYPE_MAP[eventType] ?? 0, event);
|
|
320
|
+
break;
|
|
262
321
|
}
|
|
263
|
-
|
|
322
|
+
target = target.parentElement;
|
|
264
323
|
}
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
getZxRef(element) {
|
|
269
|
-
return element.__zx_ref;
|
|
324
|
+
}, { passive: eventType.startsWith("touch") || eventType === "scroll" });
|
|
270
325
|
}
|
|
271
326
|
}
|
|
327
|
+
var DEFAULT_URL = "/assets/main.wasm";
|
|
272
328
|
async function init(options = {}) {
|
|
273
|
-
const url = options
|
|
274
|
-
const
|
|
329
|
+
const url = options.url ?? DEFAULT_URL;
|
|
330
|
+
const bridgeRef = { current: null };
|
|
331
|
+
const importObject = Object.assign({}, ZxBridge.createImportObject(bridgeRef), options.importObject);
|
|
332
|
+
const source = await WebAssembly.instantiateStreaming(fetch(url), importObject);
|
|
333
|
+
const { instance } = source;
|
|
275
334
|
jsz.memory = instance.exports.memory;
|
|
276
|
-
|
|
277
|
-
|
|
335
|
+
const bridge = new ZxBridge(instance.exports);
|
|
336
|
+
bridgeRef.current = bridge;
|
|
337
|
+
initEventDelegation(bridge, options.eventDelegationRoot ?? "body");
|
|
278
338
|
const main = instance.exports.mainClient;
|
|
279
339
|
if (typeof main === "function")
|
|
280
340
|
main();
|
|
341
|
+
return { source, bridge };
|
|
281
342
|
}
|
|
282
343
|
export {
|
|
283
|
-
|
|
344
|
+
storeValueGetRef,
|
|
345
|
+
jsz,
|
|
346
|
+
initEventDelegation,
|
|
347
|
+
init,
|
|
348
|
+
ZxBridge,
|
|
349
|
+
CallbackType
|
|
284
350
|
};
|