jskelet 0.1.1 → 0.1.3

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 (66) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +129 -2
  3. package/README.md +21 -7
  4. package/bin/jskelet.mjs +6 -6
  5. package/docs/03-routing.md +48 -9
  6. package/docs/04-render-ve-sablonlar.md +2 -2
  7. package/docs/05-islands.md +59 -6
  8. package/docs/06-cache.md +240 -26
  9. package/docs/07-yapilandirma.md +108 -7
  10. package/docs/08-build.md +4 -4
  11. package/docs/09-dev-araclari.md +5 -0
  12. package/docs/12-panel-ve-oturum.md +384 -0
  13. package/docs/README.md +25 -2
  14. package/docs/en/01-getting-started.md +292 -0
  15. package/docs/en/02-architecture.md +305 -0
  16. package/docs/en/03-routing.md +493 -0
  17. package/docs/en/04-rendering.md +504 -0
  18. package/docs/en/05-islands.md +492 -0
  19. package/docs/en/06-caching.md +640 -0
  20. package/docs/en/07-configuration.md +789 -0
  21. package/docs/en/08-build.md +383 -0
  22. package/docs/en/09-dev-tools.md +314 -0
  23. package/docs/en/10-deployment.md +332 -0
  24. package/docs/en/11-migration.md +360 -0
  25. package/docs/en/12-dashboards-and-sessions.md +392 -0
  26. package/docs/en/README.md +112 -0
  27. package/package.json +4 -2
  28. package/src/build/build.mjs +1 -1
  29. package/src/build/tasks/client.mjs +2 -2
  30. package/src/build/tasks/fonts.mjs +3 -3
  31. package/src/build/tasks/icons.mjs +1 -1
  32. package/src/build/tasks/images.mjs +2 -2
  33. package/src/client/devtools/overlay.js +196 -164
  34. package/src/client/devtools/report.js +96 -96
  35. package/src/client/form.js +192 -0
  36. package/src/client/index.js +10 -1
  37. package/src/client/registry.js +78 -4
  38. package/src/client/swap.js +188 -0
  39. package/src/config/defaults.js +83 -0
  40. package/src/config/index.js +129 -18
  41. package/src/config/pattern.js +1 -1
  42. package/src/dev-server.mjs +1 -1
  43. package/src/http/control-flow.js +16 -1
  44. package/src/http/cookies.js +257 -0
  45. package/src/http/request-context.js +162 -0
  46. package/src/index.js +26 -2
  47. package/src/init.mjs +32 -31
  48. package/src/log.mjs +8 -2
  49. package/src/logo.png +0 -0
  50. package/src/runtime/alias-hooks.mjs +1 -1
  51. package/src/server/assets.js +1 -1
  52. package/src/server/create-app.js +12 -4
  53. package/src/server/data-cache.js +244 -0
  54. package/src/server/dev/devtools.js +6 -2
  55. package/src/server/dev/report.js +8 -1
  56. package/src/server/dev/version-check.mjs +139 -0
  57. package/src/server/head-hints.js +1 -1
  58. package/src/server/html-cache.js +32 -6
  59. package/src/server/middleware/csrf.js +134 -0
  60. package/src/server/prewarm.js +164 -19
  61. package/src/server/render.js +256 -20
  62. package/src/server/router.js +14 -7
  63. package/src/server/status-page.js +1 -1
  64. package/src/version.mjs +9 -4
  65. package/src/views/components/loader.js +1 -1
  66. package/src/views/helpers/tags.js +53 -1
@@ -0,0 +1,162 @@
1
+ /**
2
+ * İstek başına yaşayan bağlam.
3
+ *
4
+ * `request-cache.js` ile aynı desen: `AsyncLocalStorage`, bağlam yoksa her şey
5
+ * sessizce devre dışı. Ayrı bir modül olmasının sebebi, taşıdığı bilginin
6
+ * memoizasyondan farklı olması — burada tutulanlar bir isteğin **cache'lenip
7
+ * cache'lenemeyeceğine** dair kararlar:
8
+ *
9
+ * private → route açıkça "bu sayfa kişiye özel" dedi
10
+ * tainted → controller cookie/Authorization okudu, yani çıktı kullanıcıya
11
+ * bağlı olabilir; HTML cache'e yazmak sızıntı olur
12
+ * csrfToken → bu istek için üretilmiş token; `csrfField()` şablonda basar
13
+ * res → token cookie'sini yazabilmek için; başka hiçbir şey için
14
+ * kullanılmaz, şablonlara yanıt nesnesi sızmaz
15
+ *
16
+ * `tainted` bir tahmin değil, gözlem: `markTainted()` yalnızca gerçekten
17
+ * kimliğe dokunan bir erişimden sonra çağrılır (bkz. `guardRequest`).
18
+ */
19
+ import { AsyncLocalStorage } from "node:async_hooks";
20
+
21
+ /**
22
+ * @typedef {object} RequestContext
23
+ * @property {boolean} private Route `private: true` ile mi kaydedildi.
24
+ * @property {boolean} tainted Kimliğe bağlı bir veri okundu mu.
25
+ * @property {string[]} taintReasons Hangi erişimler işaretledi (teşhis için).
26
+ * @property {string | null} csrfToken
27
+ * @property {import('express').Response | null} res
28
+ */
29
+
30
+ /** @type {AsyncLocalStorage<RequestContext>} */
31
+ const storage = new AsyncLocalStorage();
32
+
33
+ /**
34
+ * @param {{ private?: boolean, res?: import('express').Response }} [initial]
35
+ * @returns {RequestContext}
36
+ */
37
+ export function createRequestContext(initial = {}) {
38
+ return {
39
+ private: initial.private === true,
40
+ tainted: false,
41
+ taintReasons: [],
42
+ csrfToken: null,
43
+ res: initial.res ?? null,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * İsteği verilen bağlam içinde çalıştırır.
49
+ *
50
+ * @template T
51
+ * @param {RequestContext} context
52
+ * @param {() => T} run
53
+ * @returns {T}
54
+ */
55
+ export function withRequestContext(context, run) {
56
+ return storage.run(context, run);
57
+ }
58
+
59
+ /**
60
+ * @returns {RequestContext | undefined}
61
+ */
62
+ export function getRequestContext() {
63
+ return storage.getStore();
64
+ }
65
+
66
+ /**
67
+ * Çıktının kullanıcıya bağlı olduğunu bildirir. Bağlam yoksa (script, build,
68
+ * fragment dışı kullanım) sessizce yok sayılır.
69
+ *
70
+ * @param {string} reason Teşhis mesajında görünecek erişim adı.
71
+ */
72
+ export function markTainted(reason) {
73
+ const context = storage.getStore();
74
+ if (!context) return;
75
+
76
+ context.tainted = true;
77
+ if (!context.taintReasons.includes(reason)) {
78
+ context.taintReasons.push(reason);
79
+ }
80
+ }
81
+
82
+
83
+ /** Okunması çıktıyı kullanıcıya bağlayan başlıklar. */
84
+ const SENSITIVE_HEADERS = new Set([
85
+ "cookie",
86
+ "authorization",
87
+ "proxy-authorization",
88
+ ]);
89
+
90
+ /** Okunması çıktıyı kullanıcıya bağlayan `req` alanları. */
91
+ const SENSITIVE_PROPS = new Set(["cookies", "signedCookies", "session", "user"]);
92
+
93
+ /**
94
+ * @param {Record<string, unknown>} headers
95
+ * @returns {Record<string, unknown>}
96
+ */
97
+ function guardHeaders(headers) {
98
+ return new Proxy(headers, {
99
+ get(target, prop, receiver) {
100
+ if (typeof prop === "string" && SENSITIVE_HEADERS.has(prop.toLowerCase())) {
101
+ markTainted(`req.headers.${prop}`);
102
+ }
103
+ return Reflect.get(target, prop, receiver);
104
+ },
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Controller'a giden `req`'i kimliğe dokunan erişimleri işaretleyen bir Proxy
110
+ * ile sarar.
111
+ *
112
+ * Neden gerekli: HTML cache anahtarı yalnızca yol + query. Cookie okuyan bir
113
+ * controller cache'lenebilir bir route'a bağlanmışsa bir kullanıcının HTML'i
114
+ * bir başkasına servis edilir ve bu hiçbir yerde hata olarak görünmez.
115
+ * İşaretleme, sessiz sızıntıyı gürültülü bir uyarıya çeviriyor.
116
+ *
117
+ * Kapsam bilinçli olarak dar: yalnızca *okuma* yakalanır, yazma ve metot
118
+ * çağrıları hedefe dokunulmadan geçer. Proxy'nin prototipi korunduğu için
119
+ * `req instanceof IncomingMessage` gibi kontroller etkilenmez.
120
+ *
121
+ * @param {import('express').Request} req
122
+ * @returns {import('express').Request}
123
+ */
124
+ export function guardRequest(req) {
125
+ /** @type {Record<string, unknown> | null} */
126
+ let headersProxy = null;
127
+
128
+ return new Proxy(req, {
129
+ get(target, prop, receiver) {
130
+ if (prop === "headers" || prop === "headersDistinct") {
131
+ const raw = Reflect.get(target, prop, receiver);
132
+ if (!raw || typeof raw !== "object") return raw;
133
+ if (prop === "headers") {
134
+ headersProxy ??= guardHeaders(/** @type {Record<string, unknown>} */ (raw));
135
+ return headersProxy;
136
+ }
137
+ return guardHeaders(/** @type {Record<string, unknown>} */ (raw));
138
+ }
139
+
140
+ if (prop === "get" || prop === "header") {
141
+ const original = Reflect.get(target, prop, receiver);
142
+ if (typeof original !== "function") return original;
143
+ return function guardedHeaderLookup(/** @type {string} */ name) {
144
+ if (typeof name === "string" && SENSITIVE_HEADERS.has(name.toLowerCase())) {
145
+ markTainted(`req.get("${name}")`);
146
+ }
147
+ return original.call(target, name);
148
+ };
149
+ }
150
+
151
+ if (typeof prop === "string" && SENSITIVE_PROPS.has(prop)) {
152
+ markTainted(`req.${prop}`);
153
+ }
154
+
155
+ const value = Reflect.get(target, prop, receiver);
156
+ // Express metotları kendi iç alanlarına `this` üzerinden erişiyor;
157
+ // proxy'yi receiver olarak bırakmak bu erişimleri de yakalayıp
158
+ // gereksiz işaretlemeye yol açar.
159
+ return typeof value === "function" ? value.bind(target) : value;
160
+ },
161
+ });
162
+ }
package/src/index.js CHANGED
@@ -5,19 +5,36 @@
5
5
  * controller'lar bu yüzeyi kullanır. Alt yollardan (`jskelet/server`)
6
6
  * ithal etmek de mümkün; buradaki liste "kararlı" sayılan yüzeydir.
7
7
  */
8
- export { route, renderPage, renderView, renderNotFound } from "./server/render.js";
8
+ export {
9
+ route,
10
+ fragment,
11
+ renderPage,
12
+ renderView,
13
+ renderNotFound,
14
+ } from "./server/render.js";
9
15
  export { renderStatusPage, statusFromError } from "./server/status-page.js";
10
16
  export { createApp, startServer } from "./server/create-app.js";
11
17
  export {
12
18
  notFound,
13
19
  redirect,
14
20
  permanentRedirect,
21
+ seeOther,
15
22
  isNotFoundError,
16
23
  isRedirectError,
17
24
  NotFoundError,
18
25
  RedirectError,
19
26
  } from "./http/control-flow.js";
20
27
  export { cache, withRequestCache } from "./http/request-cache.js";
28
+ export {
29
+ clearCookie,
30
+ getSignedCookie,
31
+ parseCookies,
32
+ randomToken,
33
+ safeEqual,
34
+ serializeCookie,
35
+ setCookie,
36
+ setSignedCookie,
37
+ } from "./http/cookies.js";
21
38
  export { reportUpstreamFailure } from "./server/upstream-tracking.js";
22
39
  export { asset, hasAsset, optimizedImage, getSpriteIds } from "./server/assets.js";
23
40
  export { headHints } from "./server/head-hints.js";
@@ -28,8 +45,15 @@ export {
28
45
  getHtmlCacheSize,
29
46
  withHtmlCache,
30
47
  } from "./server/html-cache.js";
48
+ export {
49
+ clearDataCache,
50
+ dataCache,
51
+ getDataCacheEntries,
52
+ getDataCacheSize,
53
+ withDataCache,
54
+ } from "./server/data-cache.js";
31
55
  export { prewarm, prewarmProgress } from "./server/prewarm.js";
32
56
  export { createProxy } from "./server/middleware/upstream-proxy.js";
33
57
  export { getConfig, loadConfig } from "./config/index.js";
34
58
  export { attrs, cn, cx, esc, jsonScript } from "./views/helpers/html.js";
35
- export { icon, image, link, preloadImage } from "./views/helpers/tags.js";
59
+ export { csrfField, icon, image, link, preloadImage } from "./views/helpers/tags.js";
package/src/init.mjs CHANGED
@@ -12,43 +12,43 @@ import * as log from "./log.mjs";
12
12
  /** @type {Record<string, string>} */
13
13
  const FILES = {
14
14
  "jskelet.config.mjs": `/**
15
- * JSkelet yapılandırması. Tüm alanlar opsiyoneldir; bu dosyayı silseniz de
16
- * uygulama varsayılanlarla çalışır.
15
+ * JSkelet configuration. Every field is optional; the app still runs on
16
+ * defaults if you delete this file.
17
17
  *
18
- * Ayrıntılar: node_modules/jskelet/docs/07-yapilandirma.md
18
+ * Details: node_modules/jskelet/docs/en/07-configuration.md
19
19
  */
20
20
  export default {
21
- brand: { lang: "tr" },
21
+ brand: { lang: "en" },
22
22
 
23
- /** Üçüncü taraf kaynaklar; \`<head>\`e preconnect olarak basılır. */
23
+ /** Third-party origins; emitted as preconnect in \`<head>\`. */
24
24
  preconnect: [],
25
25
 
26
26
  async cache() {
27
27
  return {
28
- /** Sayfa HTML'inin önbellekte kalma süresi (saniye). */
28
+ /** How long a page's HTML stays in the cache (seconds). */
29
29
  html: { "/": 60 },
30
30
  };
31
31
  },
32
32
 
33
33
  hooks: {
34
- /** Her sayfanın metadata varsayılanı. */
34
+ /** Metadata defaults for every page. */
35
35
  metadata() {
36
36
  return {
37
37
  titleTemplate: "%s | JSkelet",
38
- description: "JSkelet ile kurulmuş bir site.",
38
+ description: "A site built with JSkelet.",
39
39
  };
40
40
  },
41
41
 
42
- /** Layout'a her render'da eklenen local'ler. */
42
+ /** Locals added to the layout on every render. */
43
43
  layoutContext() {
44
44
  return { bodyClass: "min-h-full" };
45
45
  },
46
46
 
47
- /** 404 sayfası. */
47
+ /** 404 page. */
48
48
  notFound() {
49
49
  return {
50
50
  view: "pages/not-found",
51
- metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
51
+ metadata: { title: "Page not found", robots: { index: false } },
52
52
  };
53
53
  },
54
54
  },
@@ -56,11 +56,12 @@ export default {
56
56
  `,
57
57
 
58
58
  "routes/10-pages.mjs": `/**
59
- * Route modülü. Default export \`(app, api)\` alır; \`api.route()\` controller'ı
60
- * HTML cache'i, notFound/redirect akışı ve sıkıştırmayla sarar.
59
+ * Route module. The default export receives \`(app, api)\`; \`api.route()\` wraps
60
+ * the controller with the HTML cache, the notFound/redirect flow and
61
+ * compression.
61
62
  *
62
- * Dosya adındaki sayısal önek yükleme sırasını belirler: yakalayıcı
63
- * ("/:slug" gibi) route'lar daha yüksek numarada olmalı.
63
+ * The numeric prefix in the file name sets load order: catch-all routes
64
+ * (like "/:slug") belong to a higher number.
64
65
  */
65
66
  export default function register(app, { route }) {
66
67
  app.get(
@@ -68,8 +69,8 @@ export default function register(app, { route }) {
68
69
  route(
69
70
  async () => ({
70
71
  view: "pages/home",
71
- metadata: { title: "Ana sayfa" },
72
- data: { message: "JSkelet çalışıyor." },
72
+ metadata: { title: "Home" },
73
+ data: { message: "JSkelet is running." },
73
74
  }),
74
75
  { revalidate: 60 },
75
76
  ),
@@ -86,16 +87,16 @@ export default function register(app, { route }) {
86
87
 
87
88
  "views/pages/not-found.ejs": `<section class="wrapper">
88
89
  <h1>404</h1>
89
- <p>Aradığınız sayfa bulunamadı.</p>
90
- <p><a href="/">Ana sayfaya dön</a></p>
90
+ <p>The page you are looking for was not found.</p>
91
+ <p><a href="/">Back to home</a></p>
91
92
  </section>
92
93
  `,
93
94
 
94
95
  "views/components/button.js": `import { attrs, esc } from "jskelet/html";
95
96
 
96
97
  /**
97
- * \`views/components/**\` altındaki her named export şablonlarda doğrudan
98
- * kullanılabilir: \`<%- button({ text: "Kaydet" }) %>\`. Import gerekmez.
98
+ * Every named export under \`views/components/**\` is usable directly in
99
+ * templates: \`<%- button({ text: "Save" }) %>\`. No import needed.
99
100
  *
100
101
  * @param {{ text: string, href?: string, class?: string }} props
101
102
  * @returns {string}
@@ -109,8 +110,8 @@ export function button({ text, href, class: className }) {
109
110
  "client/entries/main.js": `import { registerAll, start } from "jskelet/client";
110
111
 
111
112
  /**
112
- * Island kaydı. Değerler dinamik import: modül yalnızca sayfada o island
113
- * gerçekten varsa ve görünür olduğunda indirilir.
113
+ * Island registry. Values are dynamic imports: a module is downloaded only if
114
+ * that island is actually on the page and becomes visible.
114
115
  */
115
116
  registerAll({
116
117
  counter: () => import("../islands/counter.js"),
@@ -120,8 +121,8 @@ start();
120
121
  `,
121
122
 
122
123
  "client/islands/counter.js": `/**
123
- * Island sözleşmesi: \`mount(element, props)\` adlı named export.
124
- * Dönen fonksiyon (varsa) temizlik için ayrılmıştır.
124
+ * Island contract: a named export called \`mount(element, props)\`.
125
+ * The returned function, if any, is reserved for cleanup.
125
126
  *
126
127
  * @param {HTMLElement} element
127
128
  * @param {{ start?: number }} props
@@ -133,7 +134,7 @@ export function mount(element, props) {
133
134
  button.type = "button";
134
135
 
135
136
  const paint = () => {
136
- button.textContent = \`Tıklama: \${value}\`;
137
+ button.textContent = \`Clicks: \${value}\`;
137
138
  };
138
139
 
139
140
  button.addEventListener("click", () => {
@@ -149,9 +150,9 @@ export function mount(element, props) {
149
150
  "styles/globals.css": `@import "tailwindcss" source(none);
150
151
 
151
152
  /**
152
- * Tailwind'in sınıf taraması bu direktiflere bağlıdır. Otomatik tespit
153
- * yalnızca bu dosyanın bulunduğu dizini tarar; şablonlarda geçen varyantlar
154
- * (data-[active=false]:… gibi) aksi hâlde sessizce düşer.
153
+ * Tailwind's class scanning depends on these directives. Automatic detection
154
+ * only scans the directory holding this file; variants used in templates
155
+ * (like data-[active=false]:…) would otherwise be dropped silently.
155
156
  */
156
157
  @source "../views";
157
158
  @source "../client";
@@ -213,8 +214,8 @@ export async function init(root) {
213
214
  }
214
215
 
215
216
  for (const file of created) log.line(`+ ${file}`);
216
- if (skipped.length) log.warn(`${skipped.length} dosya zaten vardı, atlandı`);
217
+ if (skipped.length) log.warn(`${skipped.length} files already existed, skipped`);
217
218
 
218
219
  log.line("");
219
- log.line("sıradaki adım: npx jskelet dev");
220
+ log.line("next step: npx jskelet dev");
220
221
  }
package/src/log.mjs CHANGED
@@ -75,9 +75,15 @@ function clearLine() {
75
75
  if (isTTY) write("\r\u001b[2K");
76
76
  }
77
77
 
78
- /** @returns {string} `01:49:02` */
78
+ /**
79
+ * Saat, yerelden bağımsız olarak 24 saatlik biçimde. Dil etiketi vermek
80
+ * sunucunun bulunduğu makinenin diline göre `ÖÖ/ÖS` ya da `AM/PM` basılmasına
81
+ * yol açıyordu; log satırının genişliği sabit kalmalı.
82
+ *
83
+ * @returns {string} `01:49:02`
84
+ */
79
85
  export function clock() {
80
- return new Date().toLocaleTimeString("tr-TR", { hour12: false });
86
+ return new Date().toLocaleTimeString("en-GB", { hour12: false });
81
87
  }
82
88
 
83
89
  /**
package/src/logo.png CHANGED
Binary file
@@ -56,7 +56,7 @@ function readAliases() {
56
56
  // Uzun önek önce: `@flags/` `@/`den önce denenmeli, yoksa `@/` yakalar.
57
57
  return aliases.sort((a, b) => b.prefix.length - a.prefix.length);
58
58
  } catch (error) {
59
- console.warn(`[alias] ${name} okunamadı, alias'lar devre dışı`, error);
59
+ console.warn(`[alias] could not read ${name}, aliases disabled`, error);
60
60
  return [];
61
61
  }
62
62
  }
@@ -30,7 +30,7 @@ function load() {
30
30
  } catch {
31
31
  if (!warned) {
32
32
  warned = true;
33
- console.warn("[assets] manifest yok — `jskelet build` çalıştırın.");
33
+ console.warn("[assets] no manifest — run `jskelet build`.");
34
34
  }
35
35
  }
36
36
 
@@ -13,14 +13,17 @@
13
13
  * static'e düşer ve middleware anında sıkıştırır (kalite 5).
14
14
  * 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
15
15
  * maliyeti ödenmesin.
16
- * 6. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
17
- * 7. route'lar → 404 → hata yönetimi.
16
+ * 6. csrf — body parser'lardan sonra olmalı: token form alanından okunuyor.
17
+ * Rewrite'lardan önce, çünkü kontrol istemcinin gördüğü yola bakar.
18
+ * 7. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
19
+ * 8. route'lar → 404 → hata yönetimi.
18
20
  */
19
21
  import path from "node:path";
20
22
  import process from "node:process";
21
23
  import express from "express";
22
24
  import { compression } from "./middleware/compression.js";
23
25
  import { headersMiddleware } from "./middleware/headers.js";
26
+ import { csrf } from "./middleware/csrf.js";
24
27
  import { staticPrecompressed } from "./middleware/static-precompressed.js";
25
28
  import { devGate } from "./middleware/dev-gate.js";
26
29
  import { redirects } from "./middleware/redirects.js";
@@ -51,8 +54,11 @@ export async function createApp(options = {}) {
51
54
  });
52
55
 
53
56
  app.set("etag", "strong");
54
- // Ters proxy arkasında doğru protokol ve istemci IP'si için.
55
- app.set("trust proxy", true);
57
+ // Ters proxy arkasında doğru protokol ve istemci IP'si için. Doğrudan
58
+ // internete açık bir sunucuda kapatılmalı: açıkken istemci kendi
59
+ // `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit log
60
+ // yanlış IP görür.
61
+ app.set("trust proxy", config.security.trustProxy);
56
62
 
57
63
  app.use(configRewrites("beforeFiles"));
58
64
  app.use(compression());
@@ -87,6 +93,8 @@ export async function createApp(options = {}) {
87
93
  app.use(express.urlencoded({ extended: false, limit: "64kb" }));
88
94
  app.use(express.json({ limit: "256kb" }));
89
95
 
96
+ app.use(csrf());
97
+
90
98
  app.use(configRewrites("afterFiles"));
91
99
 
92
100
  await registerRoutes(app);