mono-jsx-dom 0.1.1 → 0.1.3
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 +37 -68
- package/bin/build.mjs +337 -0
- package/bin/cli +18 -0
- package/bin/dev.mjs +213 -0
- package/bin/init.mjs +269 -0
- package/jsx-runtime.mjs +55 -51
- package/package.json +14 -2
- package/server/index.mjs +123 -0
- package/server/node-fetch-server.mjs +145 -0
- package/server/workerd.mjs +76 -0
- package/types/html.d.ts +9 -9
- package/types/index.d.ts +8 -9
- package/types/jsx-runtime.d.ts +1 -10
- package/types/jsx.d.ts +30 -19
- package/types/server.d.ts +23 -0
- package/bin/mono-jsx-dom +0 -12
- package/setup.mjs +0 -56
package/README.md
CHANGED
|
@@ -1,62 +1,31 @@
|
|
|
1
1
|
# 🔥 mono-jsx-dom
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
6
|
> This library is currently under active development. The API may change at any time. Use at your own risk. Please report any issues or feature requests on the issues page.
|
|
7
7
|
|
|
8
|
-
`mono-jsx-dom` is a JSX runtime
|
|
8
|
+
`mono-jsx-dom` is a JSX runtime for building web user interface.
|
|
9
9
|
|
|
10
10
|
- ⚡️ Use browser-specific APIs, no virtual DOM
|
|
11
11
|
- 🦋 Lightweight (4KB gzipped), zero dependencies
|
|
12
12
|
- 🚦 Signals as reactive primitives
|
|
13
13
|
- 💡 Complete Web API TypeScript definitions
|
|
14
14
|
- ⏳ Streaming rendering
|
|
15
|
+
- 🔩 Builtin dev/build/deploy toolchain
|
|
15
16
|
|
|
16
17
|
Playground: https://val.town/x/ije/mono-jsx-dom
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## Getting Started
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
npm install mono-jsx
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Setup JSX Runtime
|
|
25
|
-
|
|
26
|
-
To use mono-jsx-dom as your JSX runtime, add the following configuration to your `tsconfig.json` (or `deno.json` for Deno):
|
|
27
|
-
|
|
28
|
-
```jsonc
|
|
29
|
-
{
|
|
30
|
-
"compilerOptions": {
|
|
31
|
-
"module": "esnext",
|
|
32
|
-
"moduleResolution": "bundler",
|
|
33
|
-
"jsx": "react-jsx",
|
|
34
|
-
"jsxImportSource": "mono-jsx-dom"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
You can also run `mono-jsx-dom setup` to automatically add the configuration to your project:
|
|
21
|
+
You can run the `mono-jsx-dom int` command to initialize a project with mono-jsx-dom boilerplate.
|
|
40
22
|
|
|
41
23
|
```bash
|
|
42
|
-
#
|
|
43
|
-
npx mono-jsx-dom
|
|
44
|
-
|
|
45
|
-
# Deno
|
|
46
|
-
deno run -A npm:mono-jsx-dom setup
|
|
24
|
+
# node
|
|
25
|
+
npx mono-jsx-dom init
|
|
47
26
|
|
|
48
|
-
#
|
|
49
|
-
bunx mono-jsx-dom
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
You can also use the `@jsxImportSource` pragma directive to use `mono-jsx-dom` as your JSX runtime:
|
|
53
|
-
|
|
54
|
-
```tsx
|
|
55
|
-
/** @jsxImportSource mono-jsx-dom */
|
|
56
|
-
|
|
57
|
-
function App() {
|
|
58
|
-
return <div>Hello, world!</div>;
|
|
59
|
-
}
|
|
27
|
+
# bun
|
|
28
|
+
bunx --bun mono-jsx-dom init
|
|
60
29
|
```
|
|
61
30
|
|
|
62
31
|
## Usage
|
|
@@ -73,14 +42,8 @@ function App(this: FC) {
|
|
|
73
42
|
document.body.mount(<App />);
|
|
74
43
|
```
|
|
75
44
|
|
|
76
|
-
To run the app built with mono-jsx-dom in the browser, you need a TSX transformer to transform the TSX code to JavaScript code. For example, you can use [esbuild](https://esbuild.github.io) to transform the TSX code to JavaScript code:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
bunx esbuild --bundle --jsx=automatic --jsx-import-source=mono-jsx-dom --platfrom=browser --format=esm --target=es2022 --outfile=app.js app.tsx
|
|
80
|
-
```
|
|
81
|
-
|
|
82
45
|
>[!TIP]
|
|
83
|
-
> `mono-jsx-dom` is designed for client-side rendering
|
|
46
|
+
> `mono-jsx-dom` is designed for client-side rendering. You can use [mono-jsx](https://github.com/ije/mono-jsx) to render the UI on the server side.
|
|
84
47
|
|
|
85
48
|
## Using JSX
|
|
86
49
|
|
|
@@ -108,9 +71,9 @@ mono-jsx-dom allows you to compose the `class` property using arrays of strings,
|
|
|
108
71
|
/>;
|
|
109
72
|
```
|
|
110
73
|
|
|
111
|
-
### Using Pseudo
|
|
74
|
+
### Using Pseudo-Classes and Media Queries in `style`
|
|
112
75
|
|
|
113
|
-
mono-jsx-dom supports [pseudo
|
|
76
|
+
mono-jsx-dom 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:
|
|
114
77
|
|
|
115
78
|
```tsx
|
|
116
79
|
<a
|
|
@@ -161,7 +124,7 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
161
124
|
}
|
|
162
125
|
```
|
|
163
126
|
|
|
164
|
-
You can also set the `viewTransition` prop
|
|
127
|
+
You can also set the `viewTransition` prop on an HTML element that contains signal children.
|
|
165
128
|
|
|
166
129
|
```tsx
|
|
167
130
|
function App(this: FC<{ message: string }>) {
|
|
@@ -172,7 +135,7 @@ function App(this: FC<{ message: string }>) {
|
|
|
172
135
|
}
|
|
173
136
|
```
|
|
174
137
|
|
|
175
|
-
You can also set the view transition name in the style property
|
|
138
|
+
You can also set the view transition name in the `style` property by setting the `viewTransition` prop to `true`.
|
|
176
139
|
|
|
177
140
|
```tsx
|
|
178
141
|
function App(this: FC<{ message: string }>) {
|
|
@@ -278,7 +241,7 @@ function App() {
|
|
|
278
241
|
|
|
279
242
|
## Async Components
|
|
280
243
|
|
|
281
|
-
mono-jsx-dom supports async components that return a `Promise` or
|
|
244
|
+
mono-jsx-dom supports async components that return a `Promise` or are declared as async functions. With streaming rendering, async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.
|
|
282
245
|
|
|
283
246
|
```tsx
|
|
284
247
|
async function JsonViewer(props: { url: string }) {
|
|
@@ -347,7 +310,7 @@ document.body.mount(<App />);
|
|
|
347
310
|
|
|
348
311
|
## Error Handling
|
|
349
312
|
|
|
350
|
-
You can add the `catch` prop
|
|
313
|
+
You can add the `catch` prop to a function component. This allows you to catch errors in components and display a fallback UI:
|
|
351
314
|
|
|
352
315
|
```tsx
|
|
353
316
|
async function Hello() {
|
|
@@ -369,11 +332,11 @@ The `catch` prop should be a function that gets the caught error as the first ar
|
|
|
369
332
|
|
|
370
333
|
## Using Signals
|
|
371
334
|
|
|
372
|
-
mono-jsx-dom uses signals
|
|
335
|
+
mono-jsx-dom uses signals to update the view when a signal changes. Signals are similar to React's state, but they are lighter-weight and more efficient. You can use signals to manage state in your components.
|
|
373
336
|
|
|
374
337
|
### Using Component Signals
|
|
375
338
|
|
|
376
|
-
You can use the `this` keyword in your components to manage signals. Signals are bound to the component instance, can be updated directly, and
|
|
339
|
+
You can use the `this` keyword in your components to manage signals. Signals are bound to the component instance, can be updated directly, and automatically re-render the view when they change:
|
|
377
340
|
|
|
378
341
|
```tsx
|
|
379
342
|
function Counter(this: FC<{ count: number }>, props: { initialCount?: number }) {
|
|
@@ -396,11 +359,11 @@ function Counter(this: FC<{ count: number }>, props: { initialCount?: number })
|
|
|
396
359
|
}
|
|
397
360
|
```
|
|
398
361
|
|
|
399
|
-
You can use `this.
|
|
362
|
+
You can use `this.store` to create a signal store. You can use getters to create derived (computed) signals.
|
|
400
363
|
|
|
401
364
|
```tsx
|
|
402
365
|
function App(this: FC<{ count: number }>) {
|
|
403
|
-
const counter = this.
|
|
366
|
+
const counter = this.store({
|
|
404
367
|
value: 0,
|
|
405
368
|
// double is a derived(computed) signal
|
|
406
369
|
get double() {
|
|
@@ -420,10 +383,10 @@ function App(this: FC<{ count: number }>) {
|
|
|
420
383
|
|
|
421
384
|
### Using `atom` and `store`
|
|
422
385
|
|
|
423
|
-
mono-jsx-dom provides two functions
|
|
386
|
+
mono-jsx-dom provides two functions that allow you to define shared global signals. You can use them to share signals between components.
|
|
424
387
|
|
|
425
388
|
- `atom(initValue)`: Creates an atom signal.
|
|
426
|
-
- `store(initValue)`: Creates a
|
|
389
|
+
- `store(initValue)`: Creates a signal store.
|
|
427
390
|
|
|
428
391
|
```ts
|
|
429
392
|
export interface Atom<T> {
|
|
@@ -497,7 +460,7 @@ function App(this: FC<{ input: string }>) {
|
|
|
497
460
|
|
|
498
461
|
### Using Effect
|
|
499
462
|
|
|
500
|
-
You can use `this.effect` to perform side effects in components. The effect
|
|
463
|
+
You can use `this.effect` to perform side effects in components. The effect runs when the component is mounted, automatically collects used signals as dependencies, and reruns when those dependencies change.
|
|
501
464
|
|
|
502
465
|
```tsx
|
|
503
466
|
function App(this: FC<{ count: number }>) {
|
|
@@ -516,7 +479,7 @@ function App(this: FC<{ count: number }>) {
|
|
|
516
479
|
}
|
|
517
480
|
```
|
|
518
481
|
|
|
519
|
-
The callback function of `this.effect` can return a cleanup function that
|
|
482
|
+
The callback function of `this.effect` can return a cleanup function that runs once the component's element has been removed through `<show>`, `<hidden>`, or `<switch>` conditional rendering:
|
|
520
483
|
|
|
521
484
|
```tsx
|
|
522
485
|
function Counter(this: FC<{ count: number }>) {
|
|
@@ -587,7 +550,7 @@ function App(this: FC<{ hidden: boolean }>) {
|
|
|
587
550
|
}
|
|
588
551
|
```
|
|
589
552
|
|
|
590
|
-
If you need `if-else` logic in JSX, use `<switch>` element instead:
|
|
553
|
+
If you need `if-else` logic in JSX, use the `<switch>` element instead:
|
|
591
554
|
|
|
592
555
|
```tsx
|
|
593
556
|
function App(this: FC<{ ok: boolean }>) {
|
|
@@ -629,7 +592,7 @@ function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
|
|
|
629
592
|
|
|
630
593
|
### Form Input Two-way Binding
|
|
631
594
|
|
|
632
|
-
You can use the `$value` prop to bind a signal to the value of a form input element. The `$value` prop
|
|
595
|
+
You can use the `$value` prop to bind a signal to the value of a form input element. The `$value` prop provides two-way data binding, which means that when the input value changes, the signal is updated, and when the signal changes, the input value is updated.
|
|
633
596
|
|
|
634
597
|
```tsx
|
|
635
598
|
function App(this: FC<{ value: string }>) {
|
|
@@ -642,7 +605,7 @@ function App(this: FC<{ value: string }>) {
|
|
|
642
605
|
}
|
|
643
606
|
```
|
|
644
607
|
|
|
645
|
-
You can also use the `$checked` prop to bind a signal to the checked state of a checkbox or radio input
|
|
608
|
+
You can also use the `$checked` prop to bind a signal to the checked state of a checkbox or radio input.
|
|
646
609
|
|
|
647
610
|
```tsx
|
|
648
611
|
function App(this: FC<{ checked: boolean }>) {
|
|
@@ -714,20 +677,22 @@ function App(this: FC) {
|
|
|
714
677
|
|
|
715
678
|
## Using `this` in Components
|
|
716
679
|
|
|
717
|
-
mono-jsx-dom binds a scoped signals object to `this`
|
|
680
|
+
mono-jsx-dom binds a scoped signals object to `this` in your component functions. This allows you to access signals, context, and request information directly in your components.
|
|
718
681
|
|
|
719
682
|
The `this` object has the following built-in properties:
|
|
720
683
|
|
|
721
|
-
- `
|
|
684
|
+
- `atom(initValue)`: Creates an atom signal.
|
|
685
|
+
- `store(initValue)`: Creates a signal store.
|
|
722
686
|
- `init(initValue)`: Initializes the signals.
|
|
723
687
|
- `refs`: A map of refs defined in the component.
|
|
724
688
|
- `computed(fn)`: A method to create a computed signal.
|
|
725
|
-
- `$(fn)`:
|
|
689
|
+
- `$(fn)`: A shortcut for `computed(fn)`.
|
|
726
690
|
- `effect(fn)`: A method to create side effects.
|
|
727
691
|
|
|
728
692
|
```ts
|
|
729
693
|
type FC<Signals = {}, Refs = {}> = {
|
|
730
|
-
|
|
694
|
+
atom<T>(initValue: T): Atom<T>;
|
|
695
|
+
store<T extends Record<string, unknown>>(initValue: T): T;
|
|
731
696
|
init(initValue: Signals): void;
|
|
732
697
|
refs: Refs;
|
|
733
698
|
computed<T = unknown>(fn: () => T): T;
|
|
@@ -760,3 +725,7 @@ function App(this: WithRefs<FC, { input?: HTMLInputElement }>) {
|
|
|
760
725
|
)
|
|
761
726
|
}
|
|
762
727
|
```
|
|
728
|
+
|
|
729
|
+
## License
|
|
730
|
+
|
|
731
|
+
MIT
|
package/bin/build.mjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// bin/build.ts
|
|
2
|
+
import { cwd } from "node:process";
|
|
3
|
+
import { extname, join, relative } from "node:path";
|
|
4
|
+
import { lstat, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import * as esbuild from "esbuild";
|
|
6
|
+
|
|
7
|
+
// bin/utils.ts
|
|
8
|
+
import { argv, exit, stdin, stdout } from "node:process";
|
|
9
|
+
import { access, mkdir } from "node:fs/promises";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
function parseFlags() {
|
|
12
|
+
const flags = {};
|
|
13
|
+
const args = argv.slice(2);
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg.startsWith("--")) {
|
|
17
|
+
if (arg.includes("=")) {
|
|
18
|
+
const [key, value] = arg.split("=", 2);
|
|
19
|
+
flags[key] = value;
|
|
20
|
+
} else {
|
|
21
|
+
const nextArg = args[i + 1];
|
|
22
|
+
if (!nextArg || nextArg.startsWith("--")) {
|
|
23
|
+
flags[arg] = true;
|
|
24
|
+
} else {
|
|
25
|
+
flags[arg] = nextArg;
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return flags;
|
|
32
|
+
}
|
|
33
|
+
async function resolveModule(filename, exts = [".tsx", ".ts", ".jsx", ".mjs", ".js"]) {
|
|
34
|
+
for (const ext of exts) {
|
|
35
|
+
const path = filename + ext;
|
|
36
|
+
if (await exists(path)) {
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function exists(filename) {
|
|
43
|
+
try {
|
|
44
|
+
await access(filename);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function ensureDir(path) {
|
|
51
|
+
try {
|
|
52
|
+
await access(path);
|
|
53
|
+
} catch {
|
|
54
|
+
await mkdir(path, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// bin/build.ts
|
|
59
|
+
async function run() {
|
|
60
|
+
const flags = parseFlags();
|
|
61
|
+
const start = performance.now();
|
|
62
|
+
const runtime = flags.node ?? "fetch-server";
|
|
63
|
+
await build({ runtime });
|
|
64
|
+
console.log("\x1B[32m\u2728 Build completed.\x1B[0m", "\x1B[90m(%d ms)\x1B[0m", performance.now() - start);
|
|
65
|
+
}
|
|
66
|
+
async function build(options) {
|
|
67
|
+
const workDir = join(cwd(), options?.dir ?? ".");
|
|
68
|
+
const outdir = join(workDir, options?.outdir ?? "dist");
|
|
69
|
+
if (!await exists(join(workDir, "index.html"))) {
|
|
70
|
+
console.error("index.html not found");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const indexHTML = await paseIndexHtml(workDir);
|
|
74
|
+
let twEntryCSS = null;
|
|
75
|
+
for (const filename of Object.keys(indexHTML.entryPoints)) {
|
|
76
|
+
if (filename.endsWith(".css")) {
|
|
77
|
+
const css = await readFile(join(workDir, filename), "utf8");
|
|
78
|
+
if (css.search(/@import\s+["']tailwindcss["']/) !== -1) {
|
|
79
|
+
twEntryCSS = filename;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const devServer = options?.dev;
|
|
85
|
+
const isDev = !!devServer;
|
|
86
|
+
const tw = twEntryCSS ? initTailwindBuilder(workDir, twEntryCSS) : void 0;
|
|
87
|
+
const endListeners = /* @__PURE__ */ new Set();
|
|
88
|
+
const on = (kind, callback) => {
|
|
89
|
+
if (kind === "rebuild") {
|
|
90
|
+
endListeners.add(callback);
|
|
91
|
+
return () => endListeners.delete(callback);
|
|
92
|
+
}
|
|
93
|
+
throw new Error("unknown event: " + kind);
|
|
94
|
+
};
|
|
95
|
+
const resolvePlugin = {
|
|
96
|
+
name: "resolver",
|
|
97
|
+
setup(b) {
|
|
98
|
+
b.onResolve({ filter: /\.+/ }, async ({ resolveDir, path }) => {
|
|
99
|
+
if (isUrl(path) || path.endsWith("?url")) {
|
|
100
|
+
return { path, external: true };
|
|
101
|
+
}
|
|
102
|
+
let { pathname: filename } = new URL(path, "file://" + resolveDir + "/");
|
|
103
|
+
if (filename.endsWith(".css") && tw && relative(workDir, filename) === tw.entryCSS) {
|
|
104
|
+
return { path, namespace: "tw", watchFiles: [filename] };
|
|
105
|
+
}
|
|
106
|
+
if (extname(filename) === "") {
|
|
107
|
+
if (await exists(filename) && (await lstat(filename)).isDirectory()) {
|
|
108
|
+
filename = filename + "/index";
|
|
109
|
+
}
|
|
110
|
+
const resolved = await resolveModule(filename);
|
|
111
|
+
if (resolved) {
|
|
112
|
+
filename = resolved;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (filename.endsWith(".tsx") || filename.endsWith(".jsx")) {
|
|
116
|
+
await tw?.extractCandidatesFrom(filename);
|
|
117
|
+
}
|
|
118
|
+
return {};
|
|
119
|
+
});
|
|
120
|
+
b.onLoad({ filter: /\.+/, namespace: "tw" }, () => {
|
|
121
|
+
return { contents: "", loader: "css" };
|
|
122
|
+
});
|
|
123
|
+
if (isDev) {
|
|
124
|
+
b.onEnd((result) => endListeners.forEach((fn) => fn(result)));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const ctx = await esbuild.context({
|
|
129
|
+
absWorkingDir: workDir,
|
|
130
|
+
entryPoints: Object.keys(indexHTML.entryPoints),
|
|
131
|
+
outdir,
|
|
132
|
+
outbase: workDir,
|
|
133
|
+
splitting: true,
|
|
134
|
+
bundle: true,
|
|
135
|
+
treeShaking: true,
|
|
136
|
+
minify: true,
|
|
137
|
+
write: false,
|
|
138
|
+
sourcemap: isDev ? "linked" : void 0,
|
|
139
|
+
platform: "browser",
|
|
140
|
+
format: "esm",
|
|
141
|
+
target: options?.target ?? "es2022",
|
|
142
|
+
jsx: "automatic",
|
|
143
|
+
jsxImportSource: "mono-jsx-dom",
|
|
144
|
+
jsxDev: isDev,
|
|
145
|
+
plugins: [resolvePlugin]
|
|
146
|
+
});
|
|
147
|
+
const dispose = async () => {
|
|
148
|
+
await ctx.dispose();
|
|
149
|
+
await esbuild.stop();
|
|
150
|
+
};
|
|
151
|
+
if (isDev) {
|
|
152
|
+
devServer.signal?.addEventListener("abort", dispose);
|
|
153
|
+
devServer.onWatch?.({ indexHTML, tw, on });
|
|
154
|
+
await ctx.watch();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const vfs = {};
|
|
158
|
+
const { outputFiles } = await ctx.rebuild();
|
|
159
|
+
for (const file of outputFiles) {
|
|
160
|
+
const contentType = file.path.endsWith(".js") ? "application/javascript" : "text/css";
|
|
161
|
+
const filename = relative(outdir, file.path);
|
|
162
|
+
vfs[filename] = await createVFile(indexHTML, filename, file.text, contentType);
|
|
163
|
+
}
|
|
164
|
+
if (tw) {
|
|
165
|
+
const css = await tw.build();
|
|
166
|
+
vfs[tw.entryCSS] = await createVFile(indexHTML, tw.entryCSS, css, "text/css");
|
|
167
|
+
}
|
|
168
|
+
vfs["index.html"] = await createVFile(indexHTML, "index.html", indexHTML.content, "text/html");
|
|
169
|
+
await ensureDir(outdir);
|
|
170
|
+
await writeFile(join(outdir, "build.json"), JSON.stringify(vfs, null, 2));
|
|
171
|
+
const extraJS = [
|
|
172
|
+
'import server$ from "mono-jsx-dom/server";',
|
|
173
|
+
'import buildJSON$ from "./build.json" with { type: "json" };',
|
|
174
|
+
"server$.setVFS(new Map(Object.entries(buildJSON$)));"
|
|
175
|
+
].join("");
|
|
176
|
+
await buildServerJS(workDir, outdir, options?.runtime, extraJS);
|
|
177
|
+
await dispose();
|
|
178
|
+
}
|
|
179
|
+
async function buildServerJS(workDir, outdir, runtime = "fetch-server", extraJS, watch) {
|
|
180
|
+
const stdin2 = {
|
|
181
|
+
sourcefile: join(workDir, "server.js"),
|
|
182
|
+
contents: 'import server from "mono-jsx-dom/server;export default server;',
|
|
183
|
+
loader: "js"
|
|
184
|
+
};
|
|
185
|
+
for (const loader of ["ts", "tsx", "js", "jsx"]) {
|
|
186
|
+
const sourcefile = join(workDir, "server." + loader);
|
|
187
|
+
if (await exists(sourcefile)) {
|
|
188
|
+
if (runtime === "node") {
|
|
189
|
+
stdin2.sourcefile = join(workDir, "server-node.mjs");
|
|
190
|
+
stdin2.resolveDir = workDir;
|
|
191
|
+
stdin2.contents = [
|
|
192
|
+
'import { serve$ } from "mono-jsx-dom/server/node-fetch-server";',
|
|
193
|
+
'import server$ from "./server.' + loader + '";',
|
|
194
|
+
"serve$(server$);"
|
|
195
|
+
].join("\n");
|
|
196
|
+
stdin2.loader = "js";
|
|
197
|
+
} else {
|
|
198
|
+
stdin2.sourcefile = sourcefile;
|
|
199
|
+
stdin2.contents = await readFile(sourcefile, "utf8");
|
|
200
|
+
stdin2.loader = loader;
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const esbOptions = {
|
|
206
|
+
stdin: stdin2,
|
|
207
|
+
absWorkingDir: workDir,
|
|
208
|
+
outfile: join(outdir, "server.mjs"),
|
|
209
|
+
bundle: true,
|
|
210
|
+
treeShaking: true,
|
|
211
|
+
minify: true,
|
|
212
|
+
write: true,
|
|
213
|
+
platform: "node",
|
|
214
|
+
format: "esm",
|
|
215
|
+
target: "es2024",
|
|
216
|
+
external: ["mono-jsx-dom/server", "mono-jsx-dom/server/node-fetch-server"]
|
|
217
|
+
};
|
|
218
|
+
if (extraJS) {
|
|
219
|
+
esbOptions.banner = { js: extraJS };
|
|
220
|
+
}
|
|
221
|
+
if (watch?.onRebuild) {
|
|
222
|
+
esbOptions.plugins = [{
|
|
223
|
+
name: "onend",
|
|
224
|
+
setup(build2) {
|
|
225
|
+
build2.onEnd((result) => {
|
|
226
|
+
watch.onRebuild(result);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}];
|
|
230
|
+
}
|
|
231
|
+
const ctx = await esbuild.context(esbOptions);
|
|
232
|
+
if (watch) {
|
|
233
|
+
watch.signal?.addEventListener("abort", ctx.dispose.bind(ctx));
|
|
234
|
+
await ctx.watch();
|
|
235
|
+
} else {
|
|
236
|
+
await ctx.rebuild();
|
|
237
|
+
await ctx.dispose();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function initTailwindBuilder(workDir, entryCSS) {
|
|
241
|
+
const tailwind = import("tailwindcss");
|
|
242
|
+
const oxide = import("oxide-wasm").then((m) => m.init().then(() => m));
|
|
243
|
+
const builder = {
|
|
244
|
+
entryCSS,
|
|
245
|
+
etag: null,
|
|
246
|
+
builtCSS: null,
|
|
247
|
+
files: /* @__PURE__ */ new Map(),
|
|
248
|
+
candidates: /* @__PURE__ */ new Set(),
|
|
249
|
+
compiler: null,
|
|
250
|
+
async build() {
|
|
251
|
+
if (this.builtCSS !== null) {
|
|
252
|
+
return this.builtCSS;
|
|
253
|
+
}
|
|
254
|
+
const filename = join(workDir, this.entryCSS);
|
|
255
|
+
const stats = await lstat(filename);
|
|
256
|
+
const etag = stats.mtime.getTime() + "-" + stats.size;
|
|
257
|
+
if (!this.compiler || this.etag !== etag) {
|
|
258
|
+
let entryCSS2 = await readFile(filename, "utf8");
|
|
259
|
+
this.compiler = await (await tailwind).compile(entryCSS2, {
|
|
260
|
+
async loadStylesheet(id, base) {
|
|
261
|
+
if (id === "tailwindcss") {
|
|
262
|
+
const path = join(workDir, "node_modules/tailwindcss/index.css");
|
|
263
|
+
const content = await readFile(path, "utf8");
|
|
264
|
+
return { path, base, content };
|
|
265
|
+
}
|
|
266
|
+
throw new Error("not found: " + id);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
this.etag = etag;
|
|
270
|
+
}
|
|
271
|
+
return this.builtCSS = this.compiler.build([...this.candidates]);
|
|
272
|
+
},
|
|
273
|
+
async extractCandidatesFrom(filename) {
|
|
274
|
+
const stats = await lstat(filename);
|
|
275
|
+
const modtime = stats.mtime.getTime();
|
|
276
|
+
const prev = this.files.get(filename);
|
|
277
|
+
if (prev === void 0 || prev !== modtime) {
|
|
278
|
+
const candidates = (await oxide).extract(await readFile(filename, "utf8"));
|
|
279
|
+
for (const candidate of candidates) {
|
|
280
|
+
if (!this.candidates.has(candidate)) {
|
|
281
|
+
this.candidates.add(candidate);
|
|
282
|
+
this.builtCSS = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
this.files.set(filename, modtime);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return builder;
|
|
290
|
+
}
|
|
291
|
+
async function paseIndexHtml(workDir) {
|
|
292
|
+
let content = await readFile(join(workDir, "index.html"), "utf8");
|
|
293
|
+
let entryPoints = {};
|
|
294
|
+
content = content.replace(/<link(\s[^>]*?)href="([^"]+\.css)"\s*>/g, (tag, attrs, href) => {
|
|
295
|
+
if (isUrl(href)) {
|
|
296
|
+
return tag;
|
|
297
|
+
}
|
|
298
|
+
const relativePath = relative(workDir, join(workDir, href));
|
|
299
|
+
entryPoints[relativePath] = relativePath;
|
|
300
|
+
return `<link${attrs} href="/${relativePath}">`;
|
|
301
|
+
});
|
|
302
|
+
content = content.replace(/<script(\s[^>]*?)src="([^"]+\.(ts|tsx|js|jsx|mjs))"\s*>/g, (tag, attrs, src) => {
|
|
303
|
+
if (isUrl(src)) {
|
|
304
|
+
return tag;
|
|
305
|
+
}
|
|
306
|
+
const relativePath = relative(workDir, join(workDir, src));
|
|
307
|
+
const resolvedPath = relativePath.slice(0, relativePath.lastIndexOf(".")) + ".js";
|
|
308
|
+
entryPoints[relativePath] = resolvedPath;
|
|
309
|
+
return `<script${attrs} src="/${resolvedPath}">`;
|
|
310
|
+
});
|
|
311
|
+
return { content, entryPoints };
|
|
312
|
+
}
|
|
313
|
+
async function createVFile(indexHTML, filename, content, contentType) {
|
|
314
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
|
|
315
|
+
const contentHash = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
316
|
+
if (filename !== "index.html") {
|
|
317
|
+
const ext = extname(filename);
|
|
318
|
+
if (ext !== ".css" && ext !== ".js") {
|
|
319
|
+
filename = filename.slice(0, -ext.length) + ".js";
|
|
320
|
+
}
|
|
321
|
+
indexHTML.content = indexHTML.content.replace('"/' + filename + '"', '"/' + filename + "?hash=" + hash.slice(0, 8) + '"');
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
content,
|
|
325
|
+
contentType,
|
|
326
|
+
contentHash,
|
|
327
|
+
lastModified: Date.now()
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function isUrl(url) {
|
|
331
|
+
return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//");
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
build,
|
|
335
|
+
buildServerJS,
|
|
336
|
+
run
|
|
337
|
+
};
|
package/bin/cli
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { argv, exit } from "node:process";
|
|
4
|
+
|
|
5
|
+
switch (argv[2]) {
|
|
6
|
+
case "init":
|
|
7
|
+
import("./init.mjs").then(command => command.run())
|
|
8
|
+
break;
|
|
9
|
+
case "dev":
|
|
10
|
+
import("./dev.mjs").then(command => command.run())
|
|
11
|
+
break;
|
|
12
|
+
case "build": {
|
|
13
|
+
import("./build.mjs").then(command => command.run( ))
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
default:
|
|
17
|
+
exit(0);
|
|
18
|
+
}
|