Skip to content

Commit b587a4a

Browse files
authored
feat: Migrate @commontools/html into a jsx-runtime implementation. (#1958)
* @commontools/html is now a valid jsx-runtime implementation, like preact * @commontools/html is set as the Deno workspace jsx runtime * The pattern runtime still uses a global `h` render function that can be aligned in the future. * Removed react/react-dom/@types providing the React jsx runtime * Hack to support serving `iframe-bootstrap.js`, which has runtime deps not in our workspace, by renaming the file extension to work around this upstream issue: denoland/deno#27505 * Removes @babel/standalone, and enables the full removal of React deps
1 parent 79648aa commit b587a4a

File tree

11 files changed

+673
-226
lines changed

11 files changed

+673
-226
lines changed

deno.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@
3737
"initialize-db": "./tasks/initialize-db.sh"
3838
},
3939
"compilerOptions": {
40+
"jsx": "react-jsx",
41+
"jsxImportSource": "@commontools/html",
4042
"types": [
4143
"./packages/static/assets/types/jsx.d.ts"
4244
],
43-
"jsx": "react-jsxdev",
4445
"lib": [
4546
"deno.ns",
4647
"dom",
@@ -96,10 +97,6 @@
9697
]
9798
},
9899
"imports": {
99-
"react": "npm:react@^18.3.1",
100-
"react-dom": "npm:react-dom@^18.3.1",
101-
"@types/react": "npm:@types/react@^18.3.1",
102-
"@babel/standalone": "npm:@babel/standalone@^7.28.2",
103100
"commontools": "./packages/api/index.ts",
104101
"core-js/proposals/explicit-resource-management": "https://esm.sh/core-js/proposals/explicit-resource-management",
105102
"@astral/astral": "./packages/vendor-astral/mod.ts",

deno.lock

Lines changed: 159 additions & 212 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/html/deno.jsonc

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@
55
},
66
"exports": {
77
".": "./src/index.ts",
8-
"./utils": "./src/utils.ts"
8+
"./utils": "./src/utils.ts",
9+
"./jsx-runtime": "./src/jsx-runtime.ts",
10+
"./jsx-dev-runtime": "./src/jsx-dev-runtime.ts"
911
},
1012
"imports": {
1113
"htmlparser2": "npm:htmlparser2",
1214
"domhandler": "npm:domhandler",
1315
"dom-serializer": "npm:dom-serializer"
14-
},
15-
16-
"compilerOptions": {
17-
"jsx": "react",
18-
"jsxFactory": "h",
19-
"jsxFragmentFactory": "h.fragment"
2016
}
2117
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* JSX development runtime for @commontools/html
3+
*
4+
* This module provides the JSX development runtime implementation compatible with
5+
* TypeScript's "jsx": "react-jsxdev" configuration.
6+
*
7+
* The development runtime includes additional debugging information like source
8+
* file paths and line numbers, though our current implementation doesn't use these yet.
9+
*
10+
* @module jsx-dev-runtime
11+
*/
12+
13+
import { h } from "@commontools/api";
14+
import type { RenderNode, VNode } from "@commontools/api";
15+
16+
/**
17+
* Props type for JSX elements in development mode, including children and debug info
18+
*/
19+
export interface JSXDevProps {
20+
children?: RenderNode | RenderNode[];
21+
key?: string | number;
22+
[prop: string]: any;
23+
}
24+
25+
/**
26+
* Source location information for debugging
27+
*/
28+
export interface Source {
29+
fileName: string;
30+
lineNumber: number;
31+
columnNumber: number;
32+
}
33+
34+
/**
35+
* Creates a VNode for a JSX element with development-time debugging information.
36+
*
37+
* This function is used by the JSX automatic runtime in development mode.
38+
* It accepts additional parameters for debugging (__source, __self) which can be
39+
* used to provide better error messages and developer experience.
40+
*
41+
* @param type - The element type (string for HTML/SVG, function for components)
42+
* @param props - Element properties including children
43+
* @param key - Optional key for list reconciliation
44+
* @param isStaticChildren - Whether children are static (unused in our implementation)
45+
* @param __source - Source location information for debugging
46+
* @param __self - Reference to the component instance (unused in our implementation)
47+
* @returns A virtual DOM node
48+
*/
49+
export function jsxDEV(
50+
type: string | ((props: any) => VNode),
51+
props: JSXDevProps | null,
52+
_key?: string | number,
53+
_isStaticChildren?: boolean,
54+
__source?: Source,
55+
__self?: any,
56+
): VNode {
57+
const { children, ...restProps } = props ?? {};
58+
59+
// Convert children to array format expected by h()
60+
const childArray = children === undefined
61+
? []
62+
: Array.isArray(children)
63+
? children
64+
: [children];
65+
66+
// In the future, we could use __source to provide better error messages
67+
// or enhance debugging capabilities. For now, we just create the VNode.
68+
return h(type, restProps, ...childArray);
69+
}
70+
71+
/**
72+
* Fragment component for grouping elements without adding DOM nodes.
73+
*
74+
* Used when you write <></> or <React.Fragment> in JSX.
75+
* Renders as a "common-fragment" element in the virtual DOM.
76+
*/
77+
export const Fragment = h.fragment;
78+
79+
// Type exports
80+
export type { RenderNode, VNode };

packages/html/src/jsx-runtime.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* JSX automatic runtime for @commontools/html
3+
*
4+
* This module provides the JSX runtime implementation compatible with
5+
* TypeScript's "jsx": "react-jsx" configuration.
6+
*
7+
* @module jsx-runtime
8+
*/
9+
10+
import { h } from "@commontools/api";
11+
import type { RenderNode, VNode } from "@commontools/api";
12+
13+
/**
14+
* Props type for JSX elements, including children
15+
*/
16+
export interface JSXProps {
17+
children?: RenderNode | RenderNode[];
18+
key?: string | number;
19+
[prop: string]: any;
20+
}
21+
22+
/**
23+
* Creates a VNode for a JSX element.
24+
*
25+
* This is the core function used by the JSX automatic runtime for creating elements.
26+
* It handles both HTML/SVG elements (string types) and component functions.
27+
*
28+
* @param type - The element type (string for HTML/SVG, function for components)
29+
* @param props - Element properties including children
30+
* @param key - Optional key for list reconciliation (currently unused but part of JSX spec)
31+
* @returns A virtual DOM node
32+
*/
33+
export function jsx(
34+
type: string | ((props: any) => VNode),
35+
props: JSXProps | null,
36+
_key?: string | number,
37+
): VNode {
38+
const { children, ...restProps } = props ?? {};
39+
40+
// Convert children to array format expected by h()
41+
const childArray = children === undefined
42+
? []
43+
: Array.isArray(children)
44+
? children
45+
: [children];
46+
47+
return h(type, restProps, ...childArray);
48+
}
49+
50+
/**
51+
* Creates a VNode for a JSX element with static children.
52+
*
53+
* The TypeScript compiler uses this when it can determine that children are static.
54+
* For our implementation, it's identical to jsx() since we don't optimize for static children.
55+
*
56+
* @param type - The element type (string for HTML/SVG, function for components)
57+
* @param props - Element properties including children
58+
* @param key - Optional key for list reconciliation
59+
* @returns A virtual DOM node
60+
*/
61+
export const jsxs = jsx;
62+
63+
/**
64+
* Fragment component for grouping elements without adding DOM nodes.
65+
*
66+
* Used when you write <></> or <React.Fragment> in JSX.
67+
* Renders as a "common-fragment" element in the virtual DOM.
68+
*/
69+
export const Fragment = h.fragment;
70+
71+
// Type exports
72+
export type { RenderNode, VNode };
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Tests for the JSX development runtime
3+
*
4+
* These tests verify that @commontools/html provides a development runtime
5+
* compatible with TypeScript's "jsx": "react-jsxdev" configuration.
6+
*/
7+
8+
import { describe, it } from "@std/testing/bdd";
9+
import * as assert from "./assert.ts";
10+
11+
import { Fragment, jsxDEV } from "../src/jsx-dev-runtime.ts";
12+
13+
describe("JSX development runtime", () => {
14+
it("jsxDEV() creates a simple element", () => {
15+
const element = jsxDEV("div", { className: "test" });
16+
17+
assert.matchObject(element, {
18+
type: "vnode",
19+
name: "div",
20+
props: { className: "test" },
21+
children: [],
22+
});
23+
});
24+
25+
it("jsxDEV() creates an element with children", () => {
26+
const element = jsxDEV("div", {
27+
children: [jsxDEV("p", { children: "Hello" })],
28+
});
29+
30+
assert.matchObject(element, {
31+
type: "vnode",
32+
name: "div",
33+
children: [
34+
{
35+
type: "vnode",
36+
name: "p",
37+
children: ["Hello"],
38+
},
39+
],
40+
});
41+
});
42+
43+
it("jsxDEV() accepts debug parameters", () => {
44+
const element = jsxDEV(
45+
"div",
46+
{ children: "Test" },
47+
"test-key",
48+
false,
49+
{
50+
fileName: "test.tsx",
51+
lineNumber: 42,
52+
columnNumber: 10,
53+
},
54+
undefined,
55+
);
56+
57+
assert.matchObject(element, {
58+
type: "vnode",
59+
name: "div",
60+
children: ["Test"],
61+
});
62+
});
63+
64+
it("jsxDEV() handles null props", () => {
65+
const element = jsxDEV("div", null);
66+
67+
assert.matchObject(element, {
68+
type: "vnode",
69+
name: "div",
70+
props: {},
71+
children: [],
72+
});
73+
});
74+
75+
it("jsxDEV() handles component functions", () => {
76+
const MyComponent = ({ name }: { name: string }) =>
77+
jsxDEV("div", { children: `Hello, ${name}` });
78+
79+
const element = jsxDEV(MyComponent, { name: "World" });
80+
81+
assert.matchObject(element, {
82+
type: "vnode",
83+
name: "div",
84+
children: ["Hello, World"],
85+
});
86+
});
87+
88+
it("Fragment creates a common-fragment element", () => {
89+
const fragment = Fragment({
90+
children: [
91+
jsxDEV("p", { children: "Paragraph 1" }),
92+
jsxDEV("p", { children: "Paragraph 2" }),
93+
],
94+
});
95+
96+
assert.matchObject(fragment, {
97+
type: "vnode",
98+
name: "common-fragment",
99+
children: [
100+
{
101+
type: "vnode",
102+
name: "p",
103+
children: ["Paragraph 1"],
104+
},
105+
{
106+
type: "vnode",
107+
name: "p",
108+
children: ["Paragraph 2"],
109+
},
110+
],
111+
});
112+
});
113+
114+
it("jsxDEV() with static children flag", () => {
115+
const element = jsxDEV(
116+
"ul",
117+
{
118+
children: [
119+
jsxDEV("li", { children: "Item 1" }),
120+
jsxDEV("li", { children: "Item 2" }),
121+
],
122+
},
123+
undefined,
124+
true, // isStaticChildren
125+
);
126+
127+
assert.matchObject(element, {
128+
type: "vnode",
129+
name: "ul",
130+
children: [
131+
{
132+
type: "vnode",
133+
name: "li",
134+
children: ["Item 1"],
135+
},
136+
{
137+
type: "vnode",
138+
name: "li",
139+
children: ["Item 2"],
140+
},
141+
],
142+
});
143+
});
144+
});

0 commit comments

Comments
 (0)