Skip to content

Commit 06f6d5d

Browse files
authored
Merge pull request #11 from andersk/compatibility
Improve compatibility with other JSX engines
2 parents 878f780 + 0da1e55 commit 06f6d5d

File tree

3 files changed

+78
-31
lines changed

3 files changed

+78
-31
lines changed

html.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ export type HTMLChild =
44
| string
55
| number
66
| boolean
7+
| undefined
8+
| null
79
| hast.Element
810
| hast.Root
911
| hast.Text;
1012

11-
export type HTMLChildren = HTMLChild | Iterable<HTMLChild>;
13+
export type HTMLChildren = HTMLChild | Iterable<HTMLChildren>;
1214

1315
export interface HTMLVoidElement extends HTMLElement {
1416
children?: never;

jsx-runtime.ts

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import type * as svg from "./svg.ts";
44

55
export type Element = hast.Element;
66

7-
export type JSXChild = string | number | boolean | JSXElement;
7+
export type JSXChild =
8+
| string
9+
| number
10+
| boolean
11+
| undefined
12+
| null
13+
| JSXElement;
814

9-
export type JSXChildren = JSXChild | JSXChild[];
15+
export type JSXChildren = JSXChild | JSXChildren[];
1016

1117
export type JSXElement = hast.Element | hast.Root | hast.Text;
1218

@@ -18,12 +24,12 @@ export type JSXComponentProps = Record<string, unknown> & {
1824
};
1925

2026
export interface JSXComponent {
21-
(props: JSXComponentProps): JSXElement;
27+
(props: JSXComponentProps): JSXChildren;
2228
}
2329

2430
export interface JSXElementConstructor {
2531
//deno-lint-ignore no-explicit-any
26-
(...args: any[]): JSXElement;
32+
(...args: any[]): JSXChildren;
2733
}
2834

2935
declare global {
@@ -61,43 +67,47 @@ export function jsx(
6167
type: "element",
6268
tagName,
6369
properties: { ...properties, ...className },
64-
children: read(children),
70+
children: read(children).filter((child) => child.type !== "doctype"),
6571
};
6672
} else {
67-
return type({ ...props, ...(key ? { key } : {}) });
73+
return {
74+
type: "root",
75+
children: read(type({ ...props, ...(key ? { key } : {}) })),
76+
};
6877
}
6978
}
7079

7180
export const jsxs = jsx;
7281
export const jsxDEV = jsx;
7382

74-
export function Fragment(
75-
props: { children?: JSXChild | JSXChild[] },
76-
): hast.Root {
83+
export function Fragment(props: { children?: JSXChildren }): hast.Root {
7784
let { children = [] } = props;
7885
return {
7986
type: "root",
8087
children: read(children),
8188
};
8289
}
8390

84-
function read(children?: JSXChild | JSXChild[]): (hast.Element | hast.Text)[] {
85-
let nodes = Array.isArray(children) ? children : (children ? [children] : []);
86-
return nodes.flatMap((child) => {
87-
switch (typeof child) {
88-
case "number":
89-
case "boolean":
90-
case "string":
91-
return [{
92-
type: "text",
93-
value: String(child),
94-
}];
95-
default:
96-
if (child.type === "root") {
97-
return child.children as Array<hast.Element | hast.Text>;
98-
} else {
99-
return [child];
100-
}
101-
}
102-
});
91+
function read(children?: JSXChildren): hast.RootContent[] {
92+
switch (typeof children) {
93+
case "undefined":
94+
case "boolean":
95+
return [];
96+
case "number":
97+
case "string":
98+
return [{
99+
type: "text",
100+
value: String(children),
101+
}];
102+
default:
103+
if (children === null) {
104+
return [];
105+
} else if (Array.isArray(children)) {
106+
return children.flatMap(read);
107+
} else if (children.type === "root") {
108+
return children.children;
109+
} else {
110+
return [children];
111+
}
112+
}
103113
}

test/jsx.test.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it } from "jsr:@std/testing@^1/bdd";
22
import { expect } from "jsr:@std/expect@^1";
33
import { h } from "npm:hastscript@9.0.0";
4+
import type { JSXChild, JSXChildren } from "../jsx-runtime.ts";
45

56
describe("JSX runtime", () => {
67
it("generates simple tags", () => {
@@ -60,9 +61,24 @@ describe("JSX runtime", () => {
6061
);
6162
});
6263

63-
it("can embed boolean expressions inside", () => {
64+
it("ignores boolean expressions inside", () => {
6465
expect(<title>{false} witness</title>).toEqual(
65-
h("title", "false", " witness"),
66+
h("title", " witness"),
67+
);
68+
expect(<title>{true} love</title>).toEqual(
69+
h("title", " love"),
70+
);
71+
});
72+
73+
it("ignores undefined", () => {
74+
expect(<title>{undefined} behavior</title>).toEqual(
75+
h("title", " behavior"),
76+
);
77+
});
78+
79+
it("ignores null", () => {
80+
expect(<title>{null} hypothesis</title>).toEqual(
81+
h("title", " hypothesis"),
6682
);
6783
});
6884

@@ -89,6 +105,12 @@ describe("JSX runtime", () => {
89105
expect(<ul>{...[1, 2, 3].map((i) => <li>{i}</li>)}</ul>).toEqual(
90106
h("ul", h("li", "1"), h("li", "2"), h("li", "3")),
91107
);
108+
expect(
109+
<ul>
110+
<li>0</li>
111+
{...[1, 2, 3].map((i) => <li>{i}</li>)}
112+
</ul>,
113+
).toEqual(h("ul", h("li", "0"), h("li", "1"), h("li", "2"), h("li", "3")));
92114
});
93115

94116
it("passes the key attribute", () => {
@@ -114,4 +136,17 @@ describe("JSX runtime", () => {
114136
//@ts-expect-error yo.
115137
expect(el.properties.className).toEqual("button");
116138
});
139+
140+
it("strips <!DOCTYPE> from element children", () => {
141+
const root: JSXChild = { type: "root", children: [{ type: "doctype" }] };
142+
expect(<>{root}</>).toEqual(root);
143+
expect(<div>{root}</div>).toEqual(h("div"));
144+
});
145+
146+
it("allows components returning primitives or arrays", () => {
147+
const Id = ({ children }: { children?: JSXChildren }) => children;
148+
expect(<Id />).toEqual(<></>);
149+
expect(<Id>{1}</Id>).toEqual(<>{1}</>);
150+
expect(<Id>s {1}</Id>).toEqual(<>s {1}</>);
151+
});
117152
});

0 commit comments

Comments
 (0)