mono-jsx 0.0.0 β†’ 0.1.0

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,435 @@
1
+ # mono-jsx
2
+
3
+ ![`<html>` as a `Response`](./.github/html-as-a-response.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
+ ```jsx
15
+ export default {
16
+ fetch: (req) => (
17
+ <html>
18
+ <h1>Hello World!</h1>
19
+ </html>
20
+ ),
21
+ };
22
+ ```
23
+
24
+ ## Installation
25
+
26
+ mono-jsx supports all modern JavaScript runtimes including Node.js, Deno, Bun, Cloudflare Workers, etc.
27
+ You can install it via `npm i`, `deno add`, or `bun add`.
28
+
29
+ ```bash
30
+ # Node.js, Cloudflare Workers, or other node-compatible runtimes
31
+ npm i mono-jsx
32
+ # Deno
33
+ deno add npm:mono-jsx
34
+ # Bun
35
+ bun add mono-jsx
36
+ ```
37
+
38
+ ## Setup JSX Runtime
39
+
40
+ To use mono-jsx as JSX runtime, add the following configuration to your `tsconfig.json`(`deno.json` for Deno):
41
+
42
+ ```jsonc
43
+ {
44
+ "compilerOptions": {
45
+ "jsx": "react-jsx",
46
+ "jsxImportSource": "mono-jsx",
47
+ "allowJs": true // required for supporting `.jsx` extension in Node.js
48
+ }
49
+ }
50
+ ```
51
+
52
+ Alternatively, you can also use pragma directive in your JSX file.
53
+
54
+ ```js
55
+ /** @jsxImportSource mono-jsx */
56
+ ```
57
+
58
+ You can also run `mono-jsx setup` to automatically add the configuration to your `tsconfig.json` or `deno.json`.
59
+
60
+ ```bash
61
+ # Node.js, Cloudflare Workers, or other node-compatible runtimes
62
+ npx mono-jsx setup
63
+ # Deno
64
+ deno run npm:mono-jsx setup
65
+ # Bun
66
+ bunx mono-jsx setup
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ To create a html response in server-side, you just need to return a `<html>` element in the `fetch` handler.
72
+
73
+ ```tsx
74
+ // app.tsx
75
+
76
+ export default {
77
+ fetch: (req) => (
78
+ <html>
79
+ <h1>Hello World!</h1>
80
+ </html>
81
+ ),
82
+ };
83
+ ```
84
+
85
+ For Deno/Bun users, you can run the `app.tsx` directly.
86
+
87
+ ```bash
88
+ deno serve app.tsx
89
+ bun run app.tsx
90
+ ```
91
+
92
+ 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.
93
+
94
+ ```bash
95
+ npx wrangler dev app.tsx
96
+ ```
97
+
98
+ **Node.js does not support JSX syntax and declarative fetch server**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev/).
99
+
100
+ ```tsx
101
+ // app.tsx
102
+
103
+ import { serve } from "srvx";
104
+
105
+ serve({
106
+ port: 3000,
107
+ fetch: (req) => (
108
+ <html>
109
+ <h1>Hello World!</h1>
110
+ </html>
111
+ ),
112
+ });
113
+ ```
114
+
115
+ and you will need [tsx](https://www.npmjs.com/package/tsx) to start the app.
116
+
117
+ ```bash
118
+ npx tsx app.tsx
119
+ ```
120
+
121
+ ## Using JSX
122
+
123
+ mono-jsx uses [**JSX**](https://react.dev/learn/describing-the-ui) to describe the user interface, similar to React but with some differences.
124
+
125
+ ### Using Standard HTML Property Names
126
+
127
+ mono-jsx uses standard HTML property names instead of React's overthinked property names.
128
+
129
+ - `className` -> `class`
130
+ - `htmlFor` -> `for`
131
+ - `onChange` -> `onInput`
132
+
133
+ ### Composition `class`
134
+
135
+ mono-jsx allows you to compose the `class` property using an array of strings, objects, or expressions.
136
+
137
+ ```jsx
138
+ <div class={["container box", isActive && "active", { hover: isHover }]} />;
139
+ ```
140
+
141
+ ### Using Pseudo Classes and Media Queries in the `style` Property
142
+
143
+ 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.
144
+
145
+ ```jsx
146
+ <a
147
+ style={{
148
+ color: "black",
149
+ "::after": { content: "↩️" },
150
+ ":hover": { textDecoration: "underline" },
151
+ "@media (prefers-color-scheme: dark)": { color: "white" },
152
+ "& .icon": { width: "1em", height: "1em", marginRight: "0.5em" },
153
+ }}
154
+ >
155
+ <img class="icon" src="link.png" />
156
+ Link
157
+ </a>;
158
+ ```
159
+
160
+ ### `<slot>` Element
161
+
162
+ 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.
163
+
164
+ ```jsx
165
+ function Container() {
166
+ return (
167
+ <div class="container">
168
+ <slot /> {/* <h1>Hello world!</h1> */}
169
+ <slot name="desc" /> {/* <p>This is a description.</p> */}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ function App() {
175
+ return (
176
+ <Container>
177
+ <p slot="desc">This is a description.</p>
178
+ <h1>Hello world!</h1>
179
+ </Container>
180
+ );
181
+ }
182
+ ```
183
+
184
+ ### `html` Tag Function
185
+
186
+ mono-jsx doesn't support the `dangerouslySetInnerHTML` property, instead, it provides a `html` tag function to render raw HTML in JSX.
187
+
188
+ ```jsx
189
+ function App() {
190
+ return <div>{html`<h1>Hello world!</h1>`}</div>;
191
+ }
192
+ ```
193
+
194
+ The `html` tag function is a global function injected by mono-jsx, you can use it in any JSX expression without importing it.
195
+ You also can use the `css` and `js`, that are just aliases of the `html` tag function, to render CSS and JavaScript code.
196
+
197
+ ```jsx
198
+ function App() {
199
+ return (
200
+ <head>
201
+ <style>{css`h1 { font-size: 3rem; }`}</style>
202
+ <script>{js`console.log("Hello world!")`}</script>
203
+ </head>
204
+ );
205
+ }
206
+ ```
207
+
208
+ > [!WARNING]
209
+ > the `html` tag function is **unsafe** that can cause [**XSS**](https://en.wikipedia.org/wiki/Cross-site_scripting) vulnerabilities.
210
+
211
+ ### Event Handlers
212
+
213
+ mono-jsx allows you to write event handlers directly in the JSX code, like React.
214
+
215
+ ```jsx
216
+ function Button() {
217
+ return <button onClick={(evt) => alert("BOOM!")}>Click Me</button>;
218
+ }
219
+ ```
220
+
221
+ > [!NOTE]
222
+ > 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.**
223
+
224
+ ```tsx
225
+ function Button(this: FC, props: { role: string }) {
226
+ let message = "BOOM!";
227
+ console.log(message); // only print message in server-side
228
+ return (
229
+ <button
230
+ role={props.role}
231
+ onClick={(evt) => {
232
+ alert(message); // ❌ `message` is a server-side variable
233
+ console.log(props.role); // ❌ `props` is a server-side variable
234
+ Deno.exit(0); // ❌ `Deno` is unavailable in the browser
235
+ document.title = "BOOM!"; // βœ… `document` is a browser API
236
+ console.log(evt.target); // βœ… `evt` is the event object
237
+ this.count++; // βœ… update the state `count`
238
+ }}
239
+ >
240
+ Click Me
241
+ </button>
242
+ );
243
+ }
244
+ ```
245
+
246
+ Plus, mono-jsx supports the `mount` event that will be triggered when the element is mounted in the client-side.
247
+
248
+ ```jsx
249
+ function App() {
250
+ return (
251
+ <div onMount={(evt) => console.log(evt.target, "Mounted!")}>
252
+ <h1>Hello World!</h1>
253
+ </div>
254
+ );
255
+ }
256
+ ```
257
+
258
+ ## Using State
259
+
260
+ mono-jsx provides a minimal state runtime that allows you to update view based on state changes in client-side.
261
+
262
+ ```tsx
263
+ function App(
264
+ this: FC<{ count: number }>,
265
+ props: { initialCount?: number },
266
+ ) {
267
+ this.count = props.initialCount ?? 0;
268
+ return (
269
+ <div>
270
+ {/* use the state */}
271
+ <span>{this.count}</span>
272
+ {/* use computed state */}
273
+ <span>doubled: {this.computed(() => 2 * this.count)}</span>
274
+ {/* update the state in event handlers */}
275
+ <button onClick={() => this.count--}>-</button>
276
+ <button onClick={() => this.count++}>+</button>
277
+ </div>
278
+ );
279
+ }
280
+ ```
281
+
282
+ > [!WARNING]
283
+ > The state cannot be used in an arrow function component, you should use a `function` declaration instead.
284
+
285
+ ```jsx
286
+ // ❌ `this.count++` won't update the view, please use a function declaration instead
287
+ const App = () => {
288
+ this.count = 0;
289
+ return (
290
+ <div>
291
+ <span>{this.count}</span>
292
+ <button onClick={() => this.count++}>+</button>
293
+ </div>
294
+ );
295
+ };
296
+ ```
297
+
298
+ ## Built-in Elements
299
+
300
+ mono-jsx provides some built-in elements to help you build your app.
301
+
302
+ ### `<toggle>` element
303
+
304
+ `<toggle>` element allows you to toggle the visibility of the slotted content.
305
+
306
+ ```tsx
307
+ function App(this: FC<{ show: boolean }>) {
308
+ this.show = false;
309
+ return (
310
+ <div>
311
+ <toggle value={this.show}>
312
+ <h1>Hello World!</h1>
313
+ </toggle>
314
+ <button onClick={toggle}>{this.computed(() => this.show ? "Hide" : "Show")}</button>
315
+ </div>
316
+ );
317
+ function toggle() {
318
+ this.show = !this.show;
319
+ }
320
+ }
321
+ ```
322
+
323
+ ### `<switch>` element
324
+
325
+ `<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.
326
+
327
+ ```tsx
328
+ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
329
+ this.lang = "en";
330
+ return (
331
+ <div>
332
+ <switch value={this.lang}>
333
+ <h1 slot="en">Hello, world!</h1>
334
+ <h1 slot="zh">δ½ ε₯½οΌŒδΈ–η•ŒοΌ</h1>
335
+ <h1>βœ‹πŸŒŽβ—οΈ</h1>
336
+ </switch>
337
+ <button onClick={() => this.lang = "en"}>English</button>
338
+ <button onClick={() => this.lang = "zh"}>δΈ­ζ–‡</button>
339
+ <button onClick={() => this.lang = "emoji"}>Emoji</button>
340
+ </div>
341
+ );
342
+ }
343
+ ```
344
+
345
+ ## Streaming Rendering
346
+
347
+ 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.
348
+
349
+ ```jsx
350
+ async function Sleep(ms) {
351
+ await new Promise((resolve) => setTimeout(resolve, ms));
352
+ return <solt />;
353
+ }
354
+
355
+ export default {
356
+ fetch: (req) => (
357
+ <html>
358
+ <h1>Hello World!</h1>
359
+ <Sleep ms={1000} placeholder={<p>Sleeping...</p>}>
360
+ <p>After 1 second</p>
361
+ </Sleep>
362
+ </html>
363
+ ),
364
+ };
365
+ ```
366
+
367
+ 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.
368
+
369
+ ```jsx
370
+ async function Sleep(ms) {
371
+ await new Promise((resolve) => setTimeout(resolve, ms));
372
+ return <solt />;
373
+ }
374
+
375
+ export default {
376
+ fetch: (req) => (
377
+ <html>
378
+ <h1>Hello World!</h1>
379
+ <Sleep ms={1000} rendering="eager">
380
+ <p>After 1 second</p>
381
+ </Sleep>
382
+ </html>
383
+ ),
384
+ };
385
+ ```
386
+
387
+ ## Accessing Request Info
388
+
389
+ 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.
390
+
391
+ ```tsx
392
+ function RequestInfo(this: FC) {
393
+ const { request } = this;
394
+ return (
395
+ <div>
396
+ <h1>Request Info</h1>
397
+ <p>{request.method}</p>
398
+ <p>{request.url}</p>
399
+ <p>{request.headers.get("user-agent")}</p>
400
+ </div>
401
+ );
402
+ }
403
+
404
+ export default {
405
+ fetch: (req) => (
406
+ <html request={req}>
407
+ <RequestInfo />
408
+ </html>
409
+ ),
410
+ };
411
+ ```
412
+
413
+ ## Customizing Response
414
+
415
+ You can add `status` or `headers` attribute to the `<html>` element to customize the response.
416
+
417
+ ```jsx
418
+ export default {
419
+ fetch: (req) => (
420
+ <html
421
+ status={404}
422
+ headers={{
423
+ cacheControl: "public, max-age=0, must-revalidate",
424
+ setCookie: "name=value",
425
+ }}
426
+ >
427
+ <h1>Page Not Found</h1>
428
+ </html>
429
+ ),
430
+ };
431
+ ```
432
+
433
+ ## License
434
+
435
+ [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("Weclome to mono-jsx!");