kreftjs 0.0.0-stage → 0.0.2-beta
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 +103 -2
- package/bin/add.js +1074 -0
- package/bin/artisan-more.js +994 -0
- package/bin/artisan.js +1877 -0
- package/bin/backend/app.js +161 -0
- package/bin/backend/cache.js +196 -0
- package/bin/backend/collection.js +117 -0
- package/bin/backend/command.js +45 -0
- package/bin/backend/config.js +234 -0
- package/bin/backend/error-page.js +773 -0
- package/bin/backend/events.js +49 -0
- package/bin/backend/factory.js +223 -0
- package/bin/backend/fake.js +180 -0
- package/bin/backend/form.js +102 -0
- package/bin/backend/gate.js +79 -0
- package/bin/backend/gc.js +69 -0
- package/bin/backend/html.js +322 -0
- package/bin/backend/http.js +652 -0
- package/bin/backend/index.js +452 -0
- package/bin/backend/mail.js +77 -0
- package/bin/backend/mcp.js +433 -0
- package/bin/backend/migrator.js +370 -0
- package/bin/backend/model.js +781 -0
- package/bin/backend/namespace.js +213 -0
- package/bin/backend/notifications.js +68 -0
- package/bin/backend/qr.js +164 -0
- package/bin/backend/query.js +387 -0
- package/bin/backend/queue.js +412 -0
- package/bin/backend/redis.js +479 -0
- package/bin/backend/schedule.js +112 -0
- package/bin/backend/schema.js +321 -0
- package/bin/backend/seo-pages.js +100 -0
- package/bin/backend/storage.js +153 -0
- package/bin/backend/support.js +567 -0
- package/bin/backend/typescript.js +166 -0
- package/bin/build.js +540 -0
- package/bin/db-api.js +249 -0
- package/bin/db.js +275 -0
- package/bin/deps.js +138 -0
- package/bin/electron.js +509 -0
- package/bin/env.js +97 -0
- package/bin/guard.js +80 -0
- package/bin/kreftjs-config.js +40 -0
- package/bin/kreftjs.js +1373 -0
- package/bin/lang.js +241 -0
- package/bin/layout.js +74 -0
- package/bin/no-auth.js +162 -0
- package/bin/prerender.js +258 -0
- package/bin/safe-json.js +24 -0
- package/bin/server-routes.js +154 -0
- package/bin/ssr.js +133 -0
- package/bin/terminal.js +195 -0
- package/bin/to-ts.js +36 -0
- package/bin/update.js +237 -0
- package/bin/urls-file.js +27 -0
- package/bin/vite.js +228 -0
- package/docs/Docs.kreftjs +557 -0
- package/docs/README.md +2302 -0
- package/docs/markdown.js +186 -0
- package/editors/vscode/README.md +13 -0
- package/editors/vscode/extension.js +183 -0
- package/editors/vscode/language-configuration.json +17 -0
- package/editors/vscode/package.json +57 -0
- package/editors/vscode/snippets/kreftjs.json +21 -0
- package/editors/vscode/syntaxes/kreftjs.tmLanguage.json +207 -0
- package/kreftjs.d.ts +579 -0
- package/kreftjs.js +5108 -0
- package/package.json +97 -4
- package/server.d.ts +1028 -0
- package/server.js +8 -0
- package/templates/app/README.md +127 -0
- package/templates/app/_env.example +64 -0
- package/templates/app/_gitignore +5 -0
- package/templates/app/_vscode/settings.json +9 -0
- package/templates/app/backend/app/Auth/Session.js +177 -0
- package/templates/app/backend/app/Auth/Throttle.js +29 -0
- package/templates/app/backend/app/Console/Commands/.gitkeep +0 -0
- package/templates/app/backend/app/Events/.gitkeep +0 -0
- package/templates/app/backend/app/Http/Controllers/Admin/DashboardController.js +34 -0
- package/templates/app/backend/app/Http/Controllers/Admin/UserController.js +53 -0
- package/templates/app/backend/app/Http/Controllers/Auth/ApiTokenController.js +47 -0
- package/templates/app/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.js +41 -0
- package/templates/app/backend/app/Http/Controllers/Auth/ConfirmablePasswordController.js +14 -0
- package/templates/app/backend/app/Http/Controllers/Auth/EmailVerificationController.js +23 -0
- package/templates/app/backend/app/Http/Controllers/Auth/NewPasswordController.js +29 -0
- package/templates/app/backend/app/Http/Controllers/Auth/PasswordResetLinkController.js +20 -0
- package/templates/app/backend/app/Http/Controllers/Auth/RegisteredUserController.js +19 -0
- package/templates/app/backend/app/Http/Controllers/Auth/SessionController.js +36 -0
- package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorChallengeController.js +41 -0
- package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorController.js +63 -0
- package/templates/app/backend/app/Http/Controllers/ProfileController.js +66 -0
- package/templates/app/backend/app/Http/Kernel.js +8 -0
- package/templates/app/backend/app/Http/Middleware/Authenticate.js +84 -0
- package/templates/app/backend/app/Http/Middleware/CheckAbilities.js +17 -0
- package/templates/app/backend/app/Http/Middleware/CheckForAnyAbility.js +12 -0
- package/templates/app/backend/app/Http/Middleware/EnsureEmailIsVerified.js +11 -0
- package/templates/app/backend/app/Http/Middleware/EnsureHasRole.js +11 -0
- package/templates/app/backend/app/Http/Middleware/EnsureIsAdmin.js +11 -0
- package/templates/app/backend/app/Http/Middleware/RequirePassword.js +15 -0
- package/templates/app/backend/app/Http/Requests/Auth/LoginRequest.js +45 -0
- package/templates/app/backend/app/Http/Resources/.gitkeep +1 -0
- package/templates/app/backend/app/Jobs/.gitkeep +0 -0
- package/templates/app/backend/app/Listeners/.gitkeep +0 -0
- package/templates/app/backend/app/Mail/.gitkeep +0 -0
- package/templates/app/backend/app/Models/PersonalAccessToken.js +21 -0
- package/templates/app/backend/app/Models/User.js +64 -0
- package/templates/app/backend/app/Notifications/.gitkeep +0 -0
- package/templates/app/backend/app/Observers/.gitkeep +0 -0
- package/templates/app/backend/app/Policies/.gitkeep +0 -0
- package/templates/app/backend/app/Providers/AppServiceProvider.js +12 -0
- package/templates/app/backend/app/Rules/.gitkeep +1 -0
- package/templates/app/backend/config/app.js +16 -0
- package/templates/app/backend/config/auth.js +34 -0
- package/templates/app/backend/config/database.js +38 -0
- package/templates/app/backend/config/redis.js +28 -0
- package/templates/app/backend/config/session.js +36 -0
- package/templates/app/backend/database/migrations/0001_01_01_000000_create_users_table.js +49 -0
- package/templates/app/backend/database/migrations/0001_01_01_000001_create_cache_table.js +13 -0
- package/templates/app/backend/database/migrations/0001_01_01_000002_create_jobs_table.js +26 -0
- package/templates/app/backend/database/migrations/0001_01_01_000003_create_notifications_table.js +18 -0
- package/templates/app/backend/database/seeders/DatabaseSeeder.js +15 -0
- package/templates/app/backend/lang/ar/messages.json +10 -0
- package/templates/app/backend/lang/ar/validation.json +21 -0
- package/templates/app/backend/lang/en/messages.json +10 -0
- package/templates/app/backend/lang/en/validation.json +19 -0
- package/templates/app/backend/routes/api.js +25 -0
- package/templates/app/backend/routes/auth.js +52 -0
- package/templates/app/backend/routes/console.js +15 -0
- package/templates/app/frontend/App.kreftjs +4 -0
- package/templates/app/frontend/components/Field.kreftjs +19 -0
- package/templates/app/frontend/components/StatCard.kreftjs +47 -0
- package/templates/app/frontend/components/profile/ApiTokens.kreftjs +70 -0
- package/templates/app/frontend/components/profile/Sessions.kreftjs +42 -0
- package/templates/app/frontend/components/profile/TwoFactor.kreftjs +90 -0
- package/templates/app/frontend/errors/ErrorMonitor.kreftjs +264 -0
- package/templates/app/frontend/index.html +79 -0
- package/templates/app/frontend/kreftjs.d.ts +579 -0
- package/templates/app/frontend/kreftjs.js +5108 -0
- package/templates/app/frontend/layouts/app.kreftjs +718 -0
- package/templates/app/frontend/layouts/docs.kreftjs +37 -0
- package/templates/app/frontend/layouts/guest.kreftjs +18 -0
- package/templates/app/frontend/pages/Dashboard.kreftjs +273 -0
- package/templates/app/frontend/pages/ErrorMonitor.kreftjs +96 -0
- package/templates/app/frontend/pages/Home.kreftjs +617 -0
- package/templates/app/frontend/pages/Profile.kreftjs +104 -0
- package/templates/app/frontend/pages/Users.kreftjs +240 -0
- package/templates/app/frontend/pages/auth/ConfirmPassword.kreftjs +33 -0
- package/templates/app/frontend/pages/auth/ForgotPassword.kreftjs +36 -0
- package/templates/app/frontend/pages/auth/Login.kreftjs +89 -0
- package/templates/app/frontend/pages/auth/Register.kreftjs +48 -0
- package/templates/app/frontend/pages/auth/ResetPassword.kreftjs +45 -0
- package/templates/app/frontend/pages/auth/VerifyEmail.kreftjs +59 -0
- package/templates/app/frontend/routes/web.kreftjs +34 -0
- package/templates/app/frontend/tests/components/Counter.kreftjs +15 -0
- package/templates/app/frontend/tests/pages/About.kreftjs +12 -0
- package/templates/app/frontend/tests/pages/Test.kreftjs +31 -0
- package/templates/app/frontend/ui.css +517 -0
- package/templates/app/lang/ar/auth.json +6 -0
- package/templates/app/lang/ar/messages.json +6 -0
- package/templates/app/lang/ar/pagination.json +5 -0
- package/templates/app/lang/ar/passwords.json +8 -0
- package/templates/app/lang/ar/validation.json +22 -0
- package/templates/app/lang/ar.json +10 -0
- package/templates/app/lang/en/auth.json +6 -0
- package/templates/app/lang/en/messages.json +6 -0
- package/templates/app/lang/en/pagination.json +5 -0
- package/templates/app/lang/en/passwords.json +8 -0
- package/templates/app/lang/en/validation.json +22 -0
- package/templates/app/lang/en.json +10 -0
- package/templates/errors/401.kreftjs +11 -0
- package/templates/errors/402.kreftjs +11 -0
- package/templates/errors/403.kreftjs +11 -0
- package/templates/errors/404.kreftjs +11 -0
- package/templates/errors/419.kreftjs +11 -0
- package/templates/errors/429.kreftjs +11 -0
- package/templates/errors/500.kreftjs +11 -0
- package/templates/errors/503.kreftjs +11 -0
- package/templates/errors/ErrorMonitor.kreftjs +502 -0
- package/templates/errors/error.kreftjs +11 -0
- package/templates/errors/minimal.kreftjs +25 -0
- package/tools/dev-client.js +125 -0
- package/tools/devtools.js +172 -0
- package/tools/vite-plugin-kreftjs.d.ts +52 -0
- package/tools/vite-plugin-kreftjs.js +466 -0
package/docs/README.md
ADDED
|
@@ -0,0 +1,2302 @@
|
|
|
1
|
+
# Start here
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
Kreft is a small reactive framework with its own component language. You write a component in a `.kreftjs` file: declare your state, derived values and actions in plain words, then describe the page in a `<view>`. The framework also ships a full Node.js backend (routes, controllers, models, migrations, queues, mail) so one project can cover the whole stack.
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<!-- Counter.kreftjs -->
|
|
9
|
+
state count = 0
|
|
10
|
+
derive double = count * 2
|
|
11
|
+
action add() { count++ }
|
|
12
|
+
|
|
13
|
+
<view>
|
|
14
|
+
<button @click={add}>{count} × 2 = {double}</button>
|
|
15
|
+
</view>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### What you get
|
|
19
|
+
|
|
20
|
+
| Part | What it gives you |
|
|
21
|
+
|---|---|
|
|
22
|
+
| **The `.kreftjs` language** | `state`, `derive`, `action`, `watch`, props, events, slots, stores, a router with layouts and nested routes, forms with validation, animations. |
|
|
23
|
+
| **The backend** | `Route`, controllers, an Active Record `Model`, migrations, seeders, factories, jobs, mail, cache, file storage, policies, scheduling. |
|
|
24
|
+
| **Authentication** | Sign-in pages, password reset, email verification, two-factor codes, device sessions and API tokens, ready in every new project (leave them out with `--no-auth`). |
|
|
25
|
+
| **Tooling** | `kreftjs dev` with hot reload that keeps state, DevTools, an error monitor, `kreftjs build`, TypeScript, Vite and Electron support. |
|
|
26
|
+
| **Security by default** | Escaped views, CSRF protection, hashed passwords, rate limits, hardened package installs. See [Security overview](#security-overview). |
|
|
27
|
+
|
|
28
|
+
### How the docs are organised
|
|
29
|
+
|
|
30
|
+
1. **Start here** gets a project running.
|
|
31
|
+
2. **The Kreft language** is everything in the browser.
|
|
32
|
+
3. **Backend** is everything on the server.
|
|
33
|
+
4. **Tooling** covers the command line, building and deploying.
|
|
34
|
+
5. **Security** and **Reference** are for checking details.
|
|
35
|
+
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> Some chapters mention a few tools by name only (for example `kreftjs make:model`). Run `kreftjs list` to see every command that your installed version has.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
### Requirements
|
|
42
|
+
|
|
43
|
+
| You need | Why |
|
|
44
|
+
|---|---|
|
|
45
|
+
| [Node.js](https://nodejs.org) **22.5 or newer** (npm comes with it) | Runs the `kreftjs` command, the dev server and the backend. 22.5 also gives you SQLite with nothing to install. |
|
|
46
|
+
| A terminal | macOS Terminal, Windows PowerShell, or any Linux shell. |
|
|
47
|
+
| A modern browser | Chrome, Edge, Firefox or Safari. |
|
|
48
|
+
| MySQL, PostgreSQL or Redis | Only if you choose them. SQLite needs nothing. |
|
|
49
|
+
|
|
50
|
+
Check your Node version:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
node --version # v22.5.0 or higher
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Install the command
|
|
57
|
+
|
|
58
|
+
Pick one way.
|
|
59
|
+
|
|
60
|
+
**1. Once, for every project (recommended).** The `kreftjs` command then works in any folder:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm i -g kreftjs
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**2. Without installing anything.** `npx` downloads Kreft, runs it and forgets it. Good for trying it out:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx kreftjs new my-app
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**3. Inside one project.** The command is saved in the project's `package.json`, so everyone who clones the project gets the same version, and so does your server or CI:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm i -D kreftjs
|
|
76
|
+
npx kreftjs dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> [!NOTE]
|
|
80
|
+
> The scripts of a new project (`npm run dev`, `build`, `serve` …) call `kreftjs`. With way 1 that is the global command. With way 3, npm finds it in the project's `node_modules` by itself. On a server where you did not install it globally, run `npm i -D kreftjs` in the project before `npm run build` or `npm run serve`.
|
|
81
|
+
|
|
82
|
+
### Check that it works
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
kreftjs --version # for example 0.0.2-beta
|
|
86
|
+
kreftjs about # Kreft and Node.js versions, this project's settings and drivers
|
|
87
|
+
kreftjs list # every command your version has
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`kreft` is a shorter name for the same command.
|
|
91
|
+
|
|
92
|
+
### Set up your editor
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
kreftjs vscode
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
installs colors and snippets for `.kreftjs` files in VS Code (also Cursor and VSCodium). Restart the editor afterwards.
|
|
99
|
+
|
|
100
|
+
### Update
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm i -g kreftjs@latest # the command (or: npm i -D kreftjs@latest inside a project)
|
|
104
|
+
cd my-app
|
|
105
|
+
kreftjs update --check # what would change
|
|
106
|
+
kreftjs update --core # refresh this app's kreftjs.js and type files (a backup is saved)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Every app has its own copy of the browser runtime, `frontend/kreftjs.js`. Updating the command does not touch it, so an existing app keeps its old version until you run `kreftjs update --core` inside it.
|
|
110
|
+
|
|
111
|
+
### Uninstall
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm rm -g kreftjs
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Working on Kreft itself
|
|
118
|
+
|
|
119
|
+
Run `npm link` inside the Kreft folder. The `kreftjs` command then runs that folder's code, and every edit shows at once. Undo it with `npm unlink -g kreftjs`.
|
|
120
|
+
|
|
121
|
+
### If something goes wrong
|
|
122
|
+
|
|
123
|
+
| What you see | What to do |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `command not found: kreftjs` | npm's global folder is not on your `PATH`. Run `npm config get prefix`, add its `bin` folder to `PATH`, and open a new terminal. |
|
|
126
|
+
| `EACCES: permission denied` while installing | Do not use `sudo`. Install Node with [nvm](https://github.com/nvm-sh/nvm), or run `npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to `PATH`. |
|
|
127
|
+
| `SQLite needs Node.js 22.5 or newer` | Update Node.js. `node --version` must show 22.5.0 or higher. |
|
|
128
|
+
| `kreftjs --version` shows an old number | Another copy comes first on your `PATH`. Run `which -a kreftjs` (Windows: `where kreftjs`) and remove the old one. |
|
|
129
|
+
| PowerShell says "running scripts is disabled" (Windows) | Run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`, or use Command Prompt. |
|
|
130
|
+
| The browser shows a blank page or "Not found: /" | Run `kreftjs dev` from the project's top folder (the one with `frontend/` and `backend/`). The dev server serves `frontend/`. |
|
|
131
|
+
|
|
132
|
+
Next: [Your first app](#your-first-app).
|
|
133
|
+
|
|
134
|
+
### Migrating from Som.js
|
|
135
|
+
|
|
136
|
+
Kreft is Som.js under a new name (the package and command are `kreftjs`), and an existing project keeps working without edits. `kreft`, an earlier spelling of the new name, is accepted in the same places:
|
|
137
|
+
|
|
138
|
+
- **Files:** `.som` (and `.kreft`) files still compile, next to `.kreftjs` ones. A name that is missing with one extension is tried with the others. Everything the tools write is `.kreftjs`.
|
|
139
|
+
- **Templates:** `s-if`, `s-each` and every other `s-` directive are read as `k-if`, `k-each` …, and `<som-view>`, `<som-link>` … as `<kreft-view>`, `<kreft-link>` …
|
|
140
|
+
- **JavaScript:** `Som` and `Kreft` are other names for `Kreftjs` (same object; `Som` is deprecated), and `Som.compileSom` is `Kreftjs.compile`.
|
|
141
|
+
- **Command:** `som` and `kreft` still run `kreftjs`; `som` prints a one-line notice.
|
|
142
|
+
- **Server code:** `require('somjs/server')` (also `som/server`, `@somjs/server`, `kreft/server`) still works; new code uses `require('kreftjs/server')`.
|
|
143
|
+
- **Settings:** `kreft.config.json`, `som.config.json` and `SOM_` environment variables (`SOM_SSR`, `SOM_ALLOWED_HOSTS` …) are read when the `kreftjs.config.json` file or the `KREFT_` variable is missing.
|
|
144
|
+
- **Cookies:** new projects use `kreft_session`, `kreft_2fa` and `kreft_bypass`. An existing project keeps its `som_` names until you change `app/Auth/Session.js`, and the framework's CSRF check accepts both.
|
|
145
|
+
- **Kept on purpose:** the `som_migrations` table, the `som:` prefix of saved data in `localStorage`, and the `som:` Redis keys of the queue. Renaming them needs a migration, planned for 3.1.
|
|
146
|
+
|
|
147
|
+
To move a project over, rename `*.som` (or `*.kreft`) to `*.kreftjs` (and the paths that mention them), run `kreftjs update` and replace `s-` with `k-`.
|
|
148
|
+
|
|
149
|
+
## Your first app
|
|
150
|
+
|
|
151
|
+
Create a project, move into it and start the development server:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
kreftjs new my-app # asks: JavaScript or TypeScript?
|
|
155
|
+
cd my-app
|
|
156
|
+
npm run dev
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Pass `--js` or `--ts` to skip the question, and `--db sqlite` or `--ui tailwind` (also `bootstrap`, `bulma`, `pico`) to start with a database or a CSS library.
|
|
160
|
+
|
|
161
|
+
A new app comes with login, registration, password reset, email verification, a profile and an admin area. It asks whether you want them; pass `--auth` or `--no-auth` to skip the question. `--no-auth` starts a plain site: the welcome page, the docs, the error monitor and the tests (and the Todos example with `--db`), with no users table, sessions or sign-in pages.
|
|
162
|
+
|
|
163
|
+
Open the address the terminal shows (normally `http://localhost:8000`). If that port is busy, the next free one is used.
|
|
164
|
+
|
|
165
|
+
The new project already has a working login page. `npm run dev` creates the tables and a test user, **test@example.com** with the password **password**. Emails (verify, reset) are printed in the terminal until you set `MAIL_URL`.
|
|
166
|
+
|
|
167
|
+
> [!WARNING]
|
|
168
|
+
> The test user is for local development only. Delete it, or never run `--seed` against a real database.
|
|
169
|
+
|
|
170
|
+
### The smallest possible app
|
|
171
|
+
|
|
172
|
+
You can use Kreft without any project at all. An app is an HTML page plus `.kreftjs` files:
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<!-- index.html -->
|
|
176
|
+
<div id="app"></div>
|
|
177
|
+
<script src="kreftjs.js"></script>
|
|
178
|
+
<script>
|
|
179
|
+
Kreftjs.app('App.kreftjs').mount('#app')
|
|
180
|
+
</script>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<!-- App.kreftjs -->
|
|
185
|
+
state name = 'World'
|
|
186
|
+
|
|
187
|
+
<view>
|
|
188
|
+
<h1>Hello {name}</h1>
|
|
189
|
+
<input k-model="name">
|
|
190
|
+
</view>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> [!WARNING]
|
|
194
|
+
> `.kreftjs` files need a web server. Browsers refuse to load files from `file://`, so always open your app through `npm run dev` or any other server.
|
|
195
|
+
|
|
196
|
+
## Project structure
|
|
197
|
+
|
|
198
|
+
`kreftjs new` creates two folders that never mix: `frontend/` is what browsers receive, `backend/` is the server and is never reachable over HTTP.
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
my-app/
|
|
202
|
+
├── frontend/
|
|
203
|
+
│ ├── index.html entry point
|
|
204
|
+
│ ├── App.kreftjs root component and router container
|
|
205
|
+
│ ├── routes/web.kreftjs page routes
|
|
206
|
+
│ ├── layouts/ app.kreftjs (signed in), guest.kreftjs (sign-in pages)
|
|
207
|
+
│ ├── pages/ one .kreftjs file per screen, auth/ for sign-in pages
|
|
208
|
+
│ ├── components/ reusable pieces
|
|
209
|
+
│ ├── ui.css the design system
|
|
210
|
+
│ └── tests/ component tests
|
|
211
|
+
├── backend/
|
|
212
|
+
│ ├── config/ app.js, auth.js, database.js, redis.js, session.js
|
|
213
|
+
│ ├── app/
|
|
214
|
+
│ │ ├── Http/ Controllers, Middleware, Requests, Resources
|
|
215
|
+
│ │ ├── Models/ User, PersonalAccessToken
|
|
216
|
+
│ │ └── Auth/ session helpers
|
|
217
|
+
│ ├── database/ migrations, seeders, factories
|
|
218
|
+
│ └── routes/ api.js, auth.js, console.js
|
|
219
|
+
├── .env secrets and settings (never commit it)
|
|
220
|
+
├── package.json
|
|
221
|
+
└── kreftjs.config.json CLI options
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Why the two folders matter
|
|
225
|
+
|
|
226
|
+
- `kreftjs dev` and `kreftjs build` serve only `frontend/`. The `backend/` folder, database files and `.env` have no web address.
|
|
227
|
+
- On macOS and Linux `kreftjs new` gives `backend/` and `.env` permissions that only the owner can read.
|
|
228
|
+
- Files such as `.env`, `*.db`, `*.pem`, `package.json` and `kreftjs.config.json` are refused even if requested by name.
|
|
229
|
+
|
|
230
|
+
### Adding a CSS library
|
|
231
|
+
|
|
232
|
+
Any CSS library works. Add its `<link>` to `index.html`, or install it and import the file from a `.kreftjs` file; it becomes a `<link>` added once, and `kreftjs build` copies it (with its fonts and images) into `dist/vendor/`.
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
import 'bootstrap/dist/css/bootstrap.min.css' // npm i bootstrap
|
|
236
|
+
import './theme.css' // your own file, next to this .kreftjs file
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### npm packages in the browser
|
|
240
|
+
|
|
241
|
+
Install a package with `kreftjs add`, then import it in any `.kreftjs` file:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
kreftjs add dayjs nanoid lodash
|
|
245
|
+
kreftjs add -D esbuild # once per project: it prepares packages for the browser
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```html
|
|
249
|
+
import dayjs from 'dayjs'
|
|
250
|
+
import { nanoid } from 'nanoid'
|
|
251
|
+
|
|
252
|
+
state id = nanoid()
|
|
253
|
+
|
|
254
|
+
<view>
|
|
255
|
+
<p>{dayjs().format('YYYY-MM-DD')}</p>
|
|
256
|
+
</view>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`kreftjs dev` serves each package as one browser-ready file and tells the browser where it is with an import map. Old-style packages (`module.exports`, such as lodash) work too.
|
|
260
|
+
|
|
261
|
+
# The Kreft language
|
|
262
|
+
|
|
263
|
+
## Components and the .kreftjs file
|
|
264
|
+
|
|
265
|
+
A `.kreftjs` file has three parts:
|
|
266
|
+
|
|
267
|
+
1. **The script**: everything outside `<view>` and `<style>`. Each line starts with a keyword.
|
|
268
|
+
2. **`<view>`**: the HTML of the component.
|
|
269
|
+
3. **`<style>`** (optional): CSS. Add `scoped` so it applies only to this component.
|
|
270
|
+
|
|
271
|
+
### Keywords
|
|
272
|
+
|
|
273
|
+
| Keyword | Example | Meaning |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `state` | `state count = 0` | A value that can change. When it changes, the view updates. |
|
|
276
|
+
| `derive` | `derive double = count * 2` | A value calculated from other values. It updates by itself. |
|
|
277
|
+
| `action` | `action add(n) { count += n }` | A function you call from the view or from other actions. |
|
|
278
|
+
| `when` | `when count > 10 { … }` | Runs the block each time the condition becomes true. |
|
|
279
|
+
| `watch` | `watch count { … }` | Runs each time the value changes. Inside, `value` is the new value and `old` the previous one. |
|
|
280
|
+
| `on` | `on ready { … }` | Lifecycle: `on start`, `on ready`, `on update`, `on show`, `on hide`, `on destroy`. |
|
|
281
|
+
| `prop` | `prop start = 0` | A value the parent passes in. The value after `=` is the default. |
|
|
282
|
+
| `event` | `event save, remove` | Events this component sends to its parent with `send('save', data)`. |
|
|
283
|
+
| `use` | `use cart` | Use a shared store. `use mouse = trackMouse()` uses shared logic from a `.js` file. |
|
|
284
|
+
| `share` / `receive` | `share theme = 'dark'` | Give a value to everything inside this component / take a value a parent shared. |
|
|
285
|
+
| `import` | `import Card from './Card.kreftjs'` | Use another component or a JavaScript module. |
|
|
286
|
+
| `saved` | `saved todos = []` | Like `state`, but kept in the browser (localStorage). |
|
|
287
|
+
| `load` | `load users = api.get('/api/users')` | Loads data and gives `users.data`, `users.loading`, `users.error`, `users.reload()`. |
|
|
288
|
+
|
|
289
|
+
Inside the script you use names **directly**: write `count++`, not `this.count++`.
|
|
290
|
+
|
|
291
|
+
### A complete example
|
|
292
|
+
|
|
293
|
+
```html
|
|
294
|
+
<!-- TodoPage.kreftjs -->
|
|
295
|
+
state todos = []
|
|
296
|
+
state text = ''
|
|
297
|
+
|
|
298
|
+
derive remaining = todos.filter((t) => !t.done).length
|
|
299
|
+
|
|
300
|
+
action add() {
|
|
301
|
+
if (!text.trim()) return
|
|
302
|
+
todos.push({ id: Date.now(), text, done: false })
|
|
303
|
+
text = ''
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
action clearDone() {
|
|
307
|
+
todos = todos.filter((t) => !t.done)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
when todos.length && remaining === 0 {
|
|
311
|
+
bus.send('toast', 'All done!')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
<view>
|
|
315
|
+
<form @submit.prevent={add}>
|
|
316
|
+
<input k-model="text" placeholder="New todo">
|
|
317
|
+
</form>
|
|
318
|
+
<label k-each="todo in todos" k-key="todo.id">
|
|
319
|
+
<input type="checkbox" k-model="todo.done"> {todo.text}
|
|
320
|
+
</label>
|
|
321
|
+
<p>{remaining} left · <button @click={clearDone}>Clear done</button></p>
|
|
322
|
+
</view>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Good to know
|
|
326
|
+
|
|
327
|
+
- An `action` that uses `await` becomes async automatically.
|
|
328
|
+
- A line that does not start with a keyword is normal JavaScript (`let timer`, `const max = 10`). Your actions can use it, but the view cannot. Use `state`, `derive` or `action` for anything the view needs.
|
|
329
|
+
- Every Kreft function is available in the script: `bus`, `store`, `signal`, `watch`, `tick`, `send`, `props`, `$route`, `$router`.
|
|
330
|
+
- Errors name the file and line, for example `"state" needs a name, like: state count = 0 (Bad.kreftjs, line 3)`.
|
|
331
|
+
|
|
332
|
+
### Lifecycle
|
|
333
|
+
|
|
334
|
+
| Hook | Runs |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `on start` | Before the component is shown. Good for first data. |
|
|
337
|
+
| `on ready` | The component is on the screen. |
|
|
338
|
+
| `on update` | After the view changed. |
|
|
339
|
+
| `on show` / `on hide` | A [kept](#keep-and-wait) component comes back / is put away. |
|
|
340
|
+
| `on destroy` | The component is being removed. Clean up here. |
|
|
341
|
+
|
|
342
|
+
## Templates and directives
|
|
343
|
+
|
|
344
|
+
### Showing values
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<h1>Hello {name}</h1>
|
|
348
|
+
<p>{count} items · total {total.toFixed(2)}</p>
|
|
349
|
+
<p>{isAdmin ? 'Admin' : 'User'}</p>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Values are always escaped, so they are safe to show. To show a literal `{`, write `\{`.
|
|
353
|
+
|
|
354
|
+
### Attributes
|
|
355
|
+
|
|
356
|
+
```html
|
|
357
|
+
<img src={user.photo} alt={user.name}> <!-- the value as it is -->
|
|
358
|
+
<button disabled={count === 0}>Reset</button> <!-- false removes the attribute -->
|
|
359
|
+
<div class="card {active ? 'on' : ''}"> <!-- build a text -->
|
|
360
|
+
<a href="/users/{user.id}">Profile</a>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`:class` and `:style` also accept objects: `:class="{ done: todo.done }"`.
|
|
364
|
+
|
|
365
|
+
### Events
|
|
366
|
+
|
|
367
|
+
```html
|
|
368
|
+
<button @click={add}>Add</button>
|
|
369
|
+
<button @click="add(5)">Add 5</button>
|
|
370
|
+
<input @input="search($event.target.value)">
|
|
371
|
+
<form @submit.prevent={save}>
|
|
372
|
+
<input @keyup.enter={send}>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Modifiers: `.prevent` `.stop` `.self` `.once` `.capture` `.passive`. Keys: `.enter` `.esc` `.space` `.tab` `.up` `.down` `.ctrl` `.shift` `.alt` `.meta` `.exact`.
|
|
376
|
+
|
|
377
|
+
### Directives
|
|
378
|
+
|
|
379
|
+
| Directive | What it does |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `k-if` / `k-elif` / `k-else` | Adds or removes the element. |
|
|
382
|
+
| `k-each="item in list"` + `k-key="item.id"` | Repeats the element. Also `(item, i) in list`, `(value, key) in object`, `n in 10`. |
|
|
383
|
+
| `k-model="name"` | Two-way binding for input, textarea, select, checkbox and radio. Modifiers `.trim` `.number` `.lazy`. |
|
|
384
|
+
| `k-show` | Hides with CSS instead of removing. |
|
|
385
|
+
| `k-text` / `k-html` | Sets text, or HTML. `k-html` is cleaned of scripts and event handlers, but only bind HTML you trust. |
|
|
386
|
+
| `k-ref="box"` | Gives you the element: `state box = null` receives it. |
|
|
387
|
+
| `k-skip` | Leaves the element as it is (for code samples that contain `{ }`). |
|
|
388
|
+
| `k-once` | Draws the element once and never updates it. |
|
|
389
|
+
| `k-memo="[a, b]"` | Updates the element only when `a` or `b` changes. |
|
|
390
|
+
| `k-animate="fade"` | Animates the element when it comes and goes. |
|
|
391
|
+
| `<template k-if>` / `<template k-each>` | Groups elements without a wrapper. |
|
|
392
|
+
| `<kreft-component :is="name">` | Shows a component chosen while the app runs. |
|
|
393
|
+
| `<kreft-portal to="body">` | Renders content somewhere else (modals, toasts). |
|
|
394
|
+
|
|
395
|
+
### Loops
|
|
396
|
+
|
|
397
|
+
```html
|
|
398
|
+
<ul>
|
|
399
|
+
{for user in users key user.id}
|
|
400
|
+
<li>{user.name}</li>
|
|
401
|
+
{empty}
|
|
402
|
+
<li>No users yet</li>
|
|
403
|
+
{/for}
|
|
404
|
+
</ul>
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
- `key user.id` is optional; it lets Kreft keep and move the right rows.
|
|
408
|
+
- `{empty}` is optional and shows when the list is empty.
|
|
409
|
+
- `{for (user, i) in users}` gives the index, `{for (value, key) in object}` walks an object, `{for n in 5}` counts 1 to 5.
|
|
410
|
+
- `k-each` on an element does the same: `<li k-each="user in users" k-key="user.id">`.
|
|
411
|
+
|
|
412
|
+
### Animations
|
|
413
|
+
|
|
414
|
+
```html
|
|
415
|
+
<p k-if="open" k-animate="fade">Hello</p>
|
|
416
|
+
<div k-show="menu" k-animate="slide">…</div>
|
|
417
|
+
|
|
418
|
+
<ul>
|
|
419
|
+
{for item in items key item.id animate slide}
|
|
420
|
+
<li>{item.name}</li>
|
|
421
|
+
{/for}
|
|
422
|
+
</ul>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`k-animate` works with `k-if`, `k-show`, loops, `<kreft-component>` and `<kreft-view>`. Ready-made: `fade`, `slide`, `scale`, `pop`.
|
|
426
|
+
|
|
427
|
+
Your own animation is CSS with these classes (replace `name`):
|
|
428
|
+
|
|
429
|
+
| Class | When |
|
|
430
|
+
|---|---|
|
|
431
|
+
| `name-in` | The whole time it comes in (put the `transition` here). |
|
|
432
|
+
| `name-in-from` | The first moment: where it comes from. |
|
|
433
|
+
| `name-out` | The whole time it goes out. |
|
|
434
|
+
| `name-out-to` | Where it goes to. |
|
|
435
|
+
| `name-move` | A list item moving to its new place. |
|
|
436
|
+
|
|
437
|
+
```css
|
|
438
|
+
.drop-in, .drop-out { transition: all .3s ease }
|
|
439
|
+
.drop-in-from { opacity: 0; transform: translateY(-30px) }
|
|
440
|
+
.drop-out-to { opacity: 0; transform: translateX(60px) }
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
- Kreft waits for the longest `transition` or `animation` before removing the element.
|
|
444
|
+
- `k-animate.after` lets the old element leave before the new one comes in.
|
|
445
|
+
- Nothing animates when the page first opens; add `.first` to animate then too.
|
|
446
|
+
|
|
447
|
+
### Faster views
|
|
448
|
+
|
|
449
|
+
Kreft already changes only the exact text or attribute that changed. For very large pages:
|
|
450
|
+
|
|
451
|
+
```html
|
|
452
|
+
<footer k-once>Opened at {openedAt}</footer>
|
|
453
|
+
|
|
454
|
+
{for row in rows key row.id}
|
|
455
|
+
<tr k-memo="[row.id === selected]" class={row.id === selected ? 'on' : ''}>…</tr>
|
|
456
|
+
{/for}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
With `k-memo`, picking another row redraws two rows, not all of them.
|
|
460
|
+
|
|
461
|
+
### Your own directives
|
|
462
|
+
|
|
463
|
+
```js
|
|
464
|
+
app.directive('focus', {
|
|
465
|
+
ready(el, binding) { el.focus() },
|
|
466
|
+
update(el, binding) {},
|
|
467
|
+
destroy(el, binding) {},
|
|
468
|
+
})
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```html
|
|
472
|
+
<input k-focus>
|
|
473
|
+
<div k-tooltip:top={message}></div> <!-- binding.arg = 'top', binding.value = message -->
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Props, events and slots
|
|
477
|
+
|
|
478
|
+
### Using a component
|
|
479
|
+
|
|
480
|
+
Register it once by its path, or import it where you need it:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
Kreftjs.app('App.kreftjs')
|
|
484
|
+
.component('UserCard', 'components/UserCard.kreftjs')
|
|
485
|
+
.mount('#app')
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
```html
|
|
489
|
+
import UserCard from './UserCard.kreftjs'
|
|
490
|
+
|
|
491
|
+
<view>
|
|
492
|
+
<user-card name="Alice Smith" age={30}></user-card>
|
|
493
|
+
</view>
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
In the view, write component names in **kebab-case** (`<user-card>`).
|
|
497
|
+
|
|
498
|
+
### Props: parent to child
|
|
499
|
+
|
|
500
|
+
```html
|
|
501
|
+
<!-- UserCard.kreftjs -->
|
|
502
|
+
prop name
|
|
503
|
+
prop age = 0
|
|
504
|
+
prop admin = false
|
|
505
|
+
|
|
506
|
+
<view>
|
|
507
|
+
<div class="card">{name} ({age}) <b k-if="admin">admin</b></div>
|
|
508
|
+
</view>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
```html
|
|
512
|
+
<user-card name="Alice Smith" age={30} admin></user-card>
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
The default's type decides how plain text attributes are read: `age="30"` becomes the number `30`, and `admin` with no value becomes `true`. A child cannot change a prop; send an event instead.
|
|
516
|
+
|
|
517
|
+
### Events: child to parent
|
|
518
|
+
|
|
519
|
+
```html
|
|
520
|
+
<!-- TodoItem.kreftjs -->
|
|
521
|
+
prop todo
|
|
522
|
+
event remove
|
|
523
|
+
|
|
524
|
+
<view>
|
|
525
|
+
<li>{todo.text} <button @click="send('remove', todo.id)">✕</button></li>
|
|
526
|
+
</view>
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
```html
|
|
530
|
+
<!-- parent -->
|
|
531
|
+
action removeTodo(id) { todos = todos.filter((t) => t.id !== id) }
|
|
532
|
+
|
|
533
|
+
<view>
|
|
534
|
+
<todo-item k-each="t in todos" k-key="t.id" todo={t} @remove={removeTodo}></todo-item>
|
|
535
|
+
</view>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### `k-model` on a component
|
|
539
|
+
|
|
540
|
+
```html
|
|
541
|
+
<!-- Toggle.kreftjs -->
|
|
542
|
+
prop modelValue = false
|
|
543
|
+
event update:modelValue
|
|
544
|
+
|
|
545
|
+
<view>
|
|
546
|
+
<button @click="send('update:modelValue', !modelValue)">{modelValue ? 'On' : 'Off'}</button>
|
|
547
|
+
</view>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
```html
|
|
551
|
+
<toggle k-model="darkMode"></toggle>
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Named models: `k-model:title="pageTitle"` uses `prop title` and the event `update:title`.
|
|
555
|
+
|
|
556
|
+
### Slots
|
|
557
|
+
|
|
558
|
+
```html
|
|
559
|
+
<!-- Card.kreftjs -->
|
|
560
|
+
prop items = []
|
|
561
|
+
|
|
562
|
+
<view>
|
|
563
|
+
<div class="card">
|
|
564
|
+
<header><slot name="header">Default title</slot></header>
|
|
565
|
+
<slot></slot>
|
|
566
|
+
<li k-each="item in items"><slot name="item" item={item}>{item}</slot></li>
|
|
567
|
+
</div>
|
|
568
|
+
</view>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
```html
|
|
572
|
+
<card items={users}>
|
|
573
|
+
<template k-slot:header>Users</template>
|
|
574
|
+
<p>Body content</p>
|
|
575
|
+
<template k-slot:item="{ item }"><b>{item.name}</b></template>
|
|
576
|
+
</card>
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Styles
|
|
580
|
+
|
|
581
|
+
- `<style>` applies to the whole page.
|
|
582
|
+
- `<style scoped>` applies only to this component.
|
|
583
|
+
- `.parent :deep(.child)` reaches into child components from scoped CSS.
|
|
584
|
+
- Attributes such as `class` that you put on a component tag land on its root element.
|
|
585
|
+
|
|
586
|
+
### Keep and wait
|
|
587
|
+
|
|
588
|
+
**Keep** puts a component away instead of removing it. Typed text, scroll position and state are all still there when it returns:
|
|
589
|
+
|
|
590
|
+
```html
|
|
591
|
+
<kreft-component :is="tab" keep></kreft-component>
|
|
592
|
+
<kreft-view keep></kreft-view>
|
|
593
|
+
<kreft-view keep="5"></kreft-view> <!-- keep at most 5; the one used longest ago goes -->
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
```html
|
|
597
|
+
on show { timer.start() } <!-- it came back (also the first time) -->
|
|
598
|
+
on hide { timer.stop() } <!-- it was put away -->
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Wait** shows a placeholder until everything inside is ready (lazy components, every `load`, every `on start` that awaits):
|
|
602
|
+
|
|
603
|
+
```html
|
|
604
|
+
<kreft-wait>
|
|
605
|
+
<user-profile id={5}></user-profile>
|
|
606
|
+
<p k-waiting>Loading…</p>
|
|
607
|
+
<p k-failed="error">Could not load: {error.message}</p>
|
|
608
|
+
</kreft-wait>
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
## Sharing state
|
|
612
|
+
|
|
613
|
+
| Need | Use |
|
|
614
|
+
|---|---|
|
|
615
|
+
| Parent to child | `prop` |
|
|
616
|
+
| Child to parent | `event` + `send()`, or `k-model` |
|
|
617
|
+
| Many components, anywhere | stores: `Kreftjs.store()` + `use` |
|
|
618
|
+
| A parent to everything inside it | `share` / `receive` |
|
|
619
|
+
| "Something happened" messages | `bus.send()` / `bus.on()` |
|
|
620
|
+
|
|
621
|
+
### Stores
|
|
622
|
+
|
|
623
|
+
Create a store once in your page's script:
|
|
624
|
+
|
|
625
|
+
```js
|
|
626
|
+
Kreftjs.store('cart', {
|
|
627
|
+
items: [],
|
|
628
|
+
get count() { return this.items.length },
|
|
629
|
+
get total() { return this.items.reduce((s, p) => s + p.price, 0) },
|
|
630
|
+
add(product) { this.items.push(product) },
|
|
631
|
+
})
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Use it in any `.kreftjs` file. Every component sees the same data:
|
|
635
|
+
|
|
636
|
+
```html
|
|
637
|
+
use cart
|
|
638
|
+
|
|
639
|
+
<view>
|
|
640
|
+
<span>Cart: {cart.count} · {cart.total}</span>
|
|
641
|
+
<button @click="cart.add(product)">Add</button>
|
|
642
|
+
</view>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Every store also has `$patch` (change several values at once), `$reset()` (back to the start) and `$watch(fn)` (runs on every change and stops by itself when the component goes away).
|
|
646
|
+
|
|
647
|
+
### Shared logic
|
|
648
|
+
|
|
649
|
+
Logic that many components need goes in a `.js` file as a function. It can use `Kreftjs.state`, `Kreftjs.watch`, `Kreftjs.onDestroy` and the rest:
|
|
650
|
+
|
|
651
|
+
```js
|
|
652
|
+
// logic/mouse.js
|
|
653
|
+
export function trackMouse() {
|
|
654
|
+
const pos = Kreftjs.state({ x: 0, y: 0 })
|
|
655
|
+
const move = (e) => { pos.x = e.clientX; pos.y = e.clientY }
|
|
656
|
+
window.addEventListener('pointermove', move)
|
|
657
|
+
Kreftjs.onDestroy(() => window.removeEventListener('pointermove', move))
|
|
658
|
+
return pos
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
```html
|
|
663
|
+
import { trackMouse } from '../logic/mouse.js'
|
|
664
|
+
use mouse = trackMouse()
|
|
665
|
+
|
|
666
|
+
<view><p>{mouse.x}, {mouse.y}</p></view>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Share and receive
|
|
670
|
+
|
|
671
|
+
```html
|
|
672
|
+
<!-- Layout.kreftjs -->
|
|
673
|
+
state settings = { theme: 'dark', lang: 'so' }
|
|
674
|
+
share settings = settings
|
|
675
|
+
|
|
676
|
+
<!-- any component inside it, however deep -->
|
|
677
|
+
receive settings
|
|
678
|
+
|
|
679
|
+
<view><p>Theme: {settings.theme}</p></view>
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Share a `state` object and every receiver updates when it changes. `Kreftjs.app(…).share('key', value)` shares with the whole app.
|
|
683
|
+
|
|
684
|
+
### Event bus
|
|
685
|
+
|
|
686
|
+
```html
|
|
687
|
+
<!-- sending -->
|
|
688
|
+
action save() { bus.send('toast', 'Saved!') }
|
|
689
|
+
|
|
690
|
+
<!-- listening -->
|
|
691
|
+
state message = ''
|
|
692
|
+
on ready { bus.on('toast', (text) => (message = text)) }
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Listeners added in `on start` or `on ready` are removed automatically when the component goes away. Others stay until you call `bus.off('toast', fn)`.
|
|
696
|
+
|
|
697
|
+
## Saving data in the browser
|
|
698
|
+
|
|
699
|
+
Write `saved` instead of `state` and the value is kept in localStorage. It survives reloads and closing the browser.
|
|
700
|
+
|
|
701
|
+
```html
|
|
702
|
+
saved todos = []
|
|
703
|
+
saved theme = 'light'
|
|
704
|
+
saved token = '' as 'auth.token' <!-- choose the storage key yourself -->
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
- Every change is saved by itself, including deep ones: `todos.push(…)`, `todo.done = true`.
|
|
708
|
+
- The default key is `<Component>.<name>` (stored as `som:Home.todos`). Use `as '…'` to share one value between components.
|
|
709
|
+
- A change made in another tab of the same site shows up in this tab too.
|
|
710
|
+
|
|
711
|
+
A store keeps its plain values when you add `saved`:
|
|
712
|
+
|
|
713
|
+
```js
|
|
714
|
+
Kreftjs.store('cart', { items: [] }, { saved: true }) // or { saved: ['items'] } for some fields
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
From JavaScript:
|
|
718
|
+
|
|
719
|
+
```js
|
|
720
|
+
const theme = Kreftjs.saved('theme', 'light') // a signal: theme.value = 'dark' is saved
|
|
721
|
+
Kreftjs.storage.set('user', { name: 'Alice Smith' })
|
|
722
|
+
Kreftjs.storage.get('user')
|
|
723
|
+
Kreftjs.storage.remove('user')
|
|
724
|
+
Kreftjs.storage.clear() // removes only what Kreft saved
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
> [!WARNING]
|
|
728
|
+
> localStorage belongs to one browser on one site, holds about 5 MB and can be read by anyone using that browser, and by any script that runs on your page. Never store passwords or long-lived secrets in it.
|
|
729
|
+
|
|
730
|
+
## Router and layouts
|
|
731
|
+
|
|
732
|
+
### Defining routes
|
|
733
|
+
|
|
734
|
+
```js
|
|
735
|
+
const router = Kreftjs.router({
|
|
736
|
+
routes: [
|
|
737
|
+
{ path: '/', component: 'pages/Home.kreftjs', meta: { title: 'Home' } },
|
|
738
|
+
{ path: '/users/:id', name: 'user', component: 'pages/User.kreftjs', props: true },
|
|
739
|
+
{ path: '/posts/:slug?', component: 'pages/Posts.kreftjs' },
|
|
740
|
+
{ path: '/old', redirect: '/' },
|
|
741
|
+
{ path: '/admin', component: 'pages/Admin.kreftjs', guard: () => isAdmin() || '/login' },
|
|
742
|
+
{ path: '*', component: 'pages/NotFound.kreftjs' },
|
|
743
|
+
],
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
Kreftjs.app('App.kreftjs').use(router).mount('#app')
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
```html
|
|
750
|
+
<!-- App.kreftjs -->
|
|
751
|
+
<view>
|
|
752
|
+
<nav>
|
|
753
|
+
<kreft-link to="/">Home</kreft-link>
|
|
754
|
+
<kreft-link to="/users/{user.id}">Profile</kreft-link>
|
|
755
|
+
</nav>
|
|
756
|
+
<kreft-view></kreft-view>
|
|
757
|
+
</view>
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
With `props: true`, the `:id` parameter arrives as a prop:
|
|
761
|
+
|
|
762
|
+
```html
|
|
763
|
+
<!-- pages/User.kreftjs -->
|
|
764
|
+
prop id
|
|
765
|
+
derive user = users.find(id)
|
|
766
|
+
action back() { $router.back() }
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
| Feature | How |
|
|
770
|
+
|---|---|
|
|
771
|
+
| Current route | `$route.path`, `$route.params.id`, `$route.query.tab`, `$route.meta` |
|
|
772
|
+
| Navigate | `$router.go('/users/5?tab=info')`, `$router.replace(to)`, `$router.back()` |
|
|
773
|
+
| Active link | The `kreft-link-active` and `kreft-link-exact` classes |
|
|
774
|
+
| Page title | `meta: { title: '…' }` |
|
|
775
|
+
| Check before every page | `router.before((to, from) => …)`. Return `false` to stop, or a path to go elsewhere. |
|
|
776
|
+
| Check one route | `guard: (to, from) => …` |
|
|
777
|
+
| After every page | `router.after((to, from) => …)` |
|
|
778
|
+
|
|
779
|
+
Each page file is loaded the first time someone opens it.
|
|
780
|
+
|
|
781
|
+
### Route files
|
|
782
|
+
|
|
783
|
+
New projects keep their pages in `frontend/routes/web.kreftjs`, and `index.html` only points to it:
|
|
784
|
+
|
|
785
|
+
```js
|
|
786
|
+
Kreftjs.router({
|
|
787
|
+
routes: ['routes/web.kreftjs', 'routes/shop.kreftjs'],
|
|
788
|
+
layout: 'app',
|
|
789
|
+
errors: 'errors',
|
|
790
|
+
middleware: {
|
|
791
|
+
auth: (to) => (auth.user ? true : '/login?redirect=' + encodeURIComponent(to.fullPath)),
|
|
792
|
+
guest: () => (auth.user ? '/dashboard' : true),
|
|
793
|
+
},
|
|
794
|
+
})
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
```js
|
|
798
|
+
// routes/web.kreftjs
|
|
799
|
+
Route.view('/', 'pages/Home.kreftjs').name('home').title('Home')
|
|
800
|
+
Route.view('/posts/:id', 'pages/Post.kreftjs')
|
|
801
|
+
|
|
802
|
+
Route.middleware('auth').group(() => {
|
|
803
|
+
Route.view('/dashboard', 'pages/Dashboard.kreftjs').middleware('verified')
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
Route.layout('guest').middleware('guest').group(() => {
|
|
807
|
+
Route.view('/login', 'pages/auth/Login.kreftjs').title('Log in')
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
Route.prefix('/account').name('account.').middleware('auth').group(() => {
|
|
811
|
+
Route.view('/settings', 'pages/account/Settings.kreftjs').children(() => {
|
|
812
|
+
Route.view('', 'pages/account/General.kreftjs') // /account/settings
|
|
813
|
+
Route.view('mail', 'pages/account/Mail.kreftjs') // /account/settings/mail
|
|
814
|
+
})
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
Route.redirect('/home', '/')
|
|
818
|
+
Route.fallback('pages/NotFound.kreftjs')
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
| On a route | What it does |
|
|
822
|
+
|---|---|
|
|
823
|
+
| `.name('home')` | `$router.go({ name: 'home' })` |
|
|
824
|
+
| `.title('Home')` | The page title. |
|
|
825
|
+
| `.layout('guest')`, `.layout('none')` | Another layout, or none. |
|
|
826
|
+
| `.middleware('auth', 'role:editor')` | Runs these before the page. |
|
|
827
|
+
| `.meta({ … })` | Anything else, in `$route.meta`. |
|
|
828
|
+
| `.children(() => …)` | Nested pages, shown in the page's own `<kreft-view>`. |
|
|
829
|
+
|
|
830
|
+
> [!NOTE]
|
|
831
|
+
> Client-side `auth` middleware only decides which screens to show. It does not protect data: every API route must check the user on the server.
|
|
832
|
+
|
|
833
|
+
### Layouts
|
|
834
|
+
|
|
835
|
+
A layout is the frame around your pages: bar, menu, footer. Put it in `layouts/` and use `<slot>` where the page goes.
|
|
836
|
+
|
|
837
|
+
```html
|
|
838
|
+
<!-- layouts/app.kreftjs -->
|
|
839
|
+
state menuOpen = false
|
|
840
|
+
|
|
841
|
+
<view>
|
|
842
|
+
<header>…</header>
|
|
843
|
+
<main><slot></slot></main>
|
|
844
|
+
</view>
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
| Where | What you write |
|
|
848
|
+
|---|---|
|
|
849
|
+
| Every page | `Kreftjs.router({ layout: 'app', routes })` |
|
|
850
|
+
| One route and its children | `{ path: '/admin', component: …, layout: 'admin' }` |
|
|
851
|
+
| Inside a page file | `layout admin` on its own line |
|
|
852
|
+
| No layout | `layout: false` on the route, or `layout none` in the page |
|
|
853
|
+
|
|
854
|
+
When more than one applies, the route wins, then the page file, then the router. Going between two pages that use the same layout keeps the layout on screen, with its state.
|
|
855
|
+
|
|
856
|
+
A page can also fill other places in its layout:
|
|
857
|
+
|
|
858
|
+
```html
|
|
859
|
+
<!-- in the layout -->
|
|
860
|
+
<kreft-yield name="actions">shown when the page has no such section</kreft-yield>
|
|
861
|
+
|
|
862
|
+
<!-- in the page -->
|
|
863
|
+
<kreft-section name="actions">
|
|
864
|
+
<button class="primary" @click="save()">Save</button>
|
|
865
|
+
</kreft-section>
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Error pages
|
|
869
|
+
|
|
870
|
+
Each HTTP error can have its own page in `errors/`. Turn them on with `errors: 'errors'` in the router. New projects already have the folder (`kreftjs make:errors` adds it to an older one):
|
|
871
|
+
|
|
872
|
+
```text
|
|
873
|
+
errors/401.kreftjs 402.kreftjs 403.kreftjs 404.kreftjs 419.kreftjs 429.kreftjs 500.kreftjs 503.kreftjs
|
|
874
|
+
errors/error.kreftjs any other status
|
|
875
|
+
errors/minimal.kreftjs the shared frame
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
An unknown address shows `errors/404.kreftjs`; a page that cannot load shows `errors/500.kreftjs`. From any `.kreftjs` file, `abort(404)` (also `$router.abort()` and `Kreftjs.abort()`) shows the matching page:
|
|
879
|
+
|
|
880
|
+
```html
|
|
881
|
+
prop id
|
|
882
|
+
load post = api.get('/api/posts/' + id)
|
|
883
|
+
watch post.error { if (post.error && post.error.status === 404) abort(404) }
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Clean URLs
|
|
887
|
+
|
|
888
|
+
By default the router uses URLs without `#` (`/users/5`). Two things make that work:
|
|
889
|
+
|
|
890
|
+
1. A `<base href="/">` tag in `index.html` (use `<base href="/demo/">` if the app lives in a folder).
|
|
891
|
+
2. The server must send `index.html` for every page path, so reloading `/users/5` is not a 404. `npm run dev` does this for you.
|
|
892
|
+
|
|
893
|
+
If you cannot change the server, use `mode: 'hash'`; URLs then look like `/#/users/5`.
|
|
894
|
+
|
|
895
|
+
## Fetching data
|
|
896
|
+
|
|
897
|
+
The global `api` client is available in every `.kreftjs` file, and `Kreftjs.api` in plain JavaScript:
|
|
898
|
+
|
|
899
|
+
```js
|
|
900
|
+
const users = await api.get('/api/users', { query: { page: 2 } })
|
|
901
|
+
const user = await api.post('/api/users', { name: 'Alice Smith' })
|
|
902
|
+
await api.put('/api/users/1', user)
|
|
903
|
+
await api.patch('/api/users/1', { status: 'active' })
|
|
904
|
+
await api.delete('/api/users/1')
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
### Loading data: `load`
|
|
908
|
+
|
|
909
|
+
`load` declares a reactive resource. It handles the loading state, the error, and fetches again when the values it reads change:
|
|
910
|
+
|
|
911
|
+
```html
|
|
912
|
+
<!-- pages/Posts.kreftjs -->
|
|
913
|
+
state query = ''
|
|
914
|
+
state page = 1
|
|
915
|
+
|
|
916
|
+
load posts = api.get('/api/posts', { query: { q: query || undefined, page } })
|
|
917
|
+
|
|
918
|
+
<view>
|
|
919
|
+
<input type="search" k-model.lazy="query" placeholder="Search posts…">
|
|
920
|
+
|
|
921
|
+
<p k-if="posts.loading">Loading posts…</p>
|
|
922
|
+
<div k-elif="posts.error">
|
|
923
|
+
Failed to load: {posts.error.message}
|
|
924
|
+
<button @click={posts.reload}>Try again</button>
|
|
925
|
+
</div>
|
|
926
|
+
<div k-else>
|
|
927
|
+
{for post in posts.data || [] key post.id}
|
|
928
|
+
<article><h3>{post.title}</h3><p>{post.body}</p></article>
|
|
929
|
+
{empty}
|
|
930
|
+
<p>No posts found for "{query}".</p>
|
|
931
|
+
{/for}
|
|
932
|
+
</div>
|
|
933
|
+
|
|
934
|
+
<button @click={() => page++}>Next page ({page})</button>
|
|
935
|
+
</view>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
| Property | Meaning |
|
|
939
|
+
|---|---|
|
|
940
|
+
| `posts.data` | The result (`null` until loaded). |
|
|
941
|
+
| `posts.loading` | `true` while a request is running. |
|
|
942
|
+
| `posts.loaded` | `true` once the first answer arrived. |
|
|
943
|
+
| `posts.error` | The error, or `null`. |
|
|
944
|
+
| `posts.reload()` | Fetch again now. |
|
|
945
|
+
|
|
946
|
+
### Tables
|
|
947
|
+
|
|
948
|
+
For tables served by `kreftjs dev --db` or the automatic table API, `table(name)` gives ready-made calls:
|
|
949
|
+
|
|
950
|
+
```html
|
|
951
|
+
const posts = table('posts')
|
|
952
|
+
|
|
953
|
+
load list = posts.all({ q: search, _sort: '-created_at', _limit: 10 })
|
|
954
|
+
|
|
955
|
+
action createPost() {
|
|
956
|
+
const created = await posts.create({ title: 'New article' })
|
|
957
|
+
list.data = [created, ...list.data]
|
|
958
|
+
}
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
Methods: `all(query)`, `find(id)`, `count(query)`, `create(data)`, `update(id, data)` (PUT), `patch(id, data)`, `remove(id)`.
|
|
962
|
+
|
|
963
|
+
### Errors
|
|
964
|
+
|
|
965
|
+
A failed request throws an `ApiError` with `status`, `message`, `data` and `errors` (validation messages). Network problems and timeouts have `status` 0.
|
|
966
|
+
|
|
967
|
+
```html
|
|
968
|
+
state errors = {}
|
|
969
|
+
|
|
970
|
+
action save() {
|
|
971
|
+
try {
|
|
972
|
+
await api.post('/api/users', form)
|
|
973
|
+
} catch (e) {
|
|
974
|
+
errors = e.errors || {} // { email: ['The email has already been taken.'] }
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### Settings
|
|
980
|
+
|
|
981
|
+
```js
|
|
982
|
+
Kreftjs.api.setup({
|
|
983
|
+
base: '/api', // then api.get('users') is /api/users
|
|
984
|
+
token: () => Kreftjs.storage.get('token'), // sent as Authorization: Bearer …
|
|
985
|
+
headers: { 'Accept-Language': 'en' },
|
|
986
|
+
timeout: 10000,
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
Kreftjs.api.onError((e) => { if (e.status === 401) location.href = '/login' })
|
|
990
|
+
|
|
991
|
+
const github = Kreftjs.api.create({ base: 'https://api.github.com' }) // a second client
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
Options for one request: `query`, `headers`, `timeout`, `signal` (to cancel) and `full: true` (returns `{ data, status, headers }`).
|
|
995
|
+
|
|
996
|
+
Requests to your own backend also send `X-XSRF-TOKEN` from the `XSRF-TOKEN` cookie (and `X-CSRF-TOKEN` from `<meta name="csrf-token">`), plus `X-Requested-With`.
|
|
997
|
+
|
|
998
|
+
### A practice API
|
|
999
|
+
|
|
1000
|
+
```bash
|
|
1001
|
+
kreftjs dev --api db.json # a REST API at /api, made from a JSON file
|
|
1002
|
+
kreftjs dev --api db.json --delay 500 # answer slowly, to see loading states
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
It gives GET and POST on `/api/posts`, GET, PUT, PATCH and DELETE on `/api/posts/1`, search with `?q=text`, filters with `?field=value`, and standard paging and sorting.
|
|
1006
|
+
|
|
1007
|
+
> [!WARNING]
|
|
1008
|
+
> `--api` and `--db` are for development. They have no login. `kreftjs dev` answers only your own computer, and refuses requests that change data when they come from another web site, but do not expose them to the internet. In production, write real routes (see [Routing](#routing)).
|
|
1009
|
+
|
|
1010
|
+
## Forms and validation
|
|
1011
|
+
|
|
1012
|
+
Give a form its rules in declarative style. A form with mistakes is never sent:
|
|
1013
|
+
|
|
1014
|
+
```html
|
|
1015
|
+
state post = form({ title: '', email: '' }, { title: 'required|min:3|max:120', email: 'required|email' })
|
|
1016
|
+
|
|
1017
|
+
action save() {
|
|
1018
|
+
const created = await post.$submit((values) => api.post('/api/posts', values))
|
|
1019
|
+
if (created) post.$reset()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
<view>
|
|
1023
|
+
<form @submit.prevent={save}>
|
|
1024
|
+
<input k-model="post.title">
|
|
1025
|
+
<p k-if="post.$error('title')" class="error">{post.$error('title')}</p>
|
|
1026
|
+
<button disabled={post.$busy}>Save</button>
|
|
1027
|
+
</form>
|
|
1028
|
+
</view>
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
| On the form | What it is |
|
|
1032
|
+
|---|---|
|
|
1033
|
+
| `post.title`, `post.email` | The values, for `k-model`. |
|
|
1034
|
+
| `post.$error('title')` | The first message for a field, or `''`. |
|
|
1035
|
+
| `post.$errors` | Every message: `{ title: ['…'] }`. |
|
|
1036
|
+
| `post.$valid`, `post.$busy` | No mistakes / sending right now. |
|
|
1037
|
+
| `post.$submit(send)` | Checks, then calls `send(values)`. A 422 from the server fills the same errors. Gives `false` when it did not send. |
|
|
1038
|
+
| `post.$check()`, `post.$reset(values?)`, `post.$setErrors(e)` | Check now / start again / show errors from elsewhere. |
|
|
1039
|
+
|
|
1040
|
+
Errors appear after the first submit, then follow your typing.
|
|
1041
|
+
|
|
1042
|
+
Without a form, `validate(values, rules)` gives `{}` or `{ field: ['…'] }`. Custom messages go in the third argument: `form(values, rules, { messages: { 'title.required': 'Write a title.' } })`.
|
|
1043
|
+
|
|
1044
|
+
The same rule names work on the server in `req.validate()` (see [Controllers and requests](#controllers-and-requests)), so one set of rules can describe both sides.
|
|
1045
|
+
|
|
1046
|
+
## Internationalization and RTL
|
|
1047
|
+
|
|
1048
|
+
Kreft translates text and flips direction for right-to-left languages such as Arabic, Hebrew, Persian and Urdu.
|
|
1049
|
+
|
|
1050
|
+
### Translations
|
|
1051
|
+
|
|
1052
|
+
Put translation files in a `lang/` folder: `lang/` or `frontend/lang/` in your project (`backend/lang/` and `resources/lang/` are read too). Use `lang/en.json` for plain strings, or `lang/en/messages.json` for grouped keys. In any view:
|
|
1053
|
+
|
|
1054
|
+
```html
|
|
1055
|
+
<h1>{$t('messages.welcome', { name: user.name })}</h1>
|
|
1056
|
+
<p>{$choice('messages.apples', count)}</p>
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
A message uses `:name` for values. For plurals, separate the forms with `|`: `{0} No apples|{1} One apple|[2,*] :count apples`. Under the hood, categories use the browser's `Intl.PluralRules` for the current locale.
|
|
1060
|
+
|
|
1061
|
+
Change the language with `Kreftjs.locale('ar')`. The page's `<html lang>` and `dir` update, and every view re-renders. `Kreftjs.fallbackLocale('en')` sets the fallback, `Kreftjs.locales()` lists loaded languages, and `Kreftjs.translations('so', { … })` registers more text from code.
|
|
1062
|
+
|
|
1063
|
+
### Direction
|
|
1064
|
+
|
|
1065
|
+
`dir` is set for you from the locale, and you can also bind it by hand:
|
|
1066
|
+
|
|
1067
|
+
```html
|
|
1068
|
+
<view dir={isArabic ? 'rtl' : 'ltr'}>…</view>
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### Write CSS that flips by itself
|
|
1072
|
+
|
|
1073
|
+
Use CSS logical properties instead of left/right:
|
|
1074
|
+
|
|
1075
|
+
| Avoid | Use |
|
|
1076
|
+
|---|---|
|
|
1077
|
+
| `margin-left` / `margin-right` | `margin-inline-start` / `margin-inline-end` |
|
|
1078
|
+
| `padding-left` / `padding-right` | `padding-inline-start` / `padding-inline-end` |
|
|
1079
|
+
| `left: 0` / `right: 0` | `inset-inline-start: 0` / `inset-inline-end: 0` |
|
|
1080
|
+
| `text-align: left` / `right` | `text-align: start` / `end` |
|
|
1081
|
+
| `border-left` / `border-right` | `border-inline-start` / `border-inline-end` |
|
|
1082
|
+
|
|
1083
|
+
```css
|
|
1084
|
+
.sidebar {
|
|
1085
|
+
position: fixed;
|
|
1086
|
+
inset-inline-start: 0; /* left in LTR, right in RTL */
|
|
1087
|
+
padding-inline-start: 24px;
|
|
1088
|
+
border-inline-end: 1px solid var(--border);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
[dir="rtl"] .back-arrow { transform: scaleX(-1); } /* flip an icon */
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
On the server, request language comes from `?lang=`, the `lang` cookie or `Accept-Language`, and backend messages live in `backend/lang/<locale>/`.
|
|
1095
|
+
|
|
1096
|
+
## Using Kreft from JavaScript
|
|
1097
|
+
|
|
1098
|
+
You can write components as plain objects. The rules match the `.kreftjs` language: values are state, getters are derived values, functions are actions.
|
|
1099
|
+
|
|
1100
|
+
```js
|
|
1101
|
+
Kreftjs.app({
|
|
1102
|
+
count: 0,
|
|
1103
|
+
get double() { return this.count * 2 },
|
|
1104
|
+
add() { this.count++ },
|
|
1105
|
+
template: `<button @click={add}>{count} × 2 = {double}</button>`,
|
|
1106
|
+
}).mount('#app')
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
| JavaScript | Same as in `.kreftjs` |
|
|
1110
|
+
|---|---|
|
|
1111
|
+
| `Kreftjs.state({ … })` | `state` + `derive` + `action` together, for code outside components. |
|
|
1112
|
+
| `Kreftjs.signal(0)` | One value, read and written with `.value`. |
|
|
1113
|
+
| `Kreftjs.derived(() => …)` | `derive` |
|
|
1114
|
+
| `Kreftjs.watch(source, (value, old) => …)` | `watch` |
|
|
1115
|
+
| `Kreftjs.effect(() => …)` | Runs again whenever what it reads changes. |
|
|
1116
|
+
| `Kreftjs.tick()` | Wait until the page has updated. |
|
|
1117
|
+
|
|
1118
|
+
`Kreftjs.render(view, data)` turns a view and data into an HTML string, with every directive working and every value escaped. Use it for emails and print views:
|
|
1119
|
+
|
|
1120
|
+
```js
|
|
1121
|
+
Kreftjs.render(`<ul><li k-each="u in users">{u}</li></ul>`, { users: ['Alex', 'Alice'] })
|
|
1122
|
+
// <ul><li>Alex</li><li>Alice</li></ul>
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
### Server templates
|
|
1126
|
+
|
|
1127
|
+
Blade and similar engines use `{{ }}`; Kreft uses `{ }`, so they do not clash. Put `kreftjs.js` and your `.kreftjs` files in your public folder and mount as usual.
|
|
1128
|
+
|
|
1129
|
+
# Backend
|
|
1130
|
+
|
|
1131
|
+
## Routing
|
|
1132
|
+
|
|
1133
|
+
A project made with `kreftjs new` has a Node.js backend. `kreftjs dev` and `kreftjs serve` load `backend/routes/api.js` by themselves, every address in it starts with `/api`, and `kreftjs dev` picks up each change you save. The backend folder never reaches the browser.
|
|
1134
|
+
|
|
1135
|
+
```js
|
|
1136
|
+
// routes/api.js
|
|
1137
|
+
const { Route } = require('kreftjs/server')
|
|
1138
|
+
const PostController = require('../app/Http/Controllers/PostController')
|
|
1139
|
+
|
|
1140
|
+
Route.get('/hello', (req) => ({ hello: req.input('name', 'world') })) // GET /api/hello
|
|
1141
|
+
|
|
1142
|
+
Route.apiResource('posts', PostController) // index, store, show, update, destroy
|
|
1143
|
+
Route.get('/posts/{post}/comments', [PostController, 'comments']).name('posts.comments')
|
|
1144
|
+
|
|
1145
|
+
Route.middleware('auth').group(() => {
|
|
1146
|
+
Route.delete('/posts/{post}', [PostController, 'destroy'])
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
Route.prefix('admin').middleware(['auth', 'admin']).group(() => { … })
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
- Parameters are written `{post}`, or `{post?}` when optional.
|
|
1153
|
+
- A route can have a name (`.name('…')`), a pattern (`.whereNumber('id')`) and middleware (`.middleware('auth')`).
|
|
1154
|
+
- `kreftjs route:list` prints every route with its name, controller and middleware.
|
|
1155
|
+
|
|
1156
|
+
### Namespaces
|
|
1157
|
+
|
|
1158
|
+
PHP-style namespaces are supported for controllers, models and relations:
|
|
1159
|
+
|
|
1160
|
+
```js
|
|
1161
|
+
const { Route, use, namespace } = require('kreftjs/server')
|
|
1162
|
+
|
|
1163
|
+
const User = use('App\\Models\\User')
|
|
1164
|
+
const { DashboardController, UserController } = namespace('App\\Http\\Controllers\\Admin')
|
|
1165
|
+
|
|
1166
|
+
Route.namespace('Admin').group(() => {
|
|
1167
|
+
Route.get('/users', 'UserController@index')
|
|
1168
|
+
Route.apiResource('users', 'UserController')
|
|
1169
|
+
})
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
## Controllers and requests
|
|
1173
|
+
|
|
1174
|
+
A controller method gets the request first, then the route's parameters. What it returns is sent as JSON; returning nothing answers `204`.
|
|
1175
|
+
|
|
1176
|
+
```js
|
|
1177
|
+
const { response, abort } = require('kreftjs/server')
|
|
1178
|
+
const Post = require('../../Models/Post')
|
|
1179
|
+
|
|
1180
|
+
class PostController {
|
|
1181
|
+
async index(req) {
|
|
1182
|
+
return Post.with('author').where('published', true).latest().paginate(15, req.integer('page', 1))
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async store(req) {
|
|
1186
|
+
const data = await req.validate({
|
|
1187
|
+
title: 'required|max:200|unique:posts,title',
|
|
1188
|
+
author_id: 'required|exists:authors,id',
|
|
1189
|
+
})
|
|
1190
|
+
return response(await Post.create(data), 201)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async show(req, id) { return Post.findOrFail(id) } // 404 when missing
|
|
1194
|
+
async destroy(req, id) { await (await Post.findOrFail(id)).delete() }
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
module.exports = PostController
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
Generate one with `kreftjs make:controller PostController --api` (also `--model Post`, `--route=api`, `--auth`).
|
|
1201
|
+
|
|
1202
|
+
### The request
|
|
1203
|
+
|
|
1204
|
+
| Call | Gives |
|
|
1205
|
+
|---|---|
|
|
1206
|
+
| `req.input('name', default)`, `req.all()`, `req.only('a', 'b')`, `req.except(…)`, `req.has(…)`, `req.filled(…)` | Values from the body and the query. |
|
|
1207
|
+
| `req.boolean('x')`, `req.integer('page', 1)`, `req.string('q')` | A value in the right type. |
|
|
1208
|
+
| `await req.validate(rules)` | The same rules as `form()`, plus `unique:table,column` and `exists:table,column`. Returns only the checked fields, or answers `422`. |
|
|
1209
|
+
| `req.params`, `req.query`, `req.body`, `req.header('x')`, `req.bearerToken()`, `req.ip()`, `req.user` | The rest. |
|
|
1210
|
+
| `req.file('avatar').store('avatars')` | Saves an upload with a random name. |
|
|
1211
|
+
| `response(data, 201)`, `response(data).header('X', 'y')`, `abort(403, 'message')` | Answers. |
|
|
1212
|
+
|
|
1213
|
+
### Form requests, resources and policies
|
|
1214
|
+
|
|
1215
|
+
For bigger endpoints, move rules and authorization out of the controller:
|
|
1216
|
+
|
|
1217
|
+
```js
|
|
1218
|
+
// app/Http/Requests/StorePostRequest.js
|
|
1219
|
+
const { FormRequest } = require('kreftjs/server')
|
|
1220
|
+
|
|
1221
|
+
class StorePostRequest extends FormRequest {
|
|
1222
|
+
authorize(req) { return req.user != null } // false answers 403
|
|
1223
|
+
rules() { return { title: 'required|max:200', body: 'required' } }
|
|
1224
|
+
messages() { return { 'title.required': 'Give the post a title.' } }
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
module.exports = StorePostRequest
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
```js
|
|
1231
|
+
const data = await StorePostRequest.validate(req) // in a controller
|
|
1232
|
+
return PostResource.collection(await Post.paginate(15)) // { data, meta }
|
|
1233
|
+
await req.authorize('update', post) // app/Policies/PostPolicy.js
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
Generate them with `kreftjs make:request`, `kreftjs make:resource` and `kreftjs make:policy`.
|
|
1237
|
+
|
|
1238
|
+
### Passing data from controllers to pages
|
|
1239
|
+
|
|
1240
|
+
A page receives data from the API with `load` (see [Fetching data](#fetching-data)). Return plain objects, collections or paginator results from controllers; hidden model fields (`static hidden`) are never included.
|
|
1241
|
+
|
|
1242
|
+
## Models and the database
|
|
1243
|
+
|
|
1244
|
+
Models are Active Record classes, like Eloquent:
|
|
1245
|
+
|
|
1246
|
+
```js
|
|
1247
|
+
// app/Models/Post.js
|
|
1248
|
+
const { Model, Str } = require('kreftjs/server')
|
|
1249
|
+
|
|
1250
|
+
class Post extends Model {
|
|
1251
|
+
static fillable = ['title', 'body', 'user_id'] // what create() and update() may fill
|
|
1252
|
+
static hidden = [] // never sent as JSON
|
|
1253
|
+
static casts = { published: 'boolean', meta: 'json' }
|
|
1254
|
+
static softDeletes = true // delete() fills deleted_at
|
|
1255
|
+
|
|
1256
|
+
user() { return this.belongsTo(require('./User')) }
|
|
1257
|
+
comments() { return this.hasMany(require('./Comment')) }
|
|
1258
|
+
tags() { return this.belongsToMany(require('./Tag')) }
|
|
1259
|
+
|
|
1260
|
+
static scopePublished(q) { q.where('published', true) }
|
|
1261
|
+
static async creating(post) { post.slug ??= Str.slug(post.title) }
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
module.exports = Post
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
The table is the plural of the class name (`Post` is `posts`); set `static table = '…'` for another one.
|
|
1268
|
+
|
|
1269
|
+
### Querying
|
|
1270
|
+
|
|
1271
|
+
```js
|
|
1272
|
+
await Post.all()
|
|
1273
|
+
await Post.find(5) // or findOrFail(5): 404 when missing
|
|
1274
|
+
await Post.where('published', true).orderByDesc('id').limit(10).get()
|
|
1275
|
+
await Post.where('views', '>', 100).orWhere('pinned', true).count()
|
|
1276
|
+
await Post.query().published().with('user', 'comments.author').withCount('comments').paginate(15, page)
|
|
1277
|
+
await Post.query().whereHas('comments', (q) => q.where('approved', true)).get()
|
|
1278
|
+
|
|
1279
|
+
const post = await Post.create({ title: 'Hi' }) // fills created_at and updated_at
|
|
1280
|
+
await post.update({ title: 'Hello' })
|
|
1281
|
+
await post.delete()
|
|
1282
|
+
|
|
1283
|
+
await post.comments().create({ body: 'Nice' })
|
|
1284
|
+
await post.tags().sync([1, 2, 3]) // also attach(), detach(), toggle()
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
Relations: `hasOne`, `hasMany`, `belongsTo`, `belongsToMany` (with `pivot`), `hasManyThrough`. Soft deleted rows come back with `withTrashed()` and return with `restore()`.
|
|
1288
|
+
|
|
1289
|
+
Without a model:
|
|
1290
|
+
|
|
1291
|
+
```js
|
|
1292
|
+
await DB.table('orders').join('users', 'users.id', '=', 'orders.user_id').groupBy('status').get()
|
|
1293
|
+
await DB.select('SELECT * FROM posts WHERE id = ?', [5])
|
|
1294
|
+
await DB.transaction(async () => { await Order.create({ … }); await Stock.decrement(…) }) // all or nothing
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
> [!WARNING]
|
|
1298
|
+
> Values always travel as parameters (`?`). Never build SQL by joining user input into a string. `whereRaw`, `selectRaw` and `orderByRaw` take raw SQL on purpose: pass user values through their parameter array.
|
|
1299
|
+
|
|
1300
|
+
### Databases
|
|
1301
|
+
|
|
1302
|
+
The database comes from `DATABASE_URL` in `.env`:
|
|
1303
|
+
|
|
1304
|
+
```env
|
|
1305
|
+
DATABASE_URL=sqlite:backend/database/app.db # built into Node.js, nothing to install
|
|
1306
|
+
DATABASE_URL=mysql://user:pass@localhost:3306/shop # npm i mysql2
|
|
1307
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/shop # npm i pg
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
The same migrations and models work on all three.
|
|
1311
|
+
|
|
1312
|
+
## Migrations, seeders and factories
|
|
1313
|
+
|
|
1314
|
+
A migration describes a table in JavaScript; the same file works on SQLite, MySQL and PostgreSQL:
|
|
1315
|
+
|
|
1316
|
+
```js
|
|
1317
|
+
// database/migrations/2026_10_29_120000_create_posts_table.js
|
|
1318
|
+
module.exports = {
|
|
1319
|
+
up(schema) {
|
|
1320
|
+
schema.create('posts', (table) => {
|
|
1321
|
+
table.id()
|
|
1322
|
+
table.foreignId('author_id').constrained().cascadeOnDelete()
|
|
1323
|
+
table.string('title', 200).unique()
|
|
1324
|
+
table.text('body').nullable()
|
|
1325
|
+
table.boolean('published').default(false)
|
|
1326
|
+
table.json('meta').nullable()
|
|
1327
|
+
table.timestamps()
|
|
1328
|
+
})
|
|
1329
|
+
},
|
|
1330
|
+
down(schema) {
|
|
1331
|
+
schema.dropIfExists('posts')
|
|
1332
|
+
},
|
|
1333
|
+
}
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
Column types include `id`, `string`, `text`, `integer`, `bigInteger`, `decimal`, `float`, `boolean`, `date`, `dateTime`, `timestamp`, `timestamps`, `json`, `foreignId`. Plain `.sql` files in `database/migrations` also work (they run once each, in name order).
|
|
1337
|
+
|
|
1338
|
+
A seeder fills starting data:
|
|
1339
|
+
|
|
1340
|
+
```js
|
|
1341
|
+
// database/seeders/DatabaseSeeder.js
|
|
1342
|
+
const { Seeder } = require('kreftjs/server')
|
|
1343
|
+
const Post = require('../../app/Models/Post')
|
|
1344
|
+
|
|
1345
|
+
class DatabaseSeeder extends Seeder {
|
|
1346
|
+
async run() {
|
|
1347
|
+
await Post.create({ title: 'Hello', author_id: 1 })
|
|
1348
|
+
await this.call(require('./CommentSeeder'))
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
module.exports = DatabaseSeeder
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
Factories make fake data for seeders and tests:
|
|
1356
|
+
|
|
1357
|
+
```js
|
|
1358
|
+
await User.factory().count(10).has(Post.factory().count(3), 'posts').create()
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
### Commands
|
|
1362
|
+
|
|
1363
|
+
| Command | What it does |
|
|
1364
|
+
|---|---|
|
|
1365
|
+
| `kreftjs migrate` | Runs the new migrations (`kreftjs dev` does this by itself). |
|
|
1366
|
+
| `kreftjs migrate:rollback` | Undoes the last batch (`--step 2` for the last two). |
|
|
1367
|
+
| `kreftjs migrate:fresh --seed` | Drops every table, migrates again, then seeds. |
|
|
1368
|
+
| `kreftjs migrate:status` | Which migrations ran. |
|
|
1369
|
+
| `kreftjs db:seed` | Runs `DatabaseSeeder` (`--class PostSeeder` for another). |
|
|
1370
|
+
| `kreftjs make:model Post -mfsc` | A model plus migration (`-m`), factory (`-f`), seeder (`-s`), controller (`-c`), page (`-p`), route (`-r`). |
|
|
1371
|
+
| `kreftjs make:migration create_posts_table` | `add_x_to_posts_table` makes a `schema.table` one. |
|
|
1372
|
+
| `kreftjs make:seeder`, `kreftjs make:middleware` | The files, in the right folders. |
|
|
1373
|
+
|
|
1374
|
+
> [!NOTE]
|
|
1375
|
+
> `migrate:fresh` and `db:wipe` destroy data. They are meant for development databases only.
|
|
1376
|
+
|
|
1377
|
+
## Authentication
|
|
1378
|
+
|
|
1379
|
+
New projects come with every sign-in screen and the backend behind it:
|
|
1380
|
+
|
|
1381
|
+
| Page | Address | What it does |
|
|
1382
|
+
|---|---|---|
|
|
1383
|
+
| `pages/auth/Login.kreftjs` | `/login` | Log in with email and password. |
|
|
1384
|
+
| `pages/auth/Register.kreftjs` | `/register` | Make an account and get an email to verify the address. |
|
|
1385
|
+
| `pages/auth/ForgotPassword.kreftjs` | `/forgot-password` | Email a link to choose a new password. |
|
|
1386
|
+
| `pages/auth/ResetPassword.kreftjs` | `/reset-password/:token` | The link from that email. |
|
|
1387
|
+
| `pages/auth/VerifyEmail.kreftjs` | `/verify-email` | "Check your email", and the link from it opens `/verify-email/:id/:hash`. |
|
|
1388
|
+
| `pages/auth/ConfirmPassword.kreftjs` | `/confirm-password` | Type the password again before something serious. |
|
|
1389
|
+
| `pages/Dashboard.kreftjs`, `pages/Profile.kreftjs` | `/dashboard`, `/profile` | The first page after logging in; account settings. |
|
|
1390
|
+
|
|
1391
|
+
### How a login works
|
|
1392
|
+
|
|
1393
|
+
- A login puts a token in an **HttpOnly cookie**. Page scripts cannot read it, so a script injected into a page cannot steal it. Only a hash of the token is stored in the database.
|
|
1394
|
+
- Every change (POST, PUT, PATCH, DELETE) also needs the `X-XSRF-TOKEN` header to match the `XSRF-TOKEN` cookie. `Kreftjs.api` does this for you.
|
|
1395
|
+
- A script or an app that sends `device_name` when it logs in gets an **API token** (`Authorization: Bearer 5|…`) instead.
|
|
1396
|
+
- Passwords are hashed with scrypt. Reset links expire after 60 minutes and sign the user out everywhere.
|
|
1397
|
+
- Session ids change on every login, so a session cannot be planted in a browser beforehand.
|
|
1398
|
+
|
|
1399
|
+
On the server:
|
|
1400
|
+
|
|
1401
|
+
```js
|
|
1402
|
+
Route.middleware(['auth', 'verified']).group(() => {
|
|
1403
|
+
Route.get('/orders', [OrderController, 'index']) // req.user is the user
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
Route.delete('/account', …).middleware(['auth', 'password.confirm']) // asks for the password again (423)
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
Helpers you can use anywhere in the backend:
|
|
1410
|
+
|
|
1411
|
+
```js
|
|
1412
|
+
const { Hash, Str, Url, Mail, Crypt } = require('kreftjs/server')
|
|
1413
|
+
|
|
1414
|
+
Hash.make('secret') // store this, never the password
|
|
1415
|
+
Hash.check('secret', user.password) // true or false
|
|
1416
|
+
Str.random(40) // a random token
|
|
1417
|
+
Url.signed('/verify-email/5/abc', 60) // a link that works for 60 minutes, unchanged
|
|
1418
|
+
await Mail.send({ to, subject, text }) // printed in the terminal until MAIL_URL is set
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
### Protections in the sign-in routes
|
|
1422
|
+
|
|
1423
|
+
- Visitors get `throttle:6,1`: six tries a minute per address.
|
|
1424
|
+
- Each **account** also locks after `LOGIN_MAX_FAILURES` (10) wrong passwords in `LOGIN_WINDOW_SECONDS` (15 minutes), whatever address the tries come from. After that, even the right password gets `429` until the window ends.
|
|
1425
|
+
- Two-factor codes are counted per account too: `TWO_FACTOR_MAX_FAILURES` (8) wrong codes lock it, and starting a new login does not reset the count.
|
|
1426
|
+
- "Forgot your password?" answers the same whether or not the address has an account.
|
|
1427
|
+
- Changing the email address needs a password confirmation from the last 3 hours (answer `423`), because the address is what password reset trusts.
|
|
1428
|
+
|
|
1429
|
+
### Two-factor, devices and API tokens
|
|
1430
|
+
|
|
1431
|
+
- **Two-factor**: authenticator-app codes (TOTP) plus recovery codes. Each recovery code works once.
|
|
1432
|
+
- **Devices**: `/profile` lists the signed-in browsers, and any of them can be logged out.
|
|
1433
|
+
- **API tokens**: create them in `/profile` with the permissions `read`, `create`, `update`, `delete`. A token can only use the HTTP methods its permissions allow, on **every** route (`GET` needs `read`, `POST` needs `create`, `PUT`/`PATCH` need `update`, `DELETE` needs `delete`). Only browser sessions and `*` tokens can manage tokens, the password and the account.
|
|
1434
|
+
|
|
1435
|
+
### Admin users
|
|
1436
|
+
|
|
1437
|
+
An admin is a user whose `is_admin` column is true. The test user is one; give the rights to others on the Users page, or with `kreftjs make:admin you@example.com`. Use `Route.middleware(['auth', 'admin'])` for admin-only routes.
|
|
1438
|
+
|
|
1439
|
+
## Middleware
|
|
1440
|
+
|
|
1441
|
+
Middleware runs before the controller and can stop the request or change the answer:
|
|
1442
|
+
|
|
1443
|
+
```js
|
|
1444
|
+
// app/Http/Middleware/EnsureIsAdmin.js ('ensure-is-admin' in a route, or 'ensure-is-admin:editor')
|
|
1445
|
+
const { abort } = require('kreftjs/server')
|
|
1446
|
+
|
|
1447
|
+
class EnsureIsAdmin {
|
|
1448
|
+
async handle(req, next, role = 'admin') {
|
|
1449
|
+
if (!req.user || req.user.role !== role) abort(403)
|
|
1450
|
+
const response = await next(req) // the controller runs here
|
|
1451
|
+
return response.header('X-Checked', 'yes')
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
module.exports = EnsureIsAdmin
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
A name in a route finds its file in `app/Http/Middleware`: `'auth'` is `Authenticate.js`, `'ensure-is-admin'` is `EnsureIsAdmin.js`.
|
|
1459
|
+
|
|
1460
|
+
### Built in
|
|
1461
|
+
|
|
1462
|
+
| Middleware | What it does |
|
|
1463
|
+
|---|---|
|
|
1464
|
+
| `throttle:60,1` | Rate limiting: 60 requests a minute per visitor and route, then `429`. |
|
|
1465
|
+
| `csrf` | Checks the CSRF token on POST, PUT, PATCH and DELETE (`419` when wrong). |
|
|
1466
|
+
| `can:ability,model` | Policy checks through `Gate.authorize()`. |
|
|
1467
|
+
| `auth`, `guest`, `verified`, `password.confirm` | The sign-in set from `app/Http/Middleware`. |
|
|
1468
|
+
|
|
1469
|
+
For every route, list middleware in `app/Http/Kernel.js`:
|
|
1470
|
+
|
|
1471
|
+
```js
|
|
1472
|
+
module.exports = {
|
|
1473
|
+
middleware: ['throttle:120,1'],
|
|
1474
|
+
}
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
Exempt a route with `.withoutMiddleware('csrf')`, or `new VerifyCsrfToken({ except: ['webhooks/*'] })`.
|
|
1478
|
+
|
|
1479
|
+
## Queues, mail, cache and files
|
|
1480
|
+
|
|
1481
|
+
Each tool has its own folder in `backend/app/` and a `kreftjs make:` command:
|
|
1482
|
+
|
|
1483
|
+
```js
|
|
1484
|
+
await dispatch(new SendWelcome(user)) // app/Jobs: runs in the background (tries, backoff, failed())
|
|
1485
|
+
await event(new OrderShipped(order)) // app/Listeners
|
|
1486
|
+
await Mail.to(user).send(new WelcomeMail(user)) // app/Mail: subject, text, html or markdown({ … })
|
|
1487
|
+
await user.notify(new InvoicePaid(invoice)) // app/Notifications: 'mail' and 'database' channels
|
|
1488
|
+
await Cache.remember('stats', 600, () => loadStats()) // database, file, memory or redis (CACHE_STORE)
|
|
1489
|
+
const path = await req.file('avatar').store('avatars') // uploads; Storage.url(path) is /storage/avatars/…
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### Jobs
|
|
1493
|
+
|
|
1494
|
+
`Job.dispatch(...)` returns an object you can chain (`delay(seconds)`, `onQueue(name)`) and then `await`:
|
|
1495
|
+
|
|
1496
|
+
```js
|
|
1497
|
+
await SendWelcome.dispatch(user)
|
|
1498
|
+
await SendWelcome.dispatch(user).delay(60).onQueue('mail')
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
> [!WARNING]
|
|
1502
|
+
> `dispatch` sends the job when you `await` it (or call `.then`). Calling it and forgetting the `await` sends nothing.
|
|
1503
|
+
|
|
1504
|
+
Queued jobs, listeners, mail and notifications run in the background. `kreftjs dev` and `kreftjs serve` work through the `jobs` table every second. Control workers with `kreftjs queue:work`, `queue:failed`, `queue:retry all`, `queue:flush`. Choose the driver with `QUEUE_CONNECTION` (`database`, `redis`, `sync`).
|
|
1505
|
+
|
|
1506
|
+
### Uploads
|
|
1507
|
+
|
|
1508
|
+
`req.file('avatar').store('avatars')` saves with a random name and never trusts the original one. Only known plain-data extensions (images, PDF, text, office files, audio, video, archives) keep their extension; anything else is stored as `.bin`. Use `file.isImage()` to also check the file's real header, not only its name.
|
|
1509
|
+
|
|
1510
|
+
Files on the public disk are served from `/storage/…` as downloads of plain data, so an uploaded `.html` or `.svg` can never run scripts on your site.
|
|
1511
|
+
|
|
1512
|
+
### Scheduling
|
|
1513
|
+
|
|
1514
|
+
```js
|
|
1515
|
+
// backend/routes/console.js
|
|
1516
|
+
Schedule.call(async () => { await Post.onlyTrashed().forceDelete() }).daily().name('clean-trash')
|
|
1517
|
+
Schedule.job(new SendDailyReport()).weekdays().at('08:00')
|
|
1518
|
+
Schedule.command('send:report Daily').hourly()
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
`kreftjs dev` and `kreftjs serve` check the schedule every minute. Set `SCHEDULER=off` to run it from cron with `kreftjs schedule:run`; `kreftjs schedule:list` shows what is planned.
|
|
1522
|
+
|
|
1523
|
+
### Redis
|
|
1524
|
+
|
|
1525
|
+
Kreft includes its own Redis client (RESP2 over TCP or TLS) with no extra package. It works with Redis, Valkey and hosted Redis services.
|
|
1526
|
+
|
|
1527
|
+
```env
|
|
1528
|
+
CACHE_STORE=redis
|
|
1529
|
+
QUEUE_CONNECTION=redis
|
|
1530
|
+
REDIS_URL=rediss://default:secret@host:6379/0 # or REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB
|
|
1531
|
+
REDIS_PREFIX=kreft_
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
```bash
|
|
1535
|
+
kreftjs redis:ping # PONG (1ms) — 127.0.0.1:6379/0
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
Redis also gives you atomic locks (`Cache.lock('report', 30)`) and publish/subscribe.
|
|
1539
|
+
|
|
1540
|
+
## Configuration
|
|
1541
|
+
|
|
1542
|
+
Settings come from `.env`, `backend/config/*.js` and defaults, through one helper:
|
|
1543
|
+
|
|
1544
|
+
```js
|
|
1545
|
+
const { config } = require('kreftjs/server')
|
|
1546
|
+
|
|
1547
|
+
config('app.name', 'My app')
|
|
1548
|
+
config('database.default', 'sqlite')
|
|
1549
|
+
config.has('redis.connections.default')
|
|
1550
|
+
config.set('app.debug', false)
|
|
1551
|
+
```
|
|
1552
|
+
|
|
1553
|
+
Files in `backend/config/`: `app.js`, `auth.js`, `database.js`, `redis.js`, `session.js`. See [Environment variables](#environment-variables) for what `.env` controls.
|
|
1554
|
+
|
|
1555
|
+
### Sessions and inactivity
|
|
1556
|
+
|
|
1557
|
+
Two layers log out a user who walks away:
|
|
1558
|
+
|
|
1559
|
+
```env
|
|
1560
|
+
SESSION_LIFETIME=120 # minutes of inactivity before the server drops the session
|
|
1561
|
+
PUBLIC_SESSION_TIMEOUT=15 # minutes before the page itself sends the user to /login
|
|
1562
|
+
PUBLIC_SESSION_WARNING=2 # minutes of warning before that
|
|
1563
|
+
AUTH_TOKEN_DAYS=30 # a token unused for this many days stops working
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
`PUBLIC_` values are copied into the browser (`Kreftjs.env`); never put secrets in them.
|
|
1567
|
+
|
|
1568
|
+
### Behind a proxy
|
|
1569
|
+
|
|
1570
|
+
The visitor's address comes from the connection. Behind nginx, Cloudflare or a load balancer, list your proxies:
|
|
1571
|
+
|
|
1572
|
+
```env
|
|
1573
|
+
TRUSTED_PROXIES=10.0.0.1 # or *
|
|
1574
|
+
TRUSTED_PROXY_HOPS=1 # with *: how many proxies stand in front
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
`X-Forwarded-For` is read from the right, so the visitor cannot choose their own address.
|
|
1578
|
+
|
|
1579
|
+
## Testing
|
|
1580
|
+
|
|
1581
|
+
Backend code is tested with Node's own test runner and an in-memory SQLite database, so tests are fast and never touch your real data.
|
|
1582
|
+
|
|
1583
|
+
```json
|
|
1584
|
+
{
|
|
1585
|
+
"scripts": { "test": "node --test \"tests/**/*.test.js\"" }
|
|
1586
|
+
}
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
```js
|
|
1590
|
+
// tests/models/post.test.js
|
|
1591
|
+
const test = require('node:test')
|
|
1592
|
+
const assert = require('node:assert/strict')
|
|
1593
|
+
const { createSchema, setConnection } = require('kreftjs/server')
|
|
1594
|
+
const { connect } = require('kreft/bin/db.js')
|
|
1595
|
+
const Post = require('../../backend/app/Models/Post')
|
|
1596
|
+
|
|
1597
|
+
test.before(async () => {
|
|
1598
|
+
const db = await connect('sqlite::memory:') // a new empty database for this test file
|
|
1599
|
+
setConnection(db)
|
|
1600
|
+
await createSchema(db).create('posts', (table) => {
|
|
1601
|
+
table.id()
|
|
1602
|
+
table.string('title')
|
|
1603
|
+
table.timestamps()
|
|
1604
|
+
})
|
|
1605
|
+
})
|
|
1606
|
+
|
|
1607
|
+
test('a post can be created and found', async () => {
|
|
1608
|
+
const post = await Post.create({ title: 'Hello' })
|
|
1609
|
+
assert.equal((await Post.find(post.id)).title, 'Hello')
|
|
1610
|
+
})
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
To test a whole project's tables, run its real migrations instead of building the schema by hand: `migrator.migrate(db, 'backend/database/migrations')`.
|
|
1614
|
+
|
|
1615
|
+
Component pages for the browser live in `frontend/tests/` and are opened with `kreftjs dev`.
|
|
1616
|
+
|
|
1617
|
+
# Tooling
|
|
1618
|
+
|
|
1619
|
+
## Command line
|
|
1620
|
+
|
|
1621
|
+
Run `kreftjs list` to see every command your installed version has, and `kreftjs help` for the options of the main ones.
|
|
1622
|
+
|
|
1623
|
+
### The main commands
|
|
1624
|
+
|
|
1625
|
+
| Command | What it does |
|
|
1626
|
+
|---|---|
|
|
1627
|
+
| `kreftjs new my-app` | Makes a project (`--js`, `--ts`, `--db sqlite`, `--ui tailwind`, `--no-auth`). |
|
|
1628
|
+
| `kreftjs dev` | Development server with hot reload (`--port`, `--host`, `--db`, `--api`, `--server`, `--backend`, `--seed`, `--no-hot`, `--gc`). |
|
|
1629
|
+
| `kreftjs build` | Compiles the app into plain files in `dist/` (`--out`, `--base`, `--prerender`). |
|
|
1630
|
+
| `kreftjs serve [dist]` | Runs a built app for real users. Answers everyone, serves only the tables you list. |
|
|
1631
|
+
| `kreftjs update` | Updates npm packages and refreshes `kreftjs.js` (`--core`, `--npm`, `--latest`, `--check`). |
|
|
1632
|
+
| `kreftjs add <package>` | A safer `npm install`. See [Packages](#packages). |
|
|
1633
|
+
| `kreftjs about`, `kreftjs env`, `kreftjs log`, `kreftjs tinker` | Versions and folders, environment, logs, a console with your models (`await User.count()`). |
|
|
1634
|
+
| `kreftjs down --secret=x` / `kreftjs up` | Maintenance mode: visitors get `503`, and `/x` lets you in. |
|
|
1635
|
+
| `kreftjs vscode` | Installs colors, snippets and folding for `.kreftjs` files in VS Code, Cursor and VSCodium. |
|
|
1636
|
+
|
|
1637
|
+
### Generators and database
|
|
1638
|
+
|
|
1639
|
+
| Group | Commands |
|
|
1640
|
+
|---|---|
|
|
1641
|
+
| `make:` | `model`, `controller`, `page`, `routes`, `migration`, `seeder`, `factory`, `middleware`, `request`, `resource`, `policy`, `rule`, `job`, `event`, `listener`, `mail`, `notification`, `observer`, `command`, `lang`, `errors`, `admin`, `electron`, `vite` |
|
|
1642
|
+
| `migrate` | `migrate`, `:rollback`, `:fresh`, `:refresh`, `:reset`, `:status`, `:file`, `:undo` |
|
|
1643
|
+
| `db:` | `db:seed`, `db:wipe` |
|
|
1644
|
+
| `queue:` | `work`, `start`, `stop`, `pause`, `resume`, `restart`, `status`, `failed`, `retry`, `flush`, `clear`, `table` |
|
|
1645
|
+
| `schedule:` | `run`, `work`, `list` |
|
|
1646
|
+
| Others | `route:list`, `cache:clear`, `storage:link`, `key:generate`, `event:list`, `log:clear` |
|
|
1647
|
+
|
|
1648
|
+
`kreftjs dev --seed` seeds a brand-new database the first time. `kreftjs key:generate` writes a 256-bit `APP_KEY` into `.env` (`--show` prints it instead).
|
|
1649
|
+
|
|
1650
|
+
### Development server safety
|
|
1651
|
+
|
|
1652
|
+
- `kreftjs dev` answers only this computer (`127.0.0.1`) and only requests whose `Host` is a local name (`localhost`, `127.0.0.1`, `::1`, `*.localhost`). This stops DNS-rebinding attacks, where a web page tries to reach your dev server through its own domain name.
|
|
1653
|
+
- To reach it through a tunnel or another name, set `KREFT_ALLOWED_HOSTS=my-tunnel.example`. To share it on your network, use `--host 0.0.0.0`.
|
|
1654
|
+
- Requests that change data in `--api` or `--db` mode are refused when they come from another web site (a browser `Origin` that is not local).
|
|
1655
|
+
- **Moving or deleting the project folder.** The operating system cannot refuse a delete, so the server says so after it happens: if the project folder is deleted or moved to the Trash while `kreftjs dev` or `kreftjs serve` runs, it stops at once with a message. Any `kreftjs` command started from a deleted folder or one in the Trash refuses to run. Stop the server with <kbd>Ctrl</kbd>+<kbd>C</kbd> before you move or delete a project.
|
|
1656
|
+
|
|
1657
|
+
## Developer tools
|
|
1658
|
+
|
|
1659
|
+
Everything here comes with Kreft. Nothing else to install.
|
|
1660
|
+
|
|
1661
|
+
**Hot reload.** While `kreftjs dev` runs, save a `.kreftjs` file and only that component is swapped; its state stays. A `.css` file is reloaded without a page refresh, and an error shows an overlay with the file and line.
|
|
1662
|
+
|
|
1663
|
+
**Kreft DevTools.** The **S** button in the corner (or <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>) opens a panel:
|
|
1664
|
+
|
|
1665
|
+
| Tab | Shows |
|
|
1666
|
+
|---|---|
|
|
1667
|
+
| Components | The live tree. Pick one to see props, state and derived values, and change state right there. |
|
|
1668
|
+
| Stores | Every store's values, which you can change, and `$reset()`. |
|
|
1669
|
+
| Route | The current route and every route, with a button to go there. |
|
|
1670
|
+
| Timeline | Store actions and bus messages as they happen. |
|
|
1671
|
+
|
|
1672
|
+
The button and the panel only exist under `kreftjs dev`; built apps never include them.
|
|
1673
|
+
|
|
1674
|
+
**Types.** `kreftjs.d.ts` describes everything in `Kreft`, so TypeScript and your editor know it. `kreftjs new` copies it into new projects, and `kreftjs update --core` refreshes it.
|
|
1675
|
+
|
|
1676
|
+
### Error monitor
|
|
1677
|
+
|
|
1678
|
+
In development, an uncaught error on the server shows an interactive page instead of a bare message:
|
|
1679
|
+
|
|
1680
|
+
- A stack explorer that hides vendor frames and highlights yours.
|
|
1681
|
+
- A source preview around the failing line.
|
|
1682
|
+
- A suggestion engine for common mistakes (a method typo on a model, reading a property of `undefined`, a missing table that needs `kreftjs migrate`).
|
|
1683
|
+
- The request: method, URL, route and query values, body, headers and client address. Passwords, tokens, cookies and CSRF values are masked.
|
|
1684
|
+
- Copy the error as Markdown for an issue, or as JSON.
|
|
1685
|
+
|
|
1686
|
+
The page is only shown when `APP_ENV` is not `production` and `APP_DEBUG` is true. **In production the same error answers a plain `Server error.`** with no stack or file names.
|
|
1687
|
+
|
|
1688
|
+
Frontend errors use the same design through `errors/ErrorMonitor.kreftjs`; pass it a caught exception:
|
|
1689
|
+
|
|
1690
|
+
```html
|
|
1691
|
+
import ErrorMonitor from 'errors/ErrorMonitor.kreftjs'
|
|
1692
|
+
state caught = null
|
|
1693
|
+
|
|
1694
|
+
<view>
|
|
1695
|
+
<error-monitor k-if="caught" error={caught} status={500} title={caught.name} message={caught.message}></error-monitor>
|
|
1696
|
+
</view>
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
## Build and deploy
|
|
1700
|
+
|
|
1701
|
+
`kreftjs build` compiles your app ahead of time:
|
|
1702
|
+
|
|
1703
|
+
```bash
|
|
1704
|
+
npm run build # or: kreftjs build
|
|
1705
|
+
npm run preview # check the result: kreftjs dev dist
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
| File | What it is |
|
|
1709
|
+
|---|---|
|
|
1710
|
+
| `index.html` | Your page, now loading the files below. |
|
|
1711
|
+
| `kreftjs.js` | The framework. |
|
|
1712
|
+
| `app.js` | Every `.kreftjs` file, already compiled. |
|
|
1713
|
+
| `app.css` | All `<style>` blocks; scoped ones already scoped. |
|
|
1714
|
+
| `main.js` | The inline `<script>` code from `index.html`. |
|
|
1715
|
+
| `deps/`, `vendor/` | The npm packages and CSS your `.kreftjs` files import, minified. |
|
|
1716
|
+
|
|
1717
|
+
What you get:
|
|
1718
|
+
|
|
1719
|
+
- **A faster first load.** The browser downloads one `app.js` and compiles nothing.
|
|
1720
|
+
- **A strict Content-Security-Policy works.** Every expression becomes a real function, so the built app never uses `eval` or `new Function`.
|
|
1721
|
+
- **Errors before you deploy.** A mistake in any `.kreftjs` file stops the build and names the file and line.
|
|
1722
|
+
|
|
1723
|
+
Options: `kreftjs build ./site --out public` builds another folder, and `--base /` changes `<base href>`. Upload the output folder to any static host.
|
|
1724
|
+
|
|
1725
|
+
### Ready HTML for every page: `--prerender`
|
|
1726
|
+
|
|
1727
|
+
```bash
|
|
1728
|
+
npm i -D jsdom # once per project
|
|
1729
|
+
kreftjs build --prerender # every route without :params
|
|
1730
|
+
kreftjs build --prerender /users/1,/users/2 # and these addresses too
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
Each page is opened in a simulated browser, its data loaded, and the result saved as `dist/<page>/index.html`, so visitors and search engines get finished HTML.
|
|
1734
|
+
|
|
1735
|
+
The tags a page sets with `.seo({...})` or `meta` (description, canonical, Open Graph, Twitter, `hreflang`, JSON-LD, `lang` and `dir`) are saved in the page's HTML too, so link previews and crawlers that don't run JavaScript see them. A canonical written in `index.html` is kept for the home page only. A page where the app shows its error page is skipped.
|
|
1736
|
+
|
|
1737
|
+
Pages that come from your data (`/posts/:slug`) need their addresses: `kreftjs build --prerender --urls urls.js`. The file is `urls.json` (a list), `urls.txt` (one address per line), or `urls.js` (a list, or an async function that returns it).
|
|
1738
|
+
|
|
1739
|
+
### SEO for pages from your database: `backend/routes/seo.js`
|
|
1740
|
+
|
|
1741
|
+
A page whose content comes from a table can't be known at build time. List what the server should put in `<head>` for an address:
|
|
1742
|
+
|
|
1743
|
+
```js
|
|
1744
|
+
// backend/routes/seo.js
|
|
1745
|
+
const Post = require('../app/Models/Post')
|
|
1746
|
+
|
|
1747
|
+
module.exports = {
|
|
1748
|
+
'/': { title: 'My shop', description: 'Everything for the home' },
|
|
1749
|
+
'/posts/:slug': async ({ slug }) => {
|
|
1750
|
+
const post = await Post.where('slug', slug).first()
|
|
1751
|
+
if (!post) return null // a real 404, with noindex
|
|
1752
|
+
return {
|
|
1753
|
+
title: post.title,
|
|
1754
|
+
description: post.excerpt,
|
|
1755
|
+
og: { image: post.image },
|
|
1756
|
+
schema: { '@type': 'Article', headline: post.title },
|
|
1757
|
+
alternates: { en: `https://example.com/posts/${slug}`, ar: `https://example.com/ar/posts/${slug}` },
|
|
1758
|
+
}
|
|
1759
|
+
},
|
|
1760
|
+
// the addresses of your data pages, for `kreftjs sitemap`: '/path' or { path, lastmod }
|
|
1761
|
+
urls: async () => (await Post.all()).map((p) => ({ path: '/posts/' + p.slug, lastmod: p.updated_at })),
|
|
1762
|
+
}
|
|
1763
|
+
```
|
|
1764
|
+
|
|
1765
|
+
`kreftjs dev` and `kreftjs serve` answer a page request with `index.html` plus the tags of the first matching address (`:param` and a trailing `/*` work). Set `APP_URL` and each page also gets its `canonical`. An address that isn't listed is the normal page. The browser keeps these tags until the first navigation, then the router's own tags take over.
|
|
1766
|
+
|
|
1767
|
+
### Whole pages for crawlers
|
|
1768
|
+
|
|
1769
|
+
A crawler or link preview (Googlebot, Bingbot, WhatsApp, Facebook, X, Slack, Telegram …) that doesn't run JavaScript would see an empty `<div id="app">`. For these visitors `kreftjs dev` and `kreftjs serve` open the page in the same simulated browser as `--prerender`, wait until its data has loaded, and answer with the page already drawn. If the app shows its `errors/404.kreftjs` page, the answer has status 404. A page that `kreftjs build --prerender` already drew is sent as it is. Everyone else gets the normal page.
|
|
1770
|
+
|
|
1771
|
+
```bash
|
|
1772
|
+
npm i -D jsdom # once per project
|
|
1773
|
+
KREFT_SSR=bots # default: crawlers and link previews only
|
|
1774
|
+
KREFT_SSR=all # every visitor
|
|
1775
|
+
KREFT_SSR=off # never
|
|
1776
|
+
KREFT_SSR_TTL=300 # seconds a drawn page is kept (kreftjs dev never keeps them)
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
At most two pages are drawn at a time; the other crawlers wait for their turn (up to 15 seconds, 20 at once), and the same address asked twice is drawn once. A page that takes longer than 15 seconds, fails, or is not a page of the app is not tried again for 30 seconds, and the normal page is sent meanwhile, as it is when jsdom is missing. The query string is part of the address (`/search?q=sofa` is its own page). Images, styles and fonts from other sites are not downloaded while drawing, scripts are. Responses carry `Vary: User-Agent` and the same security headers as every other page. The simulated browser introduces itself with a secret `User-Agent` that changes on every start, so it is never drawn again.
|
|
1780
|
+
|
|
1781
|
+
### Sitemap and robots.txt: `kreftjs sitemap`
|
|
1782
|
+
|
|
1783
|
+
```bash
|
|
1784
|
+
kreftjs sitemap --url https://example.com # public/sitemap.xml and public/robots.txt
|
|
1785
|
+
kreftjs sitemap --url https://example.com --urls urls.js # and the addresses in that file
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
It lists the routes of `Kreftjs.router({ routes })` in `index.html` and `routes/web.kreftjs`, skipping redirects and patterns like `/posts/:id`, plus the `urls` of `backend/routes/seo.js` and `--urls`. A `lastmod` is written only when you give one (a database date such as `2026-01-02 10:00:00` becomes `2026-01-02`). A route can list `alternates: { en: '/about', ar: '/ar/about' }` for `xhtml:link` entries.
|
|
1789
|
+
|
|
1790
|
+
### Deploying the backend
|
|
1791
|
+
|
|
1792
|
+
```bash
|
|
1793
|
+
NODE_ENV=production kreftjs serve dist
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
Before you do, work through the [Production checklist](#production-checklist).
|
|
1797
|
+
|
|
1798
|
+
## TypeScript
|
|
1799
|
+
|
|
1800
|
+
TypeScript works in the browser (`.kreftjs` files and `.ts` modules) and on the server (`routes/api.ts`, `app/**/*.ts`, migrations, seeders).
|
|
1801
|
+
|
|
1802
|
+
```bash
|
|
1803
|
+
kreftjs new my-app --ts
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
Inside a `.kreftjs` file you can write `interface`, `type`, `enum`, `import type`, and typed `prop`, `state`, `derive` and `saved`. `kreftjs make:*` writes `.ts` files when the project has a `tsconfig.json` (or with `--ts`; `--js` forces JavaScript).
|
|
1807
|
+
|
|
1808
|
+
Execution needs no setup: Kreft uses `esbuild` if installed, otherwise Node's built-in type stripping (Node 22.6+), otherwise its own zero-dependency stripper.
|
|
1809
|
+
|
|
1810
|
+
> [!NOTE]
|
|
1811
|
+
> Kreft removes types; it does not check them. Run `tsc --noEmit` in your CI for type checking.
|
|
1812
|
+
|
|
1813
|
+
## Vite and Electron
|
|
1814
|
+
|
|
1815
|
+
**Vite.** If your team prefers the Vite ecosystem, Kreft has a plugin that compiles `.kreftjs` files on the fly:
|
|
1816
|
+
|
|
1817
|
+
```bash
|
|
1818
|
+
kreftjs vite:init # writes vite.config.js with the Kreft plugin
|
|
1819
|
+
kreftjs vite:dev # Vite dev server with the backend API proxied
|
|
1820
|
+
kreftjs vite:build # optimised bundle with code splitting
|
|
1821
|
+
kreftjs vite:preview
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
**Electron.** Turn the same app into a desktop application. See [Desktop apps with Electron](#desktop-apps-with-electron) below.
|
|
1825
|
+
|
|
1826
|
+
## Desktop apps with Electron
|
|
1827
|
+
|
|
1828
|
+
Kreft can package your app as a desktop application for Windows, macOS and Linux. The pages are the same `.kreftjs` files you already have; Electron only supplies the window.
|
|
1829
|
+
|
|
1830
|
+
### Quick start
|
|
1831
|
+
|
|
1832
|
+
```bash
|
|
1833
|
+
kreftjs new my-app --electron # a new app with desktop support
|
|
1834
|
+
cd my-app && npm install
|
|
1835
|
+
kreftjs electron:dev # dev server plus a desktop window, with hot reload
|
|
1836
|
+
kreftjs electron:build # an installer in dist-electron/
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
Add desktop support to an existing app instead:
|
|
1840
|
+
|
|
1841
|
+
```bash
|
|
1842
|
+
kreftjs electron:init # alias: kreftjs make:electron
|
|
1843
|
+
npm install # installs electron and electron-builder
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
### Commands
|
|
1847
|
+
|
|
1848
|
+
| Command | What it does |
|
|
1849
|
+
|---|---|
|
|
1850
|
+
| `kreftjs electron:init` | Writes `electron/main.js` and `electron/preload.js` and adds the scripts, the `electron` and `electron-builder` dev dependencies and a `build` block to `package.json`. Files that already exist are never overwritten. It also removes the Google Fonts links from `index.html`, so the app starts without a network. |
|
|
1851
|
+
| `kreftjs electron:dev` | Starts `kreftjs dev` and opens a window on it. Closing the window stops the server. Flags after the command go to the dev server. |
|
|
1852
|
+
| `kreftjs electron:build` | Runs `kreftjs build --base ./`, then `electron-builder`. |
|
|
1853
|
+
|
|
1854
|
+
`kreftjs electron` is the same as `kreftjs electron:dev`. If the `electron/` folder is missing, both commands create it first.
|
|
1855
|
+
|
|
1856
|
+
### Project layout
|
|
1857
|
+
|
|
1858
|
+
```text
|
|
1859
|
+
my-app/
|
|
1860
|
+
├── electron/
|
|
1861
|
+
│ ├── main.js the main process: creates the window, handles the desktop functions
|
|
1862
|
+
│ └── preload.js the safe bridge exposed to your pages as window.kreftDesktop
|
|
1863
|
+
├── dist/ the static build the packaged app loads
|
|
1864
|
+
├── dist-electron/ installers written by electron-builder
|
|
1865
|
+
└── package.json "main": "electron/main.js" and the "build" block
|
|
1866
|
+
```
|
|
1867
|
+
|
|
1868
|
+
### How it runs
|
|
1869
|
+
|
|
1870
|
+
- **Development.** The window loads `http://localhost:8000` (change it with `PORT`). Hot reload, the DevTools button and the error overlay work as in the browser.
|
|
1871
|
+
- **Packaged.** The window loads `dist/index.html` from disk. That is a static build, so the Kreft backend (`routes/`, the database, `kreftjs serve`) is **not** inside the app. Point the API client at a server you host:
|
|
1872
|
+
|
|
1873
|
+
```js
|
|
1874
|
+
const api = Kreftjs.api.create({ base: 'https://api.example.com' })
|
|
1875
|
+
```
|
|
1876
|
+
|
|
1877
|
+
Use a `PUBLIC_` value from `.env` for the address, so development and production can differ. Remember that CORS on your server must allow the app.
|
|
1878
|
+
|
|
1879
|
+
### Detecting the desktop app
|
|
1880
|
+
|
|
1881
|
+
```js
|
|
1882
|
+
if (Kreftjs.isElectron) {
|
|
1883
|
+
const info = await Kreftjs.desktop.getInfo() // { name, version, platform, arch, isPackaged }
|
|
1884
|
+
}
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
`Kreftjs.desktop` is `null` in a normal browser, so the same page works in both.
|
|
1888
|
+
|
|
1889
|
+
### The desktop bridge
|
|
1890
|
+
|
|
1891
|
+
`window.kreftDesktop` (also `Kreftjs.desktop`) is the only way your pages reach the system. Nothing else from Node.js or Electron is exposed.
|
|
1892
|
+
|
|
1893
|
+
| Function | Result |
|
|
1894
|
+
|---|---|
|
|
1895
|
+
| `getInfo()` | `{ name, version, platform, arch, isPackaged }` |
|
|
1896
|
+
| `minimize()`, `maximize()`, `close()` | Control the window. `maximize()` toggles. |
|
|
1897
|
+
| `openDevTools(mode)`, `closeDevTools()`, `toggleDevTools()` | DevTools; `mode` is `right`, `bottom`, `left` or `detach`. |
|
|
1898
|
+
| `showMessageBox(options)` | A native message dialog. |
|
|
1899
|
+
| `showOpenDialog(options)` | A native "open file" dialog. |
|
|
1900
|
+
| `showSaveDialog(options)` | A native "save file" dialog. |
|
|
1901
|
+
|
|
1902
|
+
Every call returns a promise. The dialog functions pass on only a fixed list of options (for example `title`, `filters`, `properties`); anything else is dropped.
|
|
1903
|
+
|
|
1904
|
+
```kreftjs
|
|
1905
|
+
state path = ''
|
|
1906
|
+
|
|
1907
|
+
async function pick() {
|
|
1908
|
+
const r = await Kreftjs.desktop.showOpenDialog({
|
|
1909
|
+
title: 'Choose a file',
|
|
1910
|
+
filters: [{ name: 'Images', extensions: ['png', 'jpg'] }],
|
|
1911
|
+
properties: ['openFile'],
|
|
1912
|
+
})
|
|
1913
|
+
if (!r.canceled) path = r.filePaths[0]
|
|
1914
|
+
}
|
|
1915
|
+
```
|
|
1916
|
+
|
|
1917
|
+
### Adding your own desktop function
|
|
1918
|
+
|
|
1919
|
+
1. In `electron/main.js`, add a handler: `ipcMain.handle('app:ping', () => 'pong')`.
|
|
1920
|
+
2. In `electron/preload.js`, add it to the `desktop` object: `ping: () => ipcRenderer.invoke('app:ping')`.
|
|
1921
|
+
3. Call `Kreftjs.desktop.ping()` from a page.
|
|
1922
|
+
|
|
1923
|
+
Expose one narrow function per task. Never pass a path or a command from a page straight to the file system or a shell without checking it in `main.js`.
|
|
1924
|
+
|
|
1925
|
+
### DevTools
|
|
1926
|
+
|
|
1927
|
+
They are hidden by default. Open them with a flag or an environment variable:
|
|
1928
|
+
|
|
1929
|
+
```bash
|
|
1930
|
+
kreftjs electron:dev --devtools # right side
|
|
1931
|
+
kreftjs electron:dev --devtools=bottom # bottom, left or detach
|
|
1932
|
+
kreftjs electron:dev --no-devtools
|
|
1933
|
+
ELECTRON_DEVTOOLS=bottom kreftjs electron:dev
|
|
1934
|
+
```
|
|
1935
|
+
|
|
1936
|
+
In development, F12 or Ctrl+Shift+I toggles them.
|
|
1937
|
+
|
|
1938
|
+
### Packaging and installers
|
|
1939
|
+
|
|
1940
|
+
`electron:build` writes to `dist-electron/`. The defaults in the `build` block of `package.json` are:
|
|
1941
|
+
|
|
1942
|
+
| System | Targets |
|
|
1943
|
+
|---|---|
|
|
1944
|
+
| Windows | `nsis` installer, `portable` |
|
|
1945
|
+
| macOS | `dmg`, `zip` |
|
|
1946
|
+
| Linux | `AppImage`, `deb` |
|
|
1947
|
+
|
|
1948
|
+
Edit `appId`, `productName` and the targets there. Add an `icon` per system (see the electron-builder documentation). Build for each system on that system: a macOS installer needs a Mac. Code signing and notarisation (macOS) or a signing certificate (Windows) are set up through electron-builder and are needed before you give the app to other people.
|
|
1949
|
+
|
|
1950
|
+
### Security
|
|
1951
|
+
|
|
1952
|
+
The window follows Electron's security guidance:
|
|
1953
|
+
|
|
1954
|
+
- `contextIsolation` and `sandbox` are on; `nodeIntegration` is off.
|
|
1955
|
+
- Links to other sites open in the system browser, and new windows are denied.
|
|
1956
|
+
- Navigation away from the app is blocked: only the dev server (development) or files inside `dist/` (packaged) can load.
|
|
1957
|
+
- The bridge exposes named functions only, with the option lists described above.
|
|
1958
|
+
|
|
1959
|
+
Do not turn these off to make something work. Add a bridge function instead.
|
|
1960
|
+
|
|
1961
|
+
### Troubleshooting
|
|
1962
|
+
|
|
1963
|
+
| Problem | Fix |
|
|
1964
|
+
|---|---|
|
|
1965
|
+
| `Electron is not installed locally` | `kreftjs add -D electron` |
|
|
1966
|
+
| `electron-builder is not installed` | `kreftjs add -D electron-builder` |
|
|
1967
|
+
| A white window in the packaged app | Run `electron:build` again; the build must use `--base ./`. Check that `dist/index.html` exists. |
|
|
1968
|
+
| API calls fail in the packaged app | The backend is not bundled. Set an absolute `base` for the API client (see above). |
|
|
1969
|
+
| The window opens before the server is ready | It waits up to 15 seconds. Check the terminal for a dev server error. |
|
|
1970
|
+
| Port 8000 is taken | Run with `PORT=3000 kreftjs electron:dev`. |
|
|
1971
|
+
|
|
1972
|
+
## Packages
|
|
1973
|
+
|
|
1974
|
+
`kreftjs add` is a safer `npm install`:
|
|
1975
|
+
|
|
1976
|
+
```bash
|
|
1977
|
+
kreftjs add pg mysql2 # install packages
|
|
1978
|
+
kreftjs add -D vitest # a development dependency
|
|
1979
|
+
kreftjs add --dry-run some-package # inspect without installing
|
|
1980
|
+
kreftjs add --allow-scripts better-sqlite3 # allow build scripts for a trusted native package
|
|
1981
|
+
kreftjs add --force dotenv # ignore a warning
|
|
1982
|
+
```
|
|
1983
|
+
|
|
1984
|
+
What it checks before and after installing:
|
|
1985
|
+
|
|
1986
|
+
1. **Redundancy.** It warns when Kreft already does the job natively (`dotenv`, `cors`, `body-parser`, `node-fetch`…).
|
|
1987
|
+
2. **Install scripts.** Packages install with `--ignore-scripts`, so a `postinstall` hook cannot run code, unless you pass `--allow-scripts` or the package is on the short list of known native-build packages.
|
|
1988
|
+
3. **Typosquatting.** Names close to popular ones (`axois`, `lodsh`, `crossenv`) stop the install.
|
|
1989
|
+
4. **Look-alike characters.** Names with non-ASCII or homoglyph characters are refused, as are shell metacharacters.
|
|
1990
|
+
5. **Registry age.** A package published in the last 14 days is flagged.
|
|
1991
|
+
6. **Exact versions.** `--save-exact` is the default (`--no-exact` to turn it off).
|
|
1992
|
+
7. **Audit.** `npm audit` runs afterwards and reports findings.
|
|
1993
|
+
|
|
1994
|
+
## MCP for AI agents
|
|
1995
|
+
|
|
1996
|
+
Kreft includes a Model Context Protocol server, so an AI coding agent can see your project's routes and schema, and query the database read-only.
|
|
1997
|
+
|
|
1998
|
+
| Tool or resource | What it gives |
|
|
1999
|
+
|---|---|
|
|
2000
|
+
| `query_database` | Runs one read-only `SELECT`. |
|
|
2001
|
+
| `list_routes` | Every route with methods, name and middleware. |
|
|
2002
|
+
| `server_health` | Uptime and memory. |
|
|
2003
|
+
| `kreft://routes`, `kreft://schema` | The route catalogue; the tables, columns and keys. |
|
|
2004
|
+
|
|
2005
|
+
### Local use (stdio)
|
|
2006
|
+
|
|
2007
|
+
```bash
|
|
2008
|
+
kreftjs mcp
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
```json
|
|
2012
|
+
{
|
|
2013
|
+
"mcpServers": {
|
|
2014
|
+
"kreft": { "command": "kreft", "args": ["mcp"], "cwd": "/path/to/your-project" }
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
```
|
|
2018
|
+
|
|
2019
|
+
### Over HTTP
|
|
2020
|
+
|
|
2021
|
+
`kreftjs dev` and `kreftjs serve` also answer at `/mcp`. Set a token and send it:
|
|
2022
|
+
|
|
2023
|
+
```env
|
|
2024
|
+
MCP_TOKEN=a-long-random-secret
|
|
2025
|
+
```
|
|
2026
|
+
|
|
2027
|
+
```bash
|
|
2028
|
+
curl -H "Authorization: Bearer a-long-random-secret" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' http://localhost:8000/mcp
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2031
|
+
### What protects it
|
|
2032
|
+
|
|
2033
|
+
- **`MCP_TOKEN`** is required whenever it is set. The `APP_KEY` is never accepted in its place.
|
|
2034
|
+
- **In production** (`NODE_ENV` or `APP_ENV` = `production`), the endpoint is off (`403`) until `MCP_TOKEN` is set.
|
|
2035
|
+
- **In development without a token**, only a direct local client is accepted: a loopback address, a `localhost` Host header, and no proxy headers (`X-Forwarded-*`). A remote address, a proxy or a tunnel needs `MCP_TOKEN`.
|
|
2036
|
+
- **Browsers** from foreign origins are refused. `MCP_ALLOWED_ORIGIN` adds one origin.
|
|
2037
|
+
- **`query_database` is limited**: one `SELECT`, no comments or `;`, no mutations or file functions, no `SELECT … INTO`, and no sleeping or locking. It refuses columns and tables that hold credentials (anything named like a password, token, secret, hash or authentication, and the tokens, sessions and jobs tables), whole-row JSON building, and the database server's own accounts and sessions (`mysql.*`, `performance_schema`, `sys`, `pg_authid`…). Results are masked again by shape (password hashes). The query runs in a transaction that is always rolled back, with a time limit of 5 seconds (`MCP_QUERY_TIMEOUT_MS`).
|
|
2038
|
+
|
|
2039
|
+
> [!WARNING]
|
|
2040
|
+
> The MCP database tool can still read every other table. Use a database account with read-only access and no access to credentials, and keep the token out of version control.
|
|
2041
|
+
|
|
2042
|
+
## Memory watchdog
|
|
2043
|
+
|
|
2044
|
+
Long-running servers can slowly collect memory. Start with `--gc` (or `AUTO_GC=1`) and Kreft samples the heap every 30 seconds (`AUTO_GC_INTERVAL_MS`). When it passes `AUTO_GC_THRESHOLD_MB`, Kreft clears its own caches and runs garbage collection. This is a safety net; find the leak if you see it often.
|
|
2045
|
+
|
|
2046
|
+
# Security
|
|
2047
|
+
|
|
2048
|
+
## Security overview
|
|
2049
|
+
|
|
2050
|
+
Kreft is built so the safe thing is the default. This chapter lists what is protected, where, and what stays your job.
|
|
2051
|
+
|
|
2052
|
+
### In the browser
|
|
2053
|
+
|
|
2054
|
+
- **Escaped views.** Everything in `{ }`, `k-text` and attributes is shown as text. `<script>` in a user's bio is only text.
|
|
2055
|
+
- **`k-html` is cleaned**, not trusted. Kreft parses the HTML the way the browser does, in a document that runs nothing, then removes scripts, frames, forms, SVG and MathML, every `on…` attribute, `srcdoc`, and addresses that start with `javascript:` or an unsafe `data:`. Even so, bind only HTML you trust or that the server already cleaned.
|
|
2056
|
+
- **A strict Content-Security-Policy works** with built apps. `kreftjs build` turns every expression into a function ahead of time, so there is no `eval` and no `new Function`. Without a build, views compile in the browser and a strict CSP blocks that.
|
|
2057
|
+
- **Prototype pollution is blocked.** Paths and keys such as `__proto__`, `constructor` and `prototype` are ignored in views, translations and JSON bodies.
|
|
2058
|
+
- **A login cookie is HttpOnly**, so a script on your page cannot read it.
|
|
2059
|
+
|
|
2060
|
+
### On the server
|
|
2061
|
+
|
|
2062
|
+
#### CSRF
|
|
2063
|
+
|
|
2064
|
+
Kreft uses the double-submit cookie pattern. Any safe request gets an `XSRF-TOKEN` cookie; any request that changes data (POST, PUT, PATCH, DELETE) from a browser must send the same value back in `X-XSRF-TOKEN` (or `X-CSRF-TOKEN`, a `_token` field). The comparison is constant-time, and a wrong or missing token answers `419`.
|
|
2065
|
+
|
|
2066
|
+
- `Kreftjs.api` sends the header by itself. HTML forms add `<input type="hidden" name="_token" value="{csrfToken}">`.
|
|
2067
|
+
- `Route.csrfCookie('/csrf-cookie')` gives a single-page app an endpoint to get the cookie first.
|
|
2068
|
+
- Exempt only what must be (webhooks): `.withoutMiddleware('csrf')`.
|
|
2069
|
+
- Requests that carry only an API bearer token and no cookie are not browser sessions and do not need CSRF.
|
|
2070
|
+
|
|
2071
|
+
#### HTML on the server
|
|
2072
|
+
|
|
2073
|
+
Anything built into HTML on the server should go through the helpers in `kreftjs/server`:
|
|
2074
|
+
|
|
2075
|
+
```js
|
|
2076
|
+
const { escapeHtml, cleanHtml, SafeHtml, html } = require('kreftjs/server')
|
|
2077
|
+
|
|
2078
|
+
escapeHtml(userText) // turn text into safe text
|
|
2079
|
+
cleanHtml(userHtml) // keep a small set of tags and attributes, drop the rest
|
|
2080
|
+
```
|
|
2081
|
+
|
|
2082
|
+
`cleanHtml` keeps an allow-list of tags and attributes, checks every address, and filters `style`. A tag it cannot read is turned into text (`<`), never passed through.
|
|
2083
|
+
|
|
2084
|
+
#### Passwords, keys and tokens
|
|
2085
|
+
|
|
2086
|
+
| Tool | What it does |
|
|
2087
|
+
|---|---|
|
|
2088
|
+
| `Hash.make` / `Hash.check` | Passwords with scrypt, a new salt each time, constant-time compare. |
|
|
2089
|
+
| `Crypt.encryptString` / `Crypt.decryptString` | AES-256-GCM with a fresh IV, keyed by `APP_KEY`. Tampering is detected. |
|
|
2090
|
+
| `Url.signed` / `Url.check` | Links that work only unchanged (and until a time). |
|
|
2091
|
+
| API tokens, reset tokens | Stored as a SHA-256 hash; the plain value is shown once. |
|
|
2092
|
+
|
|
2093
|
+
> [!WARNING]
|
|
2094
|
+
> `APP_KEY` encrypts sessions and signs links. Changing it signs everyone out and breaks old signed links. Keep it out of version control and out of logs.
|
|
2095
|
+
|
|
2096
|
+
#### Limits and lockouts
|
|
2097
|
+
|
|
2098
|
+
- `throttle:60,1` limits requests per visitor and route. The sign-in routes allow 6 a minute.
|
|
2099
|
+
- Accounts lock after repeated wrong passwords or two-factor codes, whatever the address. See [Authentication](#authentication).
|
|
2100
|
+
- Request bodies are limited (`MAX_BODY_KB`), and JSON bodies cannot carry `__proto__` or `constructor` keys.
|
|
2101
|
+
- Every API answer carries `X-Content-Type-Options`, `X-Frame-Options` and `Referrer-Policy`; in production also `Strict-Transport-Security`.
|
|
2102
|
+
|
|
2103
|
+
#### Plain HTTP
|
|
2104
|
+
|
|
2105
|
+
In production a request with `X-Forwarded-Proto: http` is redirected to `https`, and one with `X-Forwarded-Proto: https` is served when it comes from this machine or from a proxy listed in `TRUSTED_PROXIES` (so a proxy that ends TLS works with `FORCE_HTTPS=true` or an `https://` `APP_URL`; anyone else could write that header, so from other addresses it is ignored, and the server says so once in its log). Without that header, `FORCE_HTTPS=true` or an `https://` `APP_URL` redirects every plain request. The address in the redirect comes from `APP_URL` when set, otherwise from the `Host` header only if it looks like a host name; it cannot be used to send visitors to another site.
|
|
2106
|
+
|
|
2107
|
+
#### API tokens
|
|
2108
|
+
|
|
2109
|
+
A token made in the profile page has a list of permissions (`read`, `create`, `update`, `delete`), and the middleware enforces them from the HTTP method on every route. A read-only token cannot delete anything, even if its owner is an admin.
|
|
2110
|
+
|
|
2111
|
+
#### Uploads
|
|
2112
|
+
|
|
2113
|
+
Files get random names. Unknown extensions are stored as `.bin`. Public files are served as plain data with a sandbox policy, so they cannot run scripts. Use `isImage()` to check the real file header.
|
|
2114
|
+
|
|
2115
|
+
#### The database
|
|
2116
|
+
|
|
2117
|
+
- Values travel as parameters. Table and column names are quoted.
|
|
2118
|
+
- The automatic table API (`--db`) is a development tool with no login. `kreftjs serve` only serves the tables you list in `kreftjs.config.json`, with `write: false` and `hidden: [...]` where needed; hidden columns cannot be read or used to sort.
|
|
2119
|
+
- Raw SQL helpers (`whereRaw`, `selectRaw`, `orderByRaw`, `DB.raw`) are for your own SQL; pass user values in their parameter array.
|
|
2120
|
+
|
|
2121
|
+
#### Packages
|
|
2122
|
+
|
|
2123
|
+
`kreftjs add` blocks install scripts, typosquats and look-alike names, flags very new packages, and pins exact versions. See [Packages](#packages).
|
|
2124
|
+
|
|
2125
|
+
### What stays your job
|
|
2126
|
+
|
|
2127
|
+
- **Check access on the server.** Client route guards decide what to show, not who may read data. Every route that returns private data needs `auth` and, where it matters, `admin` or a policy.
|
|
2128
|
+
- **Never trust HTML you did not clean**, and never put secrets in `PUBLIC_` variables or localStorage.
|
|
2129
|
+
- **Keep dependencies and Node.js up to date** (`kreftjs update`, `npm audit`).
|
|
2130
|
+
- **Protect the machine.** Kreft cannot protect a database, a server or a `.env` file that others can read.
|
|
2131
|
+
|
|
2132
|
+
## Production checklist
|
|
2133
|
+
|
|
2134
|
+
Go through this before your first real user:
|
|
2135
|
+
|
|
2136
|
+
| Check | Why |
|
|
2137
|
+
|---|---|
|
|
2138
|
+
| `NODE_ENV=production` and `APP_ENV=production` | Turns off debug pages, enables HSTS and the https redirect, and makes security errors fatal. |
|
|
2139
|
+
| `APP_DEBUG=false` | Never show stack traces or source code to visitors. |
|
|
2140
|
+
| `APP_KEY` set to a 32-byte key (`kreftjs key:generate`) | In production the app refuses to start without a key of at least 16 bytes. A random per-process key would also break sessions on restart. |
|
|
2141
|
+
| `APP_URL=https://your.site` | Used for links in emails, the https redirect and secure cookies. |
|
|
2142
|
+
| `TRUSTED_PROXIES` set if you use a proxy | Otherwise every visitor shares the proxy's address and rate limits. |
|
|
2143
|
+
| A real `DATABASE_URL` and a backup plan | SQLite is fine for small apps; use MySQL or PostgreSQL for several servers. |
|
|
2144
|
+
| `CACHE_STORE=redis` or `database`, `QUEUE_CONNECTION=redis` or `database` | `memory` is per process. Account lockouts live in the cache, so they are shared between servers only with `database` or `redis`. |
|
|
2145
|
+
| `MAIL_URL` set | Otherwise emails are only printed in the terminal. |
|
|
2146
|
+
| `MCP_TOKEN` set, or MCP left off | The MCP endpoint is disabled in production without it. |
|
|
2147
|
+
| Tables listed in `kreftjs.config.json`, or no `--db` at all | `kreftjs serve` serves only the tables you list. |
|
|
2148
|
+
| No test user | Do not run `--seed` against production; delete `test@example.com` if it exists. |
|
|
2149
|
+
| HTTPS in front of the app | Cookies are `Secure` in production and on `https` URLs. |
|
|
2150
|
+
| `npm audit` and `kreftjs update` | Fix known vulnerabilities before release. |
|
|
2151
|
+
|
|
2152
|
+
> [!NOTE]
|
|
2153
|
+
> Kreft does not replace a security review. For regulated or high-risk systems, have the app, its infrastructure and its dependencies reviewed by someone who does that professionally.
|
|
2154
|
+
|
|
2155
|
+
## Known limits
|
|
2156
|
+
|
|
2157
|
+
- Without a build, views are compiled in the browser with `new Function`, which a strict Content-Security-Policy blocks. Use `kreftjs build` for production.
|
|
2158
|
+
- Inside a view, `<` directly followed by a letter starts a tag, so write `{a < b}` with a space.
|
|
2159
|
+
- The memory of which two-factor codes were already used, and login tickets, is kept per process. Behind several servers, a code can be replayed once on each. The per-account lockout (kept in the cache) still limits guessing.
|
|
2160
|
+
- The sanitizer removes unsafe markup; it is not a substitute for choosing carefully what HTML you show.
|
|
2161
|
+
- `--prerender` makes static HTML at build time. On a running server whole pages are drawn only for crawlers and link previews; every other visitor gets the page drawn by the browser, and an address the app does not have answers 200 (the app shows its own 404 page) unless `seo.js` says otherwise.
|
|
2162
|
+
- `.kreftjs` files must be served over `http://` or `https://`, not opened from `file://`.
|
|
2163
|
+
|
|
2164
|
+
# Reference
|
|
2165
|
+
|
|
2166
|
+
## Environment variables
|
|
2167
|
+
|
|
2168
|
+
Set these in `.env` (never commit it). Variables that start with `PUBLIC_` are copied into the browser as `Kreftjs.env`; never put a secret in one.
|
|
2169
|
+
|
|
2170
|
+
### App
|
|
2171
|
+
|
|
2172
|
+
| Variable | Meaning |
|
|
2173
|
+
|---|---|
|
|
2174
|
+
| `APP_NAME`, `APP_URL` | Name and public address (used in emails, https redirects, secure cookies). |
|
|
2175
|
+
| `APP_ENV`, `NODE_ENV` | `production` turns on production behaviour (either one is enough). |
|
|
2176
|
+
| `APP_DEBUG` | `true` shows the interactive error page outside production. |
|
|
2177
|
+
| `APP_KEY` | Encrypts sessions and signs links. Make it with `kreftjs key:generate`; required in production. |
|
|
2178
|
+
| `APP_LOCALE`, `APP_FALLBACK_LOCALE`, `APP_TIMEZONE` | Language and time zone defaults. |
|
|
2179
|
+
| `HOST`, `PORT` | Where `kreftjs serve` listens. |
|
|
2180
|
+
| `FORCE_HTTPS` | `true` redirects plain HTTP in production. |
|
|
2181
|
+
| `MAX_BODY_KB` | Largest request body accepted. |
|
|
2182
|
+
|
|
2183
|
+
### Database, cache and queues
|
|
2184
|
+
|
|
2185
|
+
| Variable | Meaning |
|
|
2186
|
+
|---|---|
|
|
2187
|
+
| `DATABASE_URL` | `sqlite:…`, `mysql://…` or `postgres://…`. |
|
|
2188
|
+
| `DB_CONNECTION`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_SSL` | The same as separate values. |
|
|
2189
|
+
| `CACHE_STORE` | `database`, `file`, `memory` or `redis`. Also `CACHE_PREFIX`, `CACHE_DEFAULT_TTL`. |
|
|
2190
|
+
| `QUEUE_CONNECTION` | `database`, `redis` or `sync`. Also `QUEUE_NAMES`, `QUEUE_WORKER`. |
|
|
2191
|
+
| `REDIS_URL`, or `REDIS_HOST` `REDIS_PORT` `REDIS_USERNAME` `REDIS_PASSWORD` `REDIS_DB` `REDIS_TLS` `REDIS_PREFIX` | Redis connection. |
|
|
2192
|
+
| `SCHEDULER` | `off` hands scheduling to cron (`kreftjs schedule:run`). |
|
|
2193
|
+
| `MAX_QUERY_ROWS`, `HARD_QUERY_LIMIT` | Upper limits on rows a query may return. |
|
|
2194
|
+
| `FILESYSTEM_DISK` | `local` or `public`. |
|
|
2195
|
+
|
|
2196
|
+
### Sign-in and sessions
|
|
2197
|
+
|
|
2198
|
+
| Variable | Default | Meaning |
|
|
2199
|
+
|---|---|---|
|
|
2200
|
+
| `SESSION_LIFETIME` | 120 | Minutes of inactivity before a session ends. |
|
|
2201
|
+
| `SESSION_EXPIRE_ON_CLOSE` | false | End the session when the browser closes. |
|
|
2202
|
+
| `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE`, `SESSION_DOMAIN`, `SESSION_COOKIE` | production / `Lax` | Cookie attributes. |
|
|
2203
|
+
| `AUTH_TOKEN_DAYS` | 30 | A token unused this long stops working. |
|
|
2204
|
+
| `PASSWORD_TIMEOUT` | 10800 | Seconds a password confirmation stays valid. |
|
|
2205
|
+
| `LOGIN_MAX_FAILURES`, `LOGIN_WINDOW_SECONDS` | 10, 900 | Wrong passwords per account before a lockout. |
|
|
2206
|
+
| `TWO_FACTOR_MAX_FAILURES` | 8 | Wrong two-factor codes per account before a lockout. |
|
|
2207
|
+
| `PUBLIC_SESSION_TIMEOUT`, `PUBLIC_SESSION_WARNING` | 15, 2 | Minutes of page inactivity before sign-out, and the warning before it. |
|
|
2208
|
+
|
|
2209
|
+
### Network and tools
|
|
2210
|
+
|
|
2211
|
+
| Variable | Meaning |
|
|
2212
|
+
|---|---|
|
|
2213
|
+
| `TRUSTED_PROXIES`, `TRUSTED_PROXY_HOPS` | Which proxies' `X-Forwarded-For` is believed, and how many stand in front with `*`. |
|
|
2214
|
+
| `KREFT_ALLOWED_HOSTS` | Extra host names `kreftjs dev` answers to. |
|
|
2215
|
+
| `MCP_TOKEN`, `MCP_ALLOWED_ORIGIN`, `MCP_QUERY_TIMEOUT_MS` | MCP endpoint token, one extra allowed browser origin, and the query time limit. |
|
|
2216
|
+
| `MAIL_URL`, `MAIL_FROM` | `smtp://user:password@host:587` sends real email (needs `kreftjs add nodemailer`). |
|
|
2217
|
+
| `LOG_LEVEL`, `LOG_CHANNEL` | `debug`, `info`, `notice`, `warning`, `error`, `critical`. |
|
|
2218
|
+
| `AUTO_GC`, `AUTO_GC_THRESHOLD_MB`, `AUTO_GC_INTERVAL_MS` | The memory watchdog. |
|
|
2219
|
+
|
|
2220
|
+
## API cheat sheet
|
|
2221
|
+
|
|
2222
|
+
| Group | Names |
|
|
2223
|
+
|---|---|
|
|
2224
|
+
| `.kreftjs` keywords | `state` `derive` `action` `when` `watch` `on` `prop` `event` `use` `share` `receive` `import` `saved` `load` `layout` |
|
|
2225
|
+
| In the script | `send` `props` `bus` `store` `$route` `$router` `tick` and every `Kreft` function |
|
|
2226
|
+
| In the view | `{expr}` `attr={expr}` `@event` `k-if` `k-elif` `k-else` `k-each` `k-key` `k-model` `k-show` `k-text` `k-html` `k-ref` `k-skip` `k-once` `k-memo` `k-animate` `{for}` |
|
|
2227
|
+
| App | `Kreftjs.app(rootOrPath)` then `.component()` `.directive()` `.share()` `.use()` `.mount()` `.unmount()` |
|
|
2228
|
+
| Data | `store` (`$patch` `$reset` `$watch`) `state` `signal` `derived` `readonly` `saved` `storage` |
|
|
2229
|
+
| Reacting | `watch` `effect` `tick` `onCleanup` `onReady` `onUpdate` `onShow` `onHide` `onDestroy` |
|
|
2230
|
+
| Sharing | `bus` (`on` `once` `off` `send`) `share` `receive` |
|
|
2231
|
+
| Forms | `form(values, rules)` (`$error` `$errors` `$valid` `$busy` `$submit` `$check` `$reset` `$setErrors`) and `validate(values, rules)` |
|
|
2232
|
+
| Server data | `table(name)` and `api` (`get` `post` `put` `patch` `delete` `setup` `create` `onError`) |
|
|
2233
|
+
| Languages | `$t` `$choice` `$locale` `$dir`, `Kreftjs.locale()` `Kreftjs.translations()` |
|
|
2234
|
+
| Router | `Kreftjs.router()` with `.go` `.replace` `.back` `.before` `.after` `.route`, and `children` for nested routes |
|
|
2235
|
+
| Engine | `Kreftjs.render(view, data, options)` |
|
|
2236
|
+
| Server | `Route` `Model` `DB` `Seeder` `Hash` `Str` `Url` `Mail` `Crypt` `Cache` `Storage` `Schedule` `config` `response` `abort` and the helpers in `require('kreftjs/server')` |
|
|
2237
|
+
|
|
2238
|
+
## Compared with other frameworks
|
|
2239
|
+
|
|
2240
|
+
Kreft is one framework for the browser and the server. The others below each cover part of that, so the honest comparison is "what do you put next to them". This is a map to help you choose, not a ranking: all of them are good, and most are older, bigger and better known than Kreft.
|
|
2241
|
+
|
|
2242
|
+
| | **Kreft** | **Next.js** | **Nuxt** | **Astro** | **AdonisJS** | **NestJS** |
|
|
2243
|
+
|---|---|---|---|---|---|---|
|
|
2244
|
+
| **What it is** | Frontend and backend in one | React framework | Vue framework | Content sites with islands of interactivity | Backend (MVC) | Backend (modules, dependency injection) |
|
|
2245
|
+
| **Components** | `.kreftjs` files, no Virtual DOM | React | Vue | Astro files; React, Vue, Svelte … inside | none (Edge templates, or Inertia) | none |
|
|
2246
|
+
| **Pages drawn on the server** | At build time (`--prerender`), and for crawlers and link previews on request | Yes, for every visitor | Yes, for every visitor | Yes, mostly at build time | Only with Inertia | no |
|
|
2247
|
+
| **Database layer** | Active Record `Model`, migrations, seeders, factories | none (use Prisma, Drizzle …) | none | none | Lucid (Active Record) | none (TypeORM, Prisma, MikroORM …) |
|
|
2248
|
+
| **Sign-in, 2FA, API tokens** | Built in, in every new project | none (Auth.js …) | none (a module) | none | Built in | none (Passport, your own) |
|
|
2249
|
+
| **Queues, mail, scheduler, cache** | Built in | none | none | none | Built in | Modules |
|
|
2250
|
+
| **AI agent access (MCP)** | Built in | none | none | none | none | none |
|
|
2251
|
+
| **Runtime dependencies** | None | Many | Many | Many | Some | Some |
|
|
2252
|
+
| **Language** | JavaScript or TypeScript | TypeScript first | TypeScript first | TypeScript first | TypeScript first | TypeScript first |
|
|
2253
|
+
| **Ecosystem and hiring** | Small, new | Very large | Large | Large | Medium | Large |
|
|
2254
|
+
|
|
2255
|
+
**Choose Kreft when** you want a whole application (screens, database, sign-in, jobs, mail) from one tool with almost no dependencies, in the Laravel style, and your team is small enough to learn one framework.
|
|
2256
|
+
|
|
2257
|
+
**Choose something else when:**
|
|
2258
|
+
|
|
2259
|
+
- **Next.js or Nuxt:** your team already knows React or Vue, you need the libraries of that ecosystem, or every visitor must get server-drawn HTML with streaming and incremental updates. Kreft draws whole pages on the server only for crawlers and link previews (see "Whole pages for crawlers").
|
|
2260
|
+
- **Astro:** the site is mostly content (blog, documentation, marketing) and ships almost no JavaScript. Kreft apps always ship the framework.
|
|
2261
|
+
- **AdonisJS:** you want the same Laravel-like backend with a larger community and years of production use, and you will bring your own frontend.
|
|
2262
|
+
- **NestJS:** you build a large API or microservices with many teams, and you want strict structure and dependency injection.
|
|
2263
|
+
|
|
2264
|
+
**Things to weigh before choosing Kreft:** it is young and has one small community, few third-party packages are made for `.kreftjs` files, and nobody has run it at large scale yet. See [Known limits](#known-limits).
|
|
2265
|
+
|
|
2266
|
+
## Coming from Vue
|
|
2267
|
+
|
|
2268
|
+
| Vue | Kreft |
|
|
2269
|
+
|---|---|
|
|
2270
|
+
| `.vue` single-file component | `.kreftjs` file: script, `<view>`, `<style scoped>` |
|
|
2271
|
+
| `ref()`, `reactive()`, `computed()` | `state`, `derive` (JS: `Kreftjs.signal`, `Kreftjs.state`, `Kreftjs.derived`) |
|
|
2272
|
+
| `watch`, `watchEffect` | `watch x { }`, `when cond { }` (JS: `Kreftjs.watch`, `Kreftjs.effect`) |
|
|
2273
|
+
| `{{ x }}`, `:attr`, `@click` | `{x}`, `attr={x}`, `@click={fn}` |
|
|
2274
|
+
| `v-if`, `v-else-if`, `v-else`, `v-show` | `k-if`, `k-elif`, `k-else`, `k-show` |
|
|
2275
|
+
| `v-for` with `:key` | `{for x in list key x.id}` … `{/for}`, or `k-each` + `k-key` |
|
|
2276
|
+
| `v-model`, `v-once`, `v-memo` | `k-model`, `k-once`, `k-memo` |
|
|
2277
|
+
| `defineProps`, `defineEmits`, `emit` | `prop`, `event`, `send()` |
|
|
2278
|
+
| slots, scoped slots | `<slot>`, `<template k-slot:name="{ item }">` |
|
|
2279
|
+
| `provide`, `inject` | `share`, `receive` |
|
|
2280
|
+
| `onMounted`, `onUpdated`, `onUnmounted` | `on ready`, `on update`, `on destroy` |
|
|
2281
|
+
| `<KeepAlive>`, `<Teleport>`, `<Suspense>` | `keep`, `<kreft-portal>`, `<kreft-wait>` |
|
|
2282
|
+
| `<Transition>` | `k-animate="fade"` |
|
|
2283
|
+
| Vue Router with nested routes | `Kreftjs.router()`, `<kreft-view>`, `<kreft-link>`, `children` |
|
|
2284
|
+
| Pinia | `Kreftjs.store()` + `use`, with `{ saved: true }` |
|
|
2285
|
+
| Vite dev server | `kreftjs dev` (or `kreftjs vite:dev`) |
|
|
2286
|
+
| Nuxt static generation | `kreftjs build --prerender` |
|
|
2287
|
+
| Nuxt `server/api` | `server/*.js` served at `/api/*` |
|
|
2288
|
+
|
|
2289
|
+
## Glossary
|
|
2290
|
+
|
|
2291
|
+
| Term | Meaning |
|
|
2292
|
+
|---|---|
|
|
2293
|
+
| **Component** | A `.kreftjs` file: script, view and style together. |
|
|
2294
|
+
| **State** | A value that can change; the view follows it. |
|
|
2295
|
+
| **Derived value** | A value calculated from other values; stays up to date by itself. |
|
|
2296
|
+
| **Store** | State shared by many components. |
|
|
2297
|
+
| **Layout** | The frame around pages: header, menu, footer. |
|
|
2298
|
+
| **Middleware** | A check that runs before a route (client side) or a controller (server side). |
|
|
2299
|
+
| **Migration** | A file that describes a change to the database schema. |
|
|
2300
|
+
| **Seeder / Factory** | Fills the database with starting data / makes fake rows. |
|
|
2301
|
+
| **Policy** | A class that decides whether a user may do something to a model. |
|
|
2302
|
+
| **MCP** | Model Context Protocol: how AI agents read your project through tools. |
|