PoC: feat(jsx): Introduce event handler for IntrinsicElements.
What about a feature like this, which would greatly expand the possibilities of JSX?
What can we do?
The following example shows the writing of a style element using MasterCSS(@2.0.0-beta) and the embedding of the transpiled TypeScript with esbuild.
With the following script,
import { JSXNode } from "../hono/src/jsx";
import { readFile, mkdtemp } from "fs/promises";
import { MasterCSS } from "@master/css";
import * as esbuild from "esbuild";
let resolveMasterCSS: () => void = () => {};
const resolveMasterCSSPromise = new Promise<void>(
(resolve) => (resolveMasterCSS = resolve)
);
const masterCSS = new MasterCSS();
const filename = process.argv[2];
const template: JSXNode = (await import(`./${filename}`)).default;
template
.on("renderToString", ({ node }) => {
// gather all the class names
(node.props["class"]?.toString() || "")
.split(/\s+/)
.forEach((c: string) => masterCSS.add(c));
})
.on("renderToString.style", ({ setContent }) => {
setContent(
resolveMasterCSSPromise.then(
() => `<style type="text/css">${masterCSS.text}</style>`
)
);
})
.on("renderToString.script", ({ node, setContent }) => {
setContent(
new Promise(async (resolve) => {
const dir = await mkdtemp("/tmp/mt-");
const outfile = `${dir}/out.js`;
await esbuild.build({
entryPoints: [node.props.src as string],
tsconfig: "tsconfig-for-frontend",
bundle: true,
minify: true,
outfile,
});
const content = await readFile(outfile, "utf8");
if (content.match(/<\/script>/)) {
throw new Error("Invalid script");
}
resolve(`<script>${content}</script>`);
})
);
})
.on("afterRenderToString.html", resolveMasterCSS);
console.log((await template.toString()).toString());
The following results can be obtained
Will performance be degraded?
Performance was not degraded in the existing use cases that did not use .on().
% npm run bench:node
> [email protected] bench:node
> esbuild --bundle src/benchmark.ts | node
Hono x 414,607 ops/sec ±2.37% (97 runs sampled)
React x 57,350 ops/sec ±0.48% (99 runs sampled)
Preact x 266,016 ops/sec ±0.28% (98 runs sampled)
Nano x 61,028 ops/sec ±0.32% (101 runs sampled)
Fastest is Hono
When to usejsxNode()?
(<App />) returns a JSX.Element, but JSX.Element is defined as HtmlEscapedString | Promise<HtmlEscapedString>, which can be converted to a JSXNode using as unknown as JSXNode or template instanceof JSXNode, which is a bit annoying, so we use it to convert this to a JSXNode type.
Author should do the followings, if applicable
- [x] Add tests
- [x] Run tests
- [x]
yarn denoifyto generate files for Deno
The general usage is as follows
const app = new Hono()
app.get(
'/page/*',
jsxRenderer(({ children }) => {
let finalize = () => {}
const finalizePromise = new Promise<void>((resolve) => (finalize = resolve))
return jsxNode(
<html>
<head>
<style />
</head>
<body>
<header>Menu</header>
<div>{children}</div>
</body>
</html>
)
.on('renderToString', ({ node }) => {
// gather all class name data
})
.on('renderToString.style', ({ setContent }) => {
setContent(finalizePromise.then(() => 'generated css'))
})
.on('afterRenderToString.html', finalize)
})
)
app.get('/page/about', (c) => {
return c.render(<h1 class='fs12'>About me!</h1>)
})
More
It would be great if the following statements could be eliminated
let finalize = () => {}
const finalizePromise = new Promise<void>((resolve) => (finalize = resolve))
Hi @usualoma,
This is really interesting! But, I can't imagine use cases for me right now though there are many cases. I'll give it some thought.
@usualoma
I think this is the most simplest use case, isn't it?
app.get('/', (c) => {
const node = jsxNode(
<htm>
<body>
<div>Hello</div>
<script>console.log('Hello');</script>
</body>
</htm>
).on('renderToString.script', ({ setContent, node }) => {
setContent(
new Promise((resolve) => {
resolve(`<script>${node.children[0]}</script>`)
})
)
})
return c.html(node.toString())
})
@yusukebe Thanks. Yes, that is a simple example! If you do not need a Promise, it can be written a little simpler, as follows
app.get('/', (c) => {
const node = jsxNode(
<html>
<body>
<div>Hello</div>
<script>console.log('Hello');</script>
</body>
</html>
).on('renderToString.script', ({ setContent, node }) =>
setContent(`<script>${node.children[0]}</script>`)
)
return c.html(node.toString())
})
It's not cool that c.html(node.toString()), node.toString() is needed. It would be better to be able to write c.html(node) as before. If we merge this PR I would like to fix it.
2e92927 eliminates the need to write toString().
const app = new Hono()
app.get('/', (c) => {
const node = jsxNode(
<html>
<body>
<div>Hello</div>
<script>console.log('Hello');</script>
</body>
</html>
).on('renderToString.script', ({ setContent, node }) =>
setContent(`<script>${node.children[0]}</script>`)
)
return c.html(node)
})
However, while I think this is an exciting feature, I have not found a "strong motivation or use case that requires implementation" at this time, so I guess it is a good idea to think it through without rushing to merge.
@usualoma,
I agree. I think this seems to be a "low-level" API for users, so we should explore more applicational use cases for it.