Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@
"initialize-db": "./tasks/initialize-db.sh"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@commontools/html",
"types": [
"./packages/static/assets/types/jsx.d.ts"
],
"jsx": "react-jsxdev",
"lib": [
"deno.ns",
"dom",
Expand Down Expand Up @@ -96,10 +97,6 @@
]
},
"imports": {
"react": "npm:react@^18.3.1",
"react-dom": "npm:react-dom@^18.3.1",
"@types/react": "npm:@types/react@^18.3.1",
"@babel/standalone": "npm:@babel/standalone@^7.28.2",
"commontools": "./packages/api/index.ts",
"core-js/proposals/explicit-resource-management": "https://esm.sh/core-js/proposals/explicit-resource-management",
"@astral/astral": "./packages/vendor-astral/mod.ts",
Expand Down
371 changes: 159 additions & 212 deletions deno.lock

Large diffs are not rendered by default.

10 changes: 3 additions & 7 deletions packages/html/deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,13 @@
},
"exports": {
".": "./src/index.ts",
"./utils": "./src/utils.ts"
"./utils": "./src/utils.ts",
"./jsx-runtime": "./src/jsx-runtime.ts",
"./jsx-dev-runtime": "./src/jsx-dev-runtime.ts"
},
"imports": {
"htmlparser2": "npm:htmlparser2",
"domhandler": "npm:domhandler",
"dom-serializer": "npm:dom-serializer"
},

"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "h.fragment"
}
}
80 changes: 80 additions & 0 deletions packages/html/src/jsx-dev-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* JSX development runtime for @commontools/html
*
* This module provides the JSX development runtime implementation compatible with
* TypeScript's "jsx": "react-jsxdev" configuration.
*
* The development runtime includes additional debugging information like source
* file paths and line numbers, though our current implementation doesn't use these yet.
*
* @module jsx-dev-runtime
*/

import { h } from "@commontools/api";
import type { RenderNode, VNode } from "@commontools/api";

/**
* Props type for JSX elements in development mode, including children and debug info
*/
export interface JSXDevProps {
children?: RenderNode | RenderNode[];
key?: string | number;
[prop: string]: any;
}

/**
* Source location information for debugging
*/
export interface Source {
fileName: string;
lineNumber: number;
columnNumber: number;
}

/**
* Creates a VNode for a JSX element with development-time debugging information.
*
* This function is used by the JSX automatic runtime in development mode.
* It accepts additional parameters for debugging (__source, __self) which can be
* used to provide better error messages and developer experience.
*
* @param type - The element type (string for HTML/SVG, function for components)
* @param props - Element properties including children
* @param key - Optional key for list reconciliation
* @param isStaticChildren - Whether children are static (unused in our implementation)
* @param __source - Source location information for debugging
* @param __self - Reference to the component instance (unused in our implementation)
* @returns A virtual DOM node
*/
export function jsxDEV(
type: string | ((props: any) => VNode),
props: JSXDevProps | null,
_key?: string | number,
_isStaticChildren?: boolean,
__source?: Source,
__self?: any,
): VNode {
const { children, ...restProps } = props ?? {};

// Convert children to array format expected by h()
const childArray = children === undefined
? []
: Array.isArray(children)
? children
: [children];

// In the future, we could use __source to provide better error messages
// or enhance debugging capabilities. For now, we just create the VNode.
return h(type, restProps, ...childArray);
}

/**
* Fragment component for grouping elements without adding DOM nodes.
*
* Used when you write <></> or <React.Fragment> in JSX.
* Renders as a "common-fragment" element in the virtual DOM.
*/
export const Fragment = h.fragment;

// Type exports
export type { RenderNode, VNode };
72 changes: 72 additions & 0 deletions packages/html/src/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* JSX automatic runtime for @commontools/html
*
* This module provides the JSX runtime implementation compatible with
* TypeScript's "jsx": "react-jsx" configuration.
*
* @module jsx-runtime
*/

import { h } from "@commontools/api";
import type { RenderNode, VNode } from "@commontools/api";

/**
* Props type for JSX elements, including children
*/
export interface JSXProps {
children?: RenderNode | RenderNode[];
key?: string | number;
[prop: string]: any;
}

/**
* Creates a VNode for a JSX element.
*
* This is the core function used by the JSX automatic runtime for creating elements.
* It handles both HTML/SVG elements (string types) and component functions.
*
* @param type - The element type (string for HTML/SVG, function for components)
* @param props - Element properties including children
* @param key - Optional key for list reconciliation (currently unused but part of JSX spec)
* @returns A virtual DOM node
*/
export function jsx(
type: string | ((props: any) => VNode),
props: JSXProps | null,
_key?: string | number,
): VNode {
const { children, ...restProps } = props ?? {};

// Convert children to array format expected by h()
const childArray = children === undefined
? []
: Array.isArray(children)
? children
: [children];

return h(type, restProps, ...childArray);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSX runtime ignores the _key argument, so keyed JSX elements lose their keys when compiled with jsx: "react-jsx". Without the key, list reconciliation and any component logic that depends on it will misbehave. Please merge _key back into the props before calling h.

Prompt for AI agents
Address the following comment on packages/html/src/jsx-runtime.ts at line 47:

<comment>The JSX runtime ignores the `_key` argument, so keyed JSX elements lose their keys when compiled with `jsx: &quot;react-jsx&quot;`. Without the key, list reconciliation and any component logic that depends on it will misbehave. Please merge `_key` back into the props before calling `h`.</comment>

<file context>
@@ -0,0 +1,72 @@
+    ? children
+    : [children];
+
+  return h(type, restProps, ...childArray);
+}
+
</file context>
Fix with Cubic

}

/**
* Creates a VNode for a JSX element with static children.
*
* The TypeScript compiler uses this when it can determine that children are static.
* For our implementation, it's identical to jsx() since we don't optimize for static children.
*
* @param type - The element type (string for HTML/SVG, function for components)
* @param props - Element properties including children
* @param key - Optional key for list reconciliation
* @returns A virtual DOM node
*/
export const jsxs = jsx;

/**
* Fragment component for grouping elements without adding DOM nodes.
*
* Used when you write <></> or <React.Fragment> in JSX.
* Renders as a "common-fragment" element in the virtual DOM.
*/
export const Fragment = h.fragment;

// Type exports
export type { RenderNode, VNode };
144 changes: 144 additions & 0 deletions packages/html/test/jsx-dev-runtime.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Tests for the JSX development runtime
*
* These tests verify that @commontools/html provides a development runtime
* compatible with TypeScript's "jsx": "react-jsxdev" configuration.
*/

import { describe, it } from "@std/testing/bdd";
import * as assert from "./assert.ts";

import { Fragment, jsxDEV } from "../src/jsx-dev-runtime.ts";

describe("JSX development runtime", () => {
it("jsxDEV() creates a simple element", () => {
const element = jsxDEV("div", { className: "test" });

assert.matchObject(element, {
type: "vnode",
name: "div",
props: { className: "test" },
children: [],
});
});

it("jsxDEV() creates an element with children", () => {
const element = jsxDEV("div", {
children: [jsxDEV("p", { children: "Hello" })],
});

assert.matchObject(element, {
type: "vnode",
name: "div",
children: [
{
type: "vnode",
name: "p",
children: ["Hello"],
},
],
});
});

it("jsxDEV() accepts debug parameters", () => {
const element = jsxDEV(
"div",
{ children: "Test" },
"test-key",
false,
{
fileName: "test.tsx",
lineNumber: 42,
columnNumber: 10,
},
undefined,
);

assert.matchObject(element, {
type: "vnode",
name: "div",
children: ["Test"],
});
});

it("jsxDEV() handles null props", () => {
const element = jsxDEV("div", null);

assert.matchObject(element, {
type: "vnode",
name: "div",
props: {},
children: [],
});
});

it("jsxDEV() handles component functions", () => {
const MyComponent = ({ name }: { name: string }) =>
jsxDEV("div", { children: `Hello, ${name}` });

const element = jsxDEV(MyComponent, { name: "World" });

assert.matchObject(element, {
type: "vnode",
name: "div",
children: ["Hello, World"],
});
});

it("Fragment creates a common-fragment element", () => {
const fragment = Fragment({
children: [
jsxDEV("p", { children: "Paragraph 1" }),
jsxDEV("p", { children: "Paragraph 2" }),
],
});

assert.matchObject(fragment, {
type: "vnode",
name: "common-fragment",
children: [
{
type: "vnode",
name: "p",
children: ["Paragraph 1"],
},
{
type: "vnode",
name: "p",
children: ["Paragraph 2"],
},
],
});
});

it("jsxDEV() with static children flag", () => {
const element = jsxDEV(
"ul",
{
children: [
jsxDEV("li", { children: "Item 1" }),
jsxDEV("li", { children: "Item 2" }),
],
},
undefined,
true, // isStaticChildren
);

assert.matchObject(element, {
type: "vnode",
name: "ul",
children: [
{
type: "vnode",
name: "li",
children: ["Item 1"],
},
{
type: "vnode",
name: "li",
children: ["Item 2"],
},
],
});
});
});
Loading