jskelet 0.6.1 → 0.6.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 (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -1,265 +1,265 @@
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
- * Production metni bilinçli olarak yalın: yalnızca bir şeyin ters gittiğini
9
- * söyler. 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. Development'ta
11
- * 5xx için ayrıntılı teşhis sayfası üretilir (aşağıya bak).
12
- *
13
- * Ezme yolları (öncelik sırasıyla; development 5xx hariç):
14
- * 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
15
- * 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
16
- * doğrudan HTML string döner.
17
- * 3. Aşağıdaki gömülü HTML.
18
- */
19
- import { getConfig, hook } from "../config/index.js";
20
- import { esc } from "../views/helpers/html.js";
21
-
22
- /**
23
- * Durum koduna karşılık gelen sayfayı üretir. Hiçbir koşulda fırlatmaz:
24
- * hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
25
- * başarısızlık gömülü HTML'e düşer.
26
- *
27
- * Development'ta 5xx yanıtları `hooks.error` ve gömülü 500 sayfasını atlar;
28
- * yığın izini içeren bir teşhis sayfası döner. Production'da ayrıntı
29
- * gösterilmez — sunucu içi ziyaretçiye açılmaz.
30
- *
31
- * @param {number} status
32
- * @param {{ error?: unknown }} [options]
33
- * @returns {Promise<string>}
34
- */
35
- export async function renderStatusPage(status, options = {}) {
36
- if (process.env.NODE_ENV === "development" && status >= 500) {
37
- return developmentErrorPage(status, options.error);
38
- }
39
-
40
- const page =
41
- (status === 404 ? await hook("notFound", null) : null) ??
42
- (await hook("error", null, { status, error: options.error }));
43
-
44
- if (typeof page === "string") return page;
45
- if (!page) return fallbackPage(status);
46
-
47
- try {
48
- // Dinamik import: render.js bu modülü kendisi kullanıyor, statik ithal
49
- // iki modül arasında döngü kurardı.
50
- const { renderPage } = await import("./render.js");
51
- return await renderPage({ pathname: `/${status}`, ...page });
52
- } catch (error) {
53
- console.error(`[render] failed to render the ${status} page`, error);
54
- return fallbackPage(status);
55
- }
56
- }
57
-
58
- /**
59
- * Bir hatadan HTTP durum kodu çıkarır. Uygulama kodu `error.statusCode` ya da
60
- * `error.status` ile kendi kodunu bildirebilir; tanınmayan her şey 500'dür.
61
- *
62
- * @param {unknown} error
63
- * @returns {number}
64
- */
65
- export function statusFromError(error) {
66
- const raw =
67
- error && typeof error === "object"
68
- ? /** @type {{ statusCode?: unknown, status?: unknown }} */ (error).statusCode ??
69
- /** @type {{ status?: unknown }} */ (error).status
70
- : undefined;
71
-
72
- const status = Number(raw);
73
- return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
74
- }
75
-
76
- /**
77
- * Durum başlıkları. Yalnızca framework'ün kendi ürettiği yanıtlar için;
78
- * uygulamanın diline ait metinler `hooks.error()` üzerinden gelir.
79
- *
80
- * @type {Record<string, Record<number | "4xx" | "5xx", [string, string]>>}
81
- */
82
- const MESSAGES = {
83
- tr: {
84
- 400: ["Geçersiz istek", "İstek anlaşılamadı."],
85
- 401: ["Yetki gerekiyor", "Bu sayfayı görmek için oturum açmanız gerekiyor."],
86
- 403: ["Erişim yok", "Bu sayfaya erişim izniniz yok."],
87
- 404: ["Sayfa bulunamadı", "Aradığınız sayfa burada değil."],
88
- 408: ["İstek zaman aşımına uğradı", "Lütfen tekrar deneyin."],
89
- 410: ["Sayfa kaldırıldı", "Bu sayfa artık yayında değil."],
90
- 429: ["Çok fazla istek", "Kısa bir süre sonra tekrar deneyin."],
91
- 500: ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
92
- 503: ["Servis kullanılamıyor", "Lütfen daha sonra tekrar deneyin."],
93
- "4xx": ["İstek karşılanamadı", "Lütfen adresi kontrol edin."],
94
- "5xx": ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
95
- },
96
- en: {
97
- 400: ["Bad request", "The request could not be understood."],
98
- 401: ["Sign in required", "You need to sign in to view this page."],
99
- 403: ["No access", "You do not have permission to view this page."],
100
- 404: ["Page not found", "The page you are looking for is not here."],
101
- 408: ["Request timed out", "Please try again."],
102
- 410: ["Page removed", "This page is no longer available."],
103
- 429: ["Too many requests", "Please try again in a moment."],
104
- 500: ["Something went wrong", "Please try again later."],
105
- 503: ["Service unavailable", "Please try again later."],
106
- "4xx": ["Request failed", "Please check the address."],
107
- "5xx": ["Something went wrong", "Please try again later."],
108
- },
109
- };
110
-
111
- /**
112
- * @param {number} status
113
- * @returns {{ lang: string, title: string, detail: string }}
114
- */
115
- function statusText(status) {
116
- // Config yüklenmeden de çağrılabilir (ör. loadConfig() patladıysa);
117
- // hata sayfası bu yüzden getConfig()'in fırlatmasına dayanmaz.
118
- let lang = "en";
119
- try {
120
- lang = getConfig().brand.lang ?? "en";
121
- } catch {
122
- /* varsayılan kalır */
123
- }
124
-
125
- const table = MESSAGES[lang.slice(0, 2).toLowerCase()] ?? MESSAGES.en;
126
- const [title, detail] =
127
- table[status] ?? table[status >= 500 ? "5xx" : "4xx"];
128
-
129
- return { lang, title, detail };
130
- }
131
-
132
- /**
133
- * Şablonsuz, varlıksız hata sayfası: tek istekte biter, build çıktısına ve
134
- * uygulamanın layout'una bağlı değildir. `noindex` bilinçli — hata sayfası
135
- * arama sonuçlarında görünmemeli.
136
- *
137
- * @param {number} status
138
- * @returns {string}
139
- */
140
- function fallbackPage(status) {
141
- const { lang, title, detail } = statusText(status);
142
-
143
- return `<!DOCTYPE html>
144
- <html lang="${esc(lang)}">
145
- <head>
146
- <meta charset="utf-8">
147
- <meta name="viewport" content="width=device-width, initial-scale=1">
148
- <meta name="robots" content="noindex, nofollow">
149
- <title>${status} — ${esc(title)}</title>
150
- <style>
151
- :root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; }
152
- @media (prefers-color-scheme: dark) {
153
- :root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; }
154
- }
155
- html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg); }
156
- body {
157
- display: grid; place-items: center; padding: 2rem; text-align: center;
158
- font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
159
- }
160
- .code { font-size: 3.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
161
- h1 { font-size: 1.125rem; font-weight: 600; margin: 0.75rem 0 0; }
162
- p { margin: 0.375rem 0 0; color: var(--muted); }
163
- </style>
164
- </head>
165
- <body>
166
- <main>
167
- <p class="code">${status}</p>
168
- <h1>${esc(title)}</h1>
169
- <p>${esc(detail)}</p>
170
- </main>
171
- </body>
172
- </html>`;
173
- }
174
-
175
- /**
176
- * Development teşhis sayfası: mesaj + yığın izi. Yalnızca
177
- * `NODE_ENV=development` iken `renderStatusPage` üzerinden çağrılır;
178
- * production yolu buraya hiç girmez.
179
- *
180
- * @param {number} status
181
- * @param {unknown} [error]
182
- * @returns {string}
183
- */
184
- function developmentErrorPage(status, error) {
185
- const name =
186
- error instanceof Error && error.name ? error.name : "Error";
187
- const message =
188
- error instanceof Error
189
- ? error.message || "(no message)"
190
- : error == null
191
- ? "(no error object)"
192
- : String(error);
193
- const body = serializeError(error);
194
-
195
- return `<!DOCTYPE html>
196
- <html lang="en">
197
- <head>
198
- <meta charset="utf-8">
199
- <meta name="viewport" content="width=device-width, initial-scale=1">
200
- <meta name="robots" content="noindex, nofollow">
201
- <title>${status} — ${esc(name)}: ${esc(message)}</title>
202
- <style>
203
- :root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; --panel: #fff; --border: #e4e4e7; }
204
- @media (prefers-color-scheme: dark) {
205
- :root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; --panel: #18181b; --border: #27272a; }
206
- }
207
- html, body { margin: 0; background: var(--bg); color: var(--fg); }
208
- body {
209
- padding: 1.5rem; max-width: 56rem; margin: 0 auto;
210
- font: 1rem/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
211
- }
212
- .banner {
213
- display: inline-block; margin: 0 0 1rem; padding: 0.2rem 0.5rem;
214
- font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em;
215
- text-transform: uppercase; color: var(--muted); border: 1px solid var(--border);
216
- }
217
- .code { font-size: 2.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
218
- h1 { font-size: 1.25rem; font-weight: 600; margin: 0.5rem 0 0; word-break: break-word; }
219
- pre {
220
- margin: 1.25rem 0 0; padding: 1rem; overflow: auto;
221
- background: var(--panel); border: 1px solid var(--border);
222
- font: 0.8125rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
223
- white-space: pre-wrap; word-break: break-word;
224
- }
225
- </style>
226
- </head>
227
- <body>
228
- <p class="banner">Development only — not shown in production</p>
229
- <p class="code">${status}</p>
230
- <h1>${esc(name)}: ${esc(message)}</h1>
231
- <pre>${esc(body)}</pre>
232
- </body>
233
- </html>`;
234
- }
235
-
236
- /**
237
- * Hata nesnesini yığın izi + `cause` zinciriyle metne çevirir.
238
- *
239
- * @param {unknown} error
240
- * @returns {string}
241
- */
242
- function serializeError(error) {
243
- if (error == null) return "(no error object)";
244
-
245
- /** @type {string[]} */
246
- const parts = [];
247
- /** @type {unknown} */
248
- let current = error;
249
- let depth = 0;
250
-
251
- while (current != null && depth < 6) {
252
- if (depth > 0) parts.push("\nCaused by:");
253
-
254
- if (current instanceof Error) {
255
- parts.push(current.stack || `${current.name}: ${current.message}`);
256
- current = "cause" in current ? current.cause : undefined;
257
- } else {
258
- parts.push(String(current));
259
- break;
260
- }
261
- depth += 1;
262
- }
263
-
264
- return parts.join("\n");
265
- }
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
+ * Production metni bilinçli olarak yalın: yalnızca bir şeyin ters gittiğini
9
+ * söyler. 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. Development'ta
11
+ * 5xx için ayrıntılı teşhis sayfası üretilir (aşağıya bak).
12
+ *
13
+ * Ezme yolları (öncelik sırasıyla; development 5xx hariç):
14
+ * 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
15
+ * 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
16
+ * doğrudan HTML string döner.
17
+ * 3. Aşağıdaki gömülü HTML.
18
+ */
19
+ import { getConfig, hook } from "../config/index.js";
20
+ import { esc } from "../views/helpers/html.js";
21
+
22
+ /**
23
+ * Durum koduna karşılık gelen sayfayı üretir. Hiçbir koşulda fırlatmaz:
24
+ * hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
25
+ * başarısızlık gömülü HTML'e düşer.
26
+ *
27
+ * Development'ta 5xx yanıtları `hooks.error` ve gömülü 500 sayfasını atlar;
28
+ * yığın izini içeren bir teşhis sayfası döner. Production'da ayrıntı
29
+ * gösterilmez — sunucu içi ziyaretçiye açılmaz.
30
+ *
31
+ * @param {number} status
32
+ * @param {{ error?: unknown }} [options]
33
+ * @returns {Promise<string>}
34
+ */
35
+ export async function renderStatusPage(status, options = {}) {
36
+ if (process.env.NODE_ENV === "development" && status >= 500) {
37
+ return developmentErrorPage(status, options.error);
38
+ }
39
+
40
+ const page =
41
+ (status === 404 ? await hook("notFound", null) : null) ??
42
+ (await hook("error", null, { status, error: options.error }));
43
+
44
+ if (typeof page === "string") return page;
45
+ if (!page) return fallbackPage(status);
46
+
47
+ try {
48
+ // Dinamik import: render.js bu modülü kendisi kullanıyor, statik ithal
49
+ // iki modül arasında döngü kurardı.
50
+ const { renderPage } = await import("./render.js");
51
+ return await renderPage({ pathname: `/${status}`, ...page });
52
+ } catch (error) {
53
+ console.error(`[render] failed to render the ${status} page`, error);
54
+ return fallbackPage(status);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Bir hatadan HTTP durum kodu çıkarır. Uygulama kodu `error.statusCode` ya da
60
+ * `error.status` ile kendi kodunu bildirebilir; tanınmayan her şey 500'dür.
61
+ *
62
+ * @param {unknown} error
63
+ * @returns {number}
64
+ */
65
+ export function statusFromError(error) {
66
+ const raw =
67
+ error && typeof error === "object"
68
+ ? /** @type {{ statusCode?: unknown, status?: unknown }} */ (error).statusCode ??
69
+ /** @type {{ status?: unknown }} */ (error).status
70
+ : undefined;
71
+
72
+ const status = Number(raw);
73
+ return Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
74
+ }
75
+
76
+ /**
77
+ * Durum başlıkları. Yalnızca framework'ün kendi ürettiği yanıtlar için;
78
+ * uygulamanın diline ait metinler `hooks.error()` üzerinden gelir.
79
+ *
80
+ * @type {Record<string, Record<number | "4xx" | "5xx", [string, string]>>}
81
+ */
82
+ const MESSAGES = {
83
+ tr: {
84
+ 400: ["Geçersiz istek", "İstek anlaşılamadı."],
85
+ 401: ["Yetki gerekiyor", "Bu sayfayı görmek için oturum açmanız gerekiyor."],
86
+ 403: ["Erişim yok", "Bu sayfaya erişim izniniz yok."],
87
+ 404: ["Sayfa bulunamadı", "Aradığınız sayfa burada değil."],
88
+ 408: ["İstek zaman aşımına uğradı", "Lütfen tekrar deneyin."],
89
+ 410: ["Sayfa kaldırıldı", "Bu sayfa artık yayında değil."],
90
+ 429: ["Çok fazla istek", "Kısa bir süre sonra tekrar deneyin."],
91
+ 500: ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
92
+ 503: ["Servis kullanılamıyor", "Lütfen daha sonra tekrar deneyin."],
93
+ "4xx": ["İstek karşılanamadı", "Lütfen adresi kontrol edin."],
94
+ "5xx": ["Bir hata oluştu", "Lütfen daha sonra tekrar deneyin."],
95
+ },
96
+ en: {
97
+ 400: ["Bad request", "The request could not be understood."],
98
+ 401: ["Sign in required", "You need to sign in to view this page."],
99
+ 403: ["No access", "You do not have permission to view this page."],
100
+ 404: ["Page not found", "The page you are looking for is not here."],
101
+ 408: ["Request timed out", "Please try again."],
102
+ 410: ["Page removed", "This page is no longer available."],
103
+ 429: ["Too many requests", "Please try again in a moment."],
104
+ 500: ["Something went wrong", "Please try again later."],
105
+ 503: ["Service unavailable", "Please try again later."],
106
+ "4xx": ["Request failed", "Please check the address."],
107
+ "5xx": ["Something went wrong", "Please try again later."],
108
+ },
109
+ };
110
+
111
+ /**
112
+ * @param {number} status
113
+ * @returns {{ lang: string, title: string, detail: string }}
114
+ */
115
+ function statusText(status) {
116
+ // Config yüklenmeden de çağrılabilir (ör. loadConfig() patladıysa);
117
+ // hata sayfası bu yüzden getConfig()'in fırlatmasına dayanmaz.
118
+ let lang = "en";
119
+ try {
120
+ lang = getConfig().brand.lang ?? "en";
121
+ } catch {
122
+ /* varsayılan kalır */
123
+ }
124
+
125
+ const table = MESSAGES[lang.slice(0, 2).toLowerCase()] ?? MESSAGES.en;
126
+ const [title, detail] =
127
+ table[status] ?? table[status >= 500 ? "5xx" : "4xx"];
128
+
129
+ return { lang, title, detail };
130
+ }
131
+
132
+ /**
133
+ * Şablonsuz, varlıksız hata sayfası: tek istekte biter, build çıktısına ve
134
+ * uygulamanın layout'una bağlı değildir. `noindex` bilinçli — hata sayfası
135
+ * arama sonuçlarında görünmemeli.
136
+ *
137
+ * @param {number} status
138
+ * @returns {string}
139
+ */
140
+ function fallbackPage(status) {
141
+ const { lang, title, detail } = statusText(status);
142
+
143
+ return `<!DOCTYPE html>
144
+ <html lang="${esc(lang)}">
145
+ <head>
146
+ <meta charset="utf-8">
147
+ <meta name="viewport" content="width=device-width, initial-scale=1">
148
+ <meta name="robots" content="noindex, nofollow">
149
+ <title>${status} — ${esc(title)}</title>
150
+ <style>
151
+ :root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; }
152
+ @media (prefers-color-scheme: dark) {
153
+ :root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; }
154
+ }
155
+ html, body { height: 100%; margin: 0; background: var(--bg); color: var(--fg); }
156
+ body {
157
+ display: grid; place-items: center; padding: 2rem; text-align: center;
158
+ font: 1rem/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
159
+ }
160
+ .code { font-size: 3.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
161
+ h1 { font-size: 1.125rem; font-weight: 600; margin: 0.75rem 0 0; }
162
+ p { margin: 0.375rem 0 0; color: var(--muted); }
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <main>
167
+ <p class="code">${status}</p>
168
+ <h1>${esc(title)}</h1>
169
+ <p>${esc(detail)}</p>
170
+ </main>
171
+ </body>
172
+ </html>`;
173
+ }
174
+
175
+ /**
176
+ * Development teşhis sayfası: mesaj + yığın izi. Yalnızca
177
+ * `NODE_ENV=development` iken `renderStatusPage` üzerinden çağrılır;
178
+ * production yolu buraya hiç girmez.
179
+ *
180
+ * @param {number} status
181
+ * @param {unknown} [error]
182
+ * @returns {string}
183
+ */
184
+ function developmentErrorPage(status, error) {
185
+ const name =
186
+ error instanceof Error && error.name ? error.name : "Error";
187
+ const message =
188
+ error instanceof Error
189
+ ? error.message || "(no message)"
190
+ : error == null
191
+ ? "(no error object)"
192
+ : String(error);
193
+ const body = serializeError(error);
194
+
195
+ return `<!DOCTYPE html>
196
+ <html lang="en">
197
+ <head>
198
+ <meta charset="utf-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1">
200
+ <meta name="robots" content="noindex, nofollow">
201
+ <title>${status} — ${esc(name)}: ${esc(message)}</title>
202
+ <style>
203
+ :root { color-scheme: light dark; --fg: #18181b; --muted: #71717a; --bg: #fafafa; --panel: #fff; --border: #e4e4e7; }
204
+ @media (prefers-color-scheme: dark) {
205
+ :root { --fg: #f4f4f5; --muted: #a1a1aa; --bg: #09090b; --panel: #18181b; --border: #27272a; }
206
+ }
207
+ html, body { margin: 0; background: var(--bg); color: var(--fg); }
208
+ body {
209
+ padding: 1.5rem; max-width: 56rem; margin: 0 auto;
210
+ font: 1rem/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
211
+ }
212
+ .banner {
213
+ display: inline-block; margin: 0 0 1rem; padding: 0.2rem 0.5rem;
214
+ font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em;
215
+ text-transform: uppercase; color: var(--muted); border: 1px solid var(--border);
216
+ }
217
+ .code { font-size: 2.5rem; font-weight: 600; letter-spacing: -0.02em; margin: 0; }
218
+ h1 { font-size: 1.25rem; font-weight: 600; margin: 0.5rem 0 0; word-break: break-word; }
219
+ pre {
220
+ margin: 1.25rem 0 0; padding: 1rem; overflow: auto;
221
+ background: var(--panel); border: 1px solid var(--border);
222
+ font: 0.8125rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
223
+ white-space: pre-wrap; word-break: break-word;
224
+ }
225
+ </style>
226
+ </head>
227
+ <body>
228
+ <p class="banner">Development only — not shown in production</p>
229
+ <p class="code">${status}</p>
230
+ <h1>${esc(name)}: ${esc(message)}</h1>
231
+ <pre>${esc(body)}</pre>
232
+ </body>
233
+ </html>`;
234
+ }
235
+
236
+ /**
237
+ * Hata nesnesini yığın izi + `cause` zinciriyle metne çevirir.
238
+ *
239
+ * @param {unknown} error
240
+ * @returns {string}
241
+ */
242
+ function serializeError(error) {
243
+ if (error == null) return "(no error object)";
244
+
245
+ /** @type {string[]} */
246
+ const parts = [];
247
+ /** @type {unknown} */
248
+ let current = error;
249
+ let depth = 0;
250
+
251
+ while (current != null && depth < 6) {
252
+ if (depth > 0) parts.push("\nCaused by:");
253
+
254
+ if (current instanceof Error) {
255
+ parts.push(current.stack || `${current.name}: ${current.message}`);
256
+ current = "cause" in current ? current.cause : undefined;
257
+ } else {
258
+ parts.push(String(current));
259
+ break;
260
+ }
261
+ depth += 1;
262
+ }
263
+
264
+ return parts.join("\n");
265
+ }