mono-jsx 0.1.2 → 0.2.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/README.md CHANGED
@@ -2,32 +2,34 @@
2
2
 
3
3
  ![`<html>` as a `Response`](./.github/og-image.png)
4
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.
5
+ mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in JavaScript runtimes like Node.js, Deno, Bun, Cloudflare Workers, etc.
6
6
 
7
7
  - 🚀 No build step needed
8
8
  - 🦋 Lightweight (8KB gzipped), zero dependencies
9
9
  - 🔫 Minimal state runtime
10
- - 🚨 Complete Web API Typescript definitions
10
+ - 🚨 Complete Web API TypeScript definitions
11
11
  - ⏳ Streaming rendering
12
12
  - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
13
13
 
14
14
  ## Installation
15
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`.
16
+ mono-jsx supports all modern JavaScript runtimes including Node.js, Deno, Bun, and Cloudflare Workers.
17
+ You can install it via `npm`, `deno`, or `bun`:
18
18
 
19
19
  ```bash
20
20
  # Node.js, Cloudflare Workers, or other node-compatible runtimes
21
21
  npm i mono-jsx
22
+
22
23
  # Deno
23
24
  deno add npm:mono-jsx
25
+
24
26
  # Bun
25
27
  bun add mono-jsx
26
28
  ```
27
29
 
28
30
  ## Setup JSX Runtime
29
31
 
30
- To use mono-jsx as JSX runtime, add the following configuration to your `tsconfig.json`(`deno.json` for Deno):
32
+ To use mono-jsx as your JSX runtime, add the following configuration to your `tsconfig.json` (or `deno.json` for Deno):
31
33
 
32
34
  ```jsonc
33
35
  {
@@ -38,70 +40,70 @@ To use mono-jsx as JSX runtime, add the following configuration to your `tsconfi
38
40
  }
39
41
  ```
40
42
 
41
- Alternatively, you can also use pragma directive in your JSX file.
43
+ Alternatively, you can use a pragma directive in your JSX file:
42
44
 
43
45
  ```js
44
46
  /** @jsxImportSource mono-jsx */
45
47
  ```
46
48
 
47
- You can also run `mono-jsx setup` to automatically add the configuration to your `tsconfig.json` or `deno.json`.
49
+ You can also run `mono-jsx setup` to automatically add the configuration to your project:
48
50
 
49
51
  ```bash
50
52
  # Node.js, Cloudflare Workers, or other node-compatible runtimes
51
53
  npx mono-jsx setup
54
+
52
55
  # Deno
53
- deno run npm:mono-jsx setup
56
+ deno run -A npm:mono-jsx setup
57
+
54
58
  # Bun
55
59
  bunx mono-jsx setup
56
60
  ```
57
61
 
58
62
  ## Usage
59
63
 
60
- mono-jsx allows you to return an `<html>` JSX element as a `Response` object in the `fetch` handler.
64
+ mono-jsx allows you to return an `<html>` JSX element as a `Response` object in the `fetch` handler:
61
65
 
62
66
  ```tsx
63
67
  // app.tsx
64
-
65
68
  export default {
66
69
  fetch: (req) => (
67
70
  <html>
68
- <h1>Hello World!</h1>
71
+ <h1>Welcome to mono-jsx!</h1>
69
72
  </html>
70
73
  ),
71
74
  };
72
75
  ```
73
76
 
74
- For Deno/Bun users, you can run the `app.tsx` directly.
77
+ For Deno/Bun users, you can run the `app.tsx` directly:
75
78
 
76
79
  ```bash
77
80
  deno serve app.tsx
78
81
  bun run app.tsx
79
82
  ```
80
83
 
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.
84
+ If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start local development:
82
85
 
83
86
  ```bash
84
87
  npx wrangler dev app.tsx
85
88
  ```
86
89
 
87
- **Node.js does not support JSX syntax and declarative fetch server**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev/).
90
+ **Node.js doesn't support JSX syntax or declarative fetch servers**, so we recommend using mono-jsx with [srvx](https://srvx.h3.dev/):
88
91
 
89
92
  ```tsx
90
93
  // app.tsx
91
-
92
94
  import { serve } from "srvx";
93
95
 
94
96
  serve({
95
97
  port: 3000,
96
98
  fetch: (req) => (
97
99
  <html>
98
- <h1>Hello World!</h1>
100
+ <h1>Welcome to mono-jsx!</h1>
99
101
  </html>
100
102
  ),
101
103
  });
102
104
  ```
103
105
 
104
- and you will need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step.
106
+ You'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
105
107
 
106
108
  ```bash
107
109
  npx tsx app.tsx
@@ -109,27 +111,33 @@ npx tsx app.tsx
109
111
 
110
112
  ## Using JSX
111
113
 
112
- mono-jsx uses [**JSX**](https://react.dev/learn/describing-the-ui) to describe the user interface, similar to React but with some differences.
114
+ mono-jsx uses [**JSX**](https://react.dev/learn/describing-the-ui) to describe the user interface, similar to React but with key differences.
113
115
 
114
116
  ### Using Standard HTML Property Names
115
117
 
116
- mono-jsx adopts standard HTML property names, avoiding React's custom property naming conventions.
118
+ mono-jsx adopts standard HTML property names, avoiding React's custom naming conventions:
117
119
 
118
- - `className` becomes `class`
119
- - `htmlFor` becomes `for`
120
- - `onChange` becomes `onInput`
120
+ - `className` `class`
121
+ - `htmlFor` `for`
122
+ - `onChange` `onInput`
121
123
 
122
- ### Composition `class`
124
+ ### Composition with `class`
123
125
 
124
- mono-jsx allows you to compose the `class` property using an array of strings, objects, or expressions.
126
+ mono-jsx allows you to compose the `class` property using arrays of strings, objects, or expressions:
125
127
 
126
128
  ```jsx
127
- <div class={["container box", isActive && "active", { hover: isHover }]} />;
129
+ <div
130
+ class={[
131
+ "container box",
132
+ isActive && "active",
133
+ { hover: isHover },
134
+ ]}
135
+ />;
128
136
  ```
129
137
 
130
- ### Using Pseudo Classes and Media Queries in the `style` Property
138
+ ### Using Pseudo Classes and Media Queries in `style`
131
139
 
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.
140
+ mono-jsx supports [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
141
 
134
142
  ```jsx
135
143
  <a
@@ -148,14 +156,16 @@ mono-jsx allows you to use [pseudo classes](https://developer.mozilla.org/en-US/
148
156
 
149
157
  ### `<slot>` Element
150
158
 
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` property). Plus, you also can add the `name` attribute to define a named slot.
159
+ mono-jsx uses [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) elements to render slotted content (equivalent to React's `children` property). You can also add the `name` attribute to define named slots:
152
160
 
153
161
  ```jsx
154
162
  function Container() {
155
163
  return (
156
164
  <div class="container">
157
- <slot /> {/* <h1>Hello world!</h1> */}
158
- <slot name="desc" /> {/* <p>This is a description.</p> */}
165
+ {/* Default slot */}
166
+ <slot />
167
+ {/* Named slot */}
168
+ <slot name="desc" />
159
169
  </div>
160
170
  );
161
171
  }
@@ -163,7 +173,9 @@ function Container() {
163
173
  function App() {
164
174
  return (
165
175
  <Container>
176
+ {/* This goes to the named slot */}
166
177
  <p slot="desc">This is a description.</p>
178
+ {/* This goes to the default slot */}
167
179
  <h1>Hello world!</h1>
168
180
  </Container>
169
181
  );
@@ -172,7 +184,7 @@ function App() {
172
184
 
173
185
  ### `html` Tag Function
174
186
 
175
- mono-jsx doesn't support the `dangerouslySetInnerHTML` property, instead, it provides a `html` tag function to render raw HTML in JSX.
187
+ mono-jsx provides an `html` tag function to render raw HTML in JSX instead of React's `dangerouslySetInnerHTML`:
176
188
 
177
189
  ```jsx
178
190
  function App() {
@@ -180,8 +192,7 @@ function App() {
180
192
  }
181
193
  ```
182
194
 
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.
195
+ The `html` tag function is globally available without importing. You can also use `css` and `js` tag functions for CSS and JavaScript:
185
196
 
186
197
  ```jsx
187
198
  function App() {
@@ -195,31 +206,38 @@ function App() {
195
206
  ```
196
207
 
197
208
  > [!WARNING]
198
- > the `html` tag function is **unsafe** that can cause [**XSS**](https://en.wikipedia.org/wiki/Cross-site_scripting) vulnerabilities.
209
+ > The `html` tag function is **unsafe** and can cause [**XSS**](https://en.wikipedia.org/wiki/Cross-site_scripting) vulnerabilities.
199
210
 
200
211
  ### Event Handlers
201
212
 
202
- mono-jsx allows you to write event handlers directly in the JSX code, like React.
213
+ mono-jsx lets you write event handlers directly in JSX, similar to React:
203
214
 
204
215
  ```jsx
205
216
  function Button() {
206
- return <button onClick={(evt) => alert("BOOM!")}>Click Me</button>;
217
+ return (
218
+ <button onClick={(evt) => alert("BOOM!")}>
219
+ Click Me
220
+ </button>
221
+ );
207
222
  }
208
223
  ```
209
224
 
210
225
  > [!NOTE]
211
- > the event handler would never be called in server-side. 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.**
226
+ > Event handlers are never called on the server-side. They're serialized to strings and sent to the client. **This means you should NOT use server-side variables or functions in event handlers.**
212
227
 
213
228
  ```tsx
229
+ import { doSomething } from "some-library";
230
+
214
231
  function Button(this: FC, props: { role: string }) {
215
232
  let message = "BOOM!";
216
- console.log(message); // only print message in server-side
233
+ console.log(message); // only executes on server-side
217
234
  return (
218
235
  <button
219
236
  role={props.role}
220
237
  onClick={(evt) => {
221
238
  alert(message); // ❌ `message` is a server-side variable
222
239
  console.log(props.role); // ❌ `props` is a server-side variable
240
+ doSomething(); // ❌ `doSomething` is imported on the server-side
223
241
  Deno.exit(0); // ❌ `Deno` is unavailable in the browser
224
242
  document.title = "BOOM!"; // ✅ `document` is a browser API
225
243
  console.log(evt.target); // ✅ `evt` is the event object
@@ -232,35 +250,58 @@ function Button(this: FC, props: { role: string }) {
232
250
  }
233
251
  ```
234
252
 
235
- Plus, mono-jsx supports the `mount` event that will be triggered when the element is mounted in the client-side.
253
+ Additionally, mono-jsx supports the `mount` event for when elements are mounted in the client-side DOM:
236
254
 
237
255
  ```jsx
238
256
  function App() {
239
257
  return (
240
258
  <div onMount={(evt) => console.log(evt.target, "Mounted!")}>
241
- <h1>Hello World!</h1>
259
+ <h1>Welcome to mono-jsx!</h1>
242
260
  </div>
243
261
  );
244
262
  }
245
263
  ```
246
264
 
247
- ## Using State
265
+ mono-jsx also accepts functions for the `action` property on `form` elements, which will be called on form submission:
266
+
267
+ ```tsx
268
+ function App() {
269
+ return (
270
+ <form
271
+ action={(data: FormData, event: SubmitEvent) => {
272
+ event.preventDefault(); // true
273
+ console.log(data.get("name"));
274
+ }}
275
+ >
276
+ <input type="text" name="name" />
277
+ <button type="submit">Submit</button>
278
+ </form>
279
+ );
280
+ }
281
+ ```
282
+
283
+ ## Reactive
284
+
285
+ mono-jsx provides a minimal state runtime for updating the view based on client-side state changes:
286
+
287
+ ### Using State
248
288
 
249
- mono-jsx provides a minimal state runtime that allows you to update view based on state changes in client-side.
289
+ You can use `this` to define state in your components. The view will automatically update when the state changes:
250
290
 
251
291
  ```tsx
252
292
  function Counter(
253
293
  this: FC<{ count: number }>,
254
294
  props: { initialCount?: number },
255
295
  ) {
296
+ // Initialize state
256
297
  this.count = props.initialCount ?? 0;
298
+
257
299
  return (
258
300
  <div>
259
- {/* use the state */}
301
+ {/* render state */}
260
302
  <span>{this.count}</span>
261
- {/* use computed state */}
262
- <span>doubled: {this.computed(() => 2 * this.count)}</span>
263
- {/* update the state in event handlers */}
303
+
304
+ {/* Update state to trigger re-render */}
264
305
  <button onClick={() => this.count--}>-</button>
265
306
  <button onClick={() => this.count++}>+</button>
266
307
  </div>
@@ -268,11 +309,36 @@ function Counter(
268
309
  }
269
310
  ```
270
311
 
271
- > [!WARNING]
272
- > The state cannot be used in an arrow function component, you should use a `function` declaration instead.
312
+ ### Using Computed Properties
273
313
 
274
- ```jsx
275
- // ❌ `this.count++` won't update the view, please use a function declaration instead
314
+ You can use `this.computed` to create computed properties based on state. The computed property will automatically update when the state changes:
315
+
316
+ ```tsx
317
+ function App(this: FC<{ input: string }>) {
318
+ this.input = 'Hello, world';
319
+ return (
320
+ <div>
321
+ <h1>{this.computed(() = this.input + "!")}</h1>
322
+
323
+ <form
324
+ action={(data: FormData ) => {
325
+ this.input = data.get("input") as string;
326
+ }}
327
+ >
328
+ <input type="text" name="input" value={this.input} />
329
+ <button type="submit">Submit</button>
330
+ </form>
331
+ </div>
332
+ );
333
+ }
334
+ ```
335
+
336
+ ### Limitation of States
337
+
338
+ 1\. States cannot be used in arrow function components.
339
+
340
+ ```tsx
341
+ // ❌ Won't work - state updates won't refresh the view
276
342
  const App = () => {
277
343
  this.count = 0;
278
344
  return (
@@ -282,40 +348,87 @@ const App = () => {
282
348
  </div>
283
349
  );
284
350
  };
351
+
352
+ // ✅ Works correctly
353
+ function App(this: FC) {
354
+ this.count = 0;
355
+ return (
356
+ <div>
357
+ <span>{this.count}</span>
358
+ <button onClick={() => this.count++}>+</button>
359
+ </div>
360
+ );
361
+ }
362
+ ```
363
+
364
+ 2\. States cannot be computed outside of the `this.computed` method.
365
+
366
+ ```tsx
367
+ // ❌ Won't work - state updates won't refresh the view
368
+ function App(this: FC<{ message: string }>) {
369
+ this.message = "Hello, world";
370
+ return (
371
+ <div>
372
+ <h1 title={this.message + "!"}>{this.message + "!"}</h1>
373
+ <button onClick={() => this.message = "Clicked"}>
374
+ Click Me
375
+ </button>
376
+ </div>
377
+ );
378
+ }
379
+
380
+ // ✅ Works correctly
381
+ function App(this: FC) {
382
+ this.message = "Hello, world";
383
+ return (
384
+ <div>
385
+ <h1 title={this.computed(() => this.message + "!")}>{this.computed(() => this.message + "!")}</h1>
386
+ <button onClick={() => this.message = "Clicked"}>
387
+ Click Me
388
+ </button>
389
+ </div>
390
+ );
391
+ }
285
392
  ```
286
393
 
287
394
  ## Built-in Elements
288
395
 
289
- mono-jsx provides some built-in elements to help you build your app.
396
+ mono-jsx provides built-in elements to help you build reactive UIs.
290
397
 
291
398
  ### `<toggle>` element
292
399
 
293
- `<toggle>` element allows you to toggle the visibility of the slotted content.
400
+ The `<toggle>` element conditionally renders content based on a boolean value:
294
401
 
295
402
  ```tsx
296
403
  function App(this: FC<{ show: boolean }>) {
297
404
  this.show = false;
405
+
406
+ function toggle() {
407
+ this.show = !this.show;
408
+ }
409
+
298
410
  return (
299
411
  <div>
300
412
  <toggle value={this.show}>
301
- <h1>Hello World!</h1>
413
+ <h1>Welcome to mono-jsx!</h1>
302
414
  </toggle>
303
- <button onClick={toggle}>{this.computed(() => this.show ? "Hide" : "Show")}</button>
415
+
416
+ <button onClick={toggle}>
417
+ {this.computed(() => this.show ? "Hide" : "Show")}
418
+ </button>
304
419
  </div>
305
420
  );
306
- function toggle() {
307
- this.show = !this.show;
308
- }
309
421
  }
310
422
  ```
311
423
 
312
424
  ### `<switch>` element
313
425
 
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.
426
+ The `<switch>` element renders different content based on a value. Elements with matching `slot` attributes are displayed when their value matches, otherwise default content is shown:
315
427
 
316
428
  ```tsx
317
429
  function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
318
430
  this.lang = "en";
431
+
319
432
  return (
320
433
  <div>
321
434
  <switch value={this.lang}>
@@ -323,6 +436,7 @@ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
323
436
  <h1 slot="zh">你好,世界!</h1>
324
437
  <h1>✋🌎❗️</h1>
325
438
  </switch>
439
+
326
440
  <button onClick={() => this.lang = "en"}>English</button>
327
441
  <button onClick={() => this.lang = "zh"}>中文</button>
328
442
  <button onClick={() => this.lang = "emoji"}>Emoji</button>
@@ -333,10 +447,10 @@ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
333
447
 
334
448
  ## Streaming Rendering
335
449
 
336
- mono-jsx renders your `<html>` as a readable stream, that allows async function components are rendered asynchronously. You can set a `placeholder` attribute to show a loading state while the async component is loading.
450
+ mono-jsx renders your `<html>` as a readable stream, allowing async components to render asynchronously. You can use `placeholder` to display a loading state while waiting for async components to render:
337
451
 
338
452
  ```jsx
339
- async function Sleep(ms) {
453
+ async function Sleep({ ms }) {
340
454
  await new Promise((resolve) => setTimeout(resolve, ms));
341
455
  return <slot />;
342
456
  }
@@ -344,7 +458,8 @@ async function Sleep(ms) {
344
458
  export default {
345
459
  fetch: (req) => (
346
460
  <html>
347
- <h1>Hello World!</h1>
461
+ <h1>Welcome to mono-jsx!</h1>
462
+
348
463
  <Sleep ms={1000} placeholder={<p>Sleeping...</p>}>
349
464
  <p>After 1 second</p>
350
465
  </Sleep>
@@ -353,10 +468,10 @@ export default {
353
468
  };
354
469
  ```
355
470
 
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.
471
+ You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
357
472
 
358
473
  ```jsx
359
- async function Sleep(ms) {
474
+ async function Sleep({ ms }) {
360
475
  await new Promise((resolve) => setTimeout(resolve, ms));
361
476
  return <slot />;
362
477
  }
@@ -364,7 +479,8 @@ async function Sleep(ms) {
364
479
  export default {
365
480
  fetch: (req) => (
366
481
  <html>
367
- <h1>Hello World!</h1>
482
+ <h1>Welcome to mono-jsx!</h1>
483
+
368
484
  <Sleep ms={1000} rendering="eager">
369
485
  <p>After 1 second</p>
370
486
  </Sleep>
@@ -373,9 +489,36 @@ export default {
373
489
  };
374
490
  ```
375
491
 
492
+ ## Using Context
493
+
494
+ You can use the `context` property in `this` to access context values in your components. The context is defined on the root `<html>` element:
495
+
496
+ ```tsx
497
+ function Dash(this: FC<{}, { auth: { uuid: string; name: string } }>) {
498
+ const { auth } = this.context;
499
+ return (
500
+ <div>
501
+ <h1>Welcome back, {auth.name}!</h1>;
502
+ <p>Your UUID is {auth.uuid}</p>
503
+ </div>
504
+ );
505
+ }
506
+
507
+ export default {
508
+ fetch: (req) => {
509
+ const auth = doAuth(req);
510
+ return (
511
+ <html context={{ auth }} request={req}>
512
+ <Dash />
513
+ </html>
514
+ );
515
+ },
516
+ };
517
+ ```
518
+
376
519
  ## Accessing Request Info
377
520
 
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.
521
+ You can access request information in components via the `request` property in `this` which is set on the root `<html>` element:
379
522
 
380
523
  ```tsx
381
524
  function RequestInfo(this: FC) {
@@ -401,7 +544,7 @@ export default {
401
544
 
402
545
  ## Customizing Response
403
546
 
404
- You can add `status` or `headers` attribute to the `<html>` element to customize the response.
547
+ Add `status` or `headers` attributes to the root `<html>` element to customize the response:
405
548
 
406
549
  ```jsx
407
550
  export default {
package/bin/mono-jsx CHANGED
@@ -1,34 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { existsSync } from "node:fs";
3
4
  import { readFile, writeFile } from "node:fs/promises";
4
5
  import process from "node:process";
5
6
 
6
7
  switch (process.argv[2]) {
7
8
  case "setup":
8
- setup().then(() => {
9
- console.log("✅ JSX runtime setup complete.");
10
- });
9
+ setup()
11
10
  break;
12
11
  default:
13
12
  process.exit(0);
14
13
  }
15
14
 
16
15
  async function setup() {
17
- let tsConfigPath = globalThis.Deno ? "deno.json" : "tsconfig.json";
16
+ if (globalThis.Deno && existsSync("deno.jsonc")) {
17
+ console.log("Please add the following options to your deno.jsonc file:");
18
+ console.log(
19
+ [
20
+ `{`,
21
+ ` "compilerOptions": {`,
22
+ ` %c"jsx": "react-jsx",`,
23
+ ` "jsxImportSource": "mono-jsx",%c`,
24
+ ` }`,
25
+ `}`,
26
+ ].join("\n"),
27
+ "color:green",
28
+ "",
29
+ );
30
+ return;
31
+ }
32
+ let tsConfigFilename = globalThis.Deno ? "deno.json" : "tsconfig.json";
18
33
  let tsConfig = Object.create(null);
19
34
  try {
20
- const json = await readFile(tsConfigPath, "utf8");
21
- tsConfig = JSON.parse(json);
35
+ const data = await readFile(tsConfigFilename, "utf8");
36
+ tsConfig = JSON.parse(data);
22
37
  } catch {
23
38
  // ignore
24
39
  }
25
- tsConfig.compilerOptions = {
26
- ...tsConfig.compilerOptions,
27
- jsx: "react-jsx",
28
- jsxImportSource: "mono-jsx",
29
- };
40
+ const compilerOptions = tsConfig.compilerOptions ?? (tsConfig.compilerOptions = {});
41
+ if (compilerOptions.jsx === "react-jsx" && compilerOptions.jsxImportSource === "mono-jsx") {
42
+ console.log("%cmono-jsx already setup.","color:grey");
43
+ return;
44
+ }
30
45
  if (!globalThis.Deno) {
31
- tsConfig.compilerOptions.alllowJs = true
46
+ compilerOptions.module ??= "es2022";
47
+ compilerOptions.moduleResolution ??= "bundler";
32
48
  }
33
- await writeFile(tsConfigPath, JSON.stringify(tsConfig, null, 2));
49
+ compilerOptions.jsx = "react-jsx";
50
+ compilerOptions.jsxImportSource = "mono-jsx";
51
+ await writeFile(tsConfigFilename, JSON.stringify(tsConfig, null, 2));
52
+ console.log("✅ mono-jsx setup complete.")
34
53
  }
package/jsx-runtime.mjs CHANGED
@@ -6,7 +6,7 @@ var $state = Symbol.for("mono.state");
6
6
  var $computed = Symbol.for("mono.computed");
7
7
 
8
8
  // state.ts
9
- function createState(request) {
9
+ function createState(context, request) {
10
10
  let collectDeps;
11
11
  const computed = (fn) => {
12
12
  const deps = /* @__PURE__ */ Object.create(null);
@@ -18,24 +18,28 @@ function createState(request) {
18
18
  };
19
19
  return new Proxy(/* @__PURE__ */ Object.create(null), {
20
20
  get(target, key, receiver) {
21
- const value = Reflect.get(target, key, receiver);
22
- if (key === "request") {
23
- if (!request) {
24
- throw new Error("request is not defined");
21
+ switch (key) {
22
+ case "context":
23
+ return context ?? {};
24
+ case "request":
25
+ if (!request) {
26
+ throw new Error("request is not defined");
27
+ }
28
+ return request;
29
+ case "computed":
30
+ return computed;
31
+ default: {
32
+ const value = Reflect.get(target, key, receiver);
33
+ if (typeof key === "symbol") {
34
+ return value;
35
+ }
36
+ if (collectDeps) {
37
+ collectDeps(key, value);
38
+ return value;
39
+ }
40
+ return [$state, { key, value }, $vnode];
25
41
  }
26
- return request;
27
- }
28
- if (key === "computed") {
29
- return computed;
30
42
  }
31
- if (typeof key === "symbol") {
32
- return value;
33
- }
34
- if (collectDeps) {
35
- collectDeps(key, value);
36
- return value;
37
- }
38
- return [$state, { key, value }, $vnode];
39
43
  },
40
44
  set(target, key, value, receiver) {
41
45
  const vt = typeof value;
@@ -46,9 +50,16 @@ function createState(request) {
46
50
  }
47
51
 
48
52
  // runtime/index.ts
49
- var RUNTIME_STATE_JS = "const p=(e,i)=>e.getAttribute(i),m=(e,i)=>e.hasAttribute(i),M=new Map,T=e=>M.get(e)??M.set(e,L()).get(e);function L(){const e=Object.create(null),i=new Map;function f(n,o){let a=o;Object.defineProperty(e,n,{get:()=>a,set:u=>{if(u!==a){const r=i.get(n);r&&queueMicrotask(()=>r.forEach(s=>s())),a=u}}})}function d(n,o,a,u){let r;if(o==='toggle'){let s;r=()=>{if(!s){const t=n.firstElementChild;t&&t.tagName==='TEMPLATE'&&m(t,'m-slot')?(s=t.content.childNodes,n.innerHTML=''):s=n.childNodes}a()?n.append(...s):n.innerHTML=''}}else if(o==='switch'){let s=p(n,'match'),t,l,E=c=>t.get(c)??t.set(c,[]).get(c),h;r=()=>{if(!t){t=new Map,l=[];for(const c of n.childNodes)if(c.nodeType===1&&c.tagName==='TEMPLATE'&&m(c,'m-slot')){for(const g of c.content.childNodes)g.nodeType===1&&m(g,'slot')?E(p(g,'slot')).push(g):l.push(g);c.remove()}else s?E(s).push(c):l.push(c)}h=a(),n.innerHTML='',n.append(...t.has(h)?t.get(h):l)}}else if(o&&o.length>2&&o.startsWith('[')&&o.endsWith(']')){let s=o.slice(1,-1),t=n.parentElement;t.tagName==='M-GROUP'&&(t=t.previousElementSibling),r=()=>{const l=a();l===!1?t.removeAttribute(s):(s==='class'||s==='style')&&l&&typeof l=='object'?t.setAttribute(s,s==='class'?cx(l):styleToCSS(l)):t.setAttribute(s,l===!0?'':''+l)}}else r=()=>n.textContent=''+a();if(r)for(const s of u){let t=i.get(s);t||(t=[],i.set(s,t)),t.push(r)}}return{store:e,define:f,createEffect:d}}customElements.define('m-state',class extends HTMLElement{connectedCallback(){const e=this,i=p(e,'mode'),f=p(e,'key'),d=T(p(e,'fc'));f?d.createEffect(e,i,()=>d.store[f],[f]):m(e,'computed')&&setTimeout(()=>{const n=e.firstChild;if(n&&n.nodeType===1&&n.type==='computed'){const o=n.textContent;o&&new Function('$',o).call(d.store,(a,u)=>d.createEffect(e,i,a,u))}})}}),Object.assign(globalThis,{$state:e=>T(e).store,$defineState:(e,i)=>{const f=e.indexOf(':');f>0&&T(e.slice(0,f)).define(e.slice(f+1),i)}});";
50
- var RUNTIME_SUSPENSE_JS = "const n={},o=e=>e.getAttribute('chunk-id');c('m-portal',e=>{n[o(e)]=e}),c('m-chunk',e=>{const t=o(e),s=n[t];s&&setTimeout(()=>{s.replaceWith(...e.firstChild.content.childNodes),delete n[t],e.remove()})});function c(e,t){customElements.define(e,class extends HTMLElement{connectedCallback(){t(this)}})}";
51
- var RUNTIME_COMPONENTS_JS = { "cx": "var cx=(()=>{var e=r=>typeof r=='string',n=r=>typeof r=='object'&&r!==null;function t(r){return e(r)?r:n(r)?Array.isArray(r)?r.map(t).filter(Boolean).join(' '):Object.entries(r).filter(([,o])=>!!o).map(([o])=>o).join(' '):''}return t;})();", "styleToCSS": "var styleToCSS=(()=>{var a=new Set(['animation-iteration-count','aspect-ratio','border-image-outset','border-image-slice','border-image-width','box-flex-group','box-flex','box-ordinal-group','column-count','columns','fill-opacity','flex-grow','flex-negative','flex-order','flex-positive','flex-shrink','flex','flood-opacity','font-weight','grid-area','grid-column-end','grid-column-span','grid-column-start','grid-column','grid-row-end','grid-row-span','grid-row-start','grid-row','line-clamp','line-height','opacity','order','orphans','stop-opacity','stroke-dasharray','stroke-dashoffset','stroke-miterlimit','stroke-opacity','stroke-width','tab-size','widows','z-index','zoom']),s=r=>typeof r=='string',c=r=>typeof r=='object'&&r!==null,f=r=>r.replace(/[a-z][A-Z]/g,t=>t.charAt(0)+'-'+t.charAt(1).toLowerCase());function u(r){if(s(r))return r;if(!c(r))return'';let t='';for(let[n,o]of Array.isArray(r)?r:Object.entries(r)){if(o==null||o===!1||Number.isNaN(o)||!s(n))return'';let e=f(n),i=typeof o=='number'?a.has(e)?''+o:o+'px':''+o;t+=(t!==''?';':'')+e+':'+(e==='content'?JSON.stringify(i):i)}return t}return u;})();", "event": "window.$emit=(evt,el,fn,fc)=>fn.call(window.$state?.(fc)??el,evt);window.$onsubmit=(evt,el,fn,fc)=>{evt.preventDefault();fn.call(window.$state?.(fc)??el,new FormData(el),evt)};" };
53
+ var RUNTIME_STATE_JS = `const p=(e,i)=>e.getAttribute(i),m=(e,i)=>e.hasAttribute(i),M=new Map,T=e=>M.get(e)??M.set(e,L()).get(e);function L(){const e=Object.create(null),i=new Map;function f(n,o){let a=o;Object.defineProperty(e,n,{get:()=>a,set:u=>{if(u!==a){const r=i.get(n);r&&queueMicrotask(()=>r.forEach(s=>s())),a=u}}})}function d(n,o,a,u){let r;if(o==="toggle"){let s;r=()=>{if(!s){const t=n.firstElementChild;t&&t.tagName==="TEMPLATE"&&m(t,"m-slot")?(s=t.content.childNodes,n.innerHTML=""):s=n.childNodes}a()?n.append(...s):n.innerHTML=""}}else if(o==="switch"){let s=p(n,"match"),t,l,E=c=>t.get(c)??t.set(c,[]).get(c),h;r=()=>{if(!t){t=new Map,l=[];for(const c of n.childNodes)if(c.nodeType===1&&c.tagName==="TEMPLATE"&&m(c,"m-slot")){for(const g of c.content.childNodes)g.nodeType===1&&m(g,"slot")?E(p(g,"slot")).push(g):l.push(g);c.remove()}else s?E(s).push(c):l.push(c)}h=a(),n.innerHTML="",n.append(...t.has(h)?t.get(h):l)}}else if(o&&o.length>2&&o.startsWith("[")&&o.endsWith("]")){let s=o.slice(1,-1),t=n.parentElement;t.tagName==="M-GROUP"&&(t=t.previousElementSibling),r=()=>{const l=a();l===!1?t.removeAttribute(s):(s==="class"||s==="style")&&l&&typeof l=="object"?t.setAttribute(s,s==="class"?cx(l):styleToCSS(l)):t.setAttribute(s,l===!0?"":""+l)}}else r=()=>n.textContent=""+a();if(r)for(const s of u){let t=i.get(s);t||(t=[],i.set(s,t)),t.push(r)}}return{store:e,define:f,createEffect:d}}customElements.define("m-state",class extends HTMLElement{connectedCallback(){const e=this,i=p(e,"mode"),f=p(e,"key"),d=T(p(e,"fc"));f?d.createEffect(e,i,()=>d.store[f],[f]):m(e,"computed")&&setTimeout(()=>{const n=e.firstChild;if(n&&n.nodeType===1&&n.type==="computed"){const o=n.textContent;o&&new Function("$",o).call(d.store,(a,u)=>d.createEffect(e,i,a,u))}})}}),Object.assign(globalThis,{$state:e=>T(e).store,$defineState:(e,i)=>{const f=e.indexOf(":");f>0&&T(e.slice(0,f)).define(e.slice(f+1),i)}});`;
54
+ var RUNTIME_SUSPENSE_JS = `const n={},o=e=>e.getAttribute("chunk-id");c("m-portal",e=>{n[o(e)]=e}),c("m-chunk",e=>{const t=o(e),s=n[t];s&&setTimeout(()=>{s.replaceWith(...e.firstChild.content.childNodes),delete n[t],e.remove()})});function c(e,t){customElements.define(e,class extends HTMLElement{connectedCallback(){t(this)}})}`;
55
+ var RUNTIME_COMPONENTS_JS = {
56
+ /** cx.js (239 bytes) */
57
+ cx: `var cx=(()=>{var n=e=>typeof e=="string",o=e=>typeof e=="object"&&e!==null;function t(e){return n(e)?e:o(e)?Array.isArray(e)?e.map(t).filter(Boolean).join(" "):Object.entries(e).filter(([,r])=>!!r).map(([r])=>r).join(" "):""}return t;})();`,
58
+ /** styleToCSS.js (1203 bytes) */
59
+ styleToCSS: `var styleToCSS=(()=>{var c=new Set(["animation-iteration-count","aspect-ratio","border-image-outset","border-image-slice","border-image-width","box-flex-group","box-flex","box-ordinal-group","column-count","columns","fill-opacity","flex-grow","flex-negative","flex-order","flex-positive","flex-shrink","flex","flood-opacity","font-weight","grid-area","grid-column-end","grid-column-span","grid-column-start","grid-column","grid-row-end","grid-row-span","grid-row-start","grid-row","line-clamp","line-height","opacity","order","orphans","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-miterlimit","stroke-opacity","stroke-width","tab-size","widows","z-index","zoom"]),s=e=>typeof e=="string",u=e=>typeof e=="object"&&e!==null,f=e=>e.replace(/[a-z][A-Z]/g,r=>r.charAt(0)+"-"+r.charAt(1).toLowerCase());function l(e){if(s(e))return e;if(u(e)){let r="";for(let[o,t]of Array.isArray(e)?e:Object.entries(e)){if(t==null||t===!1||Number.isNaN(t)||!s(o))return"";let n=f(o),i=typeof t=="number"?c.has(n)?""+t:t+"px":a(""+t);r+=(r!==""?";":"")+a(n)+":"+(n==="content"?JSON.stringify(i):i)}return r}return""}function a(e){return e.replace(/["<>]/g,r=>r==="<"?"&lt;":r===">"?"&gt;":"'")}return l;})();`,
60
+ /** event.js (176 bytes) */
61
+ event: `window.$emit=(evt,el,fn,fc)=>fn.call(window.$state?.(fc)??el,evt);window.$onsubmit=(evt,el,fn,fc)=>{evt.preventDefault();fn.call(window.$state?.(fc)??el,new FormData(el),evt)};`
62
+ };
52
63
 
53
64
  // runtime/utils.ts
54
65
  var cssBareUnitProps = /* @__PURE__ */ new Set([
@@ -112,26 +123,75 @@ function cx(className) {
112
123
  return "";
113
124
  }
114
125
  function styleToCSS(style) {
115
- if (isString(style)) return style;
116
- if (!isObject(style)) return "";
117
- let css = "";
118
- for (const [k, v] of Array.isArray(style) ? style : Object.entries(style)) {
119
- if (v === null || v === void 0 || v === false || Number.isNaN(v) || !isString(k)) return "";
120
- const cssKey = toHyphenCase(k);
121
- const cssValue = typeof v === "number" ? cssBareUnitProps.has(cssKey) ? "" + v : v + "px" : "" + v;
122
- css += (css !== "" ? ";" : "") + cssKey + ":" + (cssKey === "content" ? JSON.stringify(cssValue) : cssValue);
126
+ if (isString(style)) {
127
+ return style;
128
+ }
129
+ if (isObject(style)) {
130
+ let css = "";
131
+ for (const [k, v] of Array.isArray(style) ? style : Object.entries(style)) {
132
+ if (v === null || v === void 0 || v === false || Number.isNaN(v) || !isString(k)) return "";
133
+ const cssKey = toHyphenCase(k);
134
+ const cssValue = typeof v === "number" ? cssBareUnitProps.has(cssKey) ? "" + v : v + "px" : escapeCSSText("" + v);
135
+ css += (css !== "" ? ";" : "") + escapeCSSText(cssKey) + ":" + (cssKey === "content" ? JSON.stringify(cssValue) : cssValue);
136
+ }
137
+ return css;
138
+ }
139
+ return "";
140
+ }
141
+ function escapeCSSText(str) {
142
+ return str.replace(/["<>]/g, (m) => {
143
+ if (m === "<") return "&lt;";
144
+ if (m === ">") return "&gt;";
145
+ return "'";
146
+ });
147
+ }
148
+ var regexpHtmlSafe = /["'&<>]/;
149
+ function escapeHTML(str) {
150
+ if (typeof Bun === "object" && "escapeHTML" in Bun) return Bun.escapeHTML(str);
151
+ const match = regexpHtmlSafe.exec(str);
152
+ if (!match) {
153
+ return str;
154
+ }
155
+ let escape;
156
+ let index;
157
+ let lastIndex = 0;
158
+ let html2 = "";
159
+ for (index = match.index; index < str.length; index++) {
160
+ switch (str.charCodeAt(index)) {
161
+ case 34:
162
+ escape = "&quot;";
163
+ break;
164
+ case 38:
165
+ escape = "&amp;";
166
+ break;
167
+ case 39:
168
+ escape = "&#x27;";
169
+ break;
170
+ case 60:
171
+ escape = "&lt;";
172
+ break;
173
+ case 62:
174
+ escape = "&gt;";
175
+ break;
176
+ default:
177
+ continue;
178
+ }
179
+ if (lastIndex !== index) {
180
+ html2 += str.slice(lastIndex, index);
181
+ }
182
+ lastIndex = index + 1;
183
+ html2 += escape;
123
184
  }
124
- return css;
185
+ return lastIndex !== index ? html2 + str.slice(lastIndex, index) : html2;
125
186
  }
126
187
 
127
188
  // render.ts
128
189
  var encoder = new TextEncoder();
129
190
  var regexpHtmlTag = /^[a-z][\w\-$]*$/;
130
- var regexpHtmlSafe = /["'&<>]/;
131
191
  var selfClosingTags = new Set("area,base,br,col,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(","));
132
- var toAttrStringLit = (str) => JSON.stringify(escapeHTML(str));
133
192
  var isVNode = (v) => Array.isArray(v) && v.length === 3 && v[2] === $vnode;
134
193
  var hashCode = (s) => [...s].reduce((hash, c) => Math.imul(31, hash) + c.charCodeAt(0) | 0, 0);
194
+ var toAttrStringLit = (str) => '"' + escapeHTML(str).replaceAll('"', '\\"') + '"';
135
195
  async function renderNode(ctx, node, stripSlotProp) {
136
196
  const { write, stateStore } = ctx;
137
197
  switch (typeof node) {
@@ -185,7 +245,7 @@ async function renderNode(ctx, node, stripSlotProp) {
185
245
  }
186
246
  break;
187
247
  }
188
- const fcIndex = toString36(ctx.index.fc);
248
+ const fcIndex = ctx.index.fc.toString(36);
189
249
  if (tag === $state) {
190
250
  const { key, value } = props;
191
251
  write('<m-state fc="' + fcIndex + '" key=' + toAttrStringLit(key) + ">");
@@ -319,13 +379,13 @@ async function renderNode(ctx, node, stripSlotProp) {
319
379
  eager = true;
320
380
  }
321
381
  try {
322
- const v = tag.call(createState(ctx.request), fcProps);
382
+ const v = tag.call(createState(ctx.context, ctx.request), fcProps);
323
383
  ctx.index.fc++;
324
384
  if (v instanceof Promise) {
325
385
  if (eager) {
326
386
  await renderNode({ ...ctx, eager: true, slots: children }, await v);
327
387
  } else {
328
- const chunkId = toString36(ctx.suspenses.length + 1);
388
+ const chunkId = (ctx.suspenses.length + 1).toString(36);
329
389
  ctx.suspenses.push(v.then(async (c) => {
330
390
  write('<m-chunk chunk-id="' + chunkId + '"><template>');
331
391
  await renderNode({ ...ctx, eager, slots: children }, c);
@@ -395,11 +455,11 @@ async function renderNode(ctx, node, stripSlotProp) {
395
455
  }
396
456
  switch (propName) {
397
457
  case "class":
398
- buffer += " " + renderAttr(propName, cx(propValue));
458
+ buffer += " class=" + toAttrStringLit(cx(propValue));
399
459
  break;
400
460
  case "style":
401
461
  if (isString(propValue) && propValue !== "") {
402
- buffer += " " + renderAttr(propName, cx(propValue));
462
+ buffer += ' style="' + escapeCSSText(propValue) + '"';
403
463
  } else if (isObject(propValue) && !Array.isArray(propValue)) {
404
464
  const style = [];
405
465
  const pseudoStyles = [];
@@ -409,55 +469,53 @@ async function renderNode(ctx, node, stripSlotProp) {
409
469
  switch (k.charCodeAt(0)) {
410
470
  case /* ':' */
411
471
  58:
412
- pseudoStyles.push([k, styleToCSS(v)]);
472
+ pseudoStyles.push([escapeCSSText(k), styleToCSS(v)]);
413
473
  break;
414
474
  case /* '@' */
415
475
  64:
416
- atRuleStyles.push([k, styleToCSS(v)]);
476
+ atRuleStyles.push([escapeCSSText(k), styleToCSS(v)]);
417
477
  break;
418
478
  case /* '&' */
419
479
  38:
420
- nestingStyles.push([k, styleToCSS(v)]);
480
+ nestingStyles.push([escapeCSSText(k), styleToCSS(v)]);
421
481
  break;
422
482
  default:
423
483
  style.push([k, v]);
424
484
  }
425
485
  }
426
486
  if (pseudoStyles.length > 0 || atRuleStyles.length > 0 || nestingStyles.length > 0) {
427
- let raw = "";
428
487
  let css = "";
488
+ let raw = "";
429
489
  let styleIds;
430
490
  let id;
431
491
  let cssSelector;
432
- let key;
433
- let value;
434
492
  if (style.length > 0) {
435
493
  css = styleToCSS(style);
436
494
  raw += css + "|";
437
495
  }
438
496
  raw += [pseudoStyles, atRuleStyles, nestingStyles].flat(1).map(([k, v]) => k + ">" + v).join("|");
439
497
  styleIds = ctx.styleIds ?? (ctx.styleIds = /* @__PURE__ */ new Set());
440
- id = toString36(hashCode(raw));
498
+ id = hashCode(raw).toString(36);
441
499
  cssSelector = "[data-css-" + id + "]";
442
500
  if (!styleIds.has(id)) {
443
501
  styleIds.add(id);
444
502
  if (css) {
445
503
  css = cssSelector + "{" + css + "}";
446
504
  }
447
- for ([key, value] of pseudoStyles) {
448
- css += cssSelector + key + "{" + value + "}";
505
+ for (const [p, styles] of pseudoStyles) {
506
+ css += cssSelector + p + "{" + styles + "}";
449
507
  }
450
- for ([key, value] of atRuleStyles) {
451
- css += key + "{" + cssSelector + "{" + value + "}}";
508
+ for (const [at, styles] of atRuleStyles) {
509
+ css += at + "{" + cssSelector + "{" + styles + "}}";
452
510
  }
453
- for ([key, value] of nestingStyles) {
454
- css += cssSelector + key.slice(1) + "{" + value + "}";
511
+ for (const [n, styles] of nestingStyles) {
512
+ css += cssSelector + n.slice(1) + "{" + styles + "}";
455
513
  }
456
514
  write('<style id="css-' + id + '">' + css + "</style>");
457
515
  }
458
516
  buffer += " data-css-" + id;
459
517
  } else if (style.length > 0) {
460
- buffer += " " + renderAttr(propName, styleToCSS(style));
518
+ buffer += ' style="' + styleToCSS(style) + '"';
461
519
  }
462
520
  }
463
521
  break;
@@ -468,23 +526,23 @@ async function renderNode(ctx, node, stripSlotProp) {
468
526
  break;
469
527
  case "action":
470
528
  if (typeof propValue === "function" && tag === "form") {
471
- const id = "$MF_" + toString36(ctx.index.mf++);
529
+ const id = "$MF_" + (ctx.index.mf++).toString(36);
472
530
  write("<script>function " + id + "(fd){(" + propValue.toString() + ")(fd)}<\/script>");
473
531
  buffer += ' onsubmit="$onsubmit(event,this,' + id + ",'" + fcIndex + `')"`;
474
532
  } else if (isString(propValue)) {
475
- buffer += " " + renderAttr(propName, propValue);
533
+ buffer += " action=" + toAttrStringLit(propValue);
476
534
  }
477
535
  break;
478
536
  case "slot":
479
537
  if (!stripSlotProp && isString(propValue)) {
480
- buffer += " " + renderAttr(propName, propValue);
538
+ buffer += " slot=" + toAttrStringLit(propValue);
481
539
  }
482
540
  break;
483
541
  default:
484
542
  if (regexpHtmlTag.test(propName) && propValue !== void 0) {
485
543
  if (propName.startsWith("on")) {
486
544
  if (typeof propValue === "function") {
487
- const id = "$MF_" + toString36(ctx.index.mf++);
545
+ const id = "$MF_" + (ctx.index.mf++).toString(36);
488
546
  write("<script>function " + id + "(e){(" + propValue.toString() + ")(e)}<\/script>");
489
547
  buffer += " " + propName.toLowerCase() + '="$emit(event,this,' + id + ",'" + fcIndex + `')"`;
490
548
  }
@@ -493,7 +551,7 @@ async function renderNode(ctx, node, stripSlotProp) {
493
551
  buffer += " " + propName;
494
552
  }
495
553
  } else {
496
- buffer += " " + renderAttr(propName, propValue);
554
+ buffer += " " + (propValue === true ? escapeHTML(propName) : escapeHTML(propName) + "=" + toAttrStringLit("" + propValue));
497
555
  }
498
556
  }
499
557
  }
@@ -521,10 +579,6 @@ async function renderNode(ctx, node, stripSlotProp) {
521
579
  );
522
580
  }
523
581
  }
524
- } else if (Array.isArray(node) || node && Symbol.iterator in node) {
525
- for (const child of node) {
526
- await renderNode(ctx, child);
527
- }
528
582
  }
529
583
  break;
530
584
  }
@@ -538,69 +592,26 @@ async function renderChildren(ctx, children, stripSlotProp) {
538
592
  await renderNode(ctx, children, stripSlotProp);
539
593
  }
540
594
  }
541
- function renderAttr(key, value) {
542
- return value === true ? key : key + "=" + toAttrStringLit("" + value);
543
- }
544
- function toString36(num) {
545
- return num.toString(36);
546
- }
547
- function escapeHTML(str) {
548
- const match = regexpHtmlSafe.exec(str);
549
- if (!match) {
550
- return str;
551
- }
552
- if (typeof Bun === "object" && "escapeHTML" in Bun) return Bun.escapeHTML(str);
553
- let escape;
554
- let index;
555
- let lastIndex = 0;
556
- let html2 = "";
557
- for (index = match.index; index < str.length; index++) {
558
- switch (str.charCodeAt(index)) {
559
- case 34:
560
- escape = "&quot;";
561
- break;
562
- case 38:
563
- escape = "&amp;";
564
- break;
565
- case 39:
566
- escape = "&#x27;";
567
- break;
568
- case 60:
569
- escape = "&lt;";
570
- break;
571
- case 62:
572
- escape = "&gt;";
573
- break;
574
- default:
575
- continue;
576
- }
577
- if (lastIndex !== index) {
578
- html2 += str.slice(lastIndex, index);
579
- }
580
- lastIndex = index + 1;
581
- html2 += escape;
582
- }
583
- return lastIndex !== index ? html2 + str.slice(lastIndex, index) : html2;
584
- }
585
595
  function render(node, renderOptions = {}) {
586
- const { request, status, headers: headersRaw, rendering } = renderOptions;
587
- const headers = /* @__PURE__ */ Object.create(null);
588
- if (headersRaw) {
589
- const { etag, lastModified } = headersRaw;
596
+ const { context, request, status, headers: headersInit, rendering } = renderOptions;
597
+ const headers = new Headers();
598
+ if (headersInit) {
599
+ for (const [key, value] of Object.entries(headersInit)) {
600
+ if (value) {
601
+ headers.set(toHyphenCase(key), value);
602
+ }
603
+ }
604
+ const etag = headers.get("etag");
590
605
  if (etag && request?.headers.get("if-none-match") === etag) {
591
606
  return new Response(null, { status: 304 });
592
607
  }
608
+ const lastModified = headers.get("last-modified");
593
609
  if (lastModified && request?.headers.get("if-modified-since") === lastModified) {
594
610
  return new Response(null, { status: 304 });
595
611
  }
596
- for (const [key, value] of Object.entries(headersRaw)) {
597
- if (value) {
598
- headers[toHyphenCase(key)] = value;
599
- }
600
- }
601
612
  }
602
- headers["transfer-encoding"] = "chunked";
603
- headers["content-type"] = "text/html; charset=utf-8";
613
+ headers.set("transfer-encoding", "chunked");
614
+ headers.set("content-type", "text/html; charset=utf-8");
604
615
  return new Response(
605
616
  new ReadableStream({
606
617
  async start(controller) {
@@ -610,6 +621,7 @@ function render(node, renderOptions = {}) {
610
621
  const rtComponents = { cx: false, styleToCSS: false };
611
622
  const ctx = {
612
623
  write,
624
+ context,
613
625
  request,
614
626
  stateStore,
615
627
  suspenses,
@@ -663,7 +675,7 @@ var jsx = (tag, props = /* @__PURE__ */ Object.create(null), key) => {
663
675
  }
664
676
  if (tag === "html") {
665
677
  const renderOptions = /* @__PURE__ */ Object.create(null);
666
- for (const key2 of ["request", "status", "headers", "rendering"]) {
678
+ for (const key2 of ["context", "request", "status", "headers", "rendering"]) {
667
679
  if (Object.hasOwn(props, key2)) {
668
680
  renderOptions[key2] = props[key2];
669
681
  delete props[key2];
@@ -680,8 +692,8 @@ var html = (raw, ...values) => [
680
692
  $vnode
681
693
  ];
682
694
  Object.assign(globalThis, {
683
- css: html,
684
695
  html,
696
+ css: html,
685
697
  js: html
686
698
  });
687
699
  export {
package/package.json CHANGED
@@ -1,25 +1,28 @@
1
1
  {
2
2
  "name": "mono-jsx",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "`<html>` as a `Response`.",
5
5
  "type": "module",
6
6
  "module": "./index.mjs",
7
+ "types": "./types/index.d.ts",
7
8
  "bin": {
8
9
  "mono-jsx": "./bin/mono-jsx"
9
10
  },
10
11
  "exports": {
11
12
  ".": {
13
+ "types": "./types/index.d.ts",
14
+ "node": "./index.mjs",
12
15
  "import": "./index.mjs"
13
16
  },
14
17
  "./jsx-runtime": {
15
18
  "types": "./types/jsx-runtime.d.ts",
16
- "import": "./jsx-runtime.mjs",
17
- "node": "./jsx-runtime.mjs"
19
+ "node": "./jsx-runtime.mjs",
20
+ "import": "./jsx-runtime.mjs"
18
21
  },
19
22
  "./jsx-dev-runtime": {
20
23
  "types": "./types/jsx-runtime.d.ts",
21
- "import": "./jsx-runtime.mjs",
22
- "node": "./jsx-runtime.mjs"
24
+ "node": "./jsx-runtime.mjs",
25
+ "import": "./jsx-runtime.mjs"
23
26
  }
24
27
  },
25
28
  "scripts": {
package/types/aria.d.ts CHANGED
@@ -231,7 +231,7 @@ export interface Attributes {
231
231
  }
232
232
 
233
233
  // All the WAI-ARIA 1.2 role attribute values from https://www.w3.org/TR/wai-aria-1.2/#role_definitions
234
- type Role =
234
+ export type Role =
235
235
  | "alert"
236
236
  | "alertdialog"
237
237
  | "application"
package/types/html.d.ts CHANGED
@@ -135,7 +135,7 @@ export namespace HTML {
135
135
 
136
136
  interface FormAttributes<T extends EventTarget> extends GlobalAttributes<T> {
137
137
  "accept-charset"?: string;
138
- action: string | (/* Mono specific */ (data: FormData) => void | Promise<void>);
138
+ action: string | (/* mono-jsx specific */ (data: FormData, event: SubmitEvent) => void | Promise<void>);
139
139
  autoComplete?: "on" | "off";
140
140
  encType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain";
141
141
  method?: "GET" | "POST" | "dialog";
@@ -820,8 +820,8 @@ export namespace HTML {
820
820
  }
821
821
 
822
822
  interface EventAttributes<T extends EventTarget> {
823
- // Mono specific
824
- onMount?: (e: { type: "mount"; target: T }) => void | Promise<void>;
823
+ // mono-jsx specific
824
+ onMount?: (e: { type: "mount"; currentTarget: T; target: T }) => void | Promise<void>;
825
825
 
826
826
  // Input Events
827
827
  onBeforeInput?: EventHandler<Event, T>;
@@ -0,0 +1 @@
1
+ export {};
package/types/jsx.d.ts CHANGED
@@ -4,7 +4,6 @@ import type * as Mono from "./mono.d.ts";
4
4
  import type { HTML } from "./html.d.ts";
5
5
 
6
6
  export type ChildType = VNode | string | number | bigint | boolean | null;
7
- export type Children = ChildType | (ChildType | ChildType[])[];
8
7
 
9
8
  export type VNode = readonly [
10
9
  tag: string | symbol | FC<any>,
package/types/mono.d.ts CHANGED
@@ -66,7 +66,10 @@ export interface CSSProperties extends BaseCSSProperties, AtRuleCSSProperties, P
66
66
  [key: `&${" " | "." | "["}${string}`]: CSSProperties;
67
67
  }
68
68
 
69
+ export type ChildType = JSX.Element | string | number | bigint | boolean | null;
70
+
69
71
  export interface BaseAttributes {
72
+ children?: ChildType | (ChildType)[];
70
73
  key?: string | number;
71
74
  slot?: string;
72
75
  }
@@ -98,8 +101,9 @@ export interface Elements {
98
101
  }
99
102
 
100
103
  declare global {
101
- type FC<T = Record<string, unknown>> = {
104
+ type FC<T = Record<string, unknown>, Context = Record<string, unknown>> = {
105
+ context: Context;
102
106
  request: Request;
103
- computed: <T = unknown>(fn: () => T) => T;
107
+ computed: <V = unknown>(fn: () => V) => V;
104
108
  } & Omit<T, "request" | "computed">;
105
109
  }
package/types/render.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export interface RenderOptions {
2
+ context?: Record<string, unknown>;
2
3
  request?: Request;
3
4
  status?: number;
4
5
  headers?: {