mono-jsx-dom 0.1.0 → 0.1.2
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 +38 -63
- package/bin/build.mjs +337 -0
- package/bin/cli +18 -0
- package/bin/dev.mjs +213 -0
- package/bin/init.mjs +221 -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,37 @@
|
|
|
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
|
## Installation
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
|
-
npm install mono-jsx
|
|
22
|
+
npm install mono-jsx-dom
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
##
|
|
25
|
+
## Getting Started
|
|
25
26
|
|
|
26
|
-
|
|
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:
|
|
27
|
+
You can run the `mono-jsx-dom int` command to initialize a project with mono-jsx-dom boilerplate.
|
|
40
28
|
|
|
41
29
|
```bash
|
|
42
|
-
#
|
|
43
|
-
npx mono-jsx-dom
|
|
44
|
-
|
|
45
|
-
# Deno
|
|
46
|
-
deno run -A npm:mono-jsx-dom setup
|
|
30
|
+
# node
|
|
31
|
+
npx mono-jsx-dom init
|
|
47
32
|
|
|
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
|
-
}
|
|
33
|
+
# bun
|
|
34
|
+
bunx --bun mono-jsx-dom init
|
|
60
35
|
```
|
|
61
36
|
|
|
62
37
|
## Usage
|
|
@@ -73,14 +48,8 @@ function App(this: FC) {
|
|
|
73
48
|
document.body.mount(<App />);
|
|
74
49
|
```
|
|
75
50
|
|
|
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
51
|
>[!TIP]
|
|
83
|
-
> `mono-jsx-dom` is designed for client-side rendering
|
|
52
|
+
> `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
53
|
|
|
85
54
|
## Using JSX
|
|
86
55
|
|
|
@@ -108,9 +77,9 @@ mono-jsx-dom allows you to compose the `class` property using arrays of strings,
|
|
|
108
77
|
/>;
|
|
109
78
|
```
|
|
110
79
|
|
|
111
|
-
### Using Pseudo
|
|
80
|
+
### Using Pseudo-Classes and Media Queries in `style`
|
|
112
81
|
|
|
113
|
-
mono-jsx-dom supports [pseudo
|
|
82
|
+
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
83
|
|
|
115
84
|
```tsx
|
|
116
85
|
<a
|
|
@@ -161,7 +130,7 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
161
130
|
}
|
|
162
131
|
```
|
|
163
132
|
|
|
164
|
-
You can also set the `viewTransition` prop
|
|
133
|
+
You can also set the `viewTransition` prop on an HTML element that contains signal children.
|
|
165
134
|
|
|
166
135
|
```tsx
|
|
167
136
|
function App(this: FC<{ message: string }>) {
|
|
@@ -172,7 +141,7 @@ function App(this: FC<{ message: string }>) {
|
|
|
172
141
|
}
|
|
173
142
|
```
|
|
174
143
|
|
|
175
|
-
You can also set the view transition name in the style property
|
|
144
|
+
You can also set the view transition name in the `style` property by setting the `viewTransition` prop to `true`.
|
|
176
145
|
|
|
177
146
|
```tsx
|
|
178
147
|
function App(this: FC<{ message: string }>) {
|
|
@@ -278,7 +247,7 @@ function App() {
|
|
|
278
247
|
|
|
279
248
|
## Async Components
|
|
280
249
|
|
|
281
|
-
mono-jsx-dom supports async components that return a `Promise` or
|
|
250
|
+
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
251
|
|
|
283
252
|
```tsx
|
|
284
253
|
async function JsonViewer(props: { url: string }) {
|
|
@@ -347,7 +316,7 @@ document.body.mount(<App />);
|
|
|
347
316
|
|
|
348
317
|
## Error Handling
|
|
349
318
|
|
|
350
|
-
You can add the `catch` prop
|
|
319
|
+
You can add the `catch` prop to a function component. This allows you to catch errors in components and display a fallback UI:
|
|
351
320
|
|
|
352
321
|
```tsx
|
|
353
322
|
async function Hello() {
|
|
@@ -369,11 +338,11 @@ The `catch` prop should be a function that gets the caught error as the first ar
|
|
|
369
338
|
|
|
370
339
|
## Using Signals
|
|
371
340
|
|
|
372
|
-
mono-jsx-dom uses signals
|
|
341
|
+
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
342
|
|
|
374
343
|
### Using Component Signals
|
|
375
344
|
|
|
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
|
|
345
|
+
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
346
|
|
|
378
347
|
```tsx
|
|
379
348
|
function Counter(this: FC<{ count: number }>, props: { initialCount?: number }) {
|
|
@@ -396,11 +365,11 @@ function Counter(this: FC<{ count: number }>, props: { initialCount?: number })
|
|
|
396
365
|
}
|
|
397
366
|
```
|
|
398
367
|
|
|
399
|
-
You can use `this.
|
|
368
|
+
You can use `this.store` to create a signal store. You can use getters to create derived (computed) signals.
|
|
400
369
|
|
|
401
370
|
```tsx
|
|
402
371
|
function App(this: FC<{ count: number }>) {
|
|
403
|
-
const counter = this.
|
|
372
|
+
const counter = this.store({
|
|
404
373
|
value: 0,
|
|
405
374
|
// double is a derived(computed) signal
|
|
406
375
|
get double() {
|
|
@@ -420,10 +389,10 @@ function App(this: FC<{ count: number }>) {
|
|
|
420
389
|
|
|
421
390
|
### Using `atom` and `store`
|
|
422
391
|
|
|
423
|
-
mono-jsx-dom provides two functions
|
|
392
|
+
mono-jsx-dom provides two functions that allow you to define shared global signals. You can use them to share signals between components.
|
|
424
393
|
|
|
425
394
|
- `atom(initValue)`: Creates an atom signal.
|
|
426
|
-
- `store(initValue)`: Creates a
|
|
395
|
+
- `store(initValue)`: Creates a signal store.
|
|
427
396
|
|
|
428
397
|
```ts
|
|
429
398
|
export interface Atom<T> {
|
|
@@ -497,7 +466,7 @@ function App(this: FC<{ input: string }>) {
|
|
|
497
466
|
|
|
498
467
|
### Using Effect
|
|
499
468
|
|
|
500
|
-
You can use `this.effect` to perform side effects in components. The effect
|
|
469
|
+
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
470
|
|
|
502
471
|
```tsx
|
|
503
472
|
function App(this: FC<{ count: number }>) {
|
|
@@ -516,7 +485,7 @@ function App(this: FC<{ count: number }>) {
|
|
|
516
485
|
}
|
|
517
486
|
```
|
|
518
487
|
|
|
519
|
-
The callback function of `this.effect` can return a cleanup function that
|
|
488
|
+
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
489
|
|
|
521
490
|
```tsx
|
|
522
491
|
function Counter(this: FC<{ count: number }>) {
|
|
@@ -587,7 +556,7 @@ function App(this: FC<{ hidden: boolean }>) {
|
|
|
587
556
|
}
|
|
588
557
|
```
|
|
589
558
|
|
|
590
|
-
If you need `if-else` logic in JSX, use `<switch>` element instead:
|
|
559
|
+
If you need `if-else` logic in JSX, use the `<switch>` element instead:
|
|
591
560
|
|
|
592
561
|
```tsx
|
|
593
562
|
function App(this: FC<{ ok: boolean }>) {
|
|
@@ -629,7 +598,7 @@ function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
|
|
|
629
598
|
|
|
630
599
|
### Form Input Two-way Binding
|
|
631
600
|
|
|
632
|
-
You can use the `$value` prop to bind a signal to the value of a form input element. The `$value` prop
|
|
601
|
+
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
602
|
|
|
634
603
|
```tsx
|
|
635
604
|
function App(this: FC<{ value: string }>) {
|
|
@@ -642,7 +611,7 @@ function App(this: FC<{ value: string }>) {
|
|
|
642
611
|
}
|
|
643
612
|
```
|
|
644
613
|
|
|
645
|
-
You can also use the `$checked` prop to bind a signal to the checked state of a checkbox or radio input
|
|
614
|
+
You can also use the `$checked` prop to bind a signal to the checked state of a checkbox or radio input.
|
|
646
615
|
|
|
647
616
|
```tsx
|
|
648
617
|
function App(this: FC<{ checked: boolean }>) {
|
|
@@ -714,20 +683,22 @@ function App(this: FC) {
|
|
|
714
683
|
|
|
715
684
|
## Using `this` in Components
|
|
716
685
|
|
|
717
|
-
mono-jsx-dom binds a scoped signals object to `this`
|
|
686
|
+
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
687
|
|
|
719
688
|
The `this` object has the following built-in properties:
|
|
720
689
|
|
|
721
|
-
- `
|
|
690
|
+
- `atom(initValue)`: Creates an atom signal.
|
|
691
|
+
- `store(initValue)`: Creates a signal store.
|
|
722
692
|
- `init(initValue)`: Initializes the signals.
|
|
723
693
|
- `refs`: A map of refs defined in the component.
|
|
724
694
|
- `computed(fn)`: A method to create a computed signal.
|
|
725
|
-
- `$(fn)`:
|
|
695
|
+
- `$(fn)`: A shortcut for `computed(fn)`.
|
|
726
696
|
- `effect(fn)`: A method to create side effects.
|
|
727
697
|
|
|
728
698
|
```ts
|
|
729
699
|
type FC<Signals = {}, Refs = {}> = {
|
|
730
|
-
|
|
700
|
+
atom<T>(initValue: T): Atom<T>;
|
|
701
|
+
store<T extends Record<string, unknown>>(initValue: T): T;
|
|
731
702
|
init(initValue: Signals): void;
|
|
732
703
|
refs: Refs;
|
|
733
704
|
computed<T = unknown>(fn: () => T): T;
|
|
@@ -760,3 +731,7 @@ function App(this: WithRefs<FC, { input?: HTMLInputElement }>) {
|
|
|
760
731
|
)
|
|
761
732
|
}
|
|
762
733
|
```
|
|
734
|
+
|
|
735
|
+
## License
|
|
736
|
+
|
|
737
|
+
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, 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
|
+
}
|