ziex 0.1.0-dev.460 → 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
@@ -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
- - [ ] State managment
93
- - [ ] Hydration
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
- - [ ] Component
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] Web Server
148
- - [x] Web Browser
148
+ - [x] Server
149
+ - [x] Browser
149
150
  - [ ] iOS
150
151
  - [ ] Android
151
152
  - [ ] macOS
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  var zx = {
3
3
  name: "zx",
4
- version: "0.1.0-dev.460",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ziex",
3
- "version": "0.1.0-dev.460",
3
+ "version": "0.1.0-dev.517",
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": "git+https://github.com/nurulhudaapon/zx.git"
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 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={.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 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
@@ -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,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
- * <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;
package/wasm/index.d.ts CHANGED
@@ -1,3 +1,5 @@
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;
@@ -10,15 +12,15 @@ declare class ZXInstance {
10
12
  * and uses __zx_ref to look up the corresponding VElement in WASM
11
13
  */
12
14
  initEventDelegation(rootSelector?: string): void;
13
- /** Get the VElement ID from a DOM element */
14
15
  getZxRef(element: HTMLElement): number | undefined;
15
16
  }
16
- export declare function init(options?: InitOptions): Promise<void>;
17
+ export declare function init(options?: InitOptions): Promise<WebAssembly.WebAssemblyInstantiatedSource>;
17
18
  export type InitOptions = {
18
19
  /** URL to the WASM file (default: /assets/main.wasm) */
19
20
  url?: string;
20
21
  /** CSS selector for the event delegation root element (default: 'body') */
21
22
  eventDelegationRoot?: string;
23
+ importObject?: WebAssembly.Imports;
22
24
  };
23
25
  type ZXInstanceOptions = {
24
26
  exports: ZXInstance['exports'];
package/wasm/index.js CHANGED
@@ -214,8 +214,6 @@ var EVENT_TYPE_MAP = {
214
214
  };
215
215
  var jsz = new ZigJS;
216
216
  var importObject = {
217
- module: {},
218
- env: {},
219
217
  ...jsz.importObject()
220
218
  };
221
219
 
@@ -237,17 +235,14 @@ class ZXInstance {
237
235
  if (this.#eventDelegationInitialized)
238
236
  return;
239
237
  const root = document.querySelector(rootSelector);
240
- if (!root) {
241
- console.warn(`[ZX] Event delegation root "${rootSelector}" not found`);
238
+ if (!root)
242
239
  return;
243
- }
244
240
  for (const eventType of DELEGATED_EVENTS) {
245
241
  root.addEventListener(eventType, (event) => {
246
242
  this.#handleDelegatedEvent(eventType, event);
247
243
  }, { passive: eventType.startsWith("touch") || eventType === "scroll" });
248
244
  }
249
245
  this.#eventDelegationInitialized = true;
250
- console.debug("[ZX] Event delegation initialized on", rootSelector);
251
246
  }
252
247
  #handleDelegatedEvent(eventType, event) {
253
248
  let target = event.target;
@@ -271,14 +266,17 @@ class ZXInstance {
271
266
  }
272
267
  async function init(options = {}) {
273
268
  const url = options?.url ?? DEFAULT_URL;
274
- 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;
275
271
  jsz.memory = instance.exports.memory;
276
272
  window._zx = new ZXInstance({ exports: instance.exports });
277
273
  window._zx.initEventDelegation(options.eventDelegationRoot ?? "body");
278
274
  const main = instance.exports.mainClient;
279
275
  if (typeof main === "function")
280
276
  main();
277
+ return wasmInstiatedSource;
281
278
  }
282
279
  export {
280
+ jsz,
283
281
  init
284
282
  };