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.
Files changed (72) hide show
  1. package/README.md +11 -1
  2. package/bin/backend/http.js +6 -2
  3. package/bin/backend/index.js +4 -0
  4. package/bin/backend/stream.js +74 -0
  5. package/bin/build.js +25 -7
  6. package/bin/kreftjs.js +36 -11
  7. package/book/ar/01-getting-started.md +169 -0
  8. package/book/ar/02-language.md +330 -0
  9. package/book/ar/03-components-and-state.md +258 -0
  10. package/book/ar/04-router-data-forms.md +333 -0
  11. package/book/ar/05-arabic-rtl.md +175 -0
  12. package/book/ar/06-backend.md +247 -0
  13. package/book/ar/07-auth-and-security.md +157 -0
  14. package/book/ar/08-background-work.md +250 -0
  15. package/book/ar/09-testing-and-deploy.md +234 -0
  16. package/book/ar/10-electron-desktop.md +258 -0
  17. package/book/ar/11-advanced-blog.md +370 -0
  18. package/book/ar/12-advanced-shop.md +426 -0
  19. package/book/ar/13-advanced-ui.md +440 -0
  20. package/book/ar/14-advanced-ops.md +167 -0
  21. package/book/ar/15-appendix.md +81 -0
  22. package/book/ar/16-virtual-list.md +131 -0
  23. package/book/ar/17-combobox.md +169 -0
  24. package/book/ar/18-tabs-offline.md +168 -0
  25. package/book/ar/19-wizard.md +196 -0
  26. package/book/ar/20-pipes-match-props.md +75 -0
  27. package/book/ar/README.md +73 -0
  28. package/book/kreft-book-ar.docx +0 -0
  29. package/book/~$eft-book-ar.docx +0 -0
  30. package/docs/README.md +111 -13
  31. package/editors/sublime/Comments.tmPreferences +23 -0
  32. package/editors/sublime/Component.sublime-snippet +20 -0
  33. package/editors/sublime/Kreft.sublime-syntax +10 -0
  34. package/editors/sublime/LICENSE +95 -0
  35. package/editors/sublime/README.md +7 -0
  36. package/editors/sublime/action.sublime-snippet +10 -0
  37. package/editors/sublime/derive.sublime-snippet +8 -0
  38. package/editors/sublime/for.sublime-snippet +12 -0
  39. package/editors/sublime/k-case.sublime-snippet +8 -0
  40. package/editors/sublime/k-if.sublime-snippet +8 -0
  41. package/editors/sublime/k-match.sublime-snippet +12 -0
  42. package/editors/sublime/kreft-wait.sublime-snippet +12 -0
  43. package/editors/sublime/layout.sublime-snippet +8 -0
  44. package/editors/sublime/load.sublime-snippet +8 -0
  45. package/editors/sublime/on.sublime-snippet +10 -0
  46. package/editors/sublime/pipe.sublime-snippet +8 -0
  47. package/editors/sublime/prop-typed.sublime-snippet +8 -0
  48. package/editors/sublime/saved.sublime-snippet +8 -0
  49. package/editors/sublime/section.sublime-snippet +10 -0
  50. package/editors/sublime/state.sublime-snippet +8 -0
  51. package/editors/sublime/watch.sublime-snippet +10 -0
  52. package/editors/sublime/when.sublime-snippet +10 -0
  53. package/editors/sublime/yield.sublime-snippet +8 -0
  54. package/editors/vscode/snippets/kreftjs.json +143 -15
  55. package/editors/zed/LICENSE +95 -0
  56. package/editors/zed/README.md +17 -0
  57. package/editors/zed/extension.toml +11 -0
  58. package/editors/zed/languages/kreft/brackets.scm +4 -0
  59. package/editors/zed/languages/kreft/config.toml +17 -0
  60. package/editors/zed/languages/kreft/highlights.scm +18 -0
  61. package/editors/zed/languages/kreft/indents.scm +5 -0
  62. package/editors/zed/languages/kreft/injections.scm +14 -0
  63. package/editors/zed/languages/kreft/outline.scm +3 -0
  64. package/kreftjs.d.ts +1 -1
  65. package/kreftjs.js +653 -86
  66. package/package.json +4 -3
  67. package/server.d.ts +2 -0
  68. package/templates/app/frontend/errors/ErrorMonitor.kreft +2 -2
  69. package/templates/app/frontend/kreftjs.d.ts +1 -1
  70. package/templates/app/frontend/kreftjs.js +653 -86
  71. package/templates/app/frontend/pages/Dashboard.kreft +1 -1
  72. 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 i -g kreftjs # once: makes the `kreft` command available everywhere
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) |
@@ -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
- if (kind === 'unique' && found) (errors[field] = errors[field] || []).push(`The ${label} has already been taken.`);
317
- if (kind === 'exists' && !found) (errors[field] = errors[field] || []).push(`The selected ${label} is invalid.`);
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);
@@ -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
- fs.copyFileSync(path.join(__dirname, '..', 'kreftjs.js'), path.join(dist, 'kreftjs.js'));
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
- if (expr == null) return `${JSON.stringify(p)}: {}`;
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
- res.writeHead(opts.status || 200, {
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 stream = fs.createReadStream(file);
797
- stream.on('error', () => {
798
- if (!res.headersSent) res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
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
- res.writeHead(200, { 'Content-Type': type, 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox", 'Cache-Control': 'public, max-age=3600' });
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).