From 11be5e979ca939513d1dc1ed17ce69f3c733439d Mon Sep 17 00:00:00 2001 From: mux Date: Sat, 1 Nov 2025 14:50:30 -0300 Subject: [PATCH] initial jsx support --- deno.json | 8 +++++--- deno.lock | 20 -------------------- src/app.tsx | 13 +++++++++++++ src/global.d.ts | 20 ++++++++++++++++++++ src/html.ts | 9 ++++++--- src/jsx.ts | 29 +++++++++++++++++++++++++++++ src/main.ts | 8 ++++---- 7 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 src/app.tsx create mode 100644 src/global.d.ts create mode 100644 src/jsx.ts diff --git a/deno.json b/deno.json index cf2b718..5a45916 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,9 @@ "tasks": { "dev": "deno run --allow-net src/main.ts" }, + "imports": { + "interest/jsx-runtime": "./src/jsx.ts" + }, "lint": { "rules": { "tags": [ @@ -11,14 +14,13 @@ } }, "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "interest", "lib": [ "deno.ns", "esnext", "dom", "dom.iterable" ] - }, - "imports": { - "@std/assert": "jsr:@std/assert@1" } } diff --git a/deno.lock b/deno.lock index b058552..0bdf3be 100644 --- a/deno.lock +++ b/deno.lock @@ -1,20 +1,5 @@ { "version": "5", - "specifiers": { - "jsr:@std/assert@1": "1.0.15", - "jsr:@std/internal@^1.0.12": "1.0.12" - }, - "jsr": { - "@std/assert@1.0.15": { - "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/internal@1.0.12": { - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" - } - }, "redirects": { "https://deno.land/std/fs/walk.ts": "https://deno.land/std@0.224.0/fs/walk.ts" }, @@ -46,10 +31,5 @@ "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780" - }, - "workspace": { - "dependencies": [ - "jsr:@std/assert@1" - ] } } diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..d1560ac --- /dev/null +++ b/src/app.tsx @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2025 xwra + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export default function App() { + return ( + <> +

JSX Page

+

meowing chunk by chunk

+ + ); +} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..855ec42 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2025 xwra + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/// + +import type { ChunkedStream } from "./stream.ts"; + +declare global { + namespace JSX { + type Element = (chunks: ChunkedStream) => Promise; + interface IntrinsicElements { + [key: string]: ElementProps; + } + interface ElementProps { + [key: string]: any; + } + } +} diff --git a/src/html.ts b/src/html.ts index a0f3aef..fcb9bdd 100644 --- a/src/html.ts +++ b/src/html.ts @@ -71,9 +71,9 @@ type TagFn = { (fn: () => any): TagRes; }; -type HtmlProxy = { [K in keyof HTMLElementTagNameMap]: TagFn } & { +export type HtmlProxy = { [K in keyof HTMLElementTagNameMap]: TagFn } & { [key: string]: TagFn; -}; +} & { render(child: any): Promise }; const isTemplateLiteral = (arg: any): arg is TemplateStringsArray => Array.isArray(arg) && "raw" in arg; @@ -125,5 +125,8 @@ export function html( }, }; - return new Proxy({}, handler) as HtmlProxy; + const proxy = new Proxy({}, handler) as HtmlProxy; + proxy.render = async (stuff: any) => void write(await render(stuff)); + + return proxy; } diff --git a/src/jsx.ts b/src/jsx.ts new file mode 100644 index 0000000..d508e50 --- /dev/null +++ b/src/jsx.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 xwra + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { html, type HtmlProxy } from "./html.ts"; +import { ChunkedStream } from "./stream.ts"; + +let context; + +export function jsx( + tag: string | typeof Fragment, + { children }: Record = {}, +) { + return async (chunks: ChunkedStream) => { + if (tag === Fragment) { + context = html(chunks); + for (const child of children) { + await context.render?.(child); + } + return; + } + await (context ||= html(chunks))[tag](...children); + }; +} + +export const Fragment = Symbol("Fragment"); + +export const jsxs = jsx; diff --git a/src/main.ts b/src/main.ts index e59c383..d5772a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,18 +5,18 @@ import { html } from "./html.ts"; import { createHtmlStream } from "./http.ts"; +import App from "./app.tsx"; Deno.serve({ port: 8080, async handler() { const stream = await createHtmlStream({ lang: "en" }); - const { h1, ol, p, li } = html(stream.chunks); + const { ol, li } = html(stream.chunks); - await h1`Normal Streaming Page`; - await p({ class: "oh hey" }, "meowing chunk by chunk"); + await App()(stream.chunks); ol(async () => { - const fruits = ["Apple", "Banana", "Cherry"]; + const fruits = ["TSX support", "Apple", "Banana", "Cherry"]; for (const fruit of fruits) { await new Promise((r) => setTimeout(r, 500)); await li(fruit);