0.0.0-alpha.13 • Published 6 months ago

mono-jsx v0.0.0-alpha.13

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

mono-jsx

`<html>` as a `Response`

mono-jsx is a JSX runtime that renders <html> element to a Response object in JavaScript runtimes like Node.js, Deno, Bun, Cloudflare Workers, etc.

  • No build step needed
  • Lightweight(7KB gzipped), zero dependencies
  • Minimal state runtime
  • Streaming rendering
  • Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
export default {
  fetch: (req) => (
    <html>
      <h1>Hello World!</h1>
    </html>
  ),
};

Installation

mono-jsx supports all modern JavaScript runtimes including Node.js, Deno, Bun, Cloudflare Workers, etc. You can install it via npm i, deno add, or bun add.

# Node.js, Cloudflare Workers, or other node-compatible runtimes
npm i mono-jsx
# Deno
deno add npm:mono-jsx
# Bun
bun add mono-jsx

Setup JSX runtime

To use mono-jsx as the JSX runtime, add the following configuration to your tsconfig.json(deno.json for Deno):

{
  "compilerOptions": {
    "allowJs": true, // required for `.jsx` extension in Node.js
    "jsx": "react-jsx",
    "jsxImportSource": "mono-jsx"
  }
}

You can also run npx mono-jsx setup to add the configuration automatically.

npx mono-jsx setup

Alternatively, you can use pragma directive in your JSX file.

/** @jsxImportSource mono-jsx */

Usage

To create a html response in server-side, you just need to return a <html> element in the fetch method.

// app.jsx

export default {
  fetch: (req) => (
    <html>
      <h1>Hello World!</h1>
    </html>
  ),
};

For Deno/Bun users, you can run the app.jsx directly.

deno serve app.jsx
bun run app.jsx

Node.js does not support JSX module and declarative fetch server, we recommend using mono-jsx with hono.

import { serve } from "@hono/node-server";
import { Hono } from "hono";
const app = new Hono();

app.get("/", (c) => (
  <html>
    <h1>Hello World!</h1>
  </html>
));

serve(app);

and you will need tsx to start the app.

npx tsx app.jsx

If you are building a web app with Cloudflare Workers, you can use the wrangler dev command to start the app in local development.

npx wrangler dev app.jsx

Using JSX

mono-jsx uses JSX to describe the user interface, similar to React but with some differences.

Using Standard HTML Property Names

mono-jsx uses standard HTML property names instead of React's overthinked property names.

  • className -> class
  • htmlFor -> for
  • onChange -> onInput

Composition class

mono-jsx allows you to compose the class property using an array of strings, objects, or expressions.

<div class={["container box", isActive && "active", { hover: isHover }]} />;

Using Pseudo Classes and Media Queries in the style Property

mono-jsx allows you to use pseudo classes, pseudo elements, media queries, and css nesting in the style property.

<a
  style={{
    color: "black",
    "::after": { content: "↩️" },
    ":hover": { textDecoration: "underline" },
    "@media (prefers-color-scheme: dark)": { color: "white" },
    "& .icon": { width: "1em", height: "1em", marginRight: "0.5em" },
  }}
>
  <img class="icon" src="link.png" />
  Link
</a>;

<slot> Element

mono-jsx uses <slot> element to render the slotted content(Equivalent to React's children proptery). Plus, you also can add the name attribute to define a named slot.

function Container() {
  return (
    <div class="container">
      <slot /> {/* <h1>Hello world!</h1> */}
      <slot name="desc" /> {/* <p>This is a description.</p> */}
    </div>
  );
}

function App() {
  return (
    <Container>
      <p slot="desc">This is a description.</p>
      <h1>Hello world!</h1>
    </Container>
  );
}

html Tag Function

mono-jsx doesn't support the dangerouslySetInnerHTML property, instead, it provides a html tag function to render raw HTML in JSX.

function App() {
  return <div>{html`<h1>Hello world!</h1>`}</div>;
}

The html tag function is a global function injected by mono-jsx, you can use it in any JSX expression without importing it. You also can use the css and js, that are just aliases of the html tag function, to render CSS and JavaScript code.

function App() {
  return (
    <head>
      <style>{css`h1 { font-size: 3rem; }`}</style>
      <script>{js`console.log("Hello world!")`}</script>
    </head>
  );
}

!WARNING the html tag function is unsafe that can cause XSS vulnerabilities.

Event Handlers

mono-jsx allows you to write event handlers directly in the JSX code, like React.

function Button() {
  return <button onClick={(evt) => alert("BOOM!")}>Click Me</button>;
}

!NOTE the event handler would never be called in server-side. Instead it will be serialized to a string and sent to the client-side. This means you should NOT use any server-side variables or functions in the event handler.

function Button() {
  let message = "BOOM!";
  return (
    <button
      onClick={(evt) => {
        Deno.exit(0); // ❌ Deno is unavailable in the browser
        alert(message); // ❌ message is a server-side variable
        document.title = "BOOM!"; // ✅ document is a browser API
        $state.count++; // ✅ $state is the mono-jsx specific usage
      }}
    >
      Click Me
    </button>
  );
}

Plus, mono-jsx supports the mount event that will be triggered when the element is mounted in the client-side.

function App() {
  return (
    <div onMount={(evt) => console.log(evt.target, "Mounted!")}>
      <h1>Hello World!</h1>
    </div>
  );
}

Using State

mono-jsx provides a minimal state runtime that allows you to update view based on state changes in client-side.

function App() {
  // Initialize the state 'count' with value `0`
  $state.count = 0;
  return (
    <div>
      {/* use the state */}
      <span>{$state.count}</span>
      {/* computed state */}
      <span>doubled: {$computed(() => 2 * $state.count)}</span>
      {/* update the state in event handlers */}
      <button onClick={() => $state.count--}>-</button>
      <button onClick={() => $state.count++}>+</button>
    </div>
  );
}

To support type checking in TypeScript, declare the State interface in the global scope:

declare global {
  interface State {
    count: number;
  }
}

!NOTE The $state and $computed are global variables injected by mono-jsx.

Using $context Hook

The $context hook allows you to access data which is set in the root <html> element.

interface Data {
  foo: string;
}

async function App() {
  const { request, data } = $context<Data>();
  return (
    <p>
      {request.method} {request.url} {data.foo}
    </p>
  );
}

export default {
  fetch: (req) => (
    <html request={req} data={{ foo: "bar" }}>
      <h1>Hello World!</h1>
      <App />
    </html>
  ),
};

!NOTE If you are using hooks in an async function component, you need to call these hooks before any await statement.

async function AsyncApp() {
  const { request } = $context();
  const data = await fetchData(new URL(request.url).searchParams.get("id"));
  const { request } = $context(); // ❌ request is undefined
  return (
    <p>
      {data.title}
    </p>
  );
}

Built-in Elements

mono-jsx provides some built-in elements to help you build your app.

<toggle> element

<toggle> element allows you to toggle the visibility of the slotted content.

function App() {
  $state.show = false;
  return (
    <div>
      <toggle value={$state.show}>
        <h1>Hello World!</h1>
      </toggle>
      <button onClick={toggle}>{$computed(() => $state.show ? "Hide" : "Show")}</button>
    </div>
  );
  function toggle() {
    $state.show = !$state.show;
  }
}

<switch> element

<switch> element allows you to switch the slotted content based on the value property. You need to define the slot attribute in the slotted content to match the value, otherwise, the default slots will be rendered.

function App() {
  return (
    <div>
      <switch value={$state.lang}>
        <h1 slot="en">Hello, world!</h1>
        <h1 slot="zh">你好,世界!</h1>
        <h1>✋🌎❗️</h1>
      </switch>
      <button onClick={() => $state.lang = "en"}>English</button>
      <button onClick={() => $state.lang = "zh"}>中文</button>
    </div>
  );
}

<cache> element

Work in progress...

Streaming Rendering

mono-jsx renders your <html> as a readable stream, that allows async function components are rendered asynchrously. You can set a placeholder attribute to show a loading state while the async component is loading.

async function Sleep(ms) {
  await new Promise((resolve) => setTimeout(resolve, ms));
  return <solt />;
}

export default {
  fetch: (req) => (
    <html>
      <h1>Hello World!</h1>
      <Sleep ms={1000} placeholder={<p>Sleeping...</p>}>
        <p>After 1 second</p>
      </Sleep>
    </html>
  ),
};

You can also set rendering attribute to control the rendering strategy of the async component. Currently, only eager is supported that renders the async component immediately.

async function Sleep(ms) {
  await new Promise((resolve) => setTimeout(resolve, ms));
  return <solt />;
}

export default {
  fetch: (req) => (
    <html>
      <h1>Hello World!</h1>
      <Sleep ms={1000} rendering="eager">
        <p>After 1 second</p>
      </Sleep>
    </html>
  ),
};
0.0.0-alpha.7

7 months ago

0.0.0-alpha.8

7 months ago

0.0.0-alpha.9

7 months ago

0.0.0-alpha.13

6 months ago

0.0.0-alpha.12

7 months ago

0.0.0-alpha.11

7 months ago

0.0.0-alpha.10

7 months ago

0.0.0-alpha.6

10 months ago

0.0.0-alpha.3

1 year ago

0.0.0-alpha.4

1 year ago

0.0.0-alpha.1

1 year ago

0.0.0-alpha.2

1 year ago

0.0.0-alpha.5

1 year ago

0.0.0

2 years ago