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 CHANGED
@@ -1,62 +1,37 @@
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
19
  ## Installation
19
20
 
20
21
  ```bash
21
- npm install mono-jsx
22
+ npm install mono-jsx-dom
22
23
  ```
23
24
 
24
- ## Setup JSX Runtime
25
+ ## Getting Started
25
26
 
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:
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
- # 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
30
+ # node
31
+ npx mono-jsx-dom init
47
32
 
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
- }
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, you can use [mono-jsx](https://github.com/ije/mono-jsx) to render the UI on the server side.
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 Classes and Media Queries in `style`
80
+ ### Using Pseudo-Classes and Media Queries in `style`
112
81
 
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:
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 a html element which contains signal children.
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 with the `viewTransition` prop set to `true`.
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 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.
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 when using a function component. This allows you to catch errors in components and display a fallback UI:
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 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.
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 the view will automatically re-render when a signal changes:
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.extend` to create an extended signals object. You can use getters to create derived(computed) signals.
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.extend({
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 to allows you to define global shared signals. You can use them to share signals between components.
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 signals.
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 will run when the component is mounted and automatically collect used signals as dependencies, and re-run when the dependencies change.
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 gets run once the component element has been removed via `<show>`, `<hidden>` or `<switch>` conditional rendering:
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 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.
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 element.
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` of your component functions. This allows you to access signals, context, and request information directly in your components.
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
- - `extend(initValue)`: Extends the signals object.
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)`: A shortcut for `computed(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
- extend<T extends Record<string, unknown>>(initValue: T): FC<T>;
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
+ }