mono-jsx 0.9.14 → 0.10.0-beta.10

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
@@ -11,6 +11,7 @@ mono-jsx is a JSX runtime that renders the `<html>` element to a `Response` obje
11
11
  - 💡 Complete Web API TypeScript definitions
12
12
  - ⏳ Streaming rendering
13
13
  - 🗂️ Built-in router (SPA mode)
14
+ - 📡 Built-in remote procedure call (RPC) API
14
15
  - 🔑 Session storage
15
16
  - 🥷 [htmx](#using-htmx) integration
16
17
  - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
@@ -963,8 +964,6 @@ The `<component>` element also supports the `ref` prop, which allows you to cont
963
964
  - `refresh`: A method to re-render the component with the current name and props.
964
965
 
965
966
  ```tsx
966
- import type { ComponentElement } from "mono-jsx";
967
-
968
967
  function App(this: WithRefs<FC, { component: ComponentElement }>) {
969
968
  this.effect(() => {
970
969
  // updating the component name and props will trigger a re-render of the component
@@ -1109,8 +1108,21 @@ You can access the `params` object in your route components to get the values of
1109
1108
  ```tsx
1110
1109
  // router pattern: "/post/:id"
1111
1110
  function Post(this: FC) {
1112
- this.request.url // "http://localhost:3000/post/123"
1113
- this.request.params?.id // "123"
1111
+ console.log(this.request.url) // "http://localhost:3000/post/123"
1112
+ console.log(this.request.params?.id) // "123"
1113
+ }
1114
+ ```
1115
+
1116
+ You can use `this.app.url` signal to get route URL and parameters:
1117
+
1118
+ ```tsx
1119
+ function Post(this: FC) {
1120
+ return (
1121
+ <div>
1122
+ <p>Current URL: {this.$(() => this.app.url.href)}</p>
1123
+ <p>Post id: {this.$(() => this.app.url.params?.id)}</p>
1124
+ </div>
1125
+ )
1114
1126
  }
1115
1127
  ```
1116
1128
 
@@ -1226,7 +1238,7 @@ The `hidden` prop can be used to hide the formslot payload from the form handler
1226
1238
 
1227
1239
  ### Using `this.app.url` Signal
1228
1240
 
1229
- `this.app.url` is an app-level signal that contains the current route URL and parameters. The `this.app.url` signal is automatically updated when the route changes, so you can use it to display the current URL in your components or control the view with `<show>`, `<hidden>` or `<switch>` elements:
1241
+ The `this.app.url` in a component is an app-level signal that contains the current route URL and parameters. It is automatically updated when the route changes, so you can use it to display the current URL in your components or control the view with `<show>`, `<hidden>` or `<switch>` elements:
1230
1242
 
1231
1243
  ```tsx
1232
1244
  function App(this: FC) {
@@ -1257,6 +1269,23 @@ export default {
1257
1269
  }
1258
1270
  ```
1259
1271
 
1272
+ You can also use the `navigate` method of the `<router>` element to navigate to a new route programmatically.
1273
+
1274
+ ```tsx
1275
+ function App(this: FC<{}, { router: RouterElement }>) {
1276
+ return (
1277
+ <>
1278
+ <header>
1279
+ <button
1280
+ onClick={() => this.refs.router.navigate("/about", { replace: false, refresh: false })}
1281
+ >About</button>
1282
+ </header>
1283
+ <router ref={this.refs.router} />
1284
+ </>
1285
+ )
1286
+ }
1287
+ ```
1288
+
1260
1289
  ### Nav Links
1261
1290
 
1262
1291
  Links under the `<nav>` element will be treated as navigation links by the router. When the `href` of a nav link matches a route, an active class will be added to the link element. By default, the active class is `active`, but you can customize it by setting the `data-active-class` prop on the `<nav>` element. You can add styles for the active link using nested CSS selectors in the `style` prop of the `<nav>` element.
@@ -1276,6 +1305,63 @@ export default {
1276
1305
  }
1277
1306
  ```
1278
1307
 
1308
+ ## Adding Page Metadata
1309
+
1310
+ You can add metadata to the route component by setting the `metadata` property on the route component.
1311
+
1312
+ ```tsx
1313
+ function Home(this: FC) {
1314
+ return <p>Home</p>
1315
+ }
1316
+ Home.metadata = {
1317
+ title: "Home",
1318
+ description: "Home page",
1319
+ }
1320
+
1321
+ const routes = {
1322
+ "/": Home,
1323
+ }
1324
+ ```
1325
+
1326
+ Or use `getMetadata` property on the route component to dynamically generate the metadata.
1327
+
1328
+ ```tsx
1329
+ async function Post(this: FC) {
1330
+ const post = await getPost(this.request.params.slug)
1331
+ return <div>
1332
+ <h1>{post.title}</h1>
1333
+ <h2>{post.description}</h2>
1334
+ <div>{post.content}</div>
1335
+ </div>
1336
+ }
1337
+
1338
+ Post.getMetadata = async function(this: FC) {
1339
+ const post = await getPost(this.request.params.slug)
1340
+ return {
1341
+ title: post.title,
1342
+ description: post.description,
1343
+ }
1344
+ }
1345
+
1346
+ const routes = {
1347
+ "/post/:slug": Post,
1348
+ }
1349
+ ```
1350
+
1351
+ You can also add metadata to the root `<html>` element by setting the `metadata` prop on the root `<html>` element. This will be added to all the pages in your app.
1352
+
1353
+ ```tsx
1354
+ export default {
1355
+ fetch: (req) => (
1356
+ <html request={req} routes={routes} metadata={{ title: "My App" }}>
1357
+ <head>
1358
+ <metadata />
1359
+ </head>
1360
+ </html>
1361
+ )
1362
+ }
1363
+ ```
1364
+
1279
1365
  ### Fallback (404)
1280
1366
 
1281
1367
  You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
@@ -1293,9 +1379,33 @@ export default {
1293
1379
  }
1294
1380
  ```
1295
1381
 
1382
+ ### Route Caching
1383
+
1384
+ By default, the router client caches the html content from the server. To disable the caching, you can add the `dynamic` option to the route component.
1385
+
1386
+ ```tsx
1387
+ // Home is a static route that can be cached on the client side
1388
+ function Home(this: FC) {
1389
+ return <p>Home</p>
1390
+ }
1391
+
1392
+ // Dash is a dynamic route that will not be cached on the client side
1393
+ // it will be fetched from the server on every navigation
1394
+ function Dash(this: FC) {
1395
+ const user = this.session.get<{ name: string }>("user")
1396
+ return <p>Welcome back, {user?.name}!</p>
1397
+ }
1398
+ Dash.dynamic = true;
1399
+
1400
+ const routes = {
1401
+ "/": Home,
1402
+ "/dash": Dash,
1403
+ }
1404
+ ```
1405
+
1296
1406
  ## Using Session
1297
1407
 
1298
- mono-jsx provides a built-in session storage that allows you to manage user sessions. To use the session storage, you need to set the `session` prop on the root `<html>` element with the `cookie.secret` option.
1408
+ mono-jsx provides a built-in session storage that allows you to manage sessions. To use session storage, you need to set the `session` prop on the root `<html>` element with the `cookie.secret` option.
1299
1409
 
1300
1410
  ```tsx
1301
1411
  function Index(this: FC) {
@@ -1342,43 +1452,132 @@ export default {
1342
1452
  ### Session Storage API
1343
1453
 
1344
1454
  ```ts
1345
- function Component(this: FC) {
1346
- // set a value in the session
1347
- this.session.set("user", { name: "John" })
1348
- // get a value from the session
1349
- this.session.get<{ name: string }>("user") // { name: "John" }
1350
- // get all entries from the session
1351
- this.session.entries() // [["user", { name: "John" }]]
1352
- // delete a value from the session
1353
- this.session.delete("user")
1354
- // destroy the session
1355
- this.session.destroy()
1455
+ export interface Session {
1456
+ /**
1457
+ * The session ID.
1458
+ */
1459
+ readonly sessionId: string;
1460
+ /**
1461
+ * If true, update the session cookie to the client.
1462
+ */
1463
+ readonly isDirty: boolean;
1464
+ /**
1465
+ * If true, the session is expired.
1466
+ */
1467
+ readonly isExpired: boolean;
1468
+ /**
1469
+ * Gets a value from the session.
1470
+ */
1471
+ get<T = unknown>(key: string): T | undefined;
1472
+ /**
1473
+ * Gets all the entries from the session.
1474
+ */
1475
+ all(): Record<string, unknown>;
1476
+ /**
1477
+ * Sets a value in the session.
1478
+ */
1479
+ set(key: string, value: string | number | boolean | any[] | Record<string, unknown>): void;
1480
+ /**
1481
+ * Deletes a value from the session.
1482
+ */
1483
+ delete(key: string): void;
1484
+ /**
1485
+ * Destroys the session.
1486
+ */
1487
+ destroy(): void;
1356
1488
  }
1357
1489
  ```
1358
1490
 
1359
- ## Caching
1491
+ > [!WARNING]
1492
+ > The session storage stores data with cookies. **Therefore, you should never store sensitive data in the session storage.**
1360
1493
 
1361
- mono-jsx renders HTML dynamically per request; large apps may tax your CPU resources. To improve rendering performance, mono-jsx introduces two built-in elements that can cache the rendered HTML of the children:
1494
+ ## Using RPC
1362
1495
 
1363
- - `<cache>` with specified `key` and `maxAge`
1364
- - `<static>` for elements that rarely change, such as `<svg>`
1496
+ mono-jsx provides a built-in RPC API that allows you to call functions on the server from the client side. You can use the `createRPC` function to create a RPC object that contains the functions you want to call on the client side, and then pass it to the `expose` prop on the root `<html>` element.
1365
1497
 
1366
1498
  ```tsx
1367
- function BlogPage() {
1499
+ import { createRPC } from "mono-jsx"
1500
+
1501
+ const rpc = createRPC({
1502
+ whoami: () => ({ name: "John" })
1503
+ })
1504
+
1505
+ function App(this: FC<{ user?: { name: string } }>) {
1368
1506
  return (
1369
- <cache key="blog" maxAge={86400}>
1370
- <Blog />
1371
- </cache>
1507
+ <div>
1508
+ <show when={this.user}>
1509
+ <p>Welcome, {this.user!.name}!</p>
1510
+ </show>
1511
+ <button onClick={async () => this.user = await rpc.whoami()}>Who am I?</button>
1512
+ </div>
1372
1513
  )
1373
1514
  }
1374
1515
 
1375
- function Icon() {
1516
+ export default {
1517
+ fetch: (req) => (
1518
+ <html request={req} expose={{ rpc }}>
1519
+ <App />
1520
+ </html>
1521
+ )
1522
+ }
1523
+ ```
1524
+
1525
+ > [!NOTE]
1526
+ > mono-jsx sends the rpc invoke result to the client side as a JSON object.
1527
+
1528
+ ### Accessing request info in RPC functions
1529
+
1530
+ You can access the request info in RPC functions by using the `this` scope:
1531
+
1532
+ - `this.request`: The [request](#accessing-request-info) object.
1533
+ - `this.context`: The [context](#using-context) object.
1534
+ - `this.session`: The [session](#session-storage-api) storage.
1535
+
1536
+ ```tsx
1537
+ type Admin = {
1538
+ isAdmin: (id: number) => boolean;
1539
+ }
1540
+
1541
+ const rpc = createRPC({
1542
+ whoami: function (this: WithContext<RPC, { admin: Admin }>) {
1543
+ const { admin } = this.context;
1544
+ const user = this.session.get<{ name: string }>("user")
1545
+ return {
1546
+ ip: this.request.headers.get("x-real-ip"),
1547
+ isAdmin: user ? admin.isAdmin(user.id) : false,
1548
+ user: user,
1549
+ }
1550
+ },
1551
+ })
1552
+
1553
+ function App(this: FC) {
1376
1554
  return (
1377
- <static>
1378
- <svg>...</svg>
1379
- </static>
1555
+ <button onClick={async () => console.log(await rpc.whoami())}>Who am I?</button>
1380
1556
  )
1381
1557
  }
1558
+
1559
+ export default {
1560
+ fetch: (req) => (
1561
+ <html
1562
+ request={req}
1563
+ expose={{ rpc }}
1564
+ session={{ cookie: { secret: "..." } }}
1565
+ context={{ admin: { isAdmin: (id: number) => id === 1 } }}
1566
+ >
1567
+ <App />
1568
+ </html>
1569
+ )
1570
+ }
1571
+ ```
1572
+
1573
+ To use `this` in RPC functions, you can't use the arrow function syntax. You need to use the function declaration syntax. And the `RPC` type is defined as follows:
1574
+
1575
+ ```ts
1576
+ type RPC<Context extends Record<string, unknown> = {}> = {
1577
+ request: Request;
1578
+ context: Context;
1579
+ session: Session;
1580
+ }
1382
1581
  ```
1383
1582
 
1384
1583
  ## Customizing HTML Response
package/index.mjs CHANGED
@@ -1,9 +1,6 @@
1
1
  // index.ts
2
2
  function buildRoutes(handler) {
3
3
  const { routes = {} } = handler(/* @__PURE__ */ Symbol.for("mono.setup"));
4
- return monoRoutes(routes, handler);
5
- }
6
- function monoRoutes(routes, handler) {
7
4
  const handlers = {};
8
5
  for (const [path, fc] of Object.entries(routes)) {
9
6
  handlers[path] = (request) => {
@@ -13,7 +10,17 @@ function monoRoutes(routes, handler) {
13
10
  }
14
11
  return handlers;
15
12
  }
13
+ var rpcIndex = 0;
14
+ function createRPC(rpcFunctions) {
15
+ for (const [key, value] of Object.entries(rpcFunctions)) {
16
+ if (typeof value !== "function") {
17
+ throw new Error(`createRPC: ${key} is not a function`);
18
+ }
19
+ }
20
+ Reflect.set(rpcFunctions, /* @__PURE__ */ Symbol.for("mono.rpc"), rpcIndex++);
21
+ return rpcFunctions;
22
+ }
16
23
  export {
17
24
  buildRoutes,
18
- monoRoutes
25
+ createRPC
19
26
  };