hono icon indicating copy to clipboard operation
hono copied to clipboard

PoC: feat(jsx): Introduce event handler for IntrinsicElements.

Open usualoma opened this issue 2 years ago • 7 comments

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

render

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 denoify to generate files for Deno

usualoma avatar Dec 05 '23 09:12 usualoma

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))

usualoma avatar Dec 05 '23 09:12 usualoma

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.

yusukebe avatar Dec 05 '23 10:12 yusukebe

@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 avatar Dec 05 '23 10:12 yusukebe

@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.

usualoma avatar Dec 05 '23 11:12 usualoma

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)
})

usualoma avatar Dec 05 '23 11:12 usualoma

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 avatar Dec 05 '23 11:12 usualoma

@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.

yusukebe avatar Dec 05 '23 12:12 yusukebe