Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

usualoma
Copy link
Member

@usualoma usualoma commented Dec 5, 2023

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

  • Add tests
  • Run tests
  • yarn denoify to generate files for Deno

@usualoma
Copy link
Member Author

usualoma commented Dec 5, 2023

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

@yusukebe
Copy link
Member

yusukebe commented Dec 5, 2023

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
Copy link
Member

yusukebe commented Dec 5, 2023

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

@usualoma
Copy link
Member Author

usualoma commented Dec 5, 2023

@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
Copy link
Member Author

usualoma commented Dec 5, 2023

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
Copy link
Member Author

usualoma commented Dec 5, 2023

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.

@yusukebe
Copy link
Member

yusukebe commented Dec 5, 2023

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

@usualoma usualoma changed the title feat(jsx): Introduce event handler for IntrinsicElements. PoC feat(jsx): Introduce event handler for IntrinsicElements. Dec 6, 2023
@usualoma usualoma changed the title PoC feat(jsx): Introduce event handler for IntrinsicElements. PoC: feat(jsx): Introduce event handler for IntrinsicElements. Dec 6, 2023
@usualoma usualoma mentioned this pull request Dec 25, 2023
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants