atmx-web 0.42.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.
@@ -0,0 +1,125 @@
1
+ // FILE: src/dom/scanner.ts
2
+ import { connectQuery, executeMutation, parseRpcCall } from "./lifecycle";
3
+ import { updateContext, getContext } from "../core/context";
4
+ import { queryRegistry } from "../core/query";
5
+ import { resolveRoute } from "../core/router";
6
+ import { evaluateExpression } from "../resolver/evaluator";
7
+
8
+ const intersectionObserver = new IntersectionObserver((entries) => {
9
+ entries.forEach((entry) => {
10
+ if (entry.isIntersecting) {
11
+ const el = entry.target as HTMLElement;
12
+ if (el.hasAttribute("ax-query")) connectQuery(el);
13
+ intersectionObserver.unobserve(el);
14
+ }
15
+ });
16
+ });
17
+
18
+ /**
19
+ * ✨ NEW: Evaluates dynamic prefixed attributes like :ax-query="sdk.pyExample.login()"
20
+ */
21
+ function evaluateDynamicAttributes(root: HTMLElement | Document) {
22
+ const dynamicElements = root.querySelectorAll<HTMLElement>(
23
+ "[\\:ax-query], [\\:ax-mutate]",
24
+ );
25
+ dynamicElements.forEach((el) => {
26
+ if (el.hasAttribute(":ax-query")) {
27
+ const expr = el.getAttribute(":ax-query")!;
28
+ const val = evaluateExpression(expr, getContext(el));
29
+ if (val) el.setAttribute("ax-query", String(val));
30
+ el.removeAttribute(":ax-query");
31
+ }
32
+ if (el.hasAttribute(":ax-mutate")) {
33
+ const expr = el.getAttribute(":ax-mutate")!;
34
+ const val = evaluateExpression(expr, getContext(el));
35
+ if (val) el.setAttribute("ax-mutate", String(val));
36
+ el.removeAttribute(":ax-mutate");
37
+ }
38
+ });
39
+ }
40
+
41
+ export function scanAndInitialize(root: HTMLElement | Document = document) {
42
+ // 0. Unwrap Dynamic Typescript Clients first
43
+ evaluateDynamicAttributes(root);
44
+
45
+ // 1. Initialize ax-query blocks
46
+ const queries = root.querySelectorAll("[ax-query]");
47
+ queries.forEach((el) => {
48
+ const htmlEl = el as HTMLElement;
49
+ updateContext(htmlEl, {});
50
+
51
+ const triggerMode = htmlEl.getAttribute("ax-trigger") || "load";
52
+ if (triggerMode === "load") {
53
+ connectQuery(htmlEl);
54
+ } else if (triggerMode === "intersect") {
55
+ intersectionObserver.observe(htmlEl);
56
+ } else {
57
+ htmlEl.addEventListener(triggerMode, (e) => {
58
+ e.preventDefault();
59
+ connectQuery(htmlEl);
60
+ });
61
+ }
62
+ });
63
+
64
+ // 2. Initialize ax-mutate blocks
65
+ const mutates = root.querySelectorAll("[ax-mutate]");
66
+ mutates.forEach((el) => {
67
+ const htmlEl = el as HTMLElement;
68
+ updateContext(htmlEl, {});
69
+
70
+ const trigger = htmlEl.tagName === "FORM" ? "submit" : "click";
71
+
72
+ htmlEl.addEventListener(trigger, (e) => {
73
+ if (
74
+ htmlEl.tagName === "A" ||
75
+ htmlEl.tagName === "BUTTON" ||
76
+ htmlEl.tagName === "FORM"
77
+ ) {
78
+ e.preventDefault();
79
+ }
80
+ executeMutation(htmlEl);
81
+ });
82
+ });
83
+
84
+ // 3. Action Modifiers (e.g., ax-trigger="refresh")
85
+ const refreshers = root.querySelectorAll('[ax-trigger="refresh"]');
86
+ refreshers.forEach((el) => {
87
+ const htmlEl = el as HTMLElement;
88
+ htmlEl.addEventListener("click", (e) => {
89
+ e.preventDefault();
90
+
91
+ const parentQuery = htmlEl.closest("[ax-query]") as HTMLElement;
92
+ if (parentQuery) {
93
+ const attrVal = parentQuery.getAttribute("ax-query")!;
94
+ const parsed = parseRpcCall(parentQuery, attrVal);
95
+
96
+ if (parsed) {
97
+ const route = resolveRoute(parsed.target);
98
+ if (route) {
99
+ const queryKey = `${route.namespace}:${route.id}:${JSON.stringify(parsed.args)}`;
100
+ const activeQuery = queryRegistry.get(queryKey);
101
+ if (activeQuery) {
102
+ activeQuery.fetch(true);
103
+ } else {
104
+ connectQuery(parentQuery);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ export function initMutationObserver() {
114
+ const observer = new MutationObserver((mutations) => {
115
+ mutations.forEach((mutation) => {
116
+ mutation.addedNodes.forEach((node) => {
117
+ if (node instanceof HTMLElement) {
118
+ scanAndInitialize(node);
119
+ }
120
+ });
121
+ });
122
+ });
123
+
124
+ observer.observe(document.body, { childList: true, subtree: true });
125
+ }
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ // FILE: src/index.ts
2
+ import { initWasm, setAuthToken, clearAuthToken } from "./core/wasm";
3
+ import { scanAndInitialize, initMutationObserver } from "./dom/scanner";
4
+ import { connectQuery } from "./dom/lifecycle";
5
+ import { AtmxConfig, InitResult } from "./core/types";
6
+ import "./dom/components/engine-status";
7
+
8
+ export const ATMX_VERSION = "0.42.0";
9
+
10
+ class ATMX {
11
+ public version = ATMX_VERSION;
12
+ public initialized = false;
13
+ public debug = false;
14
+
15
+ public async init(config: AtmxConfig): Promise<InitResult> {
16
+ if (this.initialized) {
17
+ return { ok: true, contracts: {}, initDurationMs: 0 };
18
+ }
19
+
20
+ const result = await initWasm(config);
21
+
22
+ if (result.ok) {
23
+ this.debug = config.debug || false;
24
+
25
+ initMutationObserver();
26
+ scanAndInitialize(document.body);
27
+
28
+ this.initialized = true;
29
+ if (this.debug)
30
+ console.log(
31
+ "%c🚀 ATMX Debug Mode: Enabled",
32
+ "color: #2563eb; font-weight: bold;",
33
+ );
34
+ console.log(
35
+ `✅ ATMX v${this.version} Online (${result.initDurationMs}ms). Reactive Engine Started.`,
36
+ );
37
+
38
+ document.dispatchEvent(new CustomEvent("atmx:ready", { detail: result }));
39
+ } else {
40
+ console.error("❌ ATMX Initialization Failed:", result.error);
41
+ // Broadcast fatal error so UI components can catch it
42
+ document.dispatchEvent(
43
+ new CustomEvent("atmx:error", { detail: result.error }),
44
+ );
45
+ }
46
+
47
+ return result;
48
+ }
49
+
50
+ public triggerQuery(elementOrSelector: string | HTMLElement) {
51
+ const el =
52
+ typeof elementOrSelector === "string"
53
+ ? (document.querySelector(elementOrSelector) as HTMLElement)
54
+ : elementOrSelector;
55
+
56
+ if (el && el instanceof HTMLElement && el.hasAttribute("ax-query")) {
57
+ connectQuery(el);
58
+ }
59
+ }
60
+
61
+ public on(
62
+ eventName: "ready" | "data" | "error" | "success" | "contract-loaded",
63
+ callback: (e: any) => void,
64
+ ) {
65
+ document.addEventListener(`atmx:${eventName}`, callback);
66
+ }
67
+
68
+ public setAuthToken(namespace: string, methodName: string, token: string) {
69
+ setAuthToken(namespace, methodName, token);
70
+ }
71
+
72
+ public clearAuthToken(namespace: string, methodName: string) {
73
+ clearAuthToken(namespace, methodName);
74
+ }
75
+ }
76
+
77
+ export const atmx = new ATMX();
78
+ if (typeof window !== "undefined") (window as any).atmx = atmx;
@@ -0,0 +1,46 @@
1
+ // FILE: src/resolver/evaluator.ts
2
+ import { AtmxContextData } from "../core/context";
3
+
4
+ /**
5
+ * Safely evaluates a JS expression from an HTML attribute against the local ATMX Context.
6
+ */
7
+ export function evaluateExpression(
8
+ expr: string,
9
+ context: AtmxContextData,
10
+ locals: Record<string, any> = {},
11
+ ): any {
12
+ if (!expr) return undefined;
13
+ try {
14
+ const keys = Object.keys(locals);
15
+ const values = Object.values(locals);
16
+
17
+ // Dynamically build a function that accepts $data, $error, $state, AND our locals ($item, etc.)
18
+ const fn = new Function(
19
+ "$data",
20
+ "$error",
21
+ "$state",
22
+ ...keys,
23
+ `return (${expr});`,
24
+ );
25
+ return fn(context.data || {}, context.error || {}, context, ...values);
26
+ } catch (e) {
27
+ console.error(`ATMX Eval Error: ${expr}`, e);
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ export function executeAction(
33
+ expr: string,
34
+ context: AtmxContextData,
35
+ locals: Record<string, any> = {},
36
+ ): void {
37
+ if (!expr) return;
38
+ try {
39
+ const keys = Object.keys(locals);
40
+ const values = Object.values(locals);
41
+ const fn = new Function("$data", "$error", "$state", ...keys, expr);
42
+ fn(context.data || {}, context.error || {}, context, ...values);
43
+ } catch (e) {
44
+ console.error(`ATMX Action Error: ${expr}`, e);
45
+ }
46
+ }
@@ -0,0 +1,196 @@
1
+ // FILE: src/resolver/render.ts
2
+ import { AtmxContextData } from "../core/context";
3
+ import { evaluateExpression } from "./evaluator";
4
+
5
+ export function renderContext(rootEl: HTMLElement, context: AtmxContextData) {
6
+ walkAndRender(rootEl, context, true, {});
7
+ }
8
+
9
+ function walkAndRender(
10
+ node: HTMLElement,
11
+ context: AtmxContextData,
12
+ isRoot: boolean,
13
+ locals: Record<string, any>,
14
+ ) {
15
+ // Stop traversal if we hit an independent reactive boundary
16
+ if (
17
+ !isRoot &&
18
+ (node.hasAttribute("ax-query") || node.hasAttribute("ax-mutate"))
19
+ ) {
20
+ return;
21
+ }
22
+
23
+ // ✨ PHASE 1: DECLARATIVE LIST RENDERING (ax-for)
24
+ if (node.hasAttribute("ax-for")) {
25
+ handleAxFor(node, context, locals);
26
+ return; // Stop normal traversal for the template node!
27
+ }
28
+
29
+ // 1. STATE RENDERING (ax-when)
30
+ if (node.hasAttribute("ax-when")) {
31
+ const expectedStates = node
32
+ .getAttribute("ax-when")!
33
+ .split(",")
34
+ .map((s) => s.trim());
35
+ if (!expectedStates.includes(context.state)) {
36
+ node.setAttribute("hidden", "true");
37
+ node.style.setProperty("display", "none", "important");
38
+ } else {
39
+ node.removeAttribute("hidden");
40
+ node.style.removeProperty("display");
41
+ }
42
+ }
43
+
44
+ // 2. CONDITIONAL RENDERING (ax-if)
45
+ if (node.hasAttribute("ax-if")) {
46
+ const result = evaluateExpression(
47
+ node.getAttribute("ax-if")!,
48
+ context,
49
+ locals,
50
+ );
51
+ if (!result) {
52
+ node.setAttribute("hidden", "");
53
+ node.style.setProperty("display", "none", "important");
54
+ } else {
55
+ node.removeAttribute("hidden");
56
+ node.style.removeProperty("display");
57
+ }
58
+ }
59
+
60
+ // 3. TEXT INTERPOLATION (ax-text)
61
+ if (node.hasAttribute("ax-text")) {
62
+ const result = evaluateExpression(
63
+ node.getAttribute("ax-text")!,
64
+ context,
65
+ locals,
66
+ );
67
+ // ✨ SECURE: textContent completely eliminates XSS vulnerabilities!
68
+ node.textContent =
69
+ result !== undefined && result !== null ? String(result) : "";
70
+ }
71
+
72
+ // 4. FIELD-LEVEL ERRORS (ax-error-for)
73
+ if (node.hasAttribute("ax-error-for")) {
74
+ const fieldName = node.getAttribute("ax-error-for")!;
75
+ let errorMessage = "";
76
+ if (
77
+ context.state === "error" &&
78
+ context.error?.code === "ValidationError"
79
+ ) {
80
+ const details = context.error.details || "";
81
+ const lines = details.split("\n");
82
+ for (const line of lines) {
83
+ if (line.startsWith(fieldName + ":")) {
84
+ errorMessage = line.substring(fieldName.length + 1).trim();
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ if (errorMessage) {
90
+ node.textContent = errorMessage;
91
+ node.removeAttribute("hidden");
92
+ node.style.removeProperty("display");
93
+ } else {
94
+ node.setAttribute("hidden", "");
95
+ node.style.setProperty("display", "none", "important");
96
+ }
97
+ }
98
+
99
+ // 5. ATTRIBUTE BINDING (ax-bind:*)
100
+ Array.from(node.attributes).forEach((attr) => {
101
+ if (attr.name.startsWith("ax-bind:")) {
102
+ const targetAttr = attr.name.substring(8);
103
+ const result = evaluateExpression(attr.value, context, locals);
104
+ if (result === false || result === null || result === undefined) {
105
+ node.removeAttribute(targetAttr);
106
+ } else {
107
+ node.setAttribute(
108
+ targetAttr,
109
+ result === true ? targetAttr : String(result),
110
+ );
111
+ }
112
+ }
113
+ });
114
+
115
+ Array.from(node.children).forEach((child) => {
116
+ walkAndRender(child as HTMLElement, context, false, locals);
117
+ });
118
+ }
119
+
120
+ function handleAxFor(
121
+ templateNode: HTMLElement,
122
+ context: AtmxContextData,
123
+ locals: Record<string, any>,
124
+ ) {
125
+ templateNode.style.display = "none"; // Hide the template node
126
+
127
+ if (!templateNode.hasAttribute("data-ax-for-id")) {
128
+ templateNode.setAttribute(
129
+ "data-ax-for-id",
130
+ Math.random().toString(36).substr(2, 9),
131
+ );
132
+ }
133
+ const forId = templateNode.getAttribute("data-ax-for-id")!;
134
+ const forExpr = templateNode.getAttribute("ax-for")!;
135
+
136
+ // Parse: "$item in $data" or "($item, $index) in $data"
137
+ const match = forExpr.match(
138
+ /^\s*(?:(?:\(\s*([\w\$]+)\s*,\s*([\w\$]+)\s*\))|([\w\$]+))\s+in\s+(.+)$/,
139
+ );
140
+ if (!match) return;
141
+
142
+ const itemVar = match[1] || match[3];
143
+ const indexVar = match[2];
144
+ const iterableExpr = match[4];
145
+
146
+ const iterable = evaluateExpression(iterableExpr, context, locals);
147
+ const parent = templateNode.parentElement;
148
+ if (!parent) return;
149
+
150
+ const existingClones = Array.from(
151
+ parent.querySelectorAll(`[data-ax-clone-of="${forId}"]`),
152
+ ) as HTMLElement[];
153
+
154
+ if (!iterable || !Array.isArray(iterable)) {
155
+ existingClones.forEach((el) => el.remove());
156
+ return;
157
+ }
158
+
159
+ const keyAttr = templateNode.getAttribute("ax-key");
160
+ const newClones: HTMLElement[] = [];
161
+
162
+ iterable.forEach((item, index) => {
163
+ const childLocals = { ...locals, [itemVar]: item };
164
+ if (indexVar) childLocals[indexVar] = index;
165
+
166
+ let keyValue = String(index);
167
+ if (keyAttr) {
168
+ keyValue = String(evaluateExpression(keyAttr, context, childLocals));
169
+ }
170
+
171
+ // Try to reuse an existing DOM node for performance
172
+ let clone = existingClones.find(
173
+ (el) => el.getAttribute("data-ax-key") === keyValue,
174
+ );
175
+ if (!clone) {
176
+ clone = templateNode.cloneNode(true) as HTMLElement;
177
+ clone.removeAttribute("ax-for");
178
+ clone.style.removeProperty("display");
179
+ if (clone.style.length === 0) clone.removeAttribute("style");
180
+ clone.setAttribute("data-ax-clone-of", forId);
181
+ clone.setAttribute("data-ax-key", keyValue);
182
+ }
183
+
184
+ // Maintain exact order in the DOM
185
+ parent.insertBefore(clone, templateNode);
186
+
187
+ // Render the contents of the clone with the injected locals!
188
+ walkAndRender(clone, context, false, childLocals);
189
+ newClones.push(clone);
190
+ });
191
+
192
+ // Cleanup old clones that are no longer in the list
193
+ existingClones.forEach((el) => {
194
+ if (!newClones.includes(el)) el.remove();
195
+ });
196
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": [
7
+ "ES2020",
8
+ "DOM",
9
+ "DOM.Iterable"
10
+ ],
11
+ "skipLibCheck": true,
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": [
23
+ "src"
24
+ ]
25
+ }
package/vite-env.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module "*?raw" {
4
+ const content: string;
5
+ export default content;
6
+ }
7
+
8
+ declare module "*?url" {
9
+ const url: string;
10
+ export default url;
11
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve } from 'path';
3
+
4
+ export default defineConfig({
5
+ server: {
6
+ open: '/example/index.html',
7
+ proxy: {
8
+ // Any request to /api will be proxied to your backend
9
+ '/api': {
10
+ target: 'http://localhost:8000',
11
+ changeOrigin: true,
12
+ rewrite: (path) => path.replace(/^\/api/, '') // Strips /api before sending to backend
13
+ }
14
+ }
15
+ },
16
+ build: {
17
+ lib: {
18
+ entry: resolve(__dirname, 'src/index.ts'),
19
+ name: 'atmx',
20
+ fileName: (format) => `atmx.${format}.js`,
21
+ formats: ['es', 'umd']
22
+ },
23
+ assetsInlineLimit: 0,
24
+ outDir: 'dist',
25
+ emptyOutDir: true
26
+ }
27
+ });