ziex 0.0.1-dev.7 → 0.1.0-dev.517

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # ZX
2
2
 
3
- A Zig library for building web applications with JSX-like syntax. Write declarative UI components using familiar JSX patterns, transpiled to efficient Zig code.
3
+ A full-stack web framework for Zig. Write declarative UI components using familiar JSX patterns, transpiled to efficient Zig code.
4
4
 
5
- ZX combines the power and performance of Zig with the expressiveness of JSX, enabling you to build fast, type-safe web applications. ZX is significantly faster than frameworks like Next.js at SSR.
5
+ ZX combines the power and performance of Zig with the expressiveness of JSX, enabling you to build fast, type-safe web applications.
6
6
 
7
- **[Full Documentation →](https://ziex.dev)**
7
+ **[Documentation →](https://ziex.dev/learn)**
8
8
 
9
9
  ## Installation
10
10
 
@@ -31,7 +31,7 @@ winget install -e --id zig.zig # Windows
31
31
  pub fn QuickExample(allocator: zx.Allocator) zx.Component {
32
32
  const is_loading = true;
33
33
  const chars = "Hello, ZX Dev!";
34
-
34
+ var i: usize = 0;
35
35
  return (
36
36
  <main @allocator={allocator}>
37
37
  <section>
@@ -39,7 +39,7 @@ pub fn QuickExample(allocator: zx.Allocator) zx.Component {
39
39
  </section>
40
40
 
41
41
  <section>
42
- {for (chars) |char| (<span>{[char:c]}</span>)}
42
+ {for (chars) |char| (<span>{char}</span>)}
43
43
  </section>
44
44
 
45
45
  <section>
@@ -47,6 +47,10 @@ pub fn QuickExample(allocator: zx.Allocator) zx.Component {
47
47
  <Profile name={user.name} age={user.age} role={user.role} />
48
48
  )}
49
49
  </section>
50
+
51
+ <section>
52
+ {while (i < 10) : (i += 1) (<p>{i}</p>)}
53
+ </section>
50
54
  </main>
51
55
  );
52
56
  }
@@ -55,7 +59,7 @@ fn Profile(allocator: zx.Allocator, user: User) zx.Component {
55
59
  return (
56
60
  <div @allocator={allocator}>
57
61
  <h1>{user.name}</h1>
58
- <p>{[user.age:d]}</p>
62
+ <p>{user.age}</p>
59
63
  {switch (user.role) {
60
64
  .admin => (<p>Admin</p>),
61
65
  .member => (<p>Member</p>),
@@ -77,10 +81,19 @@ const zx = @import("zx");
77
81
  ## Feature Checklist
78
82
 
79
83
  - [x] Server Side Rendering (SSR)
84
+ - [ ] `on`event handler
85
+ - [x] Streaming
80
86
  - [x] Static Site Generation (SSG)
87
+ - [ ] `getStaticParams`, `getStaticProps`
81
88
  - [ ] Client Side Rendering (CSR) via WebAssembly (_WIP_)
89
+ - [x] Virtual DOM and diffing
90
+ - [x] Rendering only changed nodes
91
+ - [x] `on`event handler
92
+ - [x] State managment
93
+ - [x] Hydration
94
+ - [ ] Lifecycle hook
95
+ - [ ] Server Actions
82
96
  - [x] Client Side Rendering (CSR) via React
83
- - [x] Type Safety
84
97
  - [x] Routing
85
98
  - [x] File-system Routing
86
99
  - [x] Search Parameters
@@ -88,65 +101,98 @@ const zx = @import("zx");
88
101
  - [x] Components
89
102
  - [x] Control Flow
90
103
  - [x] `if`
91
- - [ ] `if` nested
92
104
  - [x] `if/else`
93
- - [x] `if/else` nested
94
105
  - [x] `for`
95
- - [x] `for` nested
96
106
  - [x] `switch`
97
- - [x] `switch` nested
98
107
  - [x] `while`
99
- - [x] `while` nested
108
+ - [x] nesting control flows
109
+ - [x] error/optional captures in `while` and `if`
100
110
  - [x] Assets
101
111
  - [x] Copying
102
112
  - [x] Serving
103
113
  - [ ] Assets Optimization
104
114
  - [ ] Image
105
- - [ ] CSS
106
- - [ ] JS
107
- - [ ] HTML
108
- - [ ] Middleware
109
- - [ ] API Endpoints
110
- - [ ] Server Actions
115
+ - [x] CSS (via plugins such as Tailwind)
116
+ - [x] JS/TS (via esbuild)
117
+ - [x] HTML (optimized by default)
118
+ - [ ] Middleware (_cancalled_)
119
+ - [ ] Caching (configurable)
120
+ - [x] Component
121
+ - [ ] Layout
122
+ - [x] Page
123
+ - [ ] Assets
124
+ - [ ] API Route
125
+ - [ ] Plugin (_Alpha_)
126
+ - [x] Builtin TailwindCSS and Esbuild
127
+ - [x] Command based plugin system
128
+ - [ ] Source based plugin system
129
+ - [ ] Context (configurable)
130
+ - [ ] App
131
+ - [ ] Layout
132
+ - [ ] Page
133
+ - [x] Component
134
+ - [x] `error.zx` for default and per-route error page
135
+ - [x] `notfound.zx` for default and per-route error page
111
136
  - [x] CLI
112
137
  - [x] `init` Project Template
113
138
  - [x] `transpile` Transpile .zx files to Zig source code
114
139
  - [x] `serve` Serve the project
115
140
  - [x] `dev` HMR or Rebuild on Change
116
- - [x] `fmt` Format the ZX source code (_Alpha_)
141
+ - [x] `fmt` Format the ZX source code
117
142
  - [x] `export` Generate static site assets
118
143
  - [x] `bundle` Bundle the ZX executable with public/assets and exe
119
144
  - [x] `version` Show the version of the ZX CLI
120
145
  - [x] `update` Update the version of ZX dependency
121
146
  - [x] `upgrade` Upgrade the version of ZX CLI
147
+ - [ ] Platform
148
+ - [x] Server
149
+ - [x] Browser
150
+ - [ ] iOS
151
+ - [ ] Android
152
+ - [ ] macOS
153
+ - [ ] Windows
122
154
 
123
155
  #### Editor Support
124
156
 
125
- * [VSCode](https://marketplace.visualstudio.com/items?itemName=nurulhudaapon.zx)/[Cursor](https://marketplace.visualstudio.com/items?itemName=nurulhudaapon.zx) Extension
126
- - [x] Syntax Highlighting
127
- - [x] LSP Support
128
- - [x] Auto Format
129
-
130
- * Neovim
131
- - [ ] Syntax Highlighting
132
- - [ ] LSP Support
133
- - [ ] Auto Format
157
+ - ##### [VSCode](https://marketplace.visualstudio.com/items?itemName=nurulhudaapon.zx)/[VSCode Forks](https://marketplace.visualstudio.com/items?itemName=nurulhudaapon.zx) Extension
158
+ - ##### [Neovim](/editors/neovim/)
159
+ - ##### [Zed](/editors/zed/)
134
160
 
135
161
  ## Similar Projects
136
162
 
137
- * [Yew](https://github.com/yewstack/yew) - Rust / Wasm framework for creating reliable and efficient web applications
138
- * [ZTS](https://github.com/zigster64/zts) — Zig Templates made Simple, a templating system for Zig
139
- * [zmpl](https://github.com/jetzig-framework/zmpl) Mode-based templating language that compiles to Zig functions at build time, used in Jetzig
140
- * [mustache-zig](https://github.com/batiati/mustache-zig) Mustache template engine implementation in Zig
141
- * [etch](https://github.com/haze/etch) — Compile-time tuned templating engine focusing on speed and simplicity
142
- * [Zap](https://github.com/zigzap/zap) — High-performance backend framework in Zig
143
- * [http.zig](https://github.com/karlseguin/http.zig) Low-level HTTP/1.1 server written entirely in Zig (_ZX_'s backend)
144
- * [tokamak](https://github.com/cztomsik/tokamak) — Server-side framework for Zig
145
- * [zig-router](https://github.com/Cloudef/zig-router) — Straightforward HTTP-like request routing library for Zig
146
- * [zig-webui](https://github.com/webui-dev/zig-webui/) — Zig library that allows using any web browser as a GUI
147
- * [Zine](https://github.com/kristoff-it/zine) Fast, scalable, flexible static site generator (SSG) written in Zig
148
- * [Zinc](https://github.com/zon-dev/zinc/) Web framework written in pure Zig with focus on high performance, usability, security, and extensibility
149
- * [zUI](https://github.com/thienpow/zui) UI kit for Jetzig framework with reusable components and styles
163
+ ### Rust
164
+
165
+ * [Leptos](https://github.com/leptos-rs/leptos) - Full-stack, isomorphic Rust web framework with fine-grained reactivity and JSX-like syntax
166
+ * [Dioxus](https://github.com/DioxusLabs/dioxus) - Cross-platform GUI framework with React-like API, supporting web, desktop, mobile, and SSR
167
+ * [Yew](https://github.com/yewstack/yew) - Rust / Wasm framework for creating reliable and efficient web applications with component-based architecture
168
+ * [Sycamore](https://github.com/sycamore-rs/sycamore) - Reactive web framework with fine-grained reactivity and minimal bundle sizes
169
+ * [Perseus](https://github.com/framesurge/perseus) - Full-stack framework built on Sycamore with SSR, SSG, and incremental regeneration
170
+
171
+ ### Zig
172
+
173
+ * [Jetzig](https://github.com/jetzig-framework/jetzig) - Zig web framework with MVC architecture, built-in ORM, and powerful templating
174
+ * [ZTS](https://github.com/zigster64/zts) - Zig Templates made Simple, a templating system for Zig
175
+ * [zmpl](https://github.com/jetzig-framework/zmpl) - Mode-based templating language that compiles to Zig functions at build time, used in Jetzig
176
+ * [mustache-zig](https://github.com/batiati/mustache-zig) - Mustache template engine implementation in Zig
177
+ * [etch](https://github.com/haze/etch) - Compile-time tuned templating engine focusing on speed and simplicity
178
+ * [Zap](https://github.com/zigzap/zap) - High-performance backend framework in Zig
179
+ * [http.zig](https://github.com/karlseguin/http.zig) - Low-level HTTP/1.1 server written entirely in Zig (_ZX_'s backend)
180
+ * [tokamak](https://github.com/cztomsik/tokamak) - Server-side framework for Zig
181
+ * [zig-router](https://github.com/Cloudef/zig-router) - Straightforward HTTP-like request routing library for Zig
182
+ * [zig-webui](https://github.com/webui-dev/zig-webui/) - Zig library that allows using any web browser as a GUI
183
+ * [Zine](https://github.com/kristoff-it/zine) - Fast, scalable, flexible static site generator (SSG) written in Zig
184
+ * [Zinc](https://github.com/zon-dev/zinc/) - Web framework written in pure Zig with focus on high performance, usability, security, and extensibility
185
+ * [zUI](https://github.com/thienpow/zui) - UI kit for Jetzig framework with reusable components and styles
186
+ * [zig-pek](https://github.com/nektro/zig-pek) - Comptime HTML/XML parser and renderer in Zig
187
+ * [zigomponent](https://zigomp.prjct.dev/) - HTML compoenents in pure zig
188
+
189
+ ## Related Projects
190
+
191
+ * [Codeberg Mirror](https://codeberg.org/nurulhudaapon/zx) - ZX repository mirror on Codeberg
192
+ * [zx-vscode](https://github.com/nurulhudaapon/zx-vscode) - Official VSCode/Cursor extension with syntax highlighting, LSP support, and auto-formatting
193
+ * [ziex.dev](https://github.com/nurulhudaapon/zx/tree/main/site) - Official documentation site of ZX made using ZX.
194
+ * [zx-example-portfolio](https://github.com/nurulhudaapon/zx-example-portfolio) - Demo portfolio web application built with ZX
195
+ * [thegates.dev](https://github.com/nurulhudaapon/thegates.dev) - Example clone demonstrating ZX capabilities
150
196
 
151
197
  ## Contributing
152
198
 
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  var zx = {
3
3
  name: "zx",
4
- version: "0.0.1-dev.270",
4
+ version: "0.1.0-dev.517",
5
5
  description: "ZX is a framework for building web applications with Zig.",
6
6
  repository: "https://github.com/nurulhudaapon/zx",
7
7
  fingerprint: 14616285862371232000,
@@ -12,18 +12,30 @@ var zx = {
12
12
  hash: "httpz-0.0.0-PNVzrBgtBwCVkSJyophIX6WHwDR0r8XhBGQr96Kk-1El"
13
13
  },
14
14
  zli: {
15
- url: "git+https://github.com/nurulhudaapon/cliz.git#aff3b54879e7514afaf8c87f1abe22121b8992d4",
16
- hash: "zli-4.3.0-LeUjpu_fAABOSVASSCW2fFh8SFVNHrxQGDXGPNzcSE_i"
15
+ path: "vendor/cliz"
17
16
  },
18
17
  zig_js: {
19
- url: "git+https://github.com/nurulhudaapon/jsz.git#04db83c617da1956ac5adc1cb9ba1e434c1cb6fd",
20
- hash: "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi"
18
+ path: "vendor/jsz"
19
+ },
20
+ tree_sitter: {
21
+ path: "vendor/zig-tree-sitter"
22
+ },
23
+ tree_sitter_zx: {
24
+ path: "packages/tree-sitter-zx"
25
+ },
26
+ cachez: {
27
+ path: "vendor/cachez"
21
28
  }
22
29
  },
23
30
  paths: [
24
31
  "build.zig",
25
32
  "build.zig.zon",
26
- "src"
33
+ "src",
34
+ "packages/tree-sitter-zx",
35
+ "vendor/cachez",
36
+ "vendor/cliz",
37
+ "vendor/jsz",
38
+ "vendor/zig-tree-sitter"
27
39
  ]
28
40
  };
29
41
  export {
package/package.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "ziex",
3
+ "version": "0.1.0-dev.517",
3
4
  "description": "ZX is a framework for building web applications with Zig.",
4
5
  "main": "index.js",
5
6
  "type": "module",
@@ -28,6 +29,5 @@
28
29
  "author": "Nurul Huda (Apon) <me@nurulhudaapon.com>",
29
30
  "license": "MIT",
30
31
  "module": "index.js",
31
- "types": "index.d.ts",
32
- "version": "0.0.1-dev.7"
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 the DOM node that was server-rendered by ZX with the component's unique ID.
12
- * The element already exists in the DOM and contains the server-rendered fallback content.
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
- * // The DOM node corresponds to HTML like:
18
- * // <div id="zx-dcde04c415da9d1b15ca2690d8b497ae" data-props="...">...</div>
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 server-rendered HTML.
27
+ * Component props parsed from the comment marker.
27
28
  *
28
- * Props are extracted from the `data-props` attribute (JSON-encoded) on the component's
29
- * container element. If the component has children from server side then they are automatically converted to
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
- * // <div data-props='{"max_count":10,"label":"Counter"}' data-children="<span>0</span>">...</div>
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
- * Automatically added when the component has children in the ZX file. The HTML string
50
- * is extracted from the `data-children` attribute on the server-rendered element.
51
- *
52
- * @example
53
- * ```tsx
54
- * // In ZX file:
55
- * <MyComponent @rendering={.csr}>
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 DOM container, extracting
89
- * props and children from server-rendered HTML attributes, and lazy-loading the component module.
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 reads data attributes (`data-props`, `data-children`) from the DOM element
93
- * with the component's unique ID, then lazy-loads the component module for rendering.
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 container element cannot be found in the DOM. This typically
102
- * happens if the component ID doesn't match any element, the script runs before
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
- * // With async/await:
122
- * async function hydrateComponent(component: ComponentMetadata) {
123
- * try {
124
- * const { domNode, Component, props } = await prepareComponent(component);
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 domNode = document.getElementById(component.id);
4
- if (!domNode)
5
- throw new Error(`Root element ${component.id} not found`, { cause: component });
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
- filterComponents
103
+ hydrateAll,
104
+ filterComponents,
105
+ discoverComponents
21
106
  };
package/react/types.d.ts CHANGED
@@ -2,14 +2,14 @@
2
2
  * Metadata for a client-side component used within a ZX file.
3
3
  *
4
4
  * This type represents the metadata for components that are marked with the `@rendering` attribute
5
- * in ZX files. When a component is declared with `@rendering={.csr}` or `@rendering={.csz}` in a
5
+ * in ZX files. When a component is declared with `@rendering={.react}` or `@rendering={.client}` in a
6
6
  * `.zx` file, the ZX transpiler generates a `ComponentMetadata` entry that is included in the
7
7
  * generated `components` array.
8
8
  *
9
9
  * @example
10
10
  * ```tsx
11
11
  * // In a ZX file (page.zx):
12
- * <CounterComponent @rendering={.csr} max_count={10} />
12
+ * <CounterComponent @rendering={.react} max_count={10} />
13
13
  *
14
14
  * // Generated components array (components.ts):
15
15
  * export const components: ComponentMetadata[] = [
@@ -17,7 +17,7 @@
17
17
  * name: "CounterComponent",
18
18
  * path: "./components/CounterComponent.tsx",
19
19
  * id: "zx-dcde04c415da9d1b15ca2690d8b497ae",
20
- * type: "csr",
20
+ * type: "react",
21
21
  * import: () => import('./components/CounterComponent.tsx')
22
22
  * }
23
23
  * ];
@@ -44,7 +44,7 @@ export type ComponentMetadata = {
44
44
  * @example
45
45
  * ```tsx
46
46
  * // In ZX file:
47
- * <CounterComponent @rendering={.csr} />
47
+ * <CounterComponent @rendering={.react} />
48
48
  *
49
49
  * // name will be: "CounterComponent"
50
50
  * ```
@@ -54,7 +54,7 @@ export type ComponentMetadata = {
54
54
  * The file path to the component module.
55
55
  *
56
56
  * This is the relative or absolute path to the component file that will be dynamically imported
57
- * at runtime. For CSR components, this typically points to a `.tsx` or `.jsx` file. For CSZ
57
+ * at runtime. For CSR components, this typically points to a `.tsx` or `.jsx` file. For Client
58
58
  * components, this points to a Zig component file.
59
59
  *
60
60
  * The path is determined from the `@jsImport` directive in the ZX file, or defaults to
@@ -64,7 +64,7 @@ export type ComponentMetadata = {
64
64
  * ```tsx
65
65
  * // In ZX file:
66
66
  * const CounterComponent = @jsImport("components/Counter.tsx");
67
- * <CounterComponent @rendering={.csr} />
67
+ * <CounterComponent @rendering={.react} />
68
68
  *
69
69
  * // path will be: "components/Counter.tsx"
70
70
  * ```
@@ -72,7 +72,7 @@ export type ComponentMetadata = {
72
72
  * @example
73
73
  * ```tsx
74
74
  * // Without explicit @jsImport:
75
- * <MyComponent @rendering={.csr} />
75
+ * <MyComponent @rendering={.react} />
76
76
  *
77
77
  * // path will default to: "./MyComponent.tsx"
78
78
  * ```
@@ -85,17 +85,16 @@ export type ComponentMetadata = {
85
85
  */
86
86
  route: string | null;
87
87
  /**
88
- * A unique HTML element identifier for the component's root DOM node.
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 component's container
92
- * element in the DOM during client-side hydration.
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
- * When the same component is used multiple times on a page, each instance gets the same base ID
97
- * since they share the same path and name. The ZX runtime uses this ID along with `data-props`
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,36 +102,34 @@ 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
- * <div id="zx-dcde04c415da9d1b15ca2690d8b497ae"
111
- * data-props='{"max_count":10}'
112
- * data-children="...">
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;
118
115
  /**
119
116
  * The rendering type of the component, determining how it will be rendered on the client.
120
117
  *
121
- * - **"csr"** (Client Side React): The component is a React component that will be rendered
118
+ * - **"react"** (Client Side React): The component is a React component that will be rendered
122
119
  * using React's client-side rendering. The component file should export a default React
123
120
  * component function. This is the most common type for interactive UI components.
124
121
  *
125
- * - **"csz"** (Client Side Zig): The component is a Zig component that will be compiled to
122
+ * - **"client"** (Client Side Zig): The component is a Zig component that will be compiled to
126
123
  * WebAssembly and rendered on the client side. This allows you to use Zig's performance
127
124
  * and type safety for client-side components.
128
125
  *
129
- * The type is determined by the `@rendering` attribute value in the ZX file (`.csr` or `.csz`).
126
+ * The type is determined by the `@rendering` attribute value in the ZX file (`.react` or `.client`).
130
127
  *
131
128
  * @example
132
129
  * ```tsx
133
130
  * // CSR component (React):
134
- * <CounterComponent @rendering={.csr} max_count={10} />
135
- * // type: "csr"
131
+ * <CounterComponent @rendering={.react} max_count={10} />
132
+ * // type: "react"
136
133
  *
137
134
  * // Component file (CounterComponent.tsx):
138
135
  * export default function CounterComponent({ max_count }: { max_count: number }) {
@@ -142,9 +139,9 @@ export type ComponentMetadata = {
142
139
  *
143
140
  * @example
144
141
  * ```tsx
145
- * // CSZ component (Zig/WASM):
146
- * <CounterComponent @rendering={.csz} />
147
- * // type: "csz"
142
+ * // Client component (Zig/WASM):
143
+ * <CounterComponent @rendering={.client} />
144
+ * // type: "client"
148
145
  *
149
146
  * // Component file (CounterComponent.zig):
150
147
  * pub fn CounterComponent(allocator: zx.Allocator) zx.Component {
@@ -152,7 +149,7 @@ export type ComponentMetadata = {
152
149
  * }
153
150
  * ```
154
151
  */
155
- type: "csr" | "csz";
152
+ type: "react" | "client";
156
153
  /**
157
154
  * A lazy-loading function that dynamically imports the component module.
158
155
  *
@@ -161,7 +158,7 @@ export type ComponentMetadata = {
161
158
  * by only loading components when they are needed.
162
159
  *
163
160
  * For CSR components, the imported module should export a default React component.
164
- * For CSZ components, the import mechanism depends on the WASM module structure.
161
+ * For Client components, the import mechanism depends on the WASM module structure.
165
162
  *
166
163
  * The function is called during client-side hydration to load and render the component
167
164
  * into its corresponding DOM container element.
package/wasm/index.d.ts CHANGED
@@ -1,14 +1,26 @@
1
+ import { ZigJS } from "../../../../vendor/jsz/js/src";
2
+ export declare const jsz: ZigJS;
1
3
  declare class ZXInstance {
2
4
  #private;
3
5
  exports: WebAssembly.Exports;
4
6
  events: Event[];
5
- actions: Record<string, (eventId: number) => void>;
6
7
  constructor({ exports, events }: ZXInstanceOptions);
7
8
  addEvent(event: Event): number;
9
+ /**
10
+ * Initialize event delegation on a root element
11
+ * This attaches a single event listener for each event type at the root,
12
+ * and uses __zx_ref to look up the corresponding VElement in WASM
13
+ */
14
+ initEventDelegation(rootSelector?: string): void;
15
+ getZxRef(element: HTMLElement): number | undefined;
8
16
  }
9
- export declare function init(options?: InitOptions): Promise<void>;
17
+ export declare function init(options?: InitOptions): Promise<WebAssembly.WebAssemblyInstantiatedSource>;
10
18
  export type InitOptions = {
19
+ /** URL to the WASM file (default: /assets/main.wasm) */
11
20
  url?: string;
21
+ /** CSS selector for the event delegation root element (default: 'body') */
22
+ eventDelegationRoot?: string;
23
+ importObject?: WebAssembly.Imports;
12
24
  };
13
25
  type ZXInstanceOptions = {
14
26
  exports: ZXInstance['exports'];
@@ -18,5 +30,11 @@ declare global {
18
30
  interface Window {
19
31
  _zx: ZXInstance;
20
32
  }
33
+ interface HTMLElement {
34
+ /**
35
+ * The VElement ID of the element
36
+ */
37
+ __zx_ref?: number;
38
+ }
21
39
  }
22
40
  export {};
package/wasm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // node_modules/jsz/js/src/zigjs.ts
1
+ // ../../vendor/jsz/js/src/zigjs.ts
2
2
  var NAN_PREFIX = 2146959360;
3
3
  var predefined = {
4
4
  nan: 0,
@@ -170,26 +170,60 @@ class ZigJS {
170
170
  // src/wasm/index.ts
171
171
  var DEFAULT_URL = "/assets/main.wasm";
172
172
  var MAX_EVENTS = 100;
173
+ var DELEGATED_EVENTS = [
174
+ "click",
175
+ "dblclick",
176
+ "input",
177
+ "change",
178
+ "submit",
179
+ "focus",
180
+ "blur",
181
+ "keydown",
182
+ "keyup",
183
+ "keypress",
184
+ "mouseenter",
185
+ "mouseleave",
186
+ "mousedown",
187
+ "mouseup",
188
+ "mousemove",
189
+ "touchstart",
190
+ "touchend",
191
+ "touchmove",
192
+ "scroll"
193
+ ];
194
+ var EVENT_TYPE_MAP = {
195
+ click: 0,
196
+ dblclick: 1,
197
+ input: 2,
198
+ change: 3,
199
+ submit: 4,
200
+ focus: 5,
201
+ blur: 6,
202
+ keydown: 7,
203
+ keyup: 8,
204
+ keypress: 9,
205
+ mouseenter: 10,
206
+ mouseleave: 11,
207
+ mousedown: 12,
208
+ mouseup: 13,
209
+ mousemove: 14,
210
+ touchstart: 15,
211
+ touchend: 16,
212
+ touchmove: 17,
213
+ scroll: 18
214
+ };
173
215
  var jsz = new ZigJS;
174
216
  var importObject = {
175
- module: {},
176
- env: {},
177
217
  ...jsz.importObject()
178
218
  };
179
219
 
180
220
  class ZXInstance {
181
221
  exports;
182
222
  events;
183
- actions;
223
+ #eventDelegationInitialized = false;
184
224
  constructor({ exports, events = [] }) {
185
225
  this.exports = exports;
186
226
  this.events = events;
187
- this.actions = {};
188
- Object.entries(exports).forEach(([name, func]) => {
189
- if (typeof func !== "function")
190
- return;
191
- this.actions[name] = this.#actionWrapper.bind(this, name);
192
- });
193
227
  }
194
228
  addEvent(event) {
195
229
  if (this.events.length >= MAX_EVENTS)
@@ -197,23 +231,52 @@ class ZXInstance {
197
231
  const idx = this.events.push(event);
198
232
  return idx - 1;
199
233
  }
200
- #actionWrapper(name, ...args) {
201
- const func = this.exports[name];
202
- if (typeof func !== "function")
203
- throw new Error(`Action ${name} is not a function`);
204
- const eventId = this.addEvent(args[0]);
205
- return func(eventId);
234
+ initEventDelegation(rootSelector = "body") {
235
+ if (this.#eventDelegationInitialized)
236
+ return;
237
+ const root = document.querySelector(rootSelector);
238
+ if (!root)
239
+ return;
240
+ for (const eventType of DELEGATED_EVENTS) {
241
+ root.addEventListener(eventType, (event) => {
242
+ this.#handleDelegatedEvent(eventType, event);
243
+ }, { passive: eventType.startsWith("touch") || eventType === "scroll" });
244
+ }
245
+ this.#eventDelegationInitialized = true;
246
+ }
247
+ #handleDelegatedEvent(eventType, event) {
248
+ let target = event.target;
249
+ while (target && target !== document.body) {
250
+ const zxRef = target.__zx_ref;
251
+ if (zxRef !== undefined) {
252
+ const eventId = this.addEvent(event);
253
+ const handleEvent = this.exports.handleEvent;
254
+ if (typeof handleEvent === "function") {
255
+ const eventTypeId = EVENT_TYPE_MAP[eventType] ?? 0;
256
+ handleEvent(BigInt(zxRef), eventTypeId, BigInt(eventId));
257
+ }
258
+ break;
259
+ }
260
+ target = target.parentElement;
261
+ }
262
+ }
263
+ getZxRef(element) {
264
+ return element.__zx_ref;
206
265
  }
207
266
  }
208
267
  async function init(options = {}) {
209
268
  const url = options?.url ?? DEFAULT_URL;
210
- const { instance } = await WebAssembly.instantiateStreaming(fetch(url), importObject);
269
+ const wasmInstiatedSource = await WebAssembly.instantiateStreaming(fetch(url), Object.assign({}, importObject, options.importObject));
270
+ const { instance } = wasmInstiatedSource;
211
271
  jsz.memory = instance.exports.memory;
212
272
  window._zx = new ZXInstance({ exports: instance.exports });
273
+ window._zx.initEventDelegation(options.eventDelegationRoot ?? "body");
213
274
  const main = instance.exports.mainClient;
214
275
  if (typeof main === "function")
215
276
  main();
277
+ return wasmInstiatedSource;
216
278
  }
217
279
  export {
280
+ jsz,
218
281
  init
219
282
  };
package/zx.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getZxInfo(): any;