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 +228 -29
- package/index.mjs +11 -4
- package/jsx-runtime.mjs +221 -78
- package/package.json +2 -2
- package/setup.mjs +17 -22
- package/types/index.d.ts +3 -10
- package/types/jsx-runtime.d.ts +3 -0
- package/types/jsx.d.ts +40 -2
- package/types/mono.d.ts +72 -16
- package/types/render.d.ts +10 -4
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.
|
|
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
|
|
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
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
1491
|
+
> [!WARNING]
|
|
1492
|
+
> The session storage stores data with cookies. **Therefore, you should never store sensitive data in the session storage.**
|
|
1360
1493
|
|
|
1361
|
-
|
|
1494
|
+
## Using RPC
|
|
1362
1495
|
|
|
1363
|
-
-
|
|
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
|
-
|
|
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
|
-
<
|
|
1370
|
-
<
|
|
1371
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
25
|
+
createRPC
|
|
19
26
|
};
|