mono-jsx 0.2.0 → 0.3.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/README.md CHANGED
@@ -109,6 +109,9 @@ You'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a
109
109
  npx tsx app.tsx
110
110
  ```
111
111
 
112
+ > [!NOTE]
113
+ > Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx runtime.
114
+
112
115
  ## Using JSX
113
116
 
114
117
  mono-jsx uses [**JSX**](https://react.dev/learn/describing-the-ui) to describe the user interface, similar to React but with key differences.
@@ -309,22 +312,63 @@ function Counter(
309
312
  }
310
313
  ```
311
314
 
312
- ### Using Computed Properties
315
+ ### Using App State
313
316
 
314
- You can use `this.computed` to create computed properties based on state. The computed property will automatically update when the state changes:
317
+ You can define app state by adding `appState` prop to the root `<html>` element. The app state is available in all components via `this.app.<stateKey>`. Changes to the app state will trigger re-renders in all components that use it:
318
+
319
+ ```tsx
320
+ function Header(this: FC<{}, { title: string }>) {
321
+ return (
322
+ <header>
323
+ <h1>{this.app.title}</h1>
324
+ </header>
325
+ );
326
+ }
327
+
328
+ function Footer(this: FC<{}, { title: string }>) {
329
+ return (
330
+ <footer>
331
+ <p>(c) 2025 {this.app.title}</p>
332
+ </footer>
333
+ );
334
+ }
335
+
336
+ function Main(this: FC<{}, { title: string }>) {
337
+ return (
338
+ <main>
339
+ <h1>{this.app.title}</h1>
340
+ <h2>Changing the title</h2>
341
+ <input
342
+ onInput={(evt) => this.app.title = evt.target.value}
343
+ placeholder="Enter a new title"
344
+ />
345
+ </main>
346
+ );
347
+ }
348
+
349
+ export default {
350
+ fetch: (req) => (
351
+ <html appState={{ title: "Welcome to mono-jsx!" }}>
352
+ <Header />
353
+ <Main />
354
+ <Footer />
355
+ </html>
356
+ ),
357
+ };
358
+ ```
359
+
360
+ ### Using Computed State
361
+
362
+ You can use `this.computed` to create computed state based on state. The computed state will automatically update when the state changes:
315
363
 
316
364
  ```tsx
317
365
  function App(this: FC<{ input: string }>) {
318
- this.input = 'Hello, world';
366
+ this.input = "Welcome to mono-jsx!";
319
367
  return (
320
368
  <div>
321
- <h1>{this.computed(() = this.input + "!")}</h1>
369
+ <h1>{this.computed(() => this.input + "!")}</h1>
322
370
 
323
- <form
324
- action={(data: FormData ) => {
325
- this.input = data.get("input") as string;
326
- }}
327
- >
371
+ <form action={(fd) => this.input = fd.get("input") as string}>
328
372
  <input type="text" name="input" value={this.input} />
329
373
  <button type="submit">Submit</button>
330
374
  </form>
@@ -366,7 +410,7 @@ function App(this: FC) {
366
410
  ```tsx
367
411
  // ❌ Won't work - state updates won't refresh the view
368
412
  function App(this: FC<{ message: string }>) {
369
- this.message = "Hello, world";
413
+ this.message = "Welcome to mono-jsx!";
370
414
  return (
371
415
  <div>
372
416
  <h1 title={this.message + "!"}>{this.message + "!"}</h1>
@@ -379,7 +423,7 @@ function App(this: FC<{ message: string }>) {
379
423
 
380
424
  // ✅ Works correctly
381
425
  function App(this: FC) {
382
- this.message = "Hello, world";
426
+ this.message = "Welcome to mono-jsx!";
383
427
  return (
384
428
  <div>
385
429
  <h1 title={this.computed(() => this.message + "!")}>{this.computed(() => this.message + "!")}</h1>
@@ -494,22 +538,23 @@ export default {
494
538
  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
539
 
496
540
  ```tsx
497
- function Dash(this: FC<{}, { auth: { uuid: string; name: string } }>) {
541
+ function Dash(this: FC<{}, {}, { auth: { uuid: string; name: string } }>) {
498
542
  const { auth } = this.context;
499
543
  return (
500
544
  <div>
501
- <h1>Welcome back, {auth.name}!</h1>;
545
+ <h1>Welcome back, {auth.name}!</h1>
502
546
  <p>Your UUID is {auth.uuid}</p>
503
547
  </div>
504
548
  );
505
549
  }
506
550
 
507
551
  export default {
508
- fetch: (req) => {
509
- const auth = doAuth(req);
552
+ fetch: async (req) => {
553
+ const auth = await doAuth(req);
510
554
  return (
511
555
  <html context={{ auth }} request={req}>
512
- <Dash />
556
+ {!auth && <p>Please Login</p>}
557
+ {auth && <Dash />}
513
558
  </html>
514
559
  );
515
560
  },
@@ -562,6 +607,88 @@ export default {
562
607
  };
563
608
  ```
564
609
 
610
+ ## Using htmx
611
+
612
+ mono-jsx integrates with [htmx](https://htmx.org/) and [typed-htmx](https://github.com/Desdaemon/typed-htmx). To use htmx, add the `htmx` attribute to the root `<html>` element:
613
+
614
+ ```jsx
615
+ export default {
616
+ fetch: (req) => {
617
+ const url = new URL(req.url);
618
+
619
+ if (url.pathname === "/clicked") {
620
+ return (
621
+ <html>
622
+ <span>Clicked!</span>
623
+ </html>
624
+ );
625
+ }
626
+
627
+ return (
628
+ <html htmx>
629
+ <button hx-get="/clicked" hx-swap="outerHTML">
630
+ Click Me
631
+ </button>
632
+ </html>
633
+ );
634
+ },
635
+ };
636
+ ```
637
+
638
+ ### Adding htmx Extensions
639
+
640
+ You can add htmx [extensions](https://htmx.org/docs/#extensions) by adding the `htmx-ext-*` attribute to the root `<html>` element:
641
+
642
+ ```jsx
643
+ export default {
644
+ fetch: (req) => (
645
+ <html htmx htmx-ext-response-targets htmx-ext-ws>
646
+ <button hx-get="/clicked" hx-swap="outerHTML">
647
+ Click Me
648
+ </button>
649
+ </html>
650
+ ),
651
+ };
652
+ ```
653
+
654
+ ### Specifying htmx Version
655
+
656
+ You can specify the htmx version by setting the `htmx` attribute to a specific version:
657
+
658
+ ```jsx
659
+ export default {
660
+ fetch: (req) => (
661
+ <html htmx="2.0.4" htmx-ext-response-targets="2.0.2" htmx-ext-ws="2.0.2">
662
+ <button hx-get="/clicked" hx-swap="outerHTML">
663
+ Click Me
664
+ </button>
665
+ </html>
666
+ ),
667
+ };
668
+ ```
669
+
670
+ ### Installing htmx Manually
671
+
672
+ By default, mono-jsx installs htmx from [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also install htmx manually with your own CDN or local copy:
673
+
674
+ ```jsx
675
+ export default {
676
+ fetch: (req) => (
677
+ <html>
678
+ <head>
679
+ <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
680
+ <script src="https://unpkg.com/htmx-ext-ws@2.0.2" integrity="sha384-vuKxTKv5TX/b3lLzDKP2U363sOAoRo5wSvzzc3LJsbaQRSBSS+3rKKHcOx5J8doU" crossorigin="anonymous"></script>
681
+ </head>
682
+ <body>
683
+ <button hx-get="/clicked" hx-swap="outerHTML">
684
+ Click Me
685
+ </button>
686
+ </body>
687
+ </html>
688
+ ),
689
+ };
690
+ ```
691
+
565
692
  ## License
566
693
 
567
694
  [MIT](LICENSE)
package/bin/mono-jsx CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync } from "node:fs";
4
- import { readFile, writeFile } from "node:fs/promises";
5
3
  import process from "node:process";
4
+ import { setup } from "../setup.mjs";
6
5
 
7
6
  switch (process.argv[2]) {
8
7
  case "setup":
@@ -11,43 +10,3 @@ switch (process.argv[2]) {
11
10
  default:
12
11
  process.exit(0);
13
12
  }
14
-
15
- async function setup() {
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";
33
- let tsConfig = Object.create(null);
34
- try {
35
- const data = await readFile(tsConfigFilename, "utf8");
36
- tsConfig = JSON.parse(data);
37
- } catch {
38
- // ignore
39
- }
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
- }
45
- if (!globalThis.Deno) {
46
- compilerOptions.module ??= "es2022";
47
- compilerOptions.moduleResolution ??= "bundler";
48
- }
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.")
53
- }