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 CHANGED
@@ -1,62 +1,31 @@
1
1
  # 🔥 mono-jsx-dom
2
2
 
3
- ![`<html>` as a `Response`](./.github/og-image.png)
3
+ ![mono-jsx-dom](./.github/og-image.png)
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 that renders web UI with browser-specific APIs.
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
- ## Installation
19
+ ## Getting Started
19
20
 
20
- ```bash
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
- # Node.js, Cloudflare Workers, or other node-compatible runtimes
43
- npx mono-jsx-dom setup
44
-
45
- # Deno
46
- deno run -A npm:mono-jsx-dom setup
24
+ # node
25
+ npx mono-jsx-dom init
47
26
 
48
- # Bun
49
- bunx mono-jsx-dom setup
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, you can use [mono-jsx](https://github.com/ije/mono-jsx) to render the UI on the server side.
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 Classes and Media Queries in `style`
74
+ ### Using Pseudo-Classes and Media Queries in `style`
112
75
 
113
- 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:
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 a html element which contains signal children.
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 with the `viewTransition` prop set to `true`.
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 an async function. With streaming rendering, async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.
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 when using a function component. This allows you to catch errors in components and display a fallback UI:
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 for updating the view when a signal changes. Signals are similar to React's state, but they are more lightweight and efficient. You can use signals to manage state in your components.
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 the view will automatically re-render when a signal changes:
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.extend` to create an extended signals object. You can use getters to create derived(computed) signals.
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.extend({
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 to allows you to define global shared signals. You can use them to share signals between components.
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 signals.
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 will run when the component is mounted and automatically collect used signals as dependencies, and re-run when the dependencies change.
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 gets run once the component element has been removed via `<show>`, `<hidden>` or `<switch>` conditional rendering:
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 is a two-way data binding, which means that when the input value changes, the signal will be updated, and when the signal changes, the input value will be updated.
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 element.
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` of your component functions. This allows you to access signals, context, and request information directly in your components.
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
- - `extend(initValue)`: Extends the signals object.
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)`: A shortcut for `computed(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
- extend<T extends Record<string, unknown>>(initValue: T): FC<T>;
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
+ }