evikit 2.12.0 → 2.14.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 CHANGED
@@ -59,8 +59,10 @@ Our language will be standard modern JavaScript as supported by Node.js directly
59
59
  - [File upload](#file-upload)
60
60
  - [Translation](#translation)
61
61
  - [Styling](#styling)
62
+ - [EviKit CSS](#evikit-css)
62
63
  - _TODO_ [Testing](#testing)
63
64
  - [Linting](#linting)
65
+ - [esbuild](#esbuild)
64
66
 
65
67
  ### Create project
66
68
 
@@ -288,7 +290,7 @@ The following commands should work now:
288
290
  # Run dev server with hot reload for Preact and full restart on API change.
289
291
  npx vite dev
290
292
 
291
- # Build server and client for production.
293
+ # Build client for production.
292
294
  npx tsc
293
295
  npx vite build
294
296
  ```
@@ -880,7 +882,9 @@ export function acceptLanguages() {
880
882
 
881
883
  ### Styling
882
884
 
883
- EviKit is decoupled from styling, it's up to you how to design your app. I've had good experience with [daisyUI](https://daisyui.com/). Let's see how to set it up.
885
+ #### Tailwind and daisyUI
886
+
887
+ I've had good experience styling apps with [daisyUI](https://daisyui.com/). Let's see how to set it up.
884
888
 
885
889
  ```sh
886
890
  npm i -D @tailwindcss/vite daisyui
@@ -924,6 +928,126 @@ Change your `index.html` like this:
924
928
 
925
929
  Style your app however you like.
926
930
 
931
+ #### EviKit CSS
932
+
933
+ There's a stylesheet with a CSS reset and a small number of utility classes named like Tailwind and daisyUI. I use it to build my EviKit-based apps [with esbuild](#esbuild) and no plugins:
934
+
935
+ - [EviKit's own landing page](https://codeberg.org/nykula/create-evikit)
936
+ - [Lanquiz trivia game](https://codeberg.org/nykula/lanquiz)
937
+ - [Yoself container monitoring tool](https://codeberg.org/nykula/yoself)
938
+
939
+ The styles are written from scratch and aren't meant to replicate Tailwind exactly, just make apps look acceptable. In particular, they use a different palette, [X11 colors](https://www.w3.org/TR/css-color-3/) as provided by CSS itself; think late 90s, not web3.
940
+
941
+ To try them, import EviKit in `index.css`:
942
+
943
+ ```css
944
+ @import "evikit/css";
945
+ ```
946
+
947
+ And change your `index.html` like in the above instructions for Tailwind.
948
+
949
+ Margin:
950
+
951
+ - m-8
952
+ - mb-2 mb-4 sm:mb-4 -mb-4 mb-6 mb-8
953
+ - mt-0 mt-2 mt-4 -mt-4 mt-6
954
+ - mx-0 mx-auto
955
+ - my-2 my-4 my-8
956
+
957
+ Padding:
958
+
959
+ - p-4 p-6 p-8 lg:p-8
960
+ - pb-8
961
+
962
+ Gap:
963
+
964
+ - gap-2 gap-4 gap-8
965
+ - gap-x-4
966
+ - gap-y-6
967
+
968
+ Width:
969
+
970
+ - max-w-full sm:max-w-sm max-w-lg max-w-2xl max-w-5xl
971
+ - w-32 w-auto w-fit w-full
972
+
973
+ Height:
974
+
975
+ - h-auto
976
+ - min-h-screen
977
+
978
+ Font:
979
+
980
+ - font-thin font-semibold font-bold
981
+ - tabular-nums
982
+ - text-base
983
+ - text-sm lg:text-sm
984
+ - text-lg
985
+ - text-xl lg:text-xl
986
+ - text-2xl
987
+ - lg:text-3xl
988
+ - text-4xl lg:text-4xl
989
+ - tracking-wider
990
+ - uppercase
991
+ - whitespace-nowrap
992
+
993
+ Image:
994
+
995
+ - bg-center
996
+ - bg-cover
997
+ - object-contain
998
+
999
+ Layout:
1000
+
1001
+ - absolute right-2 top-2
1002
+ - contents
1003
+ - flex flex-col flex-wrap
1004
+ - grid
1005
+ - grid-cols-1
1006
+ - grid-cols-2 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-2
1007
+ - lg:grid-cols-3
1008
+ - lg:grid-cols-4
1009
+ - 2xl:grid-cols-6
1010
+ - hidden lg:hidden
1011
+ - items-center justify-center
1012
+ - overflow-auto overflow-hidden
1013
+ - row-span-2
1014
+ - text-center sm:text-center md:text-center
1015
+
1016
+ Border:
1017
+
1018
+ - border lg:border
1019
+ - border-b
1020
+ - !border-none lg:border-none
1021
+ - rounded-lg lg:rounded-lg
1022
+ - rounded-xl
1023
+ - shadow-sm lg:shadow-sm
1024
+
1025
+ Component:
1026
+
1027
+ - badge badge-info
1028
+ - btn btn-ghost btn-disabled
1029
+ - fieldset fieldset-legend label input
1030
+ - link
1031
+ - lg:table table-zebra
1032
+
1033
+ Color:
1034
+
1035
+ - opacity-0 opacity-50
1036
+ - bg-gray-50 bg-gray-100
1037
+ - !bg-transparent
1038
+ - border-gray-100
1039
+ - text-green-400
1040
+ - text-orange-400
1041
+ - text-red-400
1042
+ - fill-amber-300/[50%] stroke-amber-500 text-amber-400
1043
+ - fill-cyan-300/[50%] stroke-cyan-500 text-cyan-400
1044
+ - fill-fuchsia-300/[50%] stroke-fuchsia-500 text-fuchsia-400
1045
+ - fill-indigo-300/[50%] stroke-indigo-500 text-indigo-400
1046
+ - fill-lime-300/[50%] stroke-lime-500 text-lime-400
1047
+ - fill-purple-300/[50%] stroke-purple-500 text-purple-400
1048
+ - fill-teal-300/[50%] stroke-teal-500 text-teal-400
1049
+ - fill-yellow-300/[50%] stroke-yellow-500 text-yellow-400
1050
+
927
1051
  ### Background worker
928
1052
 
929
1053
  You can run something repeatedly in main process, not in listeners for each CPU core. In development, it will restart whenever you edit api routes.
@@ -1004,6 +1128,36 @@ npx eslint .
1004
1128
 
1005
1129
  You can add these commands (without npx) as `format` and `lint` scripts in your `package.json`.
1006
1130
 
1131
+ ## esbuild
1132
+
1133
+ If you don't need Tailwind (e.g. [EviKit CSS](#evikit-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.
1134
+
1135
+ ```sh
1136
+ npm i -D esbuild
1137
+ npm install-scripts approve esbuild
1138
+ npm rm @preact/preset-vite vite vite-bundle-analyzer
1139
+ rm -f vite.config.js
1140
+ ```
1141
+
1142
+ Change the `production` call in your `index.js`:
1143
+
1144
+ ```js
1145
+ app.use(production({ prerender }));
1146
+ ```
1147
+
1148
+ The following commands should work now:
1149
+
1150
+ ```sh
1151
+ # Run dev server with watch mode.
1152
+ node --env-file=.env index.js dev
1153
+
1154
+ # Build client for production.
1155
+ npx tsc
1156
+ node --env-file=.env index.js build
1157
+ ```
1158
+
1159
+ You can change the `dev` and `build` scripts in your `package.json` accordingly.
1160
+
1007
1161
  ## License
1008
1162
 
1009
1163
  SPDX-License-Identifier: MIT
package/index.css ADDED
@@ -0,0 +1,717 @@
1
+ * {
2
+ color: inherit;
3
+ font: inherit;
4
+ list-style: none;
5
+ margin: 0;
6
+ padding: 0;
7
+ text-align: inherit;
8
+ text-decoration: none;
9
+ }
10
+
11
+ html {
12
+ font-family: system-ui, sans-serif;
13
+ line-height: 1.5rem;
14
+ }
15
+
16
+ b {
17
+ font-weight: bold;
18
+ }
19
+
20
+ code {
21
+ font-family: monospace;
22
+ }
23
+
24
+ pre {
25
+ font-family: monospace;
26
+ font-size: 1rem;
27
+ }
28
+
29
+ summary {
30
+ list-style: revert;
31
+ }
32
+
33
+ /* Margin. */
34
+
35
+ .m-8 {
36
+ margin: 2rem;
37
+ }
38
+
39
+ .mb-2 {
40
+ margin-bottom: 0.5rem;
41
+ }
42
+
43
+ .mb-4 {
44
+ margin-bottom: 1rem;
45
+ }
46
+
47
+ @media (min-width: 640px) {
48
+ .sm\:mb-4 {
49
+ margin-bottom: 1rem;
50
+ }
51
+ }
52
+
53
+ .\-mb-4 {
54
+ margin-bottom: -1rem;
55
+ }
56
+
57
+ .mb-6 {
58
+ margin-bottom: 1.5rem;
59
+ }
60
+
61
+ .mb-8 {
62
+ margin-bottom: 2rem;
63
+ }
64
+
65
+ .mt-0 {
66
+ margin-top: 0;
67
+ }
68
+
69
+ .mt-2 {
70
+ margin-top: 0.5rem;
71
+ }
72
+
73
+ .mt-4 {
74
+ margin-top: 1rem;
75
+ }
76
+
77
+ .-mt-4 {
78
+ margin-top: -1rem;
79
+ }
80
+
81
+ .mt-6 {
82
+ margin-top: 1.5rem;
83
+ }
84
+
85
+ .mx-0 {
86
+ margin-left: 0;
87
+ margin-right: 0;
88
+ }
89
+
90
+ .mx-auto {
91
+ margin-left: auto;
92
+ margin-right: auto;
93
+ }
94
+
95
+ .my-2 {
96
+ margin-bottom: 0.5rem;
97
+ margin-top: 0.5rem;
98
+ }
99
+
100
+ .my-4 {
101
+ margin-bottom: 1rem;
102
+ margin-top: 1rem;
103
+ }
104
+
105
+ .my-8 {
106
+ margin-bottom: 2rem;
107
+ margin-top: 2rem;
108
+ }
109
+
110
+ /* Padding. */
111
+
112
+ .p-4 {
113
+ padding: 1rem;
114
+ }
115
+
116
+ .p-6 {
117
+ padding: 1.5rem;
118
+ }
119
+
120
+ .p-8 {
121
+ padding: 2rem;
122
+ }
123
+
124
+ @media (min-width: 992px) {
125
+ .lg\:p-8 {
126
+ padding: 2rem;
127
+ }
128
+ }
129
+
130
+ .pb-8 {
131
+ padding-bottom: 2rem;
132
+ }
133
+
134
+ /* Gap. */
135
+
136
+ .gap-2 {
137
+ gap: 0.5rem;
138
+ }
139
+
140
+ .gap-4 {
141
+ gap: 1rem;
142
+ }
143
+
144
+ .gap-8 {
145
+ gap: 2rem;
146
+ }
147
+
148
+ .gap-x-4 {
149
+ column-gap: 1rem;
150
+ }
151
+
152
+ .gap-y-6 {
153
+ row-gap: 1.5rem;
154
+ }
155
+
156
+ /* Width. */
157
+
158
+ .max-w-full {
159
+ max-width: 100%;
160
+ }
161
+
162
+ @media (min-width: 640px) {
163
+ .sm\:max-w-sm {
164
+ max-width: 22rem;
165
+ }
166
+ }
167
+
168
+ .max-w-lg {
169
+ max-width: 32rem;
170
+ }
171
+
172
+ .max-w-2xl {
173
+ max-width: 40rem;
174
+ }
175
+
176
+ .max-w-5xl {
177
+ max-width: 60rem;
178
+ }
179
+
180
+ .w-32 {
181
+ width: 8rem;
182
+ }
183
+
184
+ .w-auto {
185
+ width: auto;
186
+ }
187
+
188
+ .w-fit {
189
+ width: fit-content;
190
+ }
191
+
192
+ .w-full {
193
+ width: 100%;
194
+ }
195
+
196
+ /* Height. */
197
+
198
+ .h-auto {
199
+ height: auto;
200
+ }
201
+
202
+ .min-h-screen {
203
+ min-height: 100vh;
204
+ }
205
+
206
+ /* Font. */
207
+
208
+ .font-thin {
209
+ font-weight: 300;
210
+ }
211
+
212
+ .font-semibold {
213
+ font-weight: 600;
214
+ }
215
+
216
+ .font-bold {
217
+ font-weight: 700;
218
+ }
219
+
220
+ .tabular-nums {
221
+ font-variant-numeric: tabular-nums;
222
+ }
223
+
224
+ .text-base {
225
+ font-size: 1rem;
226
+ line-height: 1.5rem;
227
+ }
228
+
229
+ .text-sm {
230
+ font-size: 0.875rem;
231
+ line-height: 1.25rem;
232
+ }
233
+
234
+ @media (min-width: 992px) {
235
+ .lg\:text-sm {
236
+ font-size: 0.875rem;
237
+ line-height: 1.25rem;
238
+ }
239
+ }
240
+
241
+ .text-lg {
242
+ font-size: 1.125rem;
243
+ line-height: 1.625rem;
244
+ }
245
+
246
+ .text-xl {
247
+ font-size: 1.25rem;
248
+ line-height: 1.75rem;
249
+ }
250
+
251
+ @media (min-width: 992px) {
252
+ .lg\:text-xl {
253
+ font-size: 1.25rem;
254
+ line-height: 1.75rem;
255
+ }
256
+ }
257
+
258
+ .text-2xl {
259
+ font-size: 1.5rem;
260
+ line-height: 2.125rem;
261
+ }
262
+
263
+ @media (min-width: 992px) {
264
+ .lg\:text-3xl {
265
+ font-size: 1.875rem;
266
+ line-height: 2.25rem;
267
+ }
268
+ }
269
+
270
+ .text-4xl {
271
+ font-size: 2.25rem;
272
+ line-height: 2.75rem;
273
+ }
274
+
275
+ @media (min-width: 992px) {
276
+ .lg\:text-4xl {
277
+ font-size: 2.25rem;
278
+ line-height: 2.75rem;
279
+ }
280
+ }
281
+
282
+ .tracking-wider {
283
+ letter-spacing: 0.05rem;
284
+ }
285
+
286
+ .uppercase {
287
+ text-transform: uppercase;
288
+ }
289
+
290
+ .whitespace-nowrap {
291
+ white-space: nowrap;
292
+ }
293
+
294
+ /* Image. */
295
+
296
+ .bg-center {
297
+ background-position: center;
298
+ }
299
+
300
+ .bg-cover {
301
+ background-size: cover;
302
+ }
303
+
304
+ .object-contain {
305
+ object-fit: contain;
306
+ }
307
+
308
+ /* Layout. */
309
+
310
+ .absolute {
311
+ position: absolute;
312
+ }
313
+
314
+ .contents {
315
+ display: contents;
316
+ }
317
+
318
+ .flex {
319
+ display: flex;
320
+ }
321
+
322
+ .flex-col {
323
+ flex-direction: column;
324
+ }
325
+
326
+ .flex-wrap {
327
+ flex-wrap: wrap;
328
+ }
329
+
330
+ .grid {
331
+ display: grid;
332
+ }
333
+
334
+ .grid-cols-1 {
335
+ grid-template-columns: 1fr;
336
+ }
337
+
338
+ .grid-cols-2 {
339
+ grid-template-columns: 1fr 1fr;
340
+ }
341
+
342
+ @media (min-width: 640px) {
343
+ .sm\:grid-cols-2 {
344
+ grid-template-columns: 1fr 1fr;
345
+ }
346
+ }
347
+
348
+ @media (min-width: 768px) {
349
+ .md\:grid-cols-2 {
350
+ grid-template-columns: 1fr 1fr;
351
+ }
352
+ }
353
+
354
+ @media (min-width: 992px) {
355
+ .lg\:grid-cols-2 {
356
+ grid-template-columns: 1fr 1fr;
357
+ }
358
+ }
359
+
360
+ @media (min-width: 992px) {
361
+ .lg\:grid-cols-3 {
362
+ grid-template-columns: 1fr 1fr 1fr;
363
+ }
364
+ }
365
+
366
+ @media (min-width: 992px) {
367
+ .lg\:grid-cols-4 {
368
+ grid-template-columns: 1fr 1fr 1fr 1fr;
369
+ }
370
+ }
371
+
372
+ @media (min-width: 1200px) {
373
+ .\2xl\:grid-cols-6 {
374
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
375
+ }
376
+ }
377
+
378
+ .hidden {
379
+ display: none;
380
+ }
381
+
382
+ @media (min-width: 992px) {
383
+ .lg\:hidden {
384
+ display: none;
385
+ }
386
+ }
387
+
388
+ .items-center {
389
+ align-items: center;
390
+ }
391
+
392
+ .justify-center {
393
+ justify-content: center;
394
+ }
395
+
396
+ .overflow-auto {
397
+ overflow: auto;
398
+ }
399
+
400
+ .overflow-hidden {
401
+ overflow: hidden;
402
+ }
403
+
404
+ .right-2 {
405
+ right: 0.5rem;
406
+ }
407
+
408
+ .row-span-2 {
409
+ grid-row: 2;
410
+ }
411
+
412
+ .text-center {
413
+ text-align: center;
414
+ }
415
+
416
+ @media (min-width: 640px) {
417
+ .sm\:text-center {
418
+ text-align: center;
419
+ }
420
+ }
421
+
422
+ @media (min-width: 768px) {
423
+ .md\:text-center {
424
+ text-align: center;
425
+ }
426
+ }
427
+
428
+ .top-2 {
429
+ top: 0.5rem;
430
+ }
431
+
432
+ /* Border. */
433
+
434
+ .border {
435
+ border: 1px solid;
436
+ }
437
+
438
+ @media (min-width: 992px) {
439
+ .lg\:border {
440
+ border: 1px solid;
441
+ }
442
+ }
443
+
444
+ .border-b {
445
+ border-bottom: 1px solid;
446
+ }
447
+
448
+ .\!border-none {
449
+ border: none !important;
450
+ }
451
+
452
+ @media (min-width: 992px) {
453
+ .lg\:border-none {
454
+ border: none;
455
+ }
456
+ }
457
+
458
+ .rounded-lg {
459
+ border-radius: 0.5rem;
460
+ }
461
+
462
+ @media (min-width: 992px) {
463
+ .lg\:rounded-lg {
464
+ border-radius: 0.5rem;
465
+ }
466
+ }
467
+
468
+ .rounded-xl {
469
+ border-radius: 1rem;
470
+ }
471
+
472
+ .shadow-sm {
473
+ box-shadow: 0 0.125em 0.25em rgba(0, 0, 0, 0.1);
474
+ }
475
+
476
+ @media (min-width: 992px) {
477
+ .lg\:shadow-sm {
478
+ box-shadow: 0 0.125em 0.25em rgba(0, 0, 0, 0.1);
479
+ }
480
+ }
481
+
482
+ /* Component. */
483
+
484
+ .badge {
485
+ border-radius: 0.5rem;
486
+ display: inline-flex;
487
+ gap: 0.5rem;
488
+ font-size: 0.875rem;
489
+ padding: 0.0625rem 0.75rem;
490
+ }
491
+
492
+ .badge-info {
493
+ background-color: color-mix(in srgb, darkturquoise 6.25%, transparent);
494
+ }
495
+
496
+ .btn {
497
+ background-color: rgba(0, 0, 0, 0.03125);
498
+ border: 1px solid rgba(0, 0, 0, 0.0625);
499
+ border-radius: 0.25rem;
500
+ cursor: pointer;
501
+ font-size: 0.875rem;
502
+ font-weight: 600;
503
+ padding: 0.5rem 1rem;
504
+ }
505
+
506
+ .btn:focus,
507
+ .btn:hover {
508
+ background-color: rgba(0, 0, 0, 0.125);
509
+ }
510
+
511
+ .btn-ghost:not(:focus):not(:hover) {
512
+ background-color: transparent;
513
+ border-color: transparent;
514
+ }
515
+
516
+ .btn-disabled {
517
+ background-color: rgba(0, 0, 0, 0.03125);
518
+ border-color: rgba(0, 0, 0, 0.03125);
519
+ }
520
+
521
+ .fieldset {
522
+ border: none;
523
+ display: flex;
524
+ flex-direction: column;
525
+ gap: 0.25rem;
526
+ padding-bottom: 0.25rem;
527
+ padding-top: 0.25rem;
528
+ }
529
+
530
+ .fieldset-legend {
531
+ font-size: 0.75rem;
532
+ font-weight: bold;
533
+ }
534
+
535
+ .label {
536
+ display: table;
537
+ font-size: 0.75rem;
538
+ }
539
+
540
+ .input {
541
+ border: 1px solid rgba(0, 0, 0, 0.25);
542
+ border-radius: 0.25rem;
543
+ font-size: 0.875rem;
544
+ max-width: 100%;
545
+ padding: 0.5rem 0.75rem;
546
+ width: 20rem;
547
+ }
548
+
549
+ .link {
550
+ text-decoration: underline;
551
+ }
552
+
553
+ .lg\:table {
554
+ border-collapse: collapse;
555
+ font-size: 0.875rem;
556
+ width: 100%;
557
+ }
558
+
559
+ @media (min-width: 992px) {
560
+ .lg\:table {
561
+ display: table;
562
+ }
563
+ }
564
+
565
+ .lg\:table thead th {
566
+ font-weight: 700;
567
+ opacity: 0.5;
568
+ }
569
+
570
+ .lg\:table tr > * {
571
+ padding: 0.75rem;
572
+ }
573
+
574
+ .table-zebra tr:nth-child(even) {
575
+ background-color: rgba(0, 0, 0, 0.025);
576
+ border-bottom: 1px solid rgba(0, 0, 0, 0.075);
577
+ }
578
+
579
+ @media (prefers-color-scheme: dark) {
580
+ .table-zebra tr:nth-child(even) {
581
+ background-color: rgba(255, 255, 255, 0.05);
582
+ }
583
+ }
584
+
585
+ /* Color. */
586
+
587
+ .opacity-0 {
588
+ opacity: 0;
589
+ }
590
+
591
+ .opacity-50 {
592
+ opacity: 0.5;
593
+ }
594
+
595
+ .bg-gray-50 {
596
+ background-color: rgba(0, 0, 0, 0.015625);
597
+ }
598
+
599
+ .bg-gray-100 {
600
+ background-color: rgba(0, 0, 0, 0.03125);
601
+ }
602
+
603
+ .\!bg-transparent {
604
+ background-color: transparent !important;
605
+ }
606
+
607
+ .border-gray-100 {
608
+ border-color: rgba(0, 0, 0, 0.03125);
609
+ }
610
+
611
+ .fill-amber-300\/\[50\%\] {
612
+ fill: color-mix(in srgb, chocolate 25%, transparent);
613
+ }
614
+
615
+ .fill-cyan-300\/\[50\%\] {
616
+ fill: color-mix(in srgb, darkcyan 25%, transparent);
617
+ }
618
+
619
+ .fill-fuchsia-300\/\[50\%\] {
620
+ fill: color-mix(in srgb, palevioletred 25%, transparent);
621
+ }
622
+
623
+ .fill-indigo-300\/\[50\%\] {
624
+ fill: color-mix(in srgb, darkolivegreen 25%, transparent);
625
+ }
626
+
627
+ .fill-lime-300\/\[50\%\] {
628
+ fill: color-mix(in srgb, green 25%, transparent);
629
+ }
630
+
631
+ .fill-purple-300\/\[50\%\] {
632
+ fill: color-mix(in srgb, darkorchid 25%, transparent);
633
+ }
634
+
635
+ .fill-teal-300\/\[50\%\] {
636
+ fill: color-mix(in srgb, darkturquoise 25%, transparent);
637
+ }
638
+
639
+ .fill-yellow-300\/\[50\%\] {
640
+ fill: color-mix(in srgb, yellowgreen 25%, transparent);
641
+ }
642
+
643
+ .stroke-amber-500 {
644
+ stroke: chocolate;
645
+ }
646
+
647
+ .stroke-cyan-500 {
648
+ stroke: darkcyan;
649
+ }
650
+
651
+ .stroke-fuchsia-500 {
652
+ stroke: palevioletred;
653
+ }
654
+
655
+ .stroke-indigo-500 {
656
+ stroke: darkolivegreen;
657
+ }
658
+
659
+ .stroke-lime-500 {
660
+ stroke: green;
661
+ }
662
+
663
+ .stroke-purple-500 {
664
+ stroke: darkorchid;
665
+ }
666
+
667
+ .stroke-teal-500 {
668
+ stroke: darkturquoise;
669
+ }
670
+
671
+ .stroke-yellow-500 {
672
+ stroke: yellowgreen;
673
+ }
674
+
675
+ .text-amber-400 {
676
+ color: chocolate;
677
+ }
678
+
679
+ .text-cyan-400 {
680
+ color: darkcyan;
681
+ }
682
+
683
+ .text-fuchsia-400 {
684
+ color: palevioletred;
685
+ }
686
+
687
+ .text-green-400 {
688
+ color: limegreen;
689
+ }
690
+
691
+ .text-indigo-400 {
692
+ color: darkolivegreen;
693
+ }
694
+
695
+ .text-lime-400 {
696
+ color: green;
697
+ }
698
+
699
+ .text-orange-400 {
700
+ color: orange;
701
+ }
702
+
703
+ .text-purple-400 {
704
+ color: darkorchid;
705
+ }
706
+
707
+ .text-red-400 {
708
+ color: red;
709
+ }
710
+
711
+ .text-teal-400 {
712
+ color: darkturquoise;
713
+ }
714
+
715
+ .text-yellow-400 {
716
+ color: yellowgreen;
717
+ }
package/package.json CHANGED
@@ -1,14 +1,14 @@
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.14.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
+ "./css": "./index.css",
8
9
  "./db": "./db.js",
9
10
  "./server": "./server.js",
10
- "./sqlite": "./sqlite.js",
11
- "./testing": "./testing.js"
11
+ "./sqlite": "./sqlite.js"
12
12
  },
13
13
  "bin": {
14
14
  "evikit-extract": "bin/evikit-extract.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
  }
@@ -0,0 +1,55 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ const html = await Promise.all(
4
+ [
5
+ "https://evikit.js.org",
6
+ "https://evikit.js.org/todo",
7
+ "https://yoself.nykula.com",
8
+ "https://yoself.nykula.com/404",
9
+ ].map(async (url) => await (await fetch(url)).text()),
10
+ );
11
+ const classes = [
12
+ ...new Set([
13
+ ...html.flatMap((html) =>
14
+ [...html.matchAll(/class=(?:"([^"]+)"|'([^']+)'|([^ ]+))/g)].flatMap(
15
+ ([, double, single, none]) =>
16
+ (double || single || none || "").split(" "),
17
+ ),
18
+ ),
19
+ ...(
20
+ await (
21
+ await fetch(
22
+ "https://codeberg.org/nykula/lanquiz/raw/branch/main/classes.log",
23
+ )
24
+ ).text()
25
+ )
26
+ .split("\n")
27
+ .filter(Boolean),
28
+ ]),
29
+ ]
30
+ .map((className) => ({
31
+ base: className.split(/[:!]|^!?-/).at(-1) || "",
32
+ className,
33
+ selector: `.${className.replace(/(\[|\]|[:/#!%])/g, "\\$1")}`,
34
+ }))
35
+ .filter(({ base }) => base)
36
+ .sort((a, b) => a.base.localeCompare(b.base));
37
+ const css = await readFile("./index.css", "utf-8");
38
+ const missing = classes
39
+ .filter(({ selector }) => !css.includes(selector))
40
+ .map(({ className }) => className)
41
+ .join("\n");
42
+ const extra = [...css.matchAll(/\n *(\.[^ ,]+)/g)]
43
+ .map(([, selector]) => (selector || "").replace(/:focus|:hover/, ""))
44
+ .filter((selector) => !classes.some((c) => c.selector == selector))
45
+ .map((selector) => selector?.replace(/[.\\]/g, ""))
46
+ .join("\n");
47
+ if (missing) {
48
+ console.log(`Missing:\n${missing}`);
49
+ }
50
+ if (missing && extra) {
51
+ console.log();
52
+ }
53
+ if (extra) {
54
+ console.log(`Extra:\n${extra}`);
55
+ }
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
+ }