evikit 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +32 -1
  2. package/package.json +11 -1
  3. package/server.d.ts +1 -1
  4. package/server.js +128 -19
package/README.md CHANGED
@@ -61,6 +61,7 @@ Our language will be standard modern JavaScript as supported by Node.js directly
61
61
  - [Styling](#styling)
62
62
  - _TODO_ [Testing](#testing)
63
63
  - [Linting](#linting)
64
+ - [esbuild](#esbuild)
64
65
 
65
66
  ### Create project
66
67
 
@@ -288,7 +289,7 @@ The following commands should work now:
288
289
  # Run dev server with hot reload for Preact and full restart on API change.
289
290
  npx vite dev
290
291
 
291
- # Build server and client for production.
292
+ # Build client for production.
292
293
  npx tsc
293
294
  npx vite build
294
295
  ```
@@ -1004,6 +1005,36 @@ npx eslint .
1004
1005
 
1005
1006
  You can add these commands (without npx) as `format` and `lint` scripts in your `package.json`.
1006
1007
 
1008
+ ## esbuild
1009
+
1010
+ If you don't need Tailwind (e.g. `index.css` is enough for you) or bundle analyzer, and are ok with full-page refresh instead of hot module replacement, you can have EviKit use [esbuild](https://esbuild.github.io/) as a smaller alternative to Vite.
1011
+
1012
+ ```sh
1013
+ npm i -D esbuild
1014
+ npm install-scripts approve esbuild
1015
+ npm rm @preact/preset-vite vite vite-bundle-analyzer
1016
+ rm -f vite.config.js
1017
+ ```
1018
+
1019
+ Change the `production` call in your `index.js`:
1020
+
1021
+ ```js
1022
+ app.use(production({ prerender }));
1023
+ ```
1024
+
1025
+ The following commands should work now:
1026
+
1027
+ ```sh
1028
+ # Run dev server with watch mode.
1029
+ node --env-file=.env index.js dev
1030
+
1031
+ # Build client for production.
1032
+ npx tsc
1033
+ node --env-file=.env index.js build
1034
+ ```
1035
+
1036
+ You can change the `dev` and `build` scripts in your `package.json` accordingly.
1037
+
1007
1038
  ## License
1008
1039
 
1009
1040
  SPDX-License-Identifier: MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evikit",
3
3
  "description": "Preact/SQLite SSR framework for small web apps",
4
- "version": "2.12.0",
4
+ "version": "2.13.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./index.js",
@@ -32,15 +32,22 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "accept-language": "^3.0.20",
35
+ "esbuild": "^0.28.2",
35
36
  "hoofd": "^1.7.3",
36
37
  "preact": "^10.29.2",
37
38
  "preact-iso": "^2.12.0",
38
39
  "valibot": "^1.4.0"
39
40
  },
41
+ "peerDependenciesMeta": {
42
+ "esbuild": {
43
+ "optional": true
44
+ }
45
+ },
40
46
  "devDependencies": {
41
47
  "@eslint/compat": "^2.1.0",
42
48
  "@eslint/js": "^10.0.1",
43
49
  "@types/node": "^24.13.3",
50
+ "esbuild": "^0.28.2",
44
51
  "eslint": "^10.4.0",
45
52
  "eslint-config-prettier": "^10.1.8",
46
53
  "eslint-plugin-perfectionist": "^5.9.0",
@@ -48,5 +55,8 @@
48
55
  "prettier": "^3.8.3",
49
56
  "typescript": "^6.0.3",
50
57
  "typescript-eslint": "^8.59.4"
58
+ },
59
+ "allowScripts": {
60
+ "esbuild": true
51
61
  }
52
62
  }
package/server.d.ts CHANGED
@@ -29,7 +29,7 @@ export function production({
29
29
  html,
30
30
  prerender,
31
31
  }: {
32
- html: string;
32
+ html?: string;
33
33
  prerender({ lang, url }: { lang: string; url: string }): Promise<{
34
34
  data: object;
35
35
  head: {
package/server.js CHANGED
@@ -4,7 +4,8 @@ import acceptLanguage from "accept-language";
4
4
  import { po } from "gettext-parser";
5
5
  import cluster from "node:cluster";
6
6
  import { createHash } from "node:crypto";
7
- import { readFile } from "node:fs/promises";
7
+ import { existsSync } from "node:fs";
8
+ import { readFile, unlink, writeFile } from "node:fs/promises";
8
9
  import { availableParallelism } from "node:os";
9
10
  import { h } from "preact";
10
11
  import { renderToString } from "preact-render-to-string";
@@ -207,10 +208,104 @@ export function json(output) {
207
208
  };
208
209
  }
209
210
 
211
+ let esbuild = false;
212
+
210
213
  /** @param {{ listen: (port: number, callback: () => void) => { close(): void, on(event: "error", callback: (error: unknown) => void ): void } }} app */
211
- export function listen(app) {
214
+ export async function listen(app) {
215
+ if (esbuild && cluster.isPrimary) {
216
+ let esbuildPort = 0;
217
+
218
+ async function makeHtml() {
219
+ await writeFile(
220
+ "./dist/index.html",
221
+ (await readFile("./index.html", "utf-8"))
222
+ .replace(
223
+ "/index.css",
224
+ "/index.css?" + (await sha256("./dist/index.css")),
225
+ )
226
+ .replace(
227
+ /<script[^>]+><\/script>/,
228
+ '<script src="/src/prerender.js?' +
229
+ (await sha256("./dist/src/prerender.js")) +
230
+ '"></script>' +
231
+ (esbuildPort
232
+ ? '<script>new EventSource("http://localhost:' +
233
+ esbuildPort +
234
+ '/esbuild").addEventListener("change", () => location.reload());</script>'
235
+ : ""),
236
+ ),
237
+ );
238
+ }
239
+
240
+ if (process.argv[2] == "build") {
241
+ const { build } = await import("esbuild");
242
+ if (existsSync("./dist/api.js")) {
243
+ await unlink("./dist/api.js");
244
+ }
245
+ await build({
246
+ bundle: true,
247
+ entryPoints: ["./index.css", "./src/prerender.js"],
248
+ outdir: "./dist",
249
+ });
250
+ await makeHtml();
251
+ return;
252
+ }
253
+
254
+ if (process.argv[2] == "dev" && cluster.isPrimary) {
255
+ const { context } = await import("esbuild");
256
+
257
+ const { watch: watchApi } = await context({
258
+ bundle: true,
259
+ entryPoints: ["./src/api.js"],
260
+ external: ["esbuild"],
261
+ outdir: "./dist",
262
+ platform: "node",
263
+ });
264
+ await watchApi();
265
+
266
+ const { serve: servePrerender, watch: watchPrerender } = await context({
267
+ bundle: true,
268
+ entryPoints: ["./index.css", "./src/prerender.js"],
269
+ outdir: "./dist",
270
+ });
271
+ await watchPrerender();
272
+ ({ port: esbuildPort } = await servePrerender({
273
+ cors: {
274
+ origin:
275
+ process.env["ORIGIN"] ||
276
+ `http://localhost:${Number(process.env["PORT"]) || 5173}`,
277
+ },
278
+ }));
279
+
280
+ let subprocess = cluster.fork();
281
+ let lastHash =
282
+ (await sha256("./dist/api.js")) +
283
+ (await sha256("./dist/index.css")) +
284
+ (await sha256("./dist/src/prerender.js"));
285
+ await makeHtml();
286
+ setInterval(async () => {
287
+ const hash =
288
+ (await sha256("./dist/api.js")) +
289
+ (await sha256("./dist/index.css")) +
290
+ (await sha256("./dist/src/prerender.js"));
291
+ if (hash != lastHash) {
292
+ lastHash = hash;
293
+ await makeHtml();
294
+ subprocess.kill();
295
+ subprocess = cluster.fork();
296
+ }
297
+ }, 1000);
298
+ }
299
+
300
+ await makeHtml();
301
+ }
302
+
212
303
  const port = Number(process.env["PORT"]) || 5173;
213
- if (cluster.isPrimary && parallelism() > 1) {
304
+ if (
305
+ cluster.isPrimary &&
306
+ parallelism() > 1 &&
307
+ (!esbuild || !process.argv[2])
308
+ ) {
214
309
  /** @type {{ on(event: "message", callback: (message: any) => void): void, send(message: any, _: any, options: { keepOpen: boolean }): void }[]} */
215
310
  const forks = [];
216
311
  for (let i = 0; i < parallelism(); i++) {
@@ -226,7 +321,7 @@ export function listen(app) {
226
321
  }
227
322
  });
228
323
  }
229
- } else {
324
+ } else if ((esbuild && !process.argv[2]) || cluster.isPrimary) {
230
325
  const server = app.listen(port, () => {
231
326
  if (cluster.isPrimary || process.env["i"] == "0") {
232
327
  console.log(
@@ -244,14 +339,20 @@ export function listen(app) {
244
339
  }
245
340
  }
246
341
 
247
- /** @param {{ html: string, prerender({ cookie, lang, url }: { cookie?: string | undefined, lang: string, url: string }): Promise<{ data: {}, head: { elements: Set<{ props: { [k: string]: string }, type: string }>, lang: string, title: string }, html: string }> }} props */
342
+ /** @param {{ html?: string, prerender({ cookie, lang, url }: { cookie?: string | undefined, lang: string, url: string }): Promise<{ data: {}, head: { elements: Set<{ props: { [k: string]: string }, type: string }>, lang: string, title: string }, html: string }> }} props */
248
343
  export function production({ html, prerender }) {
344
+ if (!html) {
345
+ esbuild = true;
346
+ }
249
347
  /**
250
348
  * @param {import("node:http").IncomingMessage} req
251
349
  * @param {import("node:http").ServerResponse<import("node:http").IncomingMessage>} res
252
350
  */
253
351
  return async function onRequest(req, res) {
254
352
  try {
353
+ if (!html) {
354
+ html = await readFile("./dist/index.html", "utf-8");
355
+ }
255
356
  const rendered = await prerender({
256
357
  cookie: req.headers.cookie,
257
358
  lang: acceptLanguage.get(req.headers["accept-language"]) || "en",
@@ -271,9 +372,7 @@ export function production({ html, prerender }) {
271
372
  res.end(
272
373
  html
273
374
  .replace(/lang=".+?"/, () => `lang="${rendered.head.lang}"`)
274
- .replace(/<title>.+<\/title>/, () =>
275
- renderToString(h("title", null, rendered.head.title)),
276
- )
375
+ .replace(/<title>.+<\/title>/, "")
277
376
  .replace(/<!--app-head-->[\s\S]+?<body/, (match) =>
278
377
  match
279
378
  .replace(/<link[^>]+>/g, (link) =>
@@ -283,19 +382,20 @@ export function production({ html, prerender }) {
283
382
  )
284
383
  .replace(
285
384
  `<!--app-head-->`,
286
- [...rendered.head.elements.values()]
287
- .map(
288
- ({ props, type }) =>
289
- `<${type} ` +
290
- Object.entries(props)
291
- .map(([name, value]) => `${name}="${value}"`)
292
- .join(" ") +
293
- (type == "script" ? `</${type}>` : " />"),
294
- )
295
- .join(""),
385
+ renderToString(h("title", null, rendered.head.title)) +
386
+ [...rendered.head.elements.values()]
387
+ .map(
388
+ ({ props, type }) =>
389
+ `<${type} ` +
390
+ Object.entries(props)
391
+ .map(([name, value]) => `${name}="${value}"`)
392
+ .join(" ") +
393
+ (type == "script" ? `</${type}>` : " />"),
394
+ )
395
+ .join(""),
296
396
  )
297
397
  .replace(
298
- /(<div id="app">)[\s\S]+<!--app-html-->/,
398
+ /(<div id="app">)[\s\S]*<!--app-html-->/,
299
399
  (_, div) =>
300
400
  div +
301
401
  rendered.html +
@@ -666,3 +766,12 @@ function ArrayOf(input) {
666
766
  function parallelism() {
667
767
  return Number(process.env["PARALLELISM"]) || availableParallelism();
668
768
  }
769
+
770
+ /** @param {string} file */
771
+ async function sha256(file) {
772
+ return existsSync(file)
773
+ ? createHash("sha256")
774
+ .update(await readFile(file))
775
+ .digest("hex")
776
+ : "";
777
+ }