Skip to content

Commit 28c342f

Browse files
feat: element middlewares, general cleanup
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 5cda6ea commit 28c342f

12 files changed

Lines changed: 313 additions & 94 deletions

File tree

‎biome.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
},
3131
"complexity": {
3232
"noBannedTypes": "off",
33-
"noForEach": "off"
33+
"noForEach": "off",
34+
"useLiteralKeys": "off"
3435
}
3536
}
3637
}

‎examples/simplejsx/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Run this example from the repo root:
55
```bash
66
bun run build
77
bun examples/simplejsx/index.tsx
8+
bun examples/simplejsx/middleware.tsx
89
```

‎examples/simplejsx/middleware.tsx‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/** @jsxImportSource simplejsx */
2+
import { renderAsync, type ElementMiddleware, type PropsWithChildren } from "simplejsx";
3+
4+
function Document({ children }: PropsWithChildren) {
5+
return (
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<title>simplejsx middleware</title>
10+
</head>
11+
<body>{children}</body>
12+
</html>
13+
);
14+
}
15+
16+
const externalLinks: ElementMiddleware = ({ tag, props }) => {
17+
if (tag !== "a" || typeof props["href"] !== "string") return;
18+
if (!props["href"].startsWith("https://")) return;
19+
20+
return { ...props, target: "_blank", rel: "noreferrer" };
21+
};
22+
23+
const imageMetadata: ElementMiddleware = async ({ tag, props }) => {
24+
if (tag !== "img" || typeof props["src"] !== "string") return;
25+
26+
const size = await getImageSize(props["src"]);
27+
return {
28+
...props,
29+
loading: props["loading"] ?? "lazy",
30+
width: size.width,
31+
height: size.height,
32+
};
33+
};
34+
35+
async function getImageSize(_src: string) {
36+
return { width: 1200, height: 800 };
37+
}
38+
39+
const html = await renderAsync(
40+
<Document>
41+
<main>
42+
<h1>Element middleware</h1>
43+
<a href="https://example.com">External link</a>
44+
<img src="/photo.jpg" alt="A mountain lake" />
45+
</main>
46+
</Document>,
47+
{
48+
element: [externalLinks, imageMetadata],
49+
},
50+
);
51+
52+
console.log(`<!doctype html>${html}`);

‎packages/css/package.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44
"description": "",
55
"keywords": [],
66
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/css",
7-
"repository": "https://github.com/explodingcamera/esm",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/explodingcamera/esm.git",
10+
"directory": "packages/css"
11+
},
812
"license": "MIT",
913
"author": "Henry Gressmann <npm@henrygressmann.de>",
1014
"type": "module",
15+
"sideEffects": [
16+
"*.css",
17+
"*.js"
18+
],
1119
"exports": {
1220
"./reset": "./reset.js",
1321
"./reset.css": "./reset.css",

‎packages/minify-literals/package.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111
"template-literals"
1212
],
1313
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/minify-literals",
14-
"repository": "https://github.com/explodingcamera/esm",
14+
"repository": {
15+
"type": "git",
16+
"url": "git+https://github.com/explodingcamera/esm.git",
17+
"directory": "packages/minify-literals"
18+
},
1519
"license": "MIT",
1620
"author": "Henry Gressmann <npm@henrygressmann.de>",
1721
"type": "module",
22+
"sideEffects": false,
1823
"exports": {
1924
".": {
2025
"types": "./dist/index.d.mts",
@@ -28,6 +33,9 @@
2833
"files": [
2934
"dist"
3035
],
36+
"scripts": {
37+
"prepublishOnly": "bun run --cwd ../.. build"
38+
},
3139
"tsdown": {
3240
"entry": "lib/index.ts"
3341
},

‎packages/rollup-plugin-minify-template-literals/package.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616
"template-literals"
1717
],
1818
"homepage": "https://github.com/explodingcamera/esm/tree/main/packages/rollup-plugin-minify-template-literals",
19-
"repository": "https://github.com/explodingcamera/esm",
19+
"repository": {
20+
"type": "git",
21+
"url": "git+https://github.com/explodingcamera/esm.git",
22+
"directory": "packages/rollup-plugin-minify-template-literals"
23+
},
2024
"license": "MIT",
2125
"author": "Henry Gressmann <npm@henrygressmann.de>",
2226
"type": "module",
27+
"sideEffects": false,
2328
"exports": {
2429
".": {
2530
"types": "./dist/index.d.mts",
@@ -33,6 +38,9 @@
3338
"files": [
3439
"dist"
3540
],
41+
"scripts": {
42+
"prepublishOnly": "bun run --cwd ../.. build"
43+
},
3644
"tsdown": {
3745
"entry": "lib/index.ts",
3846
"deps": {

‎packages/simplejsx/README.md‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# simplejsx
22

3-
A minimal JSX templating library for rendering safe HTML strings.
3+
Minimal JSX templating for safe HTML strings.
44

5-
`simplejsx` is a small JSX-to-HTML renderer for server-side templates, static site generation, emails,
6-
and other places where you just want to emit HTML. It is not a React replacement: there are no hooks,
7-
DOM rendering, hydration, Suspense, or React compatibility APIs.
5+
`simplejsx` renders JSX to HTML strings. It works well for server-side templates, static sites, emails,
6+
and other places where a string is the whole output. This is not a React replacement and does not handle
7+
browser interactivity.
88

99
Full API docs: [jsdocs.io/package/simplejsx](https://www.jsdocs.io/package/simplejsx)
1010
Examples: [`examples/simplejsx`](../../examples/simplejsx)
@@ -17,7 +17,7 @@ npm install simplejsx
1717

1818
## Setup
1919

20-
Use the automatic JSX runtime with `jsxImportSource` to use `simplejsx` as the JSX factory. In `tsconfig.json`:
20+
Use the automatic JSX runtime with `jsxImportSource`. In `tsconfig.json`:
2121

2222
```json
2323
{
@@ -54,11 +54,14 @@ const html = render(
5454
);
5555
```
5656

57-
Text and attributes are escaped by default. Use `renderAsync()` for async components, promise children,
58-
or promise attributes. Nested `<head>` elements are merged into the top-level `<head>`, so page
59-
components can set titles and metadata from inside a shared layout.
57+
## Behavior
58+
59+
- Text and attributes are escaped by default. Use `unsafeHTML()` only for trusted markup.
60+
- `style` supports strings and objects, e.g. `style="color:red"` or `style={{ marginTop: 8 }}`.
61+
- `renderAsync()` awaits async components, promise children, and promise attributes.
62+
- Nested `<head>` elements are merged into the top-level `<head>`.
6063

6164
## See Also
6265

63-
- [Hono JSX](https://hono.dev/docs/guides/jsx) - JSX rendering for Hono apps, with a much broader runtime + dom API.
64-
- [Preact](https://preactjs.com/) - React-compatible UI components with DOM rendering and hydration.
66+
- [Hono JSX](https://hono.dev/docs/guides/jsx) for Hono apps. It has a much broader runtime and DOM API.
67+
- [Preact](https://preactjs.com/) for React-compatible UI components, DOM rendering, and hydration.

‎packages/simplejsx/lib/index.test.tsx‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ test("attributes", () => {
6666
expect(render(<input disabled checked={false} value="name" />)).toBe('<input disabled value="name">');
6767
});
6868

69+
test("element middleware", () => {
70+
expect(
71+
render(<a href="/docs">Docs</a>, {
72+
element: [
73+
({ tag, props }) => (tag === "a" ? { ...props, target: "_blank" } : undefined),
74+
({ tag, props }) => {
75+
if (tag === "a") props["rel"] = "noreferrer";
76+
},
77+
],
78+
}),
79+
).toBe('<a href="/docs" target="_blank" rel="noreferrer">Docs</a>');
80+
expect(
81+
render(<p>Hi</p>, {
82+
element: [[({ props }) => ({ ...props, class: "message" })]],
83+
}),
84+
).toBe('<p class="message">Hi</p>');
85+
});
86+
6987
test("style", () => {
7088
expect(
7189
render(
@@ -79,6 +97,18 @@ test("style", () => {
7997
/>,
8098
),
8199
).toBe('<div style="margin-top:10px;opacity:0.5;--gap:2;background-color:red;"></div>');
100+
expect(render(<div style={'color: red; --label: "a&b";'} />)).toBe(
101+
'<div style="color: red; --label: &quot;a&amp;b&quot;;"></div>',
102+
);
103+
});
104+
105+
test("children arrays and null", () => {
106+
function Box({ children }: PropsWithChildren) {
107+
return <div>{children}</div>;
108+
}
109+
110+
expect(render(<Box>{[<span>one</span>, null, "two"]}</Box>)).toBe("<div><span>one</span>two</div>");
111+
expect(render(jsx("div", { children: null }))).toBe("<div></div>");
82112
});
83113

84114
test("head hoisting", () => {
@@ -138,6 +168,26 @@ test("async", async () => {
138168
</div>,
139169
),
140170
).resolves.toBe('<div title="a&amp;b"><strong>Async</strong>x&lt;y</div>');
171+
await expect(
172+
renderAsync(<div title={Promise.resolve(null)} data-id={Promise.resolve(undefined)} />),
173+
).resolves.toBe("<div></div>");
174+
await expect(
175+
renderAsync(jsx("img", { src: "/image.png" }), {
176+
element: async ({ tag, props }) =>
177+
tag === "img" ? { ...props, alt: Promise.resolve("A&B") } : undefined,
178+
}),
179+
).resolves.toBe('<img src="/image.png" alt="A&amp;B">');
180+
await expect(
181+
renderAsync(<img src="/photo.jpg" alt="Mountain lake" />, {
182+
element: async ({ tag, props }) => {
183+
if (tag !== "img") return;
184+
185+
await Promise.resolve();
186+
props["width"] = 1200;
187+
props["height"] = 800;
188+
},
189+
}),
190+
).resolves.toBe('<img src="/photo.jpg" alt="Mountain lake" width="1200" height="800">');
141191
});
142192

143193
test("async head", async () => {
@@ -166,6 +216,7 @@ test("sync rejects async", () => {
166216

167217
expect(() => render(<Message />)).toThrow("renderAsync");
168218
expect(() => render(<div>{Promise.resolve("Async")}</div>)).toThrow("renderAsync");
219+
expect(() => render(<div />, { element: async () => {} })).toThrow("renderAsync");
169220
});
170221

171222
test("function attrs", () => {

0 commit comments

Comments
 (0)