create-evikit 2.2.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/CNAME ADDED
@@ -0,0 +1 @@
1
+ evikit.js.org
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (C) 2026 by Denys Nykula <nykula@ukr.net>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
7
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
8
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
9
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
10
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
11
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
12
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import ownPackage from "../package.json" with { type: "json" };
8
+ import { template } from "../src/routes/api/server.js";
9
+
10
+ const ownRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const name = process.argv[2];
12
+ if (!name || !name[0] || /^[/-]/.test(name[0])) {
13
+ console.log("Try: npx create-evikit my-app");
14
+ process.exit(1);
15
+ }
16
+
17
+ const target = resolve(process.cwd(), name);
18
+ if (existsSync(target)) {
19
+ mkdirSync(target, { recursive: true });
20
+ }
21
+ for (const file of [
22
+ ...template,
23
+ ".gitignore",
24
+ "eslint.config.js",
25
+ "tsconfig.json",
26
+ ]) {
27
+ const path = join(target, file);
28
+ mkdirSync(dirname(path), { recursive: true });
29
+ writeFileSync(
30
+ path,
31
+ readFileSync(join(ownRoot, file), "utf8").replace(
32
+ /<link\s+href="\/node_modules\/[^"]+.css"\s+media="[^"]+"\s+rel="stylesheet"\s+\/>\s+/g,
33
+ "",
34
+ ),
35
+ );
36
+ }
37
+
38
+ // ---
39
+
40
+ writeFileSync(
41
+ join(target, "src/lib/index.js"),
42
+ `
43
+ import acceptLanguage from "accept-language";
44
+
45
+ export function acceptLanguages() {
46
+ acceptLanguage.languages(["en"]);
47
+ }
48
+ `.trimStart(),
49
+ );
50
+
51
+ // ---
52
+
53
+ writeFileSync(
54
+ join(target, "src/routes/api/types.js"),
55
+ `
56
+ import { object, string } from "valibot";
57
+
58
+ export const HomeGet = {
59
+ input: object({ lang: string() }),
60
+ outputs: { 200: object({ name: string(), version: string() }) },
61
+ };
62
+ `.trimStart(),
63
+ );
64
+
65
+ // ---
66
+
67
+ writeFileSync(
68
+ join(target, "src/routes/api/server.js"),
69
+ `
70
+ import { endpoint, json } from "evikit/server";
71
+
72
+ import pkg from "../../../package.json" with { type: "json" };
73
+ import { HomeGet } from "./types.js";
74
+
75
+ export const HomeServer = {
76
+ get: endpoint(HomeGet, async () =>
77
+ json({ name: pkg.name, version: pkg.version }),
78
+ ),
79
+ };
80
+ `.trimStart(),
81
+ );
82
+
83
+ // ---
84
+
85
+ writeFileSync(
86
+ join(target, "src/routes/page.js"),
87
+ `
88
+ import { a, h1, main, p, useApi, useI18n } from "evikit";
89
+ import { useTitle } from "hoofd/preact";
90
+
91
+ import { HomeGet } from "./api/types.js";
92
+
93
+ export function HomePage() {
94
+ const i18n = useI18n();
95
+ const { data } = useApi(HomeGet);
96
+ const title = data?.name || i18n.gettext("Loading...");
97
+ useTitle(title);
98
+
99
+ return main(
100
+ { class: "p-8" },
101
+ h1({ class: "text-xl" }, title),
102
+ data ? p({ class: "my-2" }, "v" + data.version) : undefined,
103
+ a({ class: "link", href: "/todo" }, i18n.gettext("Todos")),
104
+ );
105
+ }
106
+ `.trimStart(),
107
+ );
108
+
109
+ // ---
110
+
111
+ writeFileSync(
112
+ join(target, "index.css"),
113
+ `
114
+ @import "tailwindcss";
115
+ @plugin "daisyui";
116
+ `.trimStart(),
117
+ );
118
+
119
+ // ---
120
+
121
+ writeFileSync(
122
+ join(target, ".env"),
123
+ `
124
+ ORIGIN=http://localhost:8000
125
+ PORT=8000
126
+ `.trimStart(),
127
+ );
128
+
129
+ // ---
130
+
131
+ const { allowScripts, dependencies, devDependencies, scripts } = ownPackage;
132
+ /* eslint-disable perfectionist/sort-objects */
133
+ const pkg = {
134
+ name,
135
+ version: "0.1.0",
136
+ private: true,
137
+ scripts: {
138
+ dev: scripts.dev,
139
+ build: scripts.build,
140
+ start: scripts.start,
141
+ extract: scripts.extract,
142
+ format: scripts.format,
143
+ lint: scripts.lint,
144
+ test: "node --test --test-concurrency=1",
145
+ },
146
+ type: "module",
147
+ dependencies: {
148
+ "accept-language": dependencies["accept-language"],
149
+ classnames: dependencies.classnames,
150
+ evikit: dependencies.evikit,
151
+ express: dependencies.express,
152
+ "file-type": dependencies["file-type"],
153
+ hoofd: dependencies.hoofd,
154
+ preact: dependencies.preact,
155
+ "preact-iso": dependencies["preact-iso"],
156
+ "swagger-ui-express": dependencies["swagger-ui-express"],
157
+ valibot: dependencies.valibot,
158
+ },
159
+ devDependencies: {
160
+ "@eslint/compat": devDependencies["@eslint/compat"],
161
+ "@eslint/js": devDependencies["@eslint/js"],
162
+ "@preact/preset-vite": devDependencies["@preact/preset-vite"],
163
+ "@tailwindcss/vite": devDependencies["@tailwindcss/vite"],
164
+ "@types/express": devDependencies["@types/express"],
165
+ "@types/node": devDependencies["@types/node"],
166
+ "@types/swagger-ui-express": devDependencies["@types/swagger-ui-express"],
167
+ "cross-env": devDependencies["cross-env"],
168
+ daisyui: devDependencies.daisyui,
169
+ eslint: devDependencies.eslint,
170
+ "eslint-config-prettier": devDependencies["eslint-config-prettier"],
171
+ "eslint-plugin-perfectionist":
172
+ devDependencies["eslint-plugin-perfectionist"],
173
+ globals: devDependencies.globals,
174
+ prettier: devDependencies.prettier,
175
+ typescript: devDependencies.typescript,
176
+ "typescript-eslint": devDependencies["typescript-eslint"],
177
+ vite: devDependencies.vite,
178
+ "vite-bundle-analyzer": devDependencies["vite-bundle-analyzer"],
179
+ },
180
+ allowScripts,
181
+ };
182
+ /* eslint-enable perfectionist/sort-objects */
183
+ writeFileSync(
184
+ join(target, "package.json"),
185
+ JSON.stringify(pkg, null, 2) + "\n",
186
+ );
187
+ execSync("npm install", { cwd: target, stdio: "inherit" });
188
+
189
+ console.log("\n✅ Project created successfully!\n");
190
+ console.log("Next steps:");
191
+ console.log(` cd ${name}`);
192
+ console.log(` npm run dev\n`);
193
+ console.log("Documentation: https://codeberg.org/nykula/evikit");
@@ -0,0 +1,22 @@
1
+ import { includeIgnoreFile } from "@eslint/compat";
2
+ import js from "@eslint/js";
3
+ import prettier from "eslint-config-prettier/flat";
4
+ import perfectionist from "eslint-plugin-perfectionist";
5
+ import { defineConfig } from "eslint/config";
6
+ import globals from "globals";
7
+ import { fileURLToPath } from "node:url";
8
+ import ts from "typescript-eslint";
9
+
10
+ export default defineConfig(
11
+ includeIgnoreFile(fileURLToPath(new URL("./.gitignore", import.meta.url))),
12
+ js.configs.recommended,
13
+ ...ts.configs.strict,
14
+ ...ts.configs.stylistic,
15
+ perfectionist.configs["recommended-natural"],
16
+ prettier,
17
+ {
18
+ languageOptions: {
19
+ globals: { ...globals.browser, ...globals.node },
20
+ },
21
+ },
22
+ );
package/index.css ADDED
@@ -0,0 +1,55 @@
1
+ @import "tailwindcss";
2
+ @plugin "daisyui";
3
+
4
+ [lang="uk"] p {
5
+ text-wrap: balance;
6
+ }
7
+
8
+ p a:not([class]) {
9
+ color: #28f;
10
+ text-decoration: underline;
11
+ }
12
+
13
+ figcaption span,
14
+ summary span {
15
+ opacity: 0.3333;
16
+ }
17
+
18
+ [class*="shj-lang-"] {
19
+ box-shadow: none !important;
20
+ font: inherit !important;
21
+ }
22
+
23
+ .shj-inline {
24
+ padding: 0 !important;
25
+ }
26
+
27
+ figure,
28
+ details {
29
+ box-shadow: 0 0 1em rgba(0, 0, 0, 0.05);
30
+ }
31
+
32
+ nav a {
33
+ background-color: currentColor;
34
+ color: #fff;
35
+ border-radius: 1em;
36
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
37
+ display: inline-block;
38
+ margin-bottom: 1em;
39
+ padding: 1em;
40
+ }
41
+
42
+ nav a:focus,
43
+ nav a:hover {
44
+ opacity: 0.75;
45
+ }
46
+
47
+ nav a[href*="documentation"] {
48
+ background-color: #84f;
49
+ color: #fff;
50
+ }
51
+
52
+ nav a[href*="issues"] {
53
+ background-color: #7d8f;
54
+ color: #fff;
55
+ }
package/index.html ADDED
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <link href="/index.css" rel="stylesheet" />
8
+ <link
9
+ href="/node_modules/@speed-highlight/core/dist/themes/default.css"
10
+ media="(prefers-color-scheme: light)"
11
+ rel="stylesheet"
12
+ />
13
+ <link
14
+ href="/node_modules/@speed-highlight/core/dist/themes/atom-dark.css"
15
+ media="(prefers-color-scheme: dark)"
16
+ rel="stylesheet"
17
+ />
18
+ <!--app-head-->
19
+ </head>
20
+ <body>
21
+ <div id="app"><!--app-html--></div>
22
+ <script prerender type="module" src="/src/app.js"></script>
23
+ </body>
24
+ </html>
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ import { listen, production } from "evikit/server";
2
+ import express from "express";
3
+ import { readFile } from "node:fs/promises";
4
+ import swaggerUi from "swagger-ui-express";
5
+
6
+ import { api } from "./src/api.js";
7
+ import { prerender } from "./src/app.js";
8
+
9
+ const app = express();
10
+ app.use(api().onRequest);
11
+ app.use("/openapi", swaggerUi.serve, swaggerUi.setup(api().openapi));
12
+ app.use("/", express.static("./dist", { index: false, redirect: false }));
13
+ app.use(
14
+ production({
15
+ html: await readFile("./dist/index.html", "utf-8"),
16
+ prerender,
17
+ }),
18
+ );
19
+ listen(app);
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "create-evikit",
3
+ "version": "2.2.2",
4
+ "type": "module",
5
+ "bin": "bin/create-evikit.js",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "tsc && vite build",
9
+ "start": "cross-env NODE_ENV=production node --env-file=.env index.js",
10
+ "extract": "evikit-extract",
11
+ "format": "prettier --write . && eslint --fix .",
12
+ "lint": "prettier --check . && eslint ."
13
+ },
14
+ "author": "Denys Nykula <nykula@ukr.net>",
15
+ "license": "0BSD",
16
+ "devDependencies": {
17
+ "@eslint/compat": "^2.1.0",
18
+ "@eslint/js": "^10.0.1",
19
+ "@preact/preset-vite": "^2.10.5",
20
+ "@tailwindcss/vite": "^4.3.2",
21
+ "@types/express": "^5.0.6",
22
+ "@types/node": "^26.1.0",
23
+ "@types/swagger-ui-express": "^4.1.8",
24
+ "cross-env": "^10.1.0",
25
+ "daisyui": "^5.6.14",
26
+ "eslint": "^10.6.0",
27
+ "eslint-config-prettier": "^10.1.8",
28
+ "eslint-plugin-perfectionist": "^5.10.0",
29
+ "globals": "^17.7.0",
30
+ "prettier": "^3.9.4",
31
+ "typescript": "^6.0.3",
32
+ "typescript-eslint": "^8.63.0",
33
+ "vite": "^8.1.3",
34
+ "vite-bundle-analyzer": "^1.3.8"
35
+ },
36
+ "dependencies": {
37
+ "@speed-highlight/core": "^1.2.17",
38
+ "accept-language": "^3.0.20",
39
+ "classnames": "^2.5.1",
40
+ "evikit": "^2.2.2",
41
+ "express": "^5.2.1",
42
+ "file-type": "^22.0.1",
43
+ "hoofd": "^1.7.3",
44
+ "preact": "^10.29.5",
45
+ "preact-iso": "^2.12.0",
46
+ "swagger-ui-express": "^5.0.1",
47
+ "valibot": "^1.4.2"
48
+ },
49
+ "allowScripts": {
50
+ "@scarf/scarf": false
51
+ }
52
+ }
@@ -0,0 +1,84 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR Denys Nykula <nykula@ukr.net>
3
+ # This file is distributed under the same license as the create-evikit package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: create-evikit\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2026-07-07 23:06+0300\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "Language: \n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
20
+
21
+ msgid "EviKit: Preact SSR framework for small web apps"
22
+ msgstr ""
23
+
24
+ msgid "Preact SSR framework."
25
+ msgstr ""
26
+
27
+ msgid "For small web apps hosted on a <b>VPS</b> or in local network."
28
+ msgstr ""
29
+
30
+ msgid ""
31
+ "Standard modern <b>JavaScript</b>, no JSX or TS. <b>Forms</b> accessible "
32
+ "when JS doesn't load. Automatic <b>Swagger</b> specification. First-class "
33
+ "API input/output <b>validation</b>. Built-in <b>node:sqlite ORM</b>. Support "
34
+ "for UI <b>translation</b> through .po files."
35
+ msgstr ""
36
+
37
+ msgid "Documentation"
38
+ msgstr ""
39
+
40
+ msgid "Report <b>issue</b>"
41
+ msgstr ""
42
+
43
+ msgid ""
44
+ "See <a href=\"{todo}\">live demo</a> of the example code from below. Todos "
45
+ "are deleted every 10 minutes."
46
+ msgstr ""
47
+
48
+ msgid "Quick start"
49
+ msgstr ""
50
+
51
+ msgid ""
52
+ "There’s intentionally <b>not much API</b>. Documentation entirely fits in a "
53
+ "long but straightforward <b>README</b>. Like the Django tutorial, it guides "
54
+ "you through <b><a href=\"{documentation}\">writing an application</a></b> "
55
+ "using all the primary features of the EviKit framework."
56
+ msgstr ""
57
+
58
+ msgid ""
59
+ "<a href=\"{evikit}\"><b>Source code</b> at Codeberg</a> under <b>MIT</b> "
60
+ "license"
61
+ msgstr ""
62
+
63
+ msgid "Discuss on <b>Fediverse</b>"
64
+ msgstr ""
65
+
66
+ msgid "%1 todo"
67
+ msgid_plural "%1 todos"
68
+ msgstr[0] ""
69
+ msgstr[1] ""
70
+
71
+ msgid "Todos"
72
+ msgstr ""
73
+
74
+ msgid "Text"
75
+ msgstr ""
76
+
77
+ msgid "Create todo"
78
+ msgstr ""
79
+
80
+ msgid "Back to plan"
81
+ msgstr ""
82
+
83
+ msgid "Done"
84
+ msgstr ""
package/po/uk.po ADDED
@@ -0,0 +1,100 @@
1
+ # Ukrainian translations for create-evikit package.
2
+ # Copyright (C) 2026 Denys Nykula <nykula@ukr.net>
3
+ # This file is distributed under the same license as the create-evikit package.
4
+ # Denys Nykula <nykula@ukr.net>, 2026.
5
+ #
6
+ msgid ""
7
+ msgstr ""
8
+ "Project-Id-Version: create-evikit\n"
9
+ "Report-Msgid-Bugs-To: \n"
10
+ "POT-Creation-Date: 2026-07-07 23:06+0300\n"
11
+ "PO-Revision-Date: 2026-07-07 23:07+0300\n"
12
+ "Last-Translator: EMAIL@ADDRESS\n"
13
+ "Language-Team: Ukrainian\n"
14
+ "Language: uk\n"
15
+ "MIME-Version: 1.0\n"
16
+ "Content-Type: text/plain; charset=UTF-8\n"
17
+ "Content-Transfer-Encoding: 8bit\n"
18
+ "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
19
+ "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20
+ "X-Generator: Poedit 3.9\n"
21
+
22
+ msgid "EviKit: Preact SSR framework for small web apps"
23
+ msgstr "EviKit: Preact SSR-фреймворк для невеликих вебзастосунків"
24
+
25
+ msgid "Preact SSR framework."
26
+ msgstr "Preact SSR-фреймворк."
27
+
28
+ msgid "For small web apps hosted on a <b>VPS</b> or in local network."
29
+ msgstr ""
30
+ "Для невеликих вебзастосунків, розміщених на <b>VPS</b> чи в локальній мережі."
31
+
32
+ msgid ""
33
+ "Standard modern <b>JavaScript</b>, no JSX or TS. <b>Forms</b> accessible "
34
+ "when JS doesn't load. Automatic <b>Swagger</b> specification. First-class "
35
+ "API input/output <b>validation</b>. Built-in <b>node:sqlite ORM</b>. Support "
36
+ "for UI <b>translation</b> through .po files."
37
+ msgstr ""
38
+ "Стандартний сучасний <b>JavaScript</b>, без JSX чи TS. <b>Форми</b> "
39
+ "доступні, навіть коли JS не запускається. <b>Swagger</b>-специфікація "
40
+ "генерується автоматично. <b>Валідація</b> введення-виведення API - з "
41
+ "коробки. Вбудована <b>ORM</b> на основі <b>node:sqlite</b>. Підтримується "
42
+ "<b>переклад</b> інтерфейсу через .po-файли."
43
+
44
+ msgid "Documentation"
45
+ msgstr "Документація"
46
+
47
+ msgid "Report <b>issue</b>"
48
+ msgstr "Повідомити про <b>ваду</b>"
49
+
50
+ msgid ""
51
+ "See <a href=\"{todo}\">live demo</a> of the example code from below. Todos "
52
+ "are deleted every 10 minutes."
53
+ msgstr ""
54
+ "Перегляньте <a href=\"{todo}\">живе демо</a> коду зразків, наведених унизу. "
55
+ "Завдання видаляються кожні 10 хвилин."
56
+
57
+ msgid "Quick start"
58
+ msgstr "Швидкий початок"
59
+
60
+ msgid ""
61
+ "There’s intentionally <b>not much API</b>. Documentation entirely fits in a "
62
+ "long but straightforward <b>README</b>. Like the Django tutorial, it guides "
63
+ "you through <b><a href=\"{documentation}\">writing an application</a></b> "
64
+ "using all the primary features of the EviKit framework."
65
+ msgstr ""
66
+ "<b>API навмисне небагато</b>. Документація повністю вміщається в довгий, але "
67
+ "послідовний <b>README</b>-файл. Подібно до посібника Django, він містить "
68
+ "покрокові інструкції з <b><a href=\"{documentation}\">написання застосунка</"
69
+ "a></b>, що використовує всі основні функції фреймворка EviKit."
70
+
71
+ msgid ""
72
+ "<a href=\"{evikit}\"><b>Source code</b> at Codeberg</a> under <b>MIT</b> "
73
+ "license"
74
+ msgstr ""
75
+ "<a href=\"{evikit}\"><b>Вихідний код</b> на Codeberg</a> на умовах ліцензії "
76
+ "<b>MIT</b>"
77
+
78
+ msgid "Discuss on <b>Fediverse</b>"
79
+ msgstr "Обговорити в <b>Fediverse</b>"
80
+
81
+ msgid "%1 todo"
82
+ msgid_plural "%1 todos"
83
+ msgstr[0] "%1 завдання"
84
+ msgstr[1] "%1 завдання"
85
+ msgstr[2] "%1 завдань"
86
+
87
+ msgid "Todos"
88
+ msgstr "Завдання"
89
+
90
+ msgid "Text"
91
+ msgstr "Текст"
92
+
93
+ msgid "Create todo"
94
+ msgstr "Створити завдання"
95
+
96
+ msgid "Back to plan"
97
+ msgstr "Назад до плану"
98
+
99
+ msgid "Done"
100
+ msgstr "Готово"
package/src/api.js ADDED
@@ -0,0 +1,16 @@
1
+ import { route, router } from "evikit/server";
2
+
3
+ import pkg from "../package.json" with { type: "json" };
4
+ import { acceptLanguages } from "./lib/index.js";
5
+ import { HomeServer } from "./routes/api/server.js";
6
+ import { TodoServer } from "./routes/api/todo/server.js";
7
+
8
+ acceptLanguages();
9
+
10
+ export function api() {
11
+ return router(
12
+ { pkg },
13
+ route("/api", HomeServer),
14
+ route("/api/todo", TodoServer),
15
+ );
16
+ }
package/src/app.js ADDED
@@ -0,0 +1,24 @@
1
+ import { hydrate, Router, ssr } from "evikit";
2
+ import { h } from "preact";
3
+ import { Route } from "preact-iso";
4
+
5
+ import { acceptLanguages } from "./lib/index.js";
6
+ import { HomePage } from "./routes/page.js";
7
+ import { TodoPage } from "./routes/todo/page.js";
8
+
9
+ acceptLanguages();
10
+
11
+ /** @param {any} data */
12
+ export const prerender = (data) => ssr(App, data);
13
+
14
+ /** @param {{ dehydratedState?: any, i18n: any }} props */
15
+ export function App(props) {
16
+ return h(
17
+ Router,
18
+ props,
19
+ h(Route, { component: HomePage, path: "/" }),
20
+ h(Route, { component: TodoPage, path: "/todo" }),
21
+ );
22
+ }
23
+
24
+ hydrate(App, "#app");
@@ -0,0 +1,19 @@
1
+ import { sqlite } from "evikit/sqlite";
2
+
3
+ import pkg from "../../../package.json" with { type: "json" };
4
+ import * as schema from "./schema.js";
5
+
6
+ export const db = sqlite({ pkg, schema });
7
+
8
+ if (
9
+ process.env["npm_command"] == "start" ||
10
+ process.env["npm_lifecycle_event"] == "dev"
11
+ ) {
12
+ setInterval(() => {
13
+ db.Todo.delete({
14
+ text: db.Todo.select()
15
+ .filter(({ createdAt }) => createdAt < Date.now() - 10 * 60e3)
16
+ .map(({ text }) => text),
17
+ });
18
+ }, 1000);
19
+ }
@@ -0,0 +1,8 @@
1
+ import { primary } from "evikit";
2
+ import { integer, number, object, optional, pipe, string } from "valibot";
3
+
4
+ export const Todo = object({
5
+ createdAt: pipe(number(), integer()),
6
+ doneAt: optional(pipe(number(), integer())),
7
+ text: pipe(string(), primary()),
8
+ });
@@ -0,0 +1,5 @@
1
+ import acceptLanguage from "accept-language";
2
+
3
+ export function acceptLanguages() {
4
+ acceptLanguage.languages(["en", "uk"]);
5
+ }
@@ -0,0 +1,29 @@
1
+ import { endpoint, json } from "evikit/server";
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ import { HomeGet } from "./types.js";
5
+
6
+ export const template = [
7
+ "src/lib/db/schema.js",
8
+ "src/routes/api/todo/types.js",
9
+ "src/routes/api/todo/server.js",
10
+ "src/routes/todo/page.js",
11
+ "src/api.js",
12
+ "src/app.js",
13
+ "index.js",
14
+ "vite.config.js",
15
+ "src/lib/db/index.js",
16
+ "index.html",
17
+ ];
18
+
19
+ export const HomeServer = {
20
+ get: endpoint(HomeGet, async () =>
21
+ json({
22
+ File: Object.fromEntries(
23
+ await Promise.all(
24
+ template.map(async (path) => [path, await readFile(path, "utf8")]),
25
+ ),
26
+ ),
27
+ }),
28
+ ),
29
+ };
@@ -0,0 +1,21 @@
1
+ import { endpoint, json, redirect } from "evikit/server";
2
+
3
+ import { db } from "../../../lib/db/index.js";
4
+ import { TodoGet, TodoPost } from "./types.js";
5
+
6
+ export const TodoServer = {
7
+ get: endpoint(TodoGet, async () => json({ Todo: db.Todo.select() })),
8
+
9
+ post: endpoint(TodoPost, async ({ input }) => {
10
+ const todo = db.Todo.select().find(({ text }) => text == input.text);
11
+ if (todo) {
12
+ db.Todo.update({
13
+ set: { doneAt: todo.doneAt ? undefined : Date.now() },
14
+ where: { text: todo.text },
15
+ });
16
+ } else {
17
+ db.Todo.upsert({ createdAt: Date.now(), text: input.text });
18
+ }
19
+ return redirect("/todo", 302);
20
+ }),
21
+ };
@@ -0,0 +1,13 @@
1
+ import { array, object, string } from "valibot";
2
+
3
+ import * as schema from "../../../lib/db/schema.js";
4
+
5
+ export const TodoGet = {
6
+ input: object({ lang: string() }),
7
+ outputs: { 200: object({ Todo: array(schema.Todo) }) },
8
+ };
9
+
10
+ export const TodoPost = {
11
+ input: object({ lang: string(), text: string() }),
12
+ outputs: { 302: string(), 400: string() },
13
+ };
@@ -0,0 +1,6 @@
1
+ import { object, record, string } from "valibot";
2
+
3
+ export const HomeGet = {
4
+ input: object({ lang: string() }),
5
+ outputs: { 200: object({ File: record(string(), string()) }) },
6
+ };
@@ -0,0 +1,260 @@
1
+ import { highlightAll } from "@speed-highlight/core";
2
+ import { a, b, div, figure, h1, main, p, span, useApi, useI18n } from "evikit";
3
+ import { useMeta, useTitle } from "hoofd/preact";
4
+ import { h } from "preact";
5
+ import { useEffect } from "preact/hooks";
6
+
7
+ import { HomeGet } from "./api/types.js";
8
+
9
+ export function HomePage() {
10
+ const i18n = useI18n();
11
+ const { data } = useApi(HomeGet);
12
+ useTitle(i18n.gettext("EviKit: Preact SSR framework for small web apps"));
13
+ useMeta({
14
+ content:
15
+ "Standard modern JavaScript, no JSX or TS. Forms accessible when JS doesn't load. Automatic Swagger specification. First-class API input/output validation. Built-in node:sqlite ORM. Support for UI translation through .po files.",
16
+ name: "description",
17
+ });
18
+ useEffect(() => {
19
+ highlightAll();
20
+ }, [JSON.stringify(data?.File)]);
21
+
22
+ return main(
23
+ { class: "max-w-5xl mx-auto p-8" },
24
+ h1(
25
+ { class: "mb-4 text-xl lg:text-3xl lg:text-center" },
26
+ "EviKit: ",
27
+ b(null, i18n.gettext("Preact SSR framework.")),
28
+ h("br", null),
29
+ span({
30
+ class: "mt-0 text-base lg:text-sm",
31
+ ...sanitize(
32
+ i18n.gettext(
33
+ "For small web apps hosted on a <b>VPS</b> or in local network.",
34
+ ),
35
+ ),
36
+ }),
37
+ ),
38
+
39
+ p({
40
+ class: "lg:text-center",
41
+ ...sanitize(
42
+ i18n.gettext(
43
+ "Standard modern <b>JavaScript</b>, no JSX or TS. <b>Forms</b> accessible when JS doesn't load. Automatic <b>Swagger</b> specification. First-class API input/output <b>validation</b>. Built-in <b>node:sqlite ORM</b>. Support for UI <b>translation</b> through .po files.",
44
+ ),
45
+ ),
46
+ }),
47
+
48
+ h(
49
+ "nav",
50
+ { class: "gap-4 grid grid-cols-2 max-w-lg mx-auto mt-6 text-center" },
51
+
52
+ a(
53
+ { href: "https://codeberg.org/nykula/evikit#documentation" },
54
+ b(null, i18n.gettext("Documentation")),
55
+ " (v2)",
56
+ ),
57
+
58
+ a({
59
+ href: "https://codeberg.org/nykula/evikit/issues",
60
+ ...sanitize(i18n.gettext("Report <b>issue</b>")),
61
+ }),
62
+ ),
63
+
64
+ p({
65
+ class: "mb-6 lg:text-center",
66
+ ...sanitize(
67
+ i18n
68
+ .gettext(
69
+ 'See <a href="{todo}">live demo</a> of the example code from below. Todos are deleted every 10 minutes.',
70
+ )
71
+ .replace("{todo}", "/todo"),
72
+ ),
73
+ }),
74
+
75
+ h(
76
+ "blockquote",
77
+ {
78
+ class: "max-w-xs mx-auto mb-8 p-4 rounded-lg text-center",
79
+ style: {
80
+ background: "rgba(0, 0, 0, 0.025)",
81
+ border: "1px solid rgba(0, 0, 0, 0.05)",
82
+ },
83
+ },
84
+ p(null, b(null, i18n.gettext("Quick start"))),
85
+ h(
86
+ "pre",
87
+ null,
88
+ h(
89
+ "code",
90
+ { class: "!bg-transparent shj-lang-bash" },
91
+ `
92
+ npm init evikit my-app
93
+ cd my-app
94
+ npm run dev
95
+ `.trimStart(),
96
+ ),
97
+ ),
98
+ ),
99
+
100
+ div(
101
+ { class: "grid grid-cols-1 lg:grid-cols-2 gap-8 w-full" },
102
+ h(
103
+ Code,
104
+ { file: data?.File["src/lib/db/schema.js"], lang: "js" },
105
+ span(null, "src/lib/"),
106
+ b(null, "db/schema.js"),
107
+ ),
108
+
109
+ h(
110
+ Code,
111
+ { file: data?.File["src/routes/api/todo/types.js"], lang: "js" },
112
+ span(null, "src/routes/"),
113
+ b(null, "api/todo/types.js"),
114
+ ),
115
+
116
+ h(
117
+ Code,
118
+ { file: data?.File["src/routes/api/todo/server.js"], lang: "js" },
119
+ span(null, "src/routes/"),
120
+ b(null, "api/todo/server.js"),
121
+ ),
122
+
123
+ h(
124
+ Code,
125
+ { file: data?.File["src/routes/todo/page.js"], lang: "js" },
126
+ span(null, "src/routes/"),
127
+ b(null, "todo/page.js"),
128
+ ),
129
+
130
+ h(
131
+ Code,
132
+ { file: data?.File["src/api.js"], lang: "js", tag: "details" },
133
+ span(null, "src/"),
134
+ b(null, "api.js"),
135
+ ),
136
+
137
+ h(
138
+ Code,
139
+ { file: data?.File["src/app.js"], lang: "js", tag: "details" },
140
+ span(null, "src/"),
141
+ b(null, "app.js"),
142
+ ),
143
+
144
+ h(
145
+ Code,
146
+ { file: data?.File["index.js"], lang: "js", tag: "details" },
147
+ b(null, "index.js"),
148
+ ),
149
+
150
+ h(
151
+ Code,
152
+ { file: data?.File["vite.config.js"], lang: "js", tag: "details" },
153
+ b(null, "vite.config.js"),
154
+ ),
155
+
156
+ h(
157
+ Code,
158
+ { file: data?.File["src/lib/db/index.js"], lang: "js", tag: "details" },
159
+ span(null, "src/lib/"),
160
+ b(null, "db/index.js"),
161
+ ),
162
+
163
+ h(
164
+ Code,
165
+ { file: data?.File["index.html"], lang: "html", tag: "details" },
166
+ b(null, "index.html"),
167
+ ),
168
+ ),
169
+
170
+ h("hr", { class: "opacity-0" }),
171
+ h(
172
+ "blockquote",
173
+ {
174
+ class: "max-w-2xl mx-auto my-8 p-4 rounded-lg text-center",
175
+ style: {
176
+ background: "rgba(0, 0, 0, 0.025)",
177
+ border: "1px solid rgba(0, 0, 0, 0.05)",
178
+ },
179
+ },
180
+ p({
181
+ ...sanitize(
182
+ i18n
183
+ .gettext(
184
+ 'There’s intentionally <b>not much API</b>. Documentation entirely fits in a long but straightforward <b>README</b>. Like the Django tutorial, it guides you through <b><a href="{documentation}">writing an application</a></b> using all the primary features of the EviKit framework.',
185
+ )
186
+ .replace(
187
+ "{documentation}",
188
+ "https://codeberg.org/nykula/evikit#documentation",
189
+ ),
190
+ ),
191
+ }),
192
+ ),
193
+
194
+ h("hr", { class: "opacity-0" }),
195
+ h(
196
+ "footer",
197
+ { class: "text-center" },
198
+ p(
199
+ { class: "mb-4" },
200
+ "Copyright © 2026 ",
201
+ a({ href: "https://libre.net.ua" }, "Denys Nykula"),
202
+ ),
203
+ p({
204
+ class: "mb-4",
205
+ ...sanitize(
206
+ i18n
207
+ .gettext(
208
+ '<a href="{evikit}"><b>Source code</b> at Codeberg</a> under <b>MIT</b> license',
209
+ )
210
+ .replace("{evikit}", "https://codeberg.org/nykula/evikit"),
211
+ ),
212
+ }),
213
+ p(
214
+ { class: "mb-4" },
215
+ a({
216
+ href: "https://programming.dev/post/52844785",
217
+ ...sanitize(i18n.gettext("Discuss on <b>Fediverse</b>")),
218
+ }),
219
+ ),
220
+ ),
221
+ );
222
+ }
223
+
224
+ /** @param {{ children?: any, lang: string, file: string | undefined, tag?: string }} props */
225
+ function Code({ children, file, lang, tag }) {
226
+ const inner = h(
227
+ "pre",
228
+ { class: "mt-4 overflow-auto text-sm" },
229
+ h(
230
+ "code",
231
+ { class: `w-full shj-lang-${lang}` },
232
+ file
233
+ ?.replace(/.*EviKit.*\r?\n/g, "")
234
+ .replace(/\{[\s\S]*?\} from/g, (match) => match.replace(/\s+/g, " "))
235
+ .replace(/.*(HomeServer|acceptLanguage).*\r?\n/g, "")
236
+ .replace(/.*(HomePage|acceptLanguage).*\r?\n/g, "")
237
+ .replace(
238
+ /<link\s+href="\/node_modules\/[^"]+.css"\s+media="[^"]+"\s+rel="stylesheet"\s+\/>\s+/g,
239
+ "",
240
+ ),
241
+ ),
242
+ );
243
+ const style = "border-lg mb-4 mx-0 overflow-hidden p-4";
244
+ return tag == "details"
245
+ ? h("details", { class: style }, h("summary", null, children), inner)
246
+ : figure({ class: style }, h("figcaption", null, children), inner);
247
+ }
248
+
249
+ /** @param {string} html */
250
+ function sanitize(html) {
251
+ return {
252
+ dangerouslySetInnerHTML: {
253
+ __html: html.replace(/<([^>]*)>/g, (match) =>
254
+ /^<\/?[a-z]+>$/.test(match) || /^<a href="[a-z:/.#]*">$/.test(match)
255
+ ? match
256
+ : "",
257
+ ),
258
+ },
259
+ };
260
+ }
@@ -0,0 +1,55 @@
1
+ import classNames from "classnames";
2
+ import {
3
+ a,
4
+ button,
5
+ form,
6
+ input,
7
+ main,
8
+ p,
9
+ useApi,
10
+ useEnhance,
11
+ useI18n,
12
+ } from "evikit";
13
+ import { useMeta, useTitle } from "hoofd/preact";
14
+
15
+ import { TodoGet, TodoPost } from "../api/todo/types.js";
16
+
17
+ export function TodoPage() {
18
+ const { data } = useApi(TodoGet);
19
+ const { busy, onSubmit } = useEnhance(TodoPost);
20
+ const i18n = useI18n();
21
+ useTitle(
22
+ data?.Todo.length
23
+ ? i18n.ngettext("%1 todo", "%1 todos", data.Todo.length, data.Todo.length)
24
+ : i18n.gettext("Todos"),
25
+ );
26
+ useMeta({ content: "noindex", name: "robots" });
27
+
28
+ return main(
29
+ { class: "m-8" },
30
+ form(
31
+ { action: "/api/todo", method: "POST", onSubmit },
32
+ input({
33
+ class: "input",
34
+ name: "text",
35
+ placeholder: i18n.gettext("Text"),
36
+ required: true,
37
+ }),
38
+ button(
39
+ { class: classNames("btn", busy && "btn-disabled") },
40
+ i18n.gettext("Create todo"),
41
+ ),
42
+ ),
43
+ (data?.Todo || []).map(({ doneAt, text }) =>
44
+ form(
45
+ { action: "/api/todo", method: "POST", onSubmit },
46
+ button(
47
+ { class: classNames("btn", busy && "btn-disabled") },
48
+ doneAt ? i18n.gettext("Back to plan") : i18n.gettext("Done"),
49
+ ),
50
+ input({ class: "input", name: "text", readOnly: true, value: text }),
51
+ ),
52
+ ),
53
+ p({ class: "mt-4" }, a({ href: "/" }, "EviKit")),
54
+ );
55
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "dist",
4
+ "module": "nodenext",
5
+ "target": "esnext",
6
+ "types": [],
7
+ "sourceMap": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "noImplicitReturns": true,
13
+ "noImplicitOverride": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noFallthroughCasesInSwitch": true,
17
+ "noPropertyAccessFromIndexSignature": true,
18
+ "strict": true,
19
+ "jsx": "react-jsx",
20
+ "verbatimModuleSyntax": true,
21
+ "isolatedModules": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "moduleDetection": "force",
24
+ "skipLibCheck": true,
25
+ "allowJs": true,
26
+ "checkJs": true,
27
+ "noEmit": true,
28
+ "esModuleInterop": true
29
+ }
30
+ }
package/vite.config.js ADDED
@@ -0,0 +1,20 @@
1
+ import preact from "@preact/preset-vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { defineConfig } from "vite";
4
+ import { analyzer } from "vite-bundle-analyzer";
5
+
6
+ import { api } from "./src/api.js";
7
+
8
+ export default defineConfig({
9
+ plugins: [
10
+ analyzer({ analyzerMode: "static" }),
11
+ {
12
+ configureServer(server) {
13
+ server.middlewares.use(api().onRequest);
14
+ },
15
+ name: "configure-server",
16
+ },
17
+ preact({ prerender: { enabled: true, renderTarget: "#app" } }),
18
+ tailwindcss(),
19
+ ],
20
+ });