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.
- package/README.md +32 -1
- package/package.json +11 -1
- package/server.d.ts +1 -1
- 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
|
|
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.
|
|
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
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 {
|
|
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 (
|
|
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
|
|
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
|
-
|
|
287
|
-
.
|
|
288
|
-
(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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]
|
|
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
|
+
}
|