kreftjs 0.0.19-beta → 0.0.21-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 +11 -1
- package/bin/backend/http.js +6 -2
- package/bin/backend/index.js +4 -0
- package/bin/backend/stream.js +74 -0
- package/bin/build.js +25 -7
- package/bin/kreftjs.js +36 -11
- package/book/ar/01-getting-started.md +169 -0
- package/book/ar/02-language.md +330 -0
- package/book/ar/03-components-and-state.md +258 -0
- package/book/ar/04-router-data-forms.md +333 -0
- package/book/ar/05-arabic-rtl.md +175 -0
- package/book/ar/06-backend.md +247 -0
- package/book/ar/07-auth-and-security.md +157 -0
- package/book/ar/08-background-work.md +250 -0
- package/book/ar/09-testing-and-deploy.md +234 -0
- package/book/ar/10-electron-desktop.md +258 -0
- package/book/ar/11-advanced-blog.md +370 -0
- package/book/ar/12-advanced-shop.md +426 -0
- package/book/ar/13-advanced-ui.md +440 -0
- package/book/ar/14-advanced-ops.md +167 -0
- package/book/ar/15-appendix.md +81 -0
- package/book/ar/16-virtual-list.md +131 -0
- package/book/ar/17-combobox.md +169 -0
- package/book/ar/18-tabs-offline.md +168 -0
- package/book/ar/19-wizard.md +196 -0
- package/book/ar/20-pipes-match-props.md +75 -0
- package/book/ar/README.md +73 -0
- package/book/kreft-book-ar.docx +0 -0
- package/book/~$eft-book-ar.docx +0 -0
- package/docs/README.md +111 -13
- package/editors/sublime/Comments.tmPreferences +23 -0
- package/editors/sublime/Component.sublime-snippet +20 -0
- package/editors/sublime/Kreft.sublime-syntax +10 -0
- package/editors/sublime/LICENSE +95 -0
- package/editors/sublime/README.md +7 -0
- package/editors/sublime/action.sublime-snippet +10 -0
- package/editors/sublime/derive.sublime-snippet +8 -0
- package/editors/sublime/for.sublime-snippet +12 -0
- package/editors/sublime/k-case.sublime-snippet +8 -0
- package/editors/sublime/k-if.sublime-snippet +8 -0
- package/editors/sublime/k-match.sublime-snippet +12 -0
- package/editors/sublime/kreft-wait.sublime-snippet +12 -0
- package/editors/sublime/layout.sublime-snippet +8 -0
- package/editors/sublime/load.sublime-snippet +8 -0
- package/editors/sublime/on.sublime-snippet +10 -0
- package/editors/sublime/pipe.sublime-snippet +8 -0
- package/editors/sublime/prop-typed.sublime-snippet +8 -0
- package/editors/sublime/saved.sublime-snippet +8 -0
- package/editors/sublime/section.sublime-snippet +10 -0
- package/editors/sublime/state.sublime-snippet +8 -0
- package/editors/sublime/watch.sublime-snippet +10 -0
- package/editors/sublime/when.sublime-snippet +10 -0
- package/editors/sublime/yield.sublime-snippet +8 -0
- package/editors/vscode/snippets/kreftjs.json +143 -15
- package/editors/zed/LICENSE +95 -0
- package/editors/zed/README.md +17 -0
- package/editors/zed/extension.toml +11 -0
- package/editors/zed/languages/kreft/brackets.scm +4 -0
- package/editors/zed/languages/kreft/config.toml +17 -0
- package/editors/zed/languages/kreft/highlights.scm +18 -0
- package/editors/zed/languages/kreft/indents.scm +5 -0
- package/editors/zed/languages/kreft/injections.scm +14 -0
- package/editors/zed/languages/kreft/outline.scm +3 -0
- package/kreftjs.d.ts +1 -1
- package/kreftjs.js +653 -86
- package/package.json +4 -3
- package/server.d.ts +2 -0
- package/templates/app/frontend/errors/ErrorMonitor.kreft +2 -2
- package/templates/app/frontend/kreftjs.d.ts +1 -1
- package/templates/app/frontend/kreftjs.js +653 -86
- package/templates/app/frontend/pages/Dashboard.kreft +1 -1
- package/templates/errors/ErrorMonitor.kreft +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ action add() { count++ }
|
|
|
24
24
|
You need [Node.js](https://nodejs.org) 22.5 or newer.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npm
|
|
27
|
+
npm install -g kreftjs@latest # once: makes the `kreft` command available everywhere
|
|
28
28
|
kreft new my-app # asks: JavaScript or TypeScript?
|
|
29
29
|
cd my-app
|
|
30
30
|
npm run dev
|
|
@@ -42,6 +42,15 @@ Open the address in the terminal (normally `http://localhost:8000`). A new proje
|
|
|
42
42
|
| **Authentication** | Cookie sessions with CSRF protection, API tokens with permissions, device list, two-factor codes, email verification, account lockout. |
|
|
43
43
|
| **Tooling** | `kreft dev` with hot reload, DevTools, an error monitor, `kreft build` (CSP-safe), prerendering, TypeScript, Vite and Electron support, a safer `kreft add`, an MCP server for AI agents. |
|
|
44
44
|
|
|
45
|
+
## Put it online
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install kreftjs@latest # the newest version
|
|
49
|
+
npm run build # the finished site, ready to upload
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Upload what is inside the output folder (`dist/`; for this repository's own website, `dist/kreftjs-com`) to your host. The kreftjs.com steps, and releasing a new package version with `npm publish`, are in [Publish the website](docs/README.md#publish-the-website-kreftjscom).
|
|
53
|
+
|
|
45
54
|
## Desktop apps (Electron)
|
|
46
55
|
|
|
47
56
|
```bash
|
|
@@ -65,6 +74,7 @@ The app icon is `build/icon.png`, and `ELECTRON_MENU=show|auto|hide` controls th
|
|
|
65
74
|
|
|
66
75
|
| I want to… | Read |
|
|
67
76
|
|---|---|
|
|
77
|
+
| Learn Kreft in Arabic, from basics to advanced tested examples | [كتاب Kreft بالعربية](book/ar/README.md) |
|
|
68
78
|
| Build my first page | [Your first app](docs/README.md#your-first-app) and [Components](docs/README.md#components-and-the-kreft-file) |
|
|
69
79
|
| Fetch data and handle forms | [Fetching data](docs/README.md#fetching-data), [Forms](docs/README.md#forms-and-validation) |
|
|
70
80
|
| Add an API and a database | [Routing](docs/README.md#routing), [Models](docs/README.md#models-and-the-database), [Migrations](docs/README.md#migrations-seeders-and-factories) |
|
package/bin/backend/http.js
CHANGED
|
@@ -96,6 +96,8 @@ const cookieSecure = () => {
|
|
|
96
96
|
function response(body, status = 200, headers = {}) { return new Response(body, status, headers); }
|
|
97
97
|
response.json = (body, status = 200, headers = {}) => new Response(body, status, headers);
|
|
98
98
|
response.noContent = () => new Response(undefined, 204);
|
|
99
|
+
// response.stream('/path/video.mp4') - sends a file with byte-range support (seeking, Safari); { type, download, headers }
|
|
100
|
+
response.stream = (file, opts = {}) => { const r = new Response(undefined, 200); r.streamFile = { file, ...opts }; return r; };
|
|
99
101
|
const noContent = () => new Response(undefined, 204);
|
|
100
102
|
const toResponse = (v) => (v instanceof Response ? v : v === undefined ? new Response(undefined, 204) : new Response(v, 200));
|
|
101
103
|
|
|
@@ -313,8 +315,10 @@ class Request {
|
|
|
313
315
|
if (kind === 'unique' && ignore && ignore !== 'NULL') { sql += ` AND ${conn.q(idColumn)} != ?`; params.push(ignore); }
|
|
314
316
|
const found = (await conn.query(sql + ' LIMIT 1', params)).length > 0;
|
|
315
317
|
const label = (names && names[field]) || field.replace(/_/g, ' ');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
// your own message for the rule: { 'title.unique': 'That title is already used.' }
|
|
319
|
+
const own = messages && (messages[`${field}.${kind}`] || messages[kind]);
|
|
320
|
+
if (kind === 'unique' && found) (errors[field] = errors[field] || []).push(own || `The ${label} has already been taken.`);
|
|
321
|
+
if (kind === 'exists' && !found) (errors[field] = errors[field] || []).push(own || `The selected ${label} is invalid.`);
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
324
|
if (Object.keys(errors).length) throw new ValidationError(errors);
|
package/bin/backend/index.js
CHANGED
|
@@ -273,6 +273,10 @@ const SECURITY_HEADERS = {
|
|
|
273
273
|
});
|
|
274
274
|
const send = (res, r) => {
|
|
275
275
|
if (res.headersSent || res.writableEnded) return;
|
|
276
|
+
if (r.streamFile) { // response.stream(file): byte ranges for video and audio
|
|
277
|
+
const { file, ...o } = r.streamFile;
|
|
278
|
+
return require('./stream.js').sendFile(res.req, res, file, { ...o, headers: { 'Cache-Control': 'no-store', ...SECURITY_HEADERS, ...r.headers, ...(o.headers || {}) } });
|
|
279
|
+
}
|
|
276
280
|
const status = r.statusCode || 200;
|
|
277
281
|
const noBody = status === 204 || status === 304 || r.body === undefined;
|
|
278
282
|
const isHtml = typeof r.body === 'string' && (r.headers && r.headers['Content-Type'] && r.headers['Content-Type'].includes('text/html'));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Sending a file the way a video player needs it: byte ranges (206), so the browser can seek and Safari plays at all.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const MEDIA_TYPES = {
|
|
6
|
+
'.mp4': 'video/mp4', '.m4v': 'video/mp4', '.webm': 'video/webm', '.ogv': 'video/ogg', '.mov': 'video/quicktime', '.mkv': 'video/x-matroska',
|
|
7
|
+
'.mp3': 'audio/mpeg', '.m4a': 'audio/mp4', '.aac': 'audio/aac', '.ogg': 'audio/ogg', '.oga': 'audio/ogg', '.wav': 'audio/wav', '.opus': 'audio/ogg', '.flac': 'audio/flac',
|
|
8
|
+
'.m3u8': 'application/vnd.apple.mpegurl', '.ts': 'video/mp2t', '.m4s': 'video/iso.segment', '.mpd': 'application/dash+xml', '.vtt': 'text/vtt',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// "bytes=0-99", "bytes=100-", "bytes=-500" -> { start, end } | null (no range) | false (cannot be satisfied)
|
|
12
|
+
function parseRange(header, size) {
|
|
13
|
+
if (!header) return null;
|
|
14
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(String(header).trim()); // one range only; several ranges are answered with the whole file
|
|
15
|
+
if (!m || (m[1] === '' && m[2] === '')) return null;
|
|
16
|
+
let start, end;
|
|
17
|
+
if (m[1] === '') { // the last N bytes
|
|
18
|
+
const n = Number(m[2]);
|
|
19
|
+
if (n === 0) return false;
|
|
20
|
+
start = Math.max(0, size - n); end = size - 1;
|
|
21
|
+
} else {
|
|
22
|
+
start = Number(m[1]);
|
|
23
|
+
end = m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1);
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= size) return false;
|
|
26
|
+
return { start, end };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// sendFile(req, res, file, { type, headers, status, download })
|
|
30
|
+
function sendFile(req, res, file, opts = {}) {
|
|
31
|
+
req = req || res.req;
|
|
32
|
+
let stat;
|
|
33
|
+
try { stat = fs.statSync(file); if (!stat.isFile()) throw new Error('not a file'); } catch (_) {
|
|
34
|
+
if (!res.headersSent) res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
35
|
+
return res.end('Not found');
|
|
36
|
+
}
|
|
37
|
+
const size = stat.size;
|
|
38
|
+
const type = opts.type || MEDIA_TYPES[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
39
|
+
const etag = `W/"${size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`;
|
|
40
|
+
const headers = {
|
|
41
|
+
'Content-Type': type,
|
|
42
|
+
'Accept-Ranges': 'bytes',
|
|
43
|
+
'Last-Modified': stat.mtime.toUTCString(),
|
|
44
|
+
ETag: etag,
|
|
45
|
+
'X-Content-Type-Options': 'nosniff',
|
|
46
|
+
...(opts.download ? { 'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(path.basename(file))}` } : {}),
|
|
47
|
+
...(opts.headers || {}),
|
|
48
|
+
};
|
|
49
|
+
const method = (req && req.method) || 'GET';
|
|
50
|
+
const h = (req && req.headers) || {};
|
|
51
|
+
|
|
52
|
+
if (h['if-none-match'] === etag && !h.range) { res.writeHead(304, headers); return res.end(); }
|
|
53
|
+
|
|
54
|
+
let range = parseRange(h.range, size);
|
|
55
|
+
// If-Range: only honor the range when the file is still the one the client started with
|
|
56
|
+
if (range && h['if-range'] && h['if-range'] !== etag && h['if-range'] !== headers['Last-Modified']) range = null;
|
|
57
|
+
if (range === false) {
|
|
58
|
+
res.writeHead(416, { 'Content-Range': `bytes */${size}`, 'Content-Type': 'text/plain; charset=utf-8' });
|
|
59
|
+
return res.end('Range not satisfiable');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let status = opts.status || 200, start = 0, end = size - 1;
|
|
63
|
+
if (range) { status = 206; ({ start, end } = range); headers['Content-Range'] = `bytes ${start}-${end}/${size}`; }
|
|
64
|
+
headers['Content-Length'] = size === 0 ? 0 : end - start + 1;
|
|
65
|
+
res.writeHead(status, headers);
|
|
66
|
+
if (method === 'HEAD' || size === 0) return res.end();
|
|
67
|
+
|
|
68
|
+
const stream = fs.createReadStream(file, { start, end });
|
|
69
|
+
stream.on('error', () => res.destroy());
|
|
70
|
+
res.on('close', () => stream.destroy()); // the viewer seeked or left: stop reading
|
|
71
|
+
stream.pipe(res);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { sendFile, parseRange, MEDIA_TYPES };
|
package/bin/build.js
CHANGED
|
@@ -25,7 +25,7 @@ const path = require('path');
|
|
|
25
25
|
const Kreftjs = require('../kreftjs.js');
|
|
26
26
|
|
|
27
27
|
const { expandBlocks, prepareTemplate, splitKreft, splitInterp, exactBrace, stripBrace, interpToExpr, quoteBraceAttrs, expandSelfClosing,
|
|
28
|
-
scopeCss, hashString, propType, simplePathRE, fnExpRE, forAliasRE, stripTypeScript } = Kreftjs.__internal;
|
|
28
|
+
scopeCss, hashString, propType, typeCheck, caseCond, simplePathRE, fnExpRE, forAliasRE, stripTypeScript } = Kreftjs.__internal;
|
|
29
29
|
|
|
30
30
|
const deps = require('./deps.js');
|
|
31
31
|
// component files: .kreft, or .kreftjs in a project from the 0.0.x betas
|
|
@@ -34,12 +34,13 @@ const SERVER_DIRS = new Set();
|
|
|
34
34
|
const SKIP = new Set(['node_modules', 'dist', 'dist-electron', '.git', '.vscode', '.idea', 'package.json', 'package-lock.json', '.DS_Store']);
|
|
35
35
|
|
|
36
36
|
module.exports = function build(args) {
|
|
37
|
-
let folder = null, out = null, base = null, prerender = null, urlsFile = null, htaccess = true;
|
|
37
|
+
let folder = null, out = null, base = null, prerender = null, urlsFile = null, htaccess = true, minify = true;
|
|
38
38
|
const include = [];
|
|
39
39
|
for (let i = 0; i < args.length; i++) {
|
|
40
40
|
if (args[i] === '--out' || args[i] === '-o') out = args[++i];
|
|
41
41
|
else if (args[i] === '--base') base = args[++i];
|
|
42
42
|
else if (args[i] === '--no-htaccess') htaccess = false;
|
|
43
|
+
else if (args[i] === '--no-minify') minify = false; // keep kreftjs.js readable (for debugging)
|
|
43
44
|
else if (args[i] === '--include') include.push(args[++i]); // another folder to copy in, like the docs
|
|
44
45
|
else if (args[i] === '--prerender') prerender = args[i + 1] && !args[i + 1].startsWith('-') && args[i + 1].includes('/') ? args[++i].split(',') : [];
|
|
45
46
|
else if (args[i] === '--urls') urlsFile = args[++i]; // a file with more addresses to prerender (pages from your data)
|
|
@@ -154,7 +155,17 @@ ${indent(components.join('\n\n'))}
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
+
// the framework file is minified when esbuild is installed (about 40% smaller); without it, it is copied as it is
|
|
159
|
+
const runtimeSrc = path.join(__dirname, '..', 'kreftjs.js'), runtimeOut = path.join(dist, 'kreftjs.js');
|
|
160
|
+
const esb = minify ? deps.loadEsbuild(root) : null;
|
|
161
|
+
let minified = false;
|
|
162
|
+
if (esb) {
|
|
163
|
+
try {
|
|
164
|
+
fs.writeFileSync(runtimeOut, esb.transformSync(fs.readFileSync(runtimeSrc, 'utf8'), { minify: true, target: 'es2020', legalComments: 'none' }).code);
|
|
165
|
+
minified = true;
|
|
166
|
+
} catch (_) { /* fall back to the plain copy */ }
|
|
167
|
+
}
|
|
168
|
+
if (!minified) fs.copyFileSync(runtimeSrc, runtimeOut);
|
|
158
169
|
for (const dir of include) {
|
|
159
170
|
const from = path.resolve(dir);
|
|
160
171
|
if (!fs.existsSync(from)) fail(`--include: ${dir} does not exist.`);
|
|
@@ -318,12 +329,16 @@ function compileFile(file, rel, root) {
|
|
|
318
329
|
|
|
319
330
|
// props: keep the default as a function so every instance gets its own copy
|
|
320
331
|
const props = Object.entries(sp.props).map(([p, expr]) => {
|
|
321
|
-
|
|
332
|
+
// `prop size: 'sm' | 'lg'`: the type text is kept, so the built app warns about a wrong value too
|
|
333
|
+
const ti = sp.propTypes && sp.propTypes[p];
|
|
334
|
+
const simple = ti && { string: 'String', number: 'Number', boolean: 'Boolean' }[ti.type.trim()];
|
|
335
|
+
const typed = ti ? `, typeText: ${JSON.stringify(ti.type)}, check: Kreftjs.__typeCheck(${JSON.stringify(ti.type)})${ti.required ? ', required: true' : ''}` : '';
|
|
336
|
+
if (expr == null) return `${JSON.stringify(p)}: { ${simple ? `type: ${simple}` : ''}${typed} }`.replace('{ , ', '{ ').replace('{ }', '{}');
|
|
322
337
|
let sample;
|
|
323
338
|
try { sample = new Function(`return (${expr})`)(); } catch (_) { /* uses something only known at runtime */ }
|
|
324
|
-
if (typeof sample === 'function') return `${JSON.stringify(p)}: { type: Function, default: (${expr}) }`;
|
|
339
|
+
if (typeof sample === 'function') return `${JSON.stringify(p)}: { type: Function, default: (${expr})${typed} }`;
|
|
325
340
|
const type = propType(sample);
|
|
326
|
-
return `${JSON.stringify(p)}: { ${type ? `type: ${type.name}, ` : ''}default: function () { return (${expr}); } }`;
|
|
341
|
+
return `${JSON.stringify(p)}: { ${type ? `type: ${type.name}, ` : ''}default: function () { return (${expr}); }${typed} }`;
|
|
327
342
|
});
|
|
328
343
|
|
|
329
344
|
// imports: other .kreft files become component references; .js modules load with import()
|
|
@@ -428,12 +443,15 @@ function addRouterTemplates(fns) {
|
|
|
428
443
|
addHandler(fns, 'navigate($event)');
|
|
429
444
|
}
|
|
430
445
|
|
|
431
|
-
const STRUCTURAL = new Set(['if', 'elif', 'else', 'each', 'key', 'ref', 'skip', 'cloak', 'slot', 'empty', 'once', 'animate', 'waiting', 'failed']);
|
|
446
|
+
const STRUCTURAL = new Set(['match', 'case', 'default', 'if', 'elif', 'else', 'each', 'key', 'ref', 'skip', 'cloak', 'slot', 'empty', 'once', 'animate', 'waiting', 'failed']);
|
|
432
447
|
|
|
433
448
|
function collectExpressions(template, fns) {
|
|
434
449
|
const html = prepareTemplate(template);
|
|
450
|
+
const subjects = []; // k-match subjects seen so far: a k-case is compared with one of them
|
|
435
451
|
scanHtml(html, {
|
|
436
452
|
attr(name, value) {
|
|
453
|
+
if (name === 'k-match') { const v = stripBrace(value); subjects.push(v); return addEval(fns, v); }
|
|
454
|
+
if (name === 'k-case') { const raw = stripBrace(value); for (const sub of subjects) addEval(fns, caseCond(sub, raw)); return; }
|
|
437
455
|
if (name[0] === ':' || name === 'k-bind' || name.startsWith('k-bind:')) return addEval(fns, value);
|
|
438
456
|
if (name[0] === '@' || name.startsWith('k-on:')) return addHandler(fns, stripBrace(value));
|
|
439
457
|
if (name.startsWith('k-')) {
|
package/bin/kreftjs.js
CHANGED
|
@@ -165,6 +165,7 @@ function help() {
|
|
|
165
165
|
--prerender [paths] Also save every page as ready HTML (needs: npm i -D jsdom)
|
|
166
166
|
--include <folder> Also copy this folder into the output (for example docs)
|
|
167
167
|
--no-htaccess Do not write dist/.htaccess (page-address rules for Apache and Hostinger)
|
|
168
|
+
--no-minify Copy kreftjs.js as it is (by default it is minified when esbuild is installed)
|
|
168
169
|
kreft add <packages...> Install npm packages with strict security gatekeeper (alias: kreft install, kreft i)
|
|
169
170
|
-D, --dev Install as devDependency
|
|
170
171
|
-y, --yes Auto-confirm unverified community packages in CI/CD
|
|
@@ -582,6 +583,30 @@ function copyFolder(from, to) {
|
|
|
582
583
|
}
|
|
583
584
|
|
|
584
585
|
/* ------------------------------------------------------------------ kreft vscode */
|
|
586
|
+
/* VS Code only loads extensions listed in extensions.json, and after "Uninstall" it keeps the folder in .obsolete and
|
|
587
|
+
deletes it on the next start. Copying the folder is not enough after an uninstall: list it again and clear .obsolete. */
|
|
588
|
+
function registerExtension(dir, folder, version) {
|
|
589
|
+
const id = 'kreft.kreftjs';
|
|
590
|
+
const location = path.join(dir, folder);
|
|
591
|
+
try {
|
|
592
|
+
const file = path.join(dir, 'extensions.json');
|
|
593
|
+
let list = [];
|
|
594
|
+
if (fs.existsSync(file)) { try { list = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { list = []; } }
|
|
595
|
+
if (!Array.isArray(list)) list = [];
|
|
596
|
+
list = list.filter((e) => !(e && e.identifier && /^kreft\.kreft(js)?$/.test(e.identifier.id)));
|
|
597
|
+
list.push({ identifier: { id }, version, location: { $mid: 1, fsPath: location, external: 'file://' + location, path: location, scheme: 'file' }, relativeLocation: folder, metadata: { installedTimestamp: Date.now(), pinned: false, source: 'resource', isApplicationScoped: false, isMachineScoped: false, isBuiltin: false, targetPlatform: 'undefined' } });
|
|
598
|
+
fs.writeFileSync(file, JSON.stringify(list));
|
|
599
|
+
} catch {}
|
|
600
|
+
try {
|
|
601
|
+
const file = path.join(dir, '.obsolete');
|
|
602
|
+
if (fs.existsSync(file)) {
|
|
603
|
+
const o = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
604
|
+
for (const k of Object.keys(o)) if (k.startsWith('kreft.kreftjs-') || k.startsWith('kreft.kreft-')) delete o[k];
|
|
605
|
+
fs.writeFileSync(file, JSON.stringify(o));
|
|
606
|
+
}
|
|
607
|
+
} catch {}
|
|
608
|
+
}
|
|
609
|
+
|
|
585
610
|
function installVscode() {
|
|
586
611
|
const os = require('os');
|
|
587
612
|
const from = path.join(__dirname, '..', 'editors', 'vscode');
|
|
@@ -594,6 +619,7 @@ function installVscode() {
|
|
|
594
619
|
fs.mkdirSync(dir, { recursive: true });
|
|
595
620
|
fs.readdirSync(dir).filter((n) => n.startsWith('kreft.kreftjs-') || n.startsWith('kreft.kreft-')) /* the older extensions go too */.forEach((n) => fs.rmSync(path.join(dir, n), { recursive: true, force: true }));
|
|
596
621
|
copyFolder(from, path.join(dir, `kreft.kreftjs-${ext.version}`));
|
|
622
|
+
registerExtension(dir, `kreft.kreftjs-${ext.version}`, ext.version);
|
|
597
623
|
console.log(` Installed in ${path.join(dir, `kreft.kreftjs-${ext.version}`)}`);
|
|
598
624
|
}
|
|
599
625
|
// Ensure current project .vscode/settings.json maps .kreft files to Kreft and configures formatter
|
|
@@ -735,7 +761,7 @@ async function devServer(args, { production = false } = {}) {
|
|
|
735
761
|
'.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.kreft': 'text/plain', '.kreftjs': 'text/plain',
|
|
736
762
|
'.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png',
|
|
737
763
|
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp',
|
|
738
|
-
'.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff', '.txt': 'text/plain', '.md': 'text/markdown',
|
|
764
|
+
'.ico': 'image/x-icon', ...(({ '.ts': _ts, ...media }) => media)(require('./backend/stream.js').MEDIA_TYPES), '.woff2': 'font/woff2', '.woff': 'font/woff', '.txt': 'text/plain', '.md': 'text/markdown',
|
|
739
765
|
};
|
|
740
766
|
const isFile = (p) => { try { return fs.statSync(p).isFile(); } catch (_) { return false; } };
|
|
741
767
|
|
|
@@ -753,12 +779,15 @@ async function devServer(args, { production = false } = {}) {
|
|
|
753
779
|
}
|
|
754
780
|
}
|
|
755
781
|
const type = types[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
756
|
-
|
|
782
|
+
const fileHeaders = {
|
|
757
783
|
'Content-Type': type + (type.startsWith('text/') || type.endsWith('json') ? '; charset=utf-8' : ''),
|
|
758
784
|
'Cache-Control': isProd ? 'no-cache' : 'no-store', // dev: always pick up your latest changes
|
|
759
785
|
'X-Content-Type-Options': 'nosniff',
|
|
760
786
|
...prodHeaders,
|
|
761
|
-
}
|
|
787
|
+
};
|
|
788
|
+
// everything but pages is sent with byte ranges (video and audio seek, Safari plays)
|
|
789
|
+
if (type !== 'text/html') return require('./backend/stream.js').sendFile(res.req, res, file, { status: opts.status, type: fileHeaders['Content-Type'], headers: fileHeaders });
|
|
790
|
+
res.writeHead(opts.status || 200, fileHeaders);
|
|
762
791
|
if (!production && type === 'text/html') {
|
|
763
792
|
let html = fs.readFileSync(file, 'utf8');
|
|
764
793
|
// PUBLIC_ values from .env, as Kreftjs.env (kreft build puts them in app.js)
|
|
@@ -793,12 +822,9 @@ async function devServer(args, { production = false } = {}) {
|
|
|
793
822
|
return res.end(opts.seo ? require('./backend/seo-pages.js').inject(html, opts.seo) : html);
|
|
794
823
|
}
|
|
795
824
|
if (type === 'text/html' && opts.seo) return res.end(require('./backend/seo-pages.js').inject(fs.readFileSync(file, 'utf8'), opts.seo));
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
res.end('Not found');
|
|
800
|
-
});
|
|
801
|
-
stream.pipe(res);
|
|
825
|
+
const page = fs.createReadStream(file); // a page in production: headers are written, send it whole
|
|
826
|
+
page.on('error', () => res.end('Not found'));
|
|
827
|
+
page.pipe(res);
|
|
802
828
|
}
|
|
803
829
|
|
|
804
830
|
// ----- hot reload: watch the folder, tell open pages what changed -----
|
|
@@ -1108,8 +1134,7 @@ async function devServer(args, { production = false } = {}) {
|
|
|
1108
1134
|
const realPub = real(pub), realF = real(f);
|
|
1109
1135
|
if ((f.startsWith(pub + path.sep)) && !secret(urlPath) && isFile(f) && realPub && realF && realF.startsWith(realPub + path.sep)) {
|
|
1110
1136
|
const type = types[path.extname(f).toLowerCase()] || 'application/octet-stream';
|
|
1111
|
-
|
|
1112
|
-
return fs.createReadStream(f).pipe(res);
|
|
1137
|
+
return require('./backend/stream.js').sendFile(req, res, f, { type, headers: { 'Content-Security-Policy': "default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'; sandbox", 'Cache-Control': /\.(m3u8|mpd)$/i.test(f) ? 'no-cache' : 'public, max-age=3600' } }); // a live playlist changes every few seconds
|
|
1113
1138
|
}
|
|
1114
1139
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1115
1140
|
return res.end('Not found: ' + urlPath);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# الفصل الأول: البداية
|
|
2
|
+
|
|
3
|
+
## ما هو Kreft؟
|
|
4
|
+
|
|
5
|
+
Kreft (يُكتب `kreftjs`) إطار عمل صغير، تفاعلي (reactive)، له لغة مكوّنات خاصة به: ملفات `.kreft`. تكتب في الملف الواحد حالتك وقيمك المشتقة وأفعالك بكلمات بسيطة، ثم تصف الصفحة في `<view>`. ويأتي مع واجهة خلفية كاملة على Node.js (مسارات، ومتحكمات، ونماذج، وترحيلات، وطوابير، وبريد)، فيغطي المشروع الواحد التطبيق بأكمله.
|
|
6
|
+
|
|
7
|
+
> **ملاحظة:** الإصدار الحالي تجريبي (`0.0.x-beta`). كل ما في هذا الكتاب جُرِّب على الإصدار الذي صدر معه، لكن واجهات الإطار قد تتغير بين الإصدارات التجريبية. راجع `kreft --version` وملف `docs/README.md` (بالإنجليزية) عند الشك.
|
|
8
|
+
|
|
9
|
+
لماذا قد تختاره؟
|
|
10
|
+
|
|
11
|
+
- **لغة واحدة للمكوّن:** HTML ومنطق وتنسيق في ملف واحد، بلا ديكورات ولا وحدات (modules) تتعلمها.
|
|
12
|
+
- **بدون إعداد:** `kreft new` ثم `kreft dev`. لا ملف إعداد لأداة تجميع.
|
|
13
|
+
- **واجهة خلفية جاهزة:** تسجيل دخول، وتحقق من البريد، واستعادة كلمة المرور، ولوحة إدارة، ومصادقة ثنائية.
|
|
14
|
+
- **تطبيقات سطح المكتب:** `kreft new --electron` ثم تبني مثبّتات لـ Windows وmacOS وLinux.
|
|
15
|
+
- **دعم RTL من الأساس:** تبديل اللغة واتجاه الصفحة من دالة واحدة (مهم لنا نحن العرب، وله فصل كامل).
|
|
16
|
+
|
|
17
|
+
## التثبيت
|
|
18
|
+
|
|
19
|
+
المتطلبات: Node.js (يُفضَّل 22 أو أحدث) وnpm.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g kreftjs
|
|
23
|
+
kreft --version # مثال: 0.0.19-beta
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
للتحقق من أن كل شيء سليم:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
kreft about
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
وللحصول على تلوين الملفات وملفات المقتطفات (snippets) في VS Code أو Cursor:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
kreft vscode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## أول تطبيق
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
kreft new my-app # يسألك: JavaScript أم TypeScript؟
|
|
42
|
+
cd my-app
|
|
43
|
+
npm run dev
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
افتح العنوان الذي يطبعه الطرفية (عادة `http://localhost:8000`؛ وإن كان المنفذ مشغولًا يُستخدم التالي).
|
|
47
|
+
|
|
48
|
+
خيارات مفيدة عند الإنشاء:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
kreft new shop --db mysql --ts # TypeScript وقاعدة MySQL
|
|
52
|
+
kreft new site --ui tailwind --no-auth # Tailwind بدون صفحات تسجيل الدخول
|
|
53
|
+
kreft new desk --electron # مع دعم تطبيقات سطح المكتب
|
|
54
|
+
kreft new app-name --ui bootstrap --no-auth
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **تنبيه:** اكتب قيم الخيارات كما هي بالضبط: `bootstrap` و`tailwind` و`bulma` و`pico`. أي قيمة أخرى توقف الأمر مع رسالة تعرض القيم المسموحة.
|
|
58
|
+
|
|
59
|
+
يأتي المشروع الجديد (إن لم تستعمل `--no-auth`) بصفحات الدخول والتسجيل واستعادة كلمة المرور وتأكيد البريد والملف الشخصي ولوحة الإدارة. وعند تشغيل `npm run dev` تُنشأ الجداول ومستخدم تجريبي:
|
|
60
|
+
|
|
61
|
+
- البريد: `test@example.com`
|
|
62
|
+
- كلمة المرور: `password`
|
|
63
|
+
|
|
64
|
+
> **تحذير:** هذا المستخدم للتطوير المحلي فقط. احذفه أو لا تشغّل `--seed` أبدًا على قاعدة بيانات حقيقية.
|
|
65
|
+
|
|
66
|
+
رسائل البريد (تأكيد، استعادة) تُطبع في الطرفية إلى أن تضبط `MAIL_URL`.
|
|
67
|
+
|
|
68
|
+
## أصغر تطبيق ممكن
|
|
69
|
+
|
|
70
|
+
يمكنك استعمال Kreft بلا مشروع كامل. التطبيق صفحة HTML وملفات `.kreft`:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<!-- index.html -->
|
|
74
|
+
<div id="app"></div>
|
|
75
|
+
<script src="kreftjs.js"></script>
|
|
76
|
+
<script>
|
|
77
|
+
Kreftjs.app('App.kreft').mount('#app')
|
|
78
|
+
</script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<!-- App.kreft -->
|
|
83
|
+
state name = 'العالم'
|
|
84
|
+
|
|
85
|
+
<view>
|
|
86
|
+
<h1>مرحبًا يا {name}</h1>
|
|
87
|
+
<input k-model="name">
|
|
88
|
+
</view>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **تحذير:** ملفات `.kreft` تحتاج خادمًا. المتصفحات ترفض تحميل الملفات من `file://`، فافتح التطبيق دائمًا عبر `npm run dev` أو أي خادم.
|
|
92
|
+
|
|
93
|
+
## بنية المشروع
|
|
94
|
+
|
|
95
|
+
ينشئ `kreft new` مجلدين لا يختلطان أبدًا: `frontend/` هو ما يصل المتصفح، و`backend/` هو الخادم ولا يمكن الوصول إليه عبر HTTP.
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
my-app/
|
|
99
|
+
├── frontend/
|
|
100
|
+
│ ├── index.html نقطة الدخول
|
|
101
|
+
│ ├── App.kreft المكوّن الجذر وحاوية الموجِّه
|
|
102
|
+
│ ├── routes/web.kreft مسارات الصفحات
|
|
103
|
+
│ ├── layouts/ app.kreft (بعد الدخول)، guest.kreft (صفحات الدخول)
|
|
104
|
+
│ ├── pages/ ملف .kreft لكل شاشة
|
|
105
|
+
│ ├── components/ مكوّنات قابلة لإعادة الاستخدام
|
|
106
|
+
│ ├── ui.css نظام التصميم
|
|
107
|
+
│ └── tests/ اختبارات المكوّنات
|
|
108
|
+
├── backend/
|
|
109
|
+
│ ├── config/ app.js, auth.js, database.js, redis.js, session.js
|
|
110
|
+
│ ├── app/
|
|
111
|
+
│ │ ├── Http/ Controllers, Middleware, Requests, Resources
|
|
112
|
+
│ │ ├── Models/ User, PersonalAccessToken
|
|
113
|
+
│ │ └── Auth/ مساعدات الجلسات
|
|
114
|
+
│ ├── database/ migrations, seeders, factories
|
|
115
|
+
│ └── routes/ api.js, auth.js, console.js
|
|
116
|
+
├── .env الأسرار والإعدادات (لا ترفعه إلى Git أبدًا)
|
|
117
|
+
├── package.json
|
|
118
|
+
└── kreftjs.config.json خيارات سطر الأوامر
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
لماذا الفصل مهم؟
|
|
122
|
+
|
|
123
|
+
- `kreft dev` و`kreft build` يقدّمان `frontend/` فقط. أما `backend/` وملفات قاعدة البيانات و`.env` فلا عنوان ويب لها.
|
|
124
|
+
- على macOS وLinux يمنح `kreft new` المجلد `backend/` والملف `.env` صلاحيات يقرؤها المالك فقط.
|
|
125
|
+
- ملفات مثل `.env` و`*.db` و`*.pem` و`package.json` مرفوضة حتى لو طُلبت بالاسم.
|
|
126
|
+
|
|
127
|
+
## إضافة مكتبة CSS
|
|
128
|
+
|
|
129
|
+
أي مكتبة تعمل. أضف `<link>` في `index.html`، أو ثبّتها واستورد ملفها من ملف `.kreft`:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
import 'bootstrap/dist/css/bootstrap.min.css' // npm i bootstrap
|
|
133
|
+
import './theme.css' // ملفك الخاص بجانب هذا الملف
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## حزم npm في المتصفح
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
kreft add dayjs nanoid lodash
|
|
140
|
+
kreft add -D esbuild # مرة واحدة لكل مشروع: يجهّز الحزم للمتصفح
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
import dayjs from 'dayjs'
|
|
145
|
+
import { nanoid } from 'nanoid'
|
|
146
|
+
|
|
147
|
+
state id = nanoid()
|
|
148
|
+
|
|
149
|
+
<view>
|
|
150
|
+
<p>{dayjs().format('YYYY-MM-DD')}</p>
|
|
151
|
+
</view>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
الأمر `kreft add` نسخة أكثر أمانًا من `npm install`: يمنع سكربتات التثبيت افتراضيًا، ويتحقق من أخطاء الكتابة المتعمدة في أسماء الحزم (typosquatting)، وينبّه إلى الحزم التي نُشرت قبل أقل من 14 يومًا، ثم يشغّل `npm audit`.
|
|
155
|
+
|
|
156
|
+
## أوامر ستستخدمها كل يوم
|
|
157
|
+
|
|
158
|
+
| الأمر | وظيفته |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `kreft new my-app` | ينشئ مشروعًا |
|
|
161
|
+
| `kreft dev` | خادم التطوير مع إعادة تحميل فورية |
|
|
162
|
+
| `kreft build` | يصرّف التطبيق إلى ملفات عادية في `dist/` |
|
|
163
|
+
| `kreft serve dist` | يشغّل التطبيق المبني للمستخدمين الحقيقيين |
|
|
164
|
+
| `kreft list` | يعرض كل أوامر نسختك |
|
|
165
|
+
| `kreft help` | خيارات الأوامر الرئيسية |
|
|
166
|
+
| `kreft tinker` | طرفية تفاعلية مع نماذجك (`await User.count()`) |
|
|
167
|
+
| `kreft update` | يحدّث الحزم وملف `kreftjs.js` |
|
|
168
|
+
|
|
169
|
+
تدريب سريع: أنشئ مشروعًا، وغيّر كلمة في `frontend/pages/` وراقب الصفحة تتبدل في المتصفح دون إعادة تحميل وبدون ضياع الحالة. هذه هي إعادة التحميل الفورية (hot reload).
|