mono-jsx 0.0.0 β†’ 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Je Xia <i@jex.me>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,424 @@
1
+ # mono-jsx
2
+
3
+ ![`<html>` as a `Response`](./.github/og-image.png)
4
+
5
+ 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.
6
+
7
+ - πŸš€ No build step needed
8
+ - πŸ¦‹ Lightweight(8KB gzipped), zero dependencies
9
+ - πŸ”« Minimal state runtime
10
+ - 🚨 Full Web API types
11
+ - ⏳ Streaming rendering
12
+ - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
13
+
14
+ ## Installation
15
+
16
+ mono-jsx supports all modern JavaScript runtimes including Node.js, Deno, Bun, Cloudflare Workers, etc.
17
+ You can install it via `npm i`, `deno add`, or `bun add`.
18
+
19
+ ```bash
20
+ # Node.js, Cloudflare Workers, or other node-compatible runtimes
21
+ npm i mono-jsx
22
+ # Deno
23
+ deno add npm:mono-jsx
24
+ # Bun
25
+ bun add mono-jsx
26
+ ```
27
+
28
+ ## Setup JSX Runtime
29
+
30
+ To use mono-jsx as JSX runtime, add the following configuration to your `tsconfig.json`(`deno.json` for Deno):
31
+
32
+ ```jsonc
33
+ {
34
+ "compilerOptions": {
35
+ "jsx": "react-jsx",
36
+ "jsxImportSource": "mono-jsx"
37
+ }
38
+ }
39
+ ```
40
+
41
+ Alternatively, you can also use pragma directive in your JSX file.
42
+
43
+ ```js
44
+ /** @jsxImportSource mono-jsx */
45
+ ```
46
+
47
+ You can also run `mono-jsx setup` to automatically add the configuration to your `tsconfig.json` or `deno.json`.
48
+
49
+ ```bash
50
+ # Node.js, Cloudflare Workers, or other node-compatible runtimes
51
+ npx mono-jsx setup
52
+ # Deno
53
+ deno run npm:mono-jsx setup
54
+ # Bun
55
+ bunx mono-jsx setup
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ To create a html response in server-side, you just need to return a `<html>` element in the `fetch` handler.
61
+
62
+ ```tsx
63
+ // app.tsx
64
+
65
+ export default {
66
+ fetch: (req) => (
67
+ <html>
68
+ <h1>Hello World!</h1>
69
+ </html>
70
+ ),
71
+ };
72
+ ```
73
+
74
+ For Deno/Bun users, you can run the `app.tsx` directly.
75
+
76
+ ```bash
77
+ deno serve app.tsx
78
+ bun run app.tsx
79
+ ```
80
+
81
+ If you are building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` command to start the app in local development mode.
82
+
83
+ ```bash
84
+ npx wrangler dev app.tsx
85
+ ```
86
+
87
+ **Node.js does not support JSX syntax and declarative fetch server**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev/).
88
+
89
+ ```tsx
90
+ // app.tsx
91
+
92
+ import { serve } from "srvx";
93
+
94
+ serve({
95
+ port: 3000,
96
+ fetch: (req) => (
97
+ <html>
98
+ <h1>Hello World!</h1>
99
+ </html>
100
+ ),
101
+ });
102
+ ```
103
+
104
+ and you will need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step.
105
+
106
+ ```bash
107
+ npx tsx app.tsx
108
+ ```
109
+
110
+ ## Using JSX
111
+
112
+ mono-jsx uses [**JSX**](https://react.dev/learn/describing-the-ui) to describe the user interface, similar to React but with some differences.
113
+
114
+ ### Using Standard HTML Property Names
115
+
116
+ mono-jsx uses standard HTML property names instead of React's overthinked property names.
117
+
118
+ - `className` -> `class`
119
+ - `htmlFor` -> `for`
120
+ - `onChange` -> `onInput`
121
+
122
+ ### Composition `class`
123
+
124
+ mono-jsx allows you to compose the `class` property using an array of strings, objects, or expressions.
125
+
126
+ ```jsx
127
+ <div class={["container box", isActive && "active", { hover: isHover }]} />;
128
+ ```
129
+
130
+ ### Using Pseudo Classes and Media Queries in the `style` Property
131
+
132
+ mono-jsx allows you to use [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes), [pseudo elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements), [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries), and [css nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting) in the `style` property.
133
+
134
+ ```jsx
135
+ <a
136
+ style={{
137
+ color: "black",
138
+ "::after": { content: "↩️" },
139
+ ":hover": { textDecoration: "underline" },
140
+ "@media (prefers-color-scheme: dark)": { color: "white" },
141
+ "& .icon": { width: "1em", height: "1em", marginRight: "0.5em" },
142
+ }}
143
+ >
144
+ <img class="icon" src="link.png" />
145
+ Link
146
+ </a>;
147
+ ```
148
+
149
+ ### `<slot>` Element
150
+
151
+ mono-jsx uses [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/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.
152
+
153
+ ```jsx
154
+ function Container() {
155
+ return (
156
+ <div class="container">
157
+ <slot /> {/* <h1>Hello world!</h1> */}
158
+ <slot name="desc" /> {/* <p>This is a description.</p> */}
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function App() {
164
+ return (
165
+ <Container>
166
+ <p slot="desc">This is a description.</p>
167
+ <h1>Hello world!</h1>
168
+ </Container>
169
+ );
170
+ }
171
+ ```
172
+
173
+ ### `html` Tag Function
174
+
175
+ mono-jsx doesn't support the `dangerouslySetInnerHTML` property, instead, it provides a `html` tag function to render raw HTML in JSX.
176
+
177
+ ```jsx
178
+ function App() {
179
+ return <div>{html`<h1>Hello world!</h1>`}</div>;
180
+ }
181
+ ```
182
+
183
+ The `html` tag function is a global function injected by mono-jsx, you can use it in any JSX expression without importing it.
184
+ You also can use the `css` and `js`, that are just aliases of the `html` tag function, to render CSS and JavaScript code.
185
+
186
+ ```jsx
187
+ function App() {
188
+ return (
189
+ <head>
190
+ <style>{css`h1 { font-size: 3rem; }`}</style>
191
+ <script>{js`console.log("Hello world!")`}</script>
192
+ </head>
193
+ );
194
+ }
195
+ ```
196
+
197
+ > [!WARNING]
198
+ > the `html` tag function is **unsafe** that can cause [**XSS**](https://en.wikipedia.org/wiki/Cross-site_scripting) vulnerabilities.
199
+
200
+ ### Event Handlers
201
+
202
+ mono-jsx allows you to write event handlers directly in the JSX code, like React.
203
+
204
+ ```jsx
205
+ function Button() {
206
+ return <button onClick={(evt) => alert("BOOM!")}>Click Me</button>;
207
+ }
208
+ ```
209
+
210
+ > [!NOTE]
211
+ > 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.**
212
+
213
+ ```tsx
214
+ function Button(this: FC, props: { role: string }) {
215
+ let message = "BOOM!";
216
+ console.log(message); // only print message in server-side
217
+ return (
218
+ <button
219
+ role={props.role}
220
+ onClick={(evt) => {
221
+ alert(message); // ❌ `message` is a server-side variable
222
+ console.log(props.role); // ❌ `props` is a server-side variable
223
+ Deno.exit(0); // ❌ `Deno` is unavailable in the browser
224
+ document.title = "BOOM!"; // βœ… `document` is a browser API
225
+ console.log(evt.target); // βœ… `evt` is the event object
226
+ this.count++; // βœ… update the state `count`
227
+ }}
228
+ >
229
+ Click Me
230
+ </button>
231
+ );
232
+ }
233
+ ```
234
+
235
+ Plus, mono-jsx supports the `mount` event that will be triggered when the element is mounted in the client-side.
236
+
237
+ ```jsx
238
+ function App() {
239
+ return (
240
+ <div onMount={(evt) => console.log(evt.target, "Mounted!")}>
241
+ <h1>Hello World!</h1>
242
+ </div>
243
+ );
244
+ }
245
+ ```
246
+
247
+ ## Using State
248
+
249
+ mono-jsx provides a minimal state runtime that allows you to update view based on state changes in client-side.
250
+
251
+ ```tsx
252
+ function Counter(
253
+ this: FC<{ count: number }>,
254
+ props: { initialCount?: number },
255
+ ) {
256
+ this.count = props.initialCount ?? 0;
257
+ return (
258
+ <div>
259
+ {/* use the state */}
260
+ <span>{this.count}</span>
261
+ {/* use computed state */}
262
+ <span>doubled: {this.computed(() => 2 * this.count)}</span>
263
+ {/* update the state in event handlers */}
264
+ <button onClick={() => this.count--}>-</button>
265
+ <button onClick={() => this.count++}>+</button>
266
+ </div>
267
+ );
268
+ }
269
+ ```
270
+
271
+ > [!WARNING]
272
+ > The state cannot be used in an arrow function component, you should use a `function` declaration instead.
273
+
274
+ ```jsx
275
+ // ❌ `this.count++` won't update the view, please use a function declaration instead
276
+ const App = () => {
277
+ this.count = 0;
278
+ return (
279
+ <div>
280
+ <span>{this.count}</span>
281
+ <button onClick={() => this.count++}>+</button>
282
+ </div>
283
+ );
284
+ };
285
+ ```
286
+
287
+ ## Built-in Elements
288
+
289
+ mono-jsx provides some built-in elements to help you build your app.
290
+
291
+ ### `<toggle>` element
292
+
293
+ `<toggle>` element allows you to toggle the visibility of the slotted content.
294
+
295
+ ```tsx
296
+ function App(this: FC<{ show: boolean }>) {
297
+ this.show = false;
298
+ return (
299
+ <div>
300
+ <toggle value={this.show}>
301
+ <h1>Hello World!</h1>
302
+ </toggle>
303
+ <button onClick={toggle}>{this.computed(() => this.show ? "Hide" : "Show")}</button>
304
+ </div>
305
+ );
306
+ function toggle() {
307
+ this.show = !this.show;
308
+ }
309
+ }
310
+ ```
311
+
312
+ ### `<switch>` element
313
+
314
+ `<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.
315
+
316
+ ```tsx
317
+ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
318
+ this.lang = "en";
319
+ return (
320
+ <div>
321
+ <switch value={this.lang}>
322
+ <h1 slot="en">Hello, world!</h1>
323
+ <h1 slot="zh">δ½ ε₯½οΌŒδΈ–η•ŒοΌ</h1>
324
+ <h1>βœ‹πŸŒŽβ—οΈ</h1>
325
+ </switch>
326
+ <button onClick={() => this.lang = "en"}>English</button>
327
+ <button onClick={() => this.lang = "zh"}>δΈ­ζ–‡</button>
328
+ <button onClick={() => this.lang = "emoji"}>Emoji</button>
329
+ </div>
330
+ );
331
+ }
332
+ ```
333
+
334
+ ## Streaming Rendering
335
+
336
+ 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.
337
+
338
+ ```jsx
339
+ async function Sleep(ms) {
340
+ await new Promise((resolve) => setTimeout(resolve, ms));
341
+ return <solt />;
342
+ }
343
+
344
+ export default {
345
+ fetch: (req) => (
346
+ <html>
347
+ <h1>Hello World!</h1>
348
+ <Sleep ms={1000} placeholder={<p>Sleeping...</p>}>
349
+ <p>After 1 second</p>
350
+ </Sleep>
351
+ </html>
352
+ ),
353
+ };
354
+ ```
355
+
356
+ You can also set `rendering` attribute to "eager" to render the async component eagerly, which means the async component will be rendered as a sync function component and the `placeholder` will be ignored.
357
+
358
+ ```jsx
359
+ async function Sleep(ms) {
360
+ await new Promise((resolve) => setTimeout(resolve, ms));
361
+ return <solt />;
362
+ }
363
+
364
+ export default {
365
+ fetch: (req) => (
366
+ <html>
367
+ <h1>Hello World!</h1>
368
+ <Sleep ms={1000} rendering="eager">
369
+ <p>After 1 second</p>
370
+ </Sleep>
371
+ </html>
372
+ ),
373
+ };
374
+ ```
375
+
376
+ ## Accessing Request Info
377
+
378
+ You can access the request info in a function component by using the `request` property in the `this` context. And you must pass the `request` object to the root `<html>` element to make it work.
379
+
380
+ ```tsx
381
+ function RequestInfo(this: FC) {
382
+ const { request } = this;
383
+ return (
384
+ <div>
385
+ <h1>Request Info</h1>
386
+ <p>{request.method}</p>
387
+ <p>{request.url}</p>
388
+ <p>{request.headers.get("user-agent")}</p>
389
+ </div>
390
+ );
391
+ }
392
+
393
+ export default {
394
+ fetch: (req) => (
395
+ <html request={req}>
396
+ <RequestInfo />
397
+ </html>
398
+ ),
399
+ };
400
+ ```
401
+
402
+ ## Customizing Response
403
+
404
+ You can add `status` or `headers` attribute to the `<html>` element to customize the response.
405
+
406
+ ```jsx
407
+ export default {
408
+ fetch: (req) => (
409
+ <html
410
+ status={404}
411
+ headers={{
412
+ cacheControl: "public, max-age=0, must-revalidate",
413
+ setCookie: "name=value",
414
+ }}
415
+ >
416
+ <h1>Page Not Found</h1>
417
+ </html>
418
+ ),
419
+ };
420
+ ```
421
+
422
+ ## License
423
+
424
+ [MIT](LICENSE)
package/bin/mono-jsx ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import process from "node:process";
5
+
6
+ switch (process.argv[2]) {
7
+ case "setup":
8
+ setup().then(() => {
9
+ console.log("βœ… JSX runtime setup complete.");
10
+ });
11
+ break;
12
+ default:
13
+ process.exit(0);
14
+ }
15
+
16
+ async function setup() {
17
+ let tsConfigPath = globalThis.Deno ? "deno.json" : "tsconfig.json";
18
+ let tsConfig = Object.create(null);
19
+ try {
20
+ const json = await readFile(tsConfigPath, "utf8");
21
+ tsConfig = JSON.parse(json);
22
+ } catch {
23
+ // ignore
24
+ }
25
+ tsConfig.compilerOptions = {
26
+ ...tsConfig.compilerOptions,
27
+ jsx: "react-jsx",
28
+ jsxImportSource: "mono-jsx",
29
+ };
30
+ if (!globalThis.Deno) {
31
+ tsConfig.compilerOptions.alllowJs = true
32
+ }
33
+ await writeFile(tsConfigPath, JSON.stringify(tsConfig, null, 2));
34
+ }
package/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ // index.ts
2
+ console.log("Welcome to mono-jsx!");