jskelet 0.2.5 → 0.3.0

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 (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -1,128 +1,128 @@
1
- /**
2
- * Route modüllerini yükler ve Express'e bağlar.
3
- *
4
- * Dosya sistemine dayalı otomatik URL türetme **yok**: her modül kendi
5
- * yollarını `app.get(...)` ile açıkça yazar. Sebep, Next.js'ten taşınırken
6
- * öğrenilen bir şey — sıra önemli. `/:slug` gibi tek segmentli bir yakalayıcı
7
- * `/about` rotasından önce kaydedilirse "about" bir slug sanılır. Sırayı
8
- * dosya adına gizlemek yerine görünür kılmak, teşhisi kolaylaştırıyor.
9
- *
10
- * Sıra iki şekilde belirlenir:
11
- * 1. `jskelet.config.mjs` → `routes: ["./routes/api.js", …]` (açık liste)
12
- * 2. Liste yoksa `routes/` dizini alfabetik taranır. Bu durumda dosya adına
13
- * sayısal önek verin: `10-pages.js`, `50-blog.js`, `99-catch-all.js`.
14
- *
15
- * Modül sözleşmesi: default export ya da `register` adlı named export,
16
- * `(app, api) => void | Promise<void>` imzasıyla. `api` içinde `route`,
17
- * `fragment`, `renderView`, `renderPage` ve `notFound`/`redirect` hazır gelir,
18
- * böylece route dosyaları framework'ten tek tek import yapmak zorunda kalmaz.
19
- */
20
- import fs from "node:fs";
21
- import path from "node:path";
22
- import process from "node:process";
23
- import { pathToFileURL } from "node:url";
24
- import { getConfig } from "../config/index.js";
25
- import { fragment, renderPage, renderView, route } from "./render.js";
26
- import {
27
- notFound,
28
- permanentRedirect,
29
- redirect,
30
- seeOther,
31
- } from "../http/control-flow.js";
32
-
33
- const isDev = process.env.NODE_ENV === "development";
34
-
35
- /**
36
- * @param {string} dir
37
- * @param {string[]} [out]
38
- * @returns {string[]}
39
- */
40
- function discover(dir, out = []) {
41
- if (!fs.existsSync(dir)) return out;
42
-
43
- const entries = fs
44
- .readdirSync(dir, { withFileTypes: true })
45
- .sort((a, b) => a.name.localeCompare(b.name));
46
-
47
- for (const entry of entries) {
48
- const full = path.join(dir, entry.name);
49
-
50
- if (entry.isDirectory()) {
51
- discover(full, out);
52
- continue;
53
- }
54
-
55
- if (!/\.(js|mjs)$/.test(entry.name)) continue;
56
- if (entry.name.startsWith("_")) continue;
57
-
58
- out.push(full);
59
- }
60
-
61
- return out;
62
- }
63
-
64
- /**
65
- * Route dosyalarına geçilen framework yüzeyi.
66
- */
67
- const api = {
68
- route,
69
- fragment,
70
- renderView,
71
- renderPage,
72
- notFound,
73
- redirect,
74
- permanentRedirect,
75
- seeOther,
76
- };
77
-
78
- /**
79
- * @param {import('express').Express} app
80
- * @returns {Promise<number>} Bağlanan modül sayısı.
81
- */
82
- export async function registerRoutes(app) {
83
- const config = getConfig();
84
-
85
- const files = config.routes
86
- ? config.routes.map((entry) => path.resolve(config.root, entry))
87
- : discover(config.dirs.routes);
88
-
89
- if (!files.length) {
90
- console.warn(
91
- `[router] no route modules found — is ${path.relative(config.root, config.dirs.routes)}/ empty?`,
92
- );
93
- return 0;
94
- }
95
-
96
- let registered = 0;
97
-
98
- for (const file of files) {
99
- /** @type {Record<string, unknown>} */
100
- let module;
101
-
102
- try {
103
- module = await import(pathToFileURL(file).href);
104
- } catch (error) {
105
- // Dev'de eksik/bozuk bir modül tüm sunucuyu düşürmesin: uyar ve devam
106
- // et. Üretimde fırlat — yarım route tablosuyla yayına çıkmak,
107
- // sessizce 404 dönen sayfalar demek.
108
- if (isDev) {
109
- console.warn(`[router] ${path.basename(file)} failed to load, skipped`, error);
110
- continue;
111
- }
112
- throw error;
113
- }
114
-
115
- const register = module.default ?? module.register;
116
- if (typeof register !== "function") {
117
- console.warn(
118
- `[router] ${path.basename(file)} exports neither a default nor a 'register' function, skipped`,
119
- );
120
- continue;
121
- }
122
-
123
- await register(app, api);
124
- registered += 1;
125
- }
126
-
127
- return registered;
128
- }
1
+ /**
2
+ * Route modüllerini yükler ve Express'e bağlar.
3
+ *
4
+ * Dosya sistemine dayalı otomatik URL türetme **yok**: her modül kendi
5
+ * yollarını `app.get(...)` ile açıkça yazar. Sebep, Next.js'ten taşınırken
6
+ * öğrenilen bir şey — sıra önemli. `/:slug` gibi tek segmentli bir yakalayıcı
7
+ * `/about` rotasından önce kaydedilirse "about" bir slug sanılır. Sırayı
8
+ * dosya adına gizlemek yerine görünür kılmak, teşhisi kolaylaştırıyor.
9
+ *
10
+ * Sıra iki şekilde belirlenir:
11
+ * 1. `jskelet.config.mjs` → `routes: ["./routes/api.js", …]` (açık liste)
12
+ * 2. Liste yoksa `routes/` dizini alfabetik taranır. Bu durumda dosya adına
13
+ * sayısal önek verin: `10-pages.js`, `50-blog.js`, `99-catch-all.js`.
14
+ *
15
+ * Modül sözleşmesi: default export ya da `register` adlı named export,
16
+ * `(app, api) => void | Promise<void>` imzasıyla. `api` içinde `route`,
17
+ * `fragment`, `renderView`, `renderPage` ve `notFound`/`redirect` hazır gelir,
18
+ * böylece route dosyaları framework'ten tek tek import yapmak zorunda kalmaz.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import process from "node:process";
23
+ import { pathToFileURL } from "node:url";
24
+ import { getConfig } from "../config/index.js";
25
+ import { fragment, renderPage, renderView, route } from "./render.js";
26
+ import {
27
+ notFound,
28
+ permanentRedirect,
29
+ redirect,
30
+ seeOther,
31
+ } from "../http/control-flow.js";
32
+
33
+ const isDev = process.env.NODE_ENV === "development";
34
+
35
+ /**
36
+ * @param {string} dir
37
+ * @param {string[]} [out]
38
+ * @returns {string[]}
39
+ */
40
+ function discover(dir, out = []) {
41
+ if (!fs.existsSync(dir)) return out;
42
+
43
+ const entries = fs
44
+ .readdirSync(dir, { withFileTypes: true })
45
+ .sort((a, b) => a.name.localeCompare(b.name));
46
+
47
+ for (const entry of entries) {
48
+ const full = path.join(dir, entry.name);
49
+
50
+ if (entry.isDirectory()) {
51
+ discover(full, out);
52
+ continue;
53
+ }
54
+
55
+ if (!/\.(js|mjs)$/.test(entry.name)) continue;
56
+ if (entry.name.startsWith("_")) continue;
57
+
58
+ out.push(full);
59
+ }
60
+
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * Route dosyalarına geçilen framework yüzeyi.
66
+ */
67
+ const api = {
68
+ route,
69
+ fragment,
70
+ renderView,
71
+ renderPage,
72
+ notFound,
73
+ redirect,
74
+ permanentRedirect,
75
+ seeOther,
76
+ };
77
+
78
+ /**
79
+ * @param {import('express').Express} app
80
+ * @returns {Promise<number>} Bağlanan modül sayısı.
81
+ */
82
+ export async function registerRoutes(app) {
83
+ const config = getConfig();
84
+
85
+ const files = config.routes
86
+ ? config.routes.map((entry) => path.resolve(config.root, entry))
87
+ : discover(config.dirs.routes);
88
+
89
+ if (!files.length) {
90
+ console.warn(
91
+ `[router] no route modules found — is ${path.relative(config.root, config.dirs.routes)}/ empty?`,
92
+ );
93
+ return 0;
94
+ }
95
+
96
+ let registered = 0;
97
+
98
+ for (const file of files) {
99
+ /** @type {Record<string, unknown>} */
100
+ let module;
101
+
102
+ try {
103
+ module = await import(pathToFileURL(file).href);
104
+ } catch (error) {
105
+ // Dev'de eksik/bozuk bir modül tüm sunucuyu düşürmesin: uyar ve devam
106
+ // et. Üretimde fırlat — yarım route tablosuyla yayına çıkmak,
107
+ // sessizce 404 dönen sayfalar demek.
108
+ if (isDev) {
109
+ console.warn(`[router] ${path.basename(file)} failed to load, skipped`, error);
110
+ continue;
111
+ }
112
+ throw error;
113
+ }
114
+
115
+ const register = module.default ?? module.register;
116
+ if (typeof register !== "function") {
117
+ console.warn(
118
+ `[router] ${path.basename(file)} exports neither a default nor a 'register' function, skipped`,
119
+ );
120
+ continue;
121
+ }
122
+
123
+ await register(app, api);
124
+ registered += 1;
125
+ }
126
+
127
+ return registered;
128
+ }
@@ -1,164 +1,164 @@
1
- /**
2
- * Framework'ün kendi hata sayfaları (404, 500, 503…).
3
- *
4
- * Uygulama kendi sayfasını vermediğinde ziyaretçinin Express'in düz metin
5
- * "Internal Server Error" çıktısını görmesi istenmiyor; bu yüzden framework
6
- * şablonsuz, tek dosyada duran minimal bir HTML üretir.
7
- *
8
- * Metin bilinçli olarak yalın: yalnızca bir şeyin ters gittiğini söyler.
9
- * Marka adı, ürün tanıtımı ya da hata ayrıntısı yok — hata sayfası
10
- * ziyaretçiye bir şey satmaz ve sunucunun içini dışa açmaz.
11
- *
12
- * Ezme yolları (öncelik sırasıyla):
13
- * 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
14
- * 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
15
- * doğrudan HTML string döner.
16
- * 3. Aşağıdaki gömülü HTML.
17
- */
18
- import { getConfig, hook } from "../config/index.js";
19
- import { esc } from "../views/helpers/html.js";
20
-
21
- /**
22
- * Durum koduna karşılık gelen sayfayı üretir. Hiçbir koşulda fırlatmaz:
23
- * hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
24
- * başarısızlık gömülü HTML'e düşer.
25
- *
26
- * @param {number} status
27
- * @param {{ error?: unknown }} [options]
28
- * @returns {Promise<string>}
29
- */
30
- export async function renderStatusPage(status, options = {}) {
31
- const page =
32
- (status === 404 ? await hook("notFound", null) : null) ??
33
- (await hook("error", null, { status, error: options.error }));
34
-
35
- if (typeof page === "string") return page;
36
- if (!page) return fallbackPage(status);
37
-
38
- try {
39
- // Dinamik import: render.js bu modülü kendisi kullanıyor, statik ithal
40
- // iki modül arasında döngü kurardı.
41
- const { renderPage } = await import("./render.js");
42
- return await renderPage({ pathname: `/${status}`, ...page });
43
- } catch (error) {
44
- console.error(`[render] failed to render the ${status} page`, error);
45
- return fallbackPage(status);
46
- }
47
- }
48
-
49
- /**
50
- * Bir hatadan HTTP durum kodu çıkarır. Uygulama kodu `error.statusCode` ya da
51
- * `error.status` ile kendi kodunu bildirebilir; tanınmayan her şey 500'dür.
52
- *
53
- * @param {unknown} error
54
- * @returns {number}
55
- */
56
- export function statusFromError(error) {
57
- const raw =
58
- error && typeof error === "object"
59
- ? /** @type {{ statusCode?: unknown, status?: unknown }} */ (error).statusCode ??
60
- /** @type {{ status?: unknown }} */ (error).status
61
- : undefined;
62
-
63
- const status = Number(raw);
64
- return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
65
- }
66
-
67
- /**
68
- * Durum başlıkları. Yalnızca framework'ün kendi ürettiği yanıtlar için;
69
- * uygulamanın diline ait metinler `hooks.error()` üzerinden gelir.
70
- *
71
- * @type {Record<string, Record<number | "4xx" | "5xx", [string, string]>>}
72
- */
73
- const MESSAGES = {
74
- tr: {
75
- 400: ["Geçersiz istek", "İstek anlaşılamadı."],
76
- 401: ["Yetki gerekiyor", "Bu sayfayı görmek için oturum açmanız gerekiyor."],
77
- 403: ["Erişim yok", "Bu sayfaya erişim izniniz yok."],
78
- 404: ["Sayfa bulunamadı", "Aradığınız sayfa burada değil."],
79
- 408: ["İstek zaman aşımına uğradı", "Lütfen tekrar deneyin."],
80
- 410: ["Sayfa kaldırıldı", "Bu sayfa artık yayında değil."],
81
- 429: ["Çok fazla istek", "Kısa bir süre sonra tekrar deneyin."],
82
- 500: ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
83
- 503: ["Servis kullanılamıyor", "Lütfen daha sonra tekrar deneyin."],
84
- "4xx": ["İstek karşılanamadı", "Lütfen adresi kontrol edin."],
85
- "5xx": ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
86
- },
87
- en: {
88
- 400: ["Bad request", "The request could not be understood."],
89
- 401: ["Sign in required", "You need to sign in to view this page."],
90
- 403: ["No access", "You do not have permission to view this page."],
91
- 404: ["Page not found", "The page you are looking for is not here."],
92
- 408: ["Request timed out", "Please try again."],
93
- 410: ["Page removed", "This page is no longer available."],
94
- 429: ["Too many requests", "Please try again in a moment."],
95
- 500: ["Something went wrong", "Please try again later."],
96
- 503: ["Service unavailable", "Please try again later."],
97
- "4xx": ["Request failed", "Please check the address."],
98
- "5xx": ["Something went wrong", "Please try again later."],
99
- },
100
- };
101
-
102
- /**
103
- * @param {number} status
104
- * @returns {{ lang: string, title: string, detail: string }}
105
- */
106
- function statusText(status) {
107
- // Config yüklenmeden de çağrılabilir (ör. loadConfig() patladıysa);
108
- // hata sayfası bu yüzden getConfig()'in fırlatmasına dayanmaz.
109
- let lang = "en";
110
- try {
111
- lang = getConfig().brand.lang ?? "en";
112
- } catch {
113
- /* varsayılan kalır */
114
- }
115
-
116
- const table = MESSAGES[lang.slice(0, 2).toLowerCase()] ?? MESSAGES.en;
117
- const [title, detail] =
118
- table[status] ?? table[status >= 500 ? "5xx" : "4xx"];
119
-
120
- return { lang, title, detail };
121
- }
122
-
123
- /**
124
- * Şablonsuz, varlıksız hata sayfası: tek istekte biter, build çıktısına ve
125
- * uygulamanın layout'una bağlı değildir. `noindex` bilinçli — hata sayfası
126
- * arama sonuçlarında görünmemeli.
127
- *
128
- * @param {number} status
129
- * @returns {string}
130
- */
131
- function fallbackPage(status) {
132
- const { lang, title, detail } = statusText(status);
133
-
134
- return `<!DOCTYPE html>
135
- <html lang="${esc(lang)}">
136
- <head>
137
- <meta charset="utf-8">
138
- <meta name="viewport" content="width=device-width, initial-scale=1">
139
- <meta name="robots" content="noindex, nofollow">
140
- <title>${status} — ${esc(title)}</title>
141
- <style>
142
- :root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; }
143
- @media (prefers-color-scheme: dark) {
144
- :root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; }
145
- }
146
- html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg); }
147
- body {
148
- display: grid; place-items: center; padding: 2rem; text-align: center;
149
- font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
150
- }
151
- .code { font-size: 3.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
152
- h1 { font-size: 1.125rem; font-weight: 600; margin: 0.75rem 0 0; }
153
- p { margin: 0.375rem 0 0; color: var(--muted); }
154
- </style>
155
- </head>
156
- <body>
157
- <main>
158
- <p class="code">${status}</p>
159
- <h1>${esc(title)}</h1>
160
- <p>${esc(detail)}</p>
161
- </main>
162
- </body>
163
- </html>`;
164
- }
1
+ /**
2
+ * Framework'ün kendi hata sayfaları (404, 500, 503…).
3
+ *
4
+ * Uygulama kendi sayfasını vermediğinde ziyaretçinin Express'in düz metin
5
+ * "Internal Server Error" çıktısını görmesi istenmiyor; bu yüzden framework
6
+ * şablonsuz, tek dosyada duran minimal bir HTML üretir.
7
+ *
8
+ * Metin bilinçli olarak yalın: yalnızca bir şeyin ters gittiğini söyler.
9
+ * Marka adı, ürün tanıtımı ya da hata ayrıntısı yok — hata sayfası
10
+ * ziyaretçiye bir şey satmaz ve sunucunun içini dışa açmaz.
11
+ *
12
+ * Ezme yolları (öncelik sırasıyla):
13
+ * 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
14
+ * 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
15
+ * doğrudan HTML string döner.
16
+ * 3. Aşağıdaki gömülü HTML.
17
+ */
18
+ import { getConfig, hook } from "../config/index.js";
19
+ import { esc } from "../views/helpers/html.js";
20
+
21
+ /**
22
+ * Durum koduna karşılık gelen sayfayı üretir. Hiçbir koşulda fırlatmaz:
23
+ * hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
24
+ * başarısızlık gömülü HTML'e düşer.
25
+ *
26
+ * @param {number} status
27
+ * @param {{ error?: unknown }} [options]
28
+ * @returns {Promise<string>}
29
+ */
30
+ export async function renderStatusPage(status, options = {}) {
31
+ const page =
32
+ (status === 404 ? await hook("notFound", null) : null) ??
33
+ (await hook("error", null, { status, error: options.error }));
34
+
35
+ if (typeof page === "string") return page;
36
+ if (!page) return fallbackPage(status);
37
+
38
+ try {
39
+ // Dinamik import: render.js bu modülü kendisi kullanıyor, statik ithal
40
+ // iki modül arasında döngü kurardı.
41
+ const { renderPage } = await import("./render.js");
42
+ return await renderPage({ pathname: `/${status}`, ...page });
43
+ } catch (error) {
44
+ console.error(`[render] failed to render the ${status} page`, error);
45
+ return fallbackPage(status);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Bir hatadan HTTP durum kodu çıkarır. Uygulama kodu `error.statusCode` ya da
51
+ * `error.status` ile kendi kodunu bildirebilir; tanınmayan her şey 500'dür.
52
+ *
53
+ * @param {unknown} error
54
+ * @returns {number}
55
+ */
56
+ export function statusFromError(error) {
57
+ const raw =
58
+ error && typeof error === "object"
59
+ ? /** @type {{ statusCode?: unknown, status?: unknown }} */ (error).statusCode ??
60
+ /** @type {{ status?: unknown }} */ (error).status
61
+ : undefined;
62
+
63
+ const status = Number(raw);
64
+ return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
65
+ }
66
+
67
+ /**
68
+ * Durum başlıkları. Yalnızca framework'ün kendi ürettiği yanıtlar için;
69
+ * uygulamanın diline ait metinler `hooks.error()` üzerinden gelir.
70
+ *
71
+ * @type {Record<string, Record<number | "4xx" | "5xx", [string, string]>>}
72
+ */
73
+ const MESSAGES = {
74
+ tr: {
75
+ 400: ["Geçersiz istek", "İstek anlaşılamadı."],
76
+ 401: ["Yetki gerekiyor", "Bu sayfayı görmek için oturum açmanız gerekiyor."],
77
+ 403: ["Erişim yok", "Bu sayfaya erişim izniniz yok."],
78
+ 404: ["Sayfa bulunamadı", "Aradığınız sayfa burada değil."],
79
+ 408: ["İstek zaman aşımına uğradı", "Lütfen tekrar deneyin."],
80
+ 410: ["Sayfa kaldırıldı", "Bu sayfa artık yayında değil."],
81
+ 429: ["Çok fazla istek", "Kısa bir süre sonra tekrar deneyin."],
82
+ 500: ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
83
+ 503: ["Servis kullanılamıyor", "Lütfen daha sonra tekrar deneyin."],
84
+ "4xx": ["İstek karşılanamadı", "Lütfen adresi kontrol edin."],
85
+ "5xx": ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
86
+ },
87
+ en: {
88
+ 400: ["Bad request", "The request could not be understood."],
89
+ 401: ["Sign in required", "You need to sign in to view this page."],
90
+ 403: ["No access", "You do not have permission to view this page."],
91
+ 404: ["Page not found", "The page you are looking for is not here."],
92
+ 408: ["Request timed out", "Please try again."],
93
+ 410: ["Page removed", "This page is no longer available."],
94
+ 429: ["Too many requests", "Please try again in a moment."],
95
+ 500: ["Something went wrong", "Please try again later."],
96
+ 503: ["Service unavailable", "Please try again later."],
97
+ "4xx": ["Request failed", "Please check the address."],
98
+ "5xx": ["Something went wrong", "Please try again later."],
99
+ },
100
+ };
101
+
102
+ /**
103
+ * @param {number} status
104
+ * @returns {{ lang: string, title: string, detail: string }}
105
+ */
106
+ function statusText(status) {
107
+ // Config yüklenmeden de çağrılabilir (ör. loadConfig() patladıysa);
108
+ // hata sayfası bu yüzden getConfig()'in fırlatmasına dayanmaz.
109
+ let lang = "en";
110
+ try {
111
+ lang = getConfig().brand.lang ?? "en";
112
+ } catch {
113
+ /* varsayılan kalır */
114
+ }
115
+
116
+ const table = MESSAGES[lang.slice(0, 2).toLowerCase()] ?? MESSAGES.en;
117
+ const [title, detail] =
118
+ table[status] ?? table[status >= 500 ? "5xx" : "4xx"];
119
+
120
+ return { lang, title, detail };
121
+ }
122
+
123
+ /**
124
+ * Şablonsuz, varlıksız hata sayfası: tek istekte biter, build çıktısına ve
125
+ * uygulamanın layout'una bağlı değildir. `noindex` bilinçli — hata sayfası
126
+ * arama sonuçlarında görünmemeli.
127
+ *
128
+ * @param {number} status
129
+ * @returns {string}
130
+ */
131
+ function fallbackPage(status) {
132
+ const { lang, title, detail } = statusText(status);
133
+
134
+ return `<!DOCTYPE html>
135
+ <html lang="${esc(lang)}">
136
+ <head>
137
+ <meta charset="utf-8">
138
+ <meta name="viewport" content="width=device-width, initial-scale=1">
139
+ <meta name="robots" content="noindex, nofollow">
140
+ <title>${status} — ${esc(title)}</title>
141
+ <style>
142
+ :root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; }
143
+ @media (prefers-color-scheme: dark) {
144
+ :root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; }
145
+ }
146
+ html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg); }
147
+ body {
148
+ display: grid; place-items: center; padding: 2rem; text-align: center;
149
+ font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
150
+ }
151
+ .code { font-size: 3.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
152
+ h1 { font-size: 1.125rem; font-weight: 600; margin: 0.75rem 0 0; }
153
+ p { margin: 0.375rem 0 0; color: var(--muted); }
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <main>
158
+ <p class="code">${status}</p>
159
+ <h1>${esc(title)}</h1>
160
+ <p>${esc(detail)}</p>
161
+ </main>
162
+ </body>
163
+ </html>`;
164
+ }