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 +209 -66
- package/bin/mono-jsx +32 -13
- package/jsx-runtime.mjs +128 -116
- package/package.json +8 -5
- package/types/aria.d.ts +1 -1
- package/types/html.d.ts +3 -3
- package/types/index.d.ts +1 -0
- package/types/jsx.d.ts +0 -1
- package/types/mono.d.ts +6 -2
- package/types/render.d.ts +1 -0
package/README.md
CHANGED
|
@@ -2,32 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
mono-jsx is a JSX runtime that renders `<html>` element to
|
|
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
|
|
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
|
|
17
|
-
You can install it via `npm
|
|
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
|
|
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
|
|
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>
|
|
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
|
|
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
|
|
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>
|
|
100
|
+
<h1>Welcome to mono-jsx!</h1>
|
|
99
101
|
</html>
|
|
100
102
|
),
|
|
101
103
|
});
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
|
|
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
|
|
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
|
|
118
|
+
mono-jsx adopts standard HTML property names, avoiding React's custom naming conventions:
|
|
117
119
|
|
|
118
|
-
- `className`
|
|
119
|
-
- `htmlFor`
|
|
120
|
-
- `onChange`
|
|
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
|
|
126
|
+
mono-jsx allows you to compose the `class` property using arrays of strings, objects, or expressions:
|
|
125
127
|
|
|
126
128
|
```jsx
|
|
127
|
-
<div
|
|
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
|
|
138
|
+
### Using Pseudo Classes and Media Queries in `style`
|
|
131
139
|
|
|
132
|
-
mono-jsx
|
|
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)
|
|
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
|
-
|
|
158
|
-
<slot
|
|
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
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
|
217
|
+
return (
|
|
218
|
+
<button onClick={(evt) => alert("BOOM!")}>
|
|
219
|
+
Click Me
|
|
220
|
+
</button>
|
|
221
|
+
);
|
|
207
222
|
}
|
|
208
223
|
```
|
|
209
224
|
|
|
210
225
|
> [!NOTE]
|
|
211
|
-
>
|
|
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
|
|
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
|
-
|
|
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>
|
|
259
|
+
<h1>Welcome to mono-jsx!</h1>
|
|
242
260
|
</div>
|
|
243
261
|
);
|
|
244
262
|
}
|
|
245
263
|
```
|
|
246
264
|
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
{/*
|
|
301
|
+
{/* render state */}
|
|
260
302
|
<span>{this.count}</span>
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
396
|
+
mono-jsx provides built-in elements to help you build reactive UIs.
|
|
290
397
|
|
|
291
398
|
### `<toggle>` element
|
|
292
399
|
|
|
293
|
-
`<toggle>` element
|
|
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>
|
|
413
|
+
<h1>Welcome to mono-jsx!</h1>
|
|
302
414
|
</toggle>
|
|
303
|
-
|
|
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
|
|
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,
|
|
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>
|
|
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
|
|
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>
|
|
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
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
|
21
|
-
tsConfig = JSON.parse(
|
|
35
|
+
const data = await readFile(tsConfigFilename, "utf8");
|
|
36
|
+
tsConfig = JSON.parse(data);
|
|
22
37
|
} catch {
|
|
23
38
|
// ignore
|
|
24
39
|
}
|
|
25
|
-
tsConfig.compilerOptions = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
46
|
+
compilerOptions.module ??= "es2022";
|
|
47
|
+
compilerOptions.moduleResolution ??= "bundler";
|
|
32
48
|
}
|
|
33
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 =
|
|
50
|
-
var RUNTIME_SUSPENSE_JS =
|
|
51
|
-
var RUNTIME_COMPONENTS_JS = {
|
|
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==="<"?"<":r===">"?">":"'")}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))
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
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 "<";
|
|
144
|
+
if (m === ">") return ">";
|
|
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 = """;
|
|
163
|
+
break;
|
|
164
|
+
case 38:
|
|
165
|
+
escape = "&";
|
|
166
|
+
break;
|
|
167
|
+
case 39:
|
|
168
|
+
escape = "'";
|
|
169
|
+
break;
|
|
170
|
+
case 60:
|
|
171
|
+
escape = "<";
|
|
172
|
+
break;
|
|
173
|
+
case 62:
|
|
174
|
+
escape = ">";
|
|
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
|
|
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 =
|
|
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 =
|
|
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 += " " +
|
|
458
|
+
buffer += " class=" + toAttrStringLit(cx(propValue));
|
|
399
459
|
break;
|
|
400
460
|
case "style":
|
|
401
461
|
if (isString(propValue) && propValue !== "") {
|
|
402
|
-
buffer +=
|
|
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 =
|
|
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 ([
|
|
448
|
-
css += cssSelector +
|
|
505
|
+
for (const [p, styles] of pseudoStyles) {
|
|
506
|
+
css += cssSelector + p + "{" + styles + "}";
|
|
449
507
|
}
|
|
450
|
-
for ([
|
|
451
|
-
css +=
|
|
508
|
+
for (const [at, styles] of atRuleStyles) {
|
|
509
|
+
css += at + "{" + cssSelector + "{" + styles + "}}";
|
|
452
510
|
}
|
|
453
|
-
for ([
|
|
454
|
-
css += cssSelector +
|
|
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 +=
|
|
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_" +
|
|
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 += " " +
|
|
533
|
+
buffer += " action=" + toAttrStringLit(propValue);
|
|
476
534
|
}
|
|
477
535
|
break;
|
|
478
536
|
case "slot":
|
|
479
537
|
if (!stripSlotProp && isString(propValue)) {
|
|
480
|
-
buffer += " " +
|
|
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_" +
|
|
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 += " " +
|
|
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 = """;
|
|
561
|
-
break;
|
|
562
|
-
case 38:
|
|
563
|
-
escape = "&";
|
|
564
|
-
break;
|
|
565
|
-
case 39:
|
|
566
|
-
escape = "'";
|
|
567
|
-
break;
|
|
568
|
-
case 60:
|
|
569
|
-
escape = "<";
|
|
570
|
-
break;
|
|
571
|
-
case 62:
|
|
572
|
-
escape = ">";
|
|
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:
|
|
587
|
-
const headers =
|
|
588
|
-
if (
|
|
589
|
-
const
|
|
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
|
|
603
|
-
headers
|
|
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.
|
|
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
|
-
"
|
|
17
|
-
"
|
|
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
|
-
"
|
|
22
|
-
"
|
|
24
|
+
"node": "./jsx-runtime.mjs",
|
|
25
|
+
"import": "./jsx-runtime.mjs"
|
|
23
26
|
}
|
|
24
27
|
},
|
|
25
28
|
"scripts": {
|
package/types/aria.d.ts
CHANGED
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 | (/*
|
|
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
|
-
//
|
|
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>;
|
package/types/index.d.ts
ADDED
|
@@ -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: <
|
|
107
|
+
computed: <V = unknown>(fn: () => V) => V;
|
|
104
108
|
} & Omit<T, "request" | "computed">;
|
|
105
109
|
}
|