Repository: Rezact/Rezact (ts)

Issue:
BUG: Cannot assign JSX directly to Signal
<img width="399" alt="image" src="https://github.com/Rezact/Rezact/assets/2927894/bbbf8e80-2971-4d66-bbab-3e3cb04a57f7">


This does not get transformed at all by the compiler

The expected output is:

```jsx
const $elmRef = new Signal(<p>Test</p>)
```

Pull request description:
29 bug cannot assign jsx directly to signal
fix #29 

Patch:
diff --git a/src/components/appRouter.ts b/src/components/appRouter.ts
index d2794cb..17314da 100644
--- a/src/components/appRouter.ts
+++ b/src/components/appRouter.ts
@@ -92,4 +92,9 @@ router.addRoute(
   "/component-context",
   () => import("src/examples/ComponentContext/ComponentContext")
 );
+
+router.addRoute(
+  "jsx-signals",
+  () => import("src/examples/JsxSignals/JsxSignals")
+)
 export { router };
diff --git a/src/examples/JsxSignals/JsxSignals.tsx b/src/examples/JsxSignals/JsxSignals.tsx
new file mode 100644
index 0000000..538ab48
--- /dev/null
+++ b/src/examples/JsxSignals/JsxSignals.tsx
@@ -0,0 +1,41 @@
+import { MyLayout } from "../Layout/nestedLayout";
+
+export function Page() {
+  let $elmRef = <p>Test Signal Element</p>;
+  const change = () => ($elmRef = <p>Changed Signal Element</p>);
+
+  let elm1 = <p>Elm 1</p>;
+  let elm2 = <p>Elm 2</p>;
+  let elm3 = <p>Elm 3</p>;
+  let elm4 = <p>Elm 4</p>;
+  let $elmRef2 = elm1;
+  let $elmRef3 = elm3;
+
+  const change2 = () => ($elmRef2 = $elmRef2 === elm1 ? elm2 : elm1);
+
+  const change3 = () => {
+    if ($elmRef3 === elm3) {
+      $elmRef3 = elm4;
+    } else {
+      $elmRef3 = elm3;
+    }
+  };
+
+  return (
+    <>
+      <h1>JSX Signals</h1>
+      {$elmRef}
+      <button onClick={change}>Change 1</button>
+
+      <hr />
+      {$elmRef2}
+      <button onClick={change2}>Change 2</button>
+
+      <hr />
+      {$elmRef3}
+      <button onClick={change3}>Change 3</button>
+    </>
+  );
+}
+
+export const Layout = MyLayout;
diff --git a/src/examples/JsxSignals/index.html b/src/examples/JsxSignals/index.html
new file mode 100644
index 0000000..cb2b361
--- /dev/null
+++ b/src/examples/JsxSignals/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite + Rezact + TS</title>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script type="module" src="./main.ts"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/examples/JsxSignals/main.ts b/src/examples/JsxSignals/main.ts
new file mode 100644
index 0000000..488672f
--- /dev/null
+++ b/src/examples/JsxSignals/main.ts
@@ -0,0 +1,5 @@
+import { render } from "rezact";
+import { Page } from "./JsxSignals";
+
+const app = document.getElementById("app");
+render(app, Page);
diff --git a/src/examples/Layout/nestedLayout.tsx b/src/examples/Layout/nestedLayout.tsx
index a716df0..061d1cb 100644
--- a/src/examples/Layout/nestedLayout.tsx
+++ b/src/examples/Layout/nestedLayout.tsx
@@ -82,6 +82,9 @@ export function MyLayout({ Component, pageProps }: any) {
         <li>
           <a href="/component-context">Component Context</a>
         </li>
+        <li>
+          <a href="/jsx-signals">JSX Signals</a>
+        </li>
         <li>
           <a href="/asdfasdfasdfasdf">A Route that does not exist</a>
         </li>
diff --git a/src/lib/rezact/vite-plugin.ts b/src/lib/rezact/vite-plugin.ts
index 92be43f..d9bb72c 100644
--- a/src/lib/rezact/vite-plugin.ts
+++ b/src/lib/rezact/vite-plugin.ts
@@ -16,7 +16,7 @@ let mapStateUsed: any = {};
 let functionsToRun: any = [];
 let mapDeclarationTracking = {};
 
-function wrapInUseSignal(node) {
+function wrapInSignal(node) {
   signalsUsed.Signal = true;
   node.wrappedInSignal = true;
   magicString.appendLeft(node.start, `new Signal(`);
@@ -110,7 +110,7 @@ function findDependencies(startingNode, excludeDeps = {}) {
     );
 }
 
-function wrapInCreateComputed(node, explicitDeps = null, excludeDeps = {}) {
+function wrapInEffect(node, explicitDeps = null, excludeDeps = {}) {
   const _deps = explicitDeps || findDependencies(node, excludeDeps);
   if (_deps.length === 0) return;
   const deps = _deps.map((dep) => dep.dep || dep);
@@ -371,6 +371,27 @@ function wrapInSetValue(node, nestedMember = false) {
   const leftVal = node.left?.name || node.argument?.name;
   let rightVal =
     node.right?.raw || src.slice(node.right?.start || 0, node.right?.end || 0);
+
+  if (
+    node.right?.type === "ConditionalExpression" &&
+    node.right?.test?.type === "BinaryExpression"
+  ) {
+    const backupMagicString = magicString;
+    const backupMagicSrc = src;
+    src = rightVal;
+    const test = acorn.parse(rightVal, {
+      locations: true,
+      ecmaVersion: "latest",
+      sourceType: "module",
+    });
+    magicString = new MagicString(rightVal);
+    compileRezact(test);
+    rightVal = magicString.toString();
+
+    src = backupMagicSrc;
+    magicString = backupMagicString;
+  }
+
   if (node.right?.value && node.right.value[0] === "$")
     rightVal = `${node.right.value}.get()`;
 
@@ -409,10 +430,16 @@ function compileRezact(ast) {
       const name = node.id.name;
       if (!name) return;
       if (name[0] === "$") {
-        if (node.init.type === "Literal") wrapInUseSignal(node.init);
-        if (node.init.type === "UnaryExpression") wrapInUseSignal(node.init);
-        if (node.init.type === "BinaryExpression")
-          wrapInCreateComputed(node.init);
+        if (node.init.type === "Literal") wrapInSignal(node.init);
+        if (node.init.type === "Identifier") wrapInSignal(node.init);
+        if (
+          node.init.type === "CallExpression" &&
+          node.init.callee.name === "xCreateElement"
+        ) {
+          wrapInSignal(node.init);
+        }
+        if (node.init.type === "UnaryExpression") wrapInSignal(node.init);
+        if (node.init.type === "BinaryExpression") wrapInEffect(node.init);
         if (node.init.type === "ArrayExpression")
           wrapInUseMapSignal(node.init, node);
         if (
@@ -426,7 +453,7 @@ function compileRezact(ast) {
             if (mapName) mapDeclarationTracking[mapName] = node;
             wrapInCreateMapped(node.init);
           } else {
-            wrapInCreateComputed(node.init);
+            wrapInEffect(node.init);
           }
         }
       }
@@ -436,7 +463,7 @@ function compileRezact(ast) {
       if (isAttributeArg(ancestors) && !isReactiveAttribute(ancestors.at(-2))) {
         wrapInCreateComputedAttribute(node);
       } else {
-        wrapInCreateComputed(node);
+        wrapInEffect(node);
       }
     },
 
@@ -501,7 +528,7 @@ function compileRezact(ast) {
         return;
 
       const explicitDeps = findDependencies(node.left);
-      wrapInCreateComputed(node, explicitDeps);
+      wrapInEffect(node, explicitDeps);
     },
 
     MemberExpression(node: any, _state, ancestors: any) {
@@ -554,7 +581,7 @@ function compileRezact(ast) {
         ancestors.length > 2 &&
         ancestors.at(-2).callee?.name === "xCreateElement"
       ) {
-        wrapInCreateComputed(node);
+        wrapInEffect(node);
       }
 
       if (
@@ -694,10 +721,10 @@ function compileRezact(ast) {
         !isAttributeArg(ancestors) &&
         node.key.name !== node.value.name
       ) {
-        wrapInUseSignal(node.value);
+        wrapInSignal(node.value);
       }
       if (node.key.value && node.key.value[0] === "$") {
-        wrapInUseSignal(node.value);
+        wrapInSignal(node.value);
       }
       if (node.key.name === "value" && !importsUsed.useInputs)
         importsUsed.useInputs = true && functionsToRun.push("useInputs()");
@@ -717,9 +744,10 @@ function compileRezact(ast) {
         wrapInCreateComputedAttribute(node);
       } else if (
         !hasAncestor(ancestors, "TemplateLiteral") &&
+        !hasAncestor(ancestors, "AssignmentExpression") &&
         functionReturnsJSX(ancestors)
       ) {
-        wrapInCreateComputed(node);
+        wrapInEffect(node);
       }
       if (!node?.test?.name) return;
       if (node.test.name && node.test.name[0] === "$") {
@@ -751,7 +779,7 @@ function compileRezact(ast) {
         ancestors.length > 2 &&
         ancestors.at(-2).callee?.name === "xCreateElement"
       ) {
-        wrapInCreateComputed(node);
+        wrapInEffect(node);
       }
     },
 
@@ -781,10 +809,9 @@ function compileRezact(ast) {
 
     LabeledStatement(node: any) {
       if (node.label.name === "$") {
-        if (node.body.type === "BlockStatement")
-          wrapInCreateComputed(node.body);
+        if (node.body.type === "BlockStatement") wrapInEffect(node.body);
         if (node.body.type === "ExpressionStatement")
-          wrapInCreateComputed(node.body.expression);
+          wrapInEffect(node.body.expression);
       }
     },
   });


Test patch:
diff --git a/src/examples/JsxSignals/jsx-signals.test.ts b/src/examples/JsxSignals/jsx-signals.test.ts
new file mode 100644
index 0000000..1d93089
--- /dev/null
+++ b/src/examples/JsxSignals/jsx-signals.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, it } from "vitest";
+import { screen } from "@testing-library/dom";
+import { render } from "rezact";
+import { Page } from "./JsxSignals";
+import { delay } from "src/lib/utils";
+
+describe("JSX Signals suite", () => {
+  it("renders JSX Signals Page", async () => {
+    render(document.body, Page);
+    await delay(100);
+    const allHeaders = screen.getAllByText(/jsx signals/i);
+    expect(allHeaders).toHaveLength(1);
+  });
+
+  it("switches elements (method 1)", async () => {
+    const clickToggleButton = screen.getByRole("button", {
+      name: /change 1/i,
+    });
+    expect(clickToggleButton).not.toBeNull();
+    expect(screen.getByText("Test Signal Element")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Changed Signal Element")).not.toBeNull();
+  });
+
+  it("switches elements (method 2)", async () => {
+    const clickToggleButton = screen.getByRole("button", {
+      name: /change 2/i,
+    });
+    expect(clickToggleButton).not.toBeNull();
+    expect(screen.getByText("Elm 1")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Elm 2")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Elm 1")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Elm 2")).not.toBeNull();
+  });
+
+  it("switches elements (method 3)", async () => {
+    const clickToggleButton = screen.getByRole("button", {
+      name: /change 3/i,
+    });
+    expect(clickToggleButton).not.toBeNull();
+    expect(screen.getByText("Elm 3")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Elm 4")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Elm 3")).not.toBeNull();
+    await clickToggleButton.click();
+    expect(screen.getByText("Elm 4")).not.toBeNull();
+  });
+});