jskelet 0.6.2 → 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,341 +1,341 @@
1
- /**
2
- * Kullanıcının `robots.txt` gövdesinin altına framework uçlarını ekler.
3
- *
4
- * Panel, görsel proxy, auth handoff, dev araçları ve fragment parçaları
5
- * sayfa değildir; bir crawler onları dizinine alırsa indeks parçalı HTML
6
- * ve yönetim yollarıyla kirlenir. Uygulama dosyayı kendisi yazar (`public/`
7
- * ya da bir route). Framework yalnızca başarılı metin yanıtının sonuna bir
8
- * not ve `Disallow` grubu düşer — dosya yoksa uydurulmaz.
9
- *
10
- * Google, belirli bir user-agent grubunu `User-agent: *` ile birleştirmez;
11
- * aynı ajanın ikinci grubunu birleştirir. Bu yüzden alttaki grup, dosyada
12
- * adı geçen her ajanı tekrarlar. Aksi hâlde yalnızca `Googlebot` grubu olan
13
- * bir dosyada ek hiç uygulanmazdı.
14
- *
15
- * Sıkıştırılmış kopyanın (`Content-Encoding` dolu) üzerine yazılmaz: ek,
16
- * düz metnin sonuna konur, sıkıştırma dıştaki middleware'de olur.
17
- */
18
- import crypto from "node:crypto";
19
- import { getConfig } from "../../config/index.js";
20
-
21
- /** İdempotency anahtarı. Marka adı değişse de ikinci kez eklenmez. */
22
- const NOTE_MARK = "framework endpoints, not for indexing";
23
-
24
- /** Framework'ün ayırdığı önekler. Özel bir yola taşınsalar da bunlar kalır. */
25
- const RESERVED_PREFIXES = ["/_jskelet/", "/__jskelet/", "/_fragment/"];
26
-
27
- /**
28
- * @param {string} [brandName]
29
- * @returns {string}
30
- */
31
- export function frameworkRobotsNote(brandName = "JSkelet") {
32
- const name = String(brandName || "JSkelet")
33
- .replace(/[\r\n]+/g, " ")
34
- .trim();
35
- return `# ${name || "JSkelet"} — ${NOTE_MARK}`;
36
- }
37
-
38
- /**
39
- * `robots.txt`'e yazılacak `Disallow` yolları.
40
- *
41
- * Üç önek her zaman vardır. Panel, uzak görsel ve handoff yalnızca
42
- * gerçekten mount edildiklerinde ve öneklerin dışına taşındıklarında eklenir.
43
- * Dev araçlarının özel yolu yalnızca development'ta yazılır: production'da
44
- * o yol uygulamanın kendi sayfası olabilir.
45
- *
46
- * @param {object} [config]
47
- * @param {{ dev?: boolean }} [options]
48
- * @returns {string[]}
49
- */
50
- export function frameworkDisallowPaths(config = {}, options = {}) {
51
- /** @type {string[]} */
52
- const paths = [...RESERVED_PREFIXES];
53
- const dev = options.dev ?? process.env.NODE_ENV === "development";
54
-
55
- /** @type {string[]} */
56
- const extras = [];
57
-
58
- const admin = /** @type {{ enabled?: boolean, basePath?: string }} */ (
59
- config.admin
60
- );
61
- if (admin?.enabled && typeof admin.basePath === "string") {
62
- extras.push(admin.basePath);
63
- }
64
-
65
- const images = /** @type {{ remote?: { enabled?: boolean, path?: string } | false } | false} */ (
66
- config.images
67
- );
68
- const remote = images && images !== false ? images.remote : null;
69
- if (remote && remote !== false && remote.enabled && typeof remote.path === "string") {
70
- extras.push(remote.path);
71
- }
72
-
73
- const auth = /** @type {{ crossSubdomainHandoff?: boolean | { enabled?: boolean, path?: string } }} */ (
74
- config.auth
75
- );
76
- const handoff = auth?.crossSubdomainHandoff;
77
- const handoffOn =
78
- handoff === true ||
79
- (Boolean(handoff) &&
80
- typeof handoff === "object" &&
81
- handoff.enabled !== false);
82
- if (handoffOn && handoff && typeof handoff === "object" && typeof handoff.path === "string") {
83
- extras.push(handoff.path);
84
- }
85
-
86
- const brand = /** @type {{ devBasePath?: string }} */ (config.brand);
87
- if (dev && typeof brand?.devBasePath === "string") {
88
- extras.push(brand.devBasePath);
89
- }
90
-
91
- for (const raw of extras) {
92
- const path = cleanDisallowPath(raw);
93
- if (!path) continue;
94
- if (paths.some((existing) => coveredBy(existing, path))) continue;
95
- paths.push(path);
96
- }
97
-
98
- return paths;
99
- }
100
-
101
- /**
102
- * @param {string} body
103
- * @param {{ brandName?: string, paths: string[] }} options
104
- * @returns {string}
105
- */
106
- export function appendFrameworkRobots(body, options) {
107
- const text = String(body ?? "").replace(/^\uFEFF/, "");
108
- const paths = options.paths.filter((path) => cleanDisallowPath(path) || isReserved(path));
109
- if (paths.length === 0 || text.includes(NOTE_MARK)) return text;
110
-
111
- const agents = collectUserAgents(text);
112
- const block = [
113
- frameworkRobotsNote(options.brandName),
114
- ...agents.map((agent) => `User-agent: ${agent}`),
115
- ...paths.map((path) => `Disallow: ${path}`),
116
- "",
117
- ].join("\n");
118
-
119
- const base = text.replace(/\s*$/, "");
120
- return base ? `${base}\n\n${block}` : block;
121
- }
122
-
123
- /**
124
- * @returns {import('express').RequestHandler}
125
- */
126
- export function robotsTxtMiddleware() {
127
- return function robotsTxt(req, res, next) {
128
- if (req.method !== "GET" && req.method !== "HEAD") {
129
- next();
130
- return;
131
- }
132
-
133
- const pathname = req.path || "";
134
- if (pathname !== "/robots.txt") {
135
- next();
136
- return;
137
- }
138
-
139
- // Handler'ın kendi ETag'i eklenmemiş gövdeye aittir. 304, crawler'ın
140
- // framework bloğu olmayan kopyayı tutmasına yol açardı.
141
- const previousEtag = req.headers["if-none-match"];
142
- delete req.headers["if-none-match"];
143
- delete req.headers["if-modified-since"];
144
-
145
- /** @type {Buffer[]} */
146
- const chunks = [];
147
- const originalEnd = res.end;
148
- let finished = false;
149
-
150
- res.write = function write(chunk, encoding, callback) {
151
- const enc = typeof encoding === "function" ? undefined : encoding;
152
- const done = typeof encoding === "function" ? encoding : callback;
153
- const buf = takeChunk(chunk, enc);
154
- if (buf && buf.length) chunks.push(buf);
155
- if (typeof done === "function") done();
156
- return true;
157
- };
158
-
159
- res.end = function end(chunk, encoding, callback) {
160
- if (finished) return originalEnd.call(this);
161
- finished = true;
162
-
163
- const enc = typeof encoding === "function" ? undefined : encoding;
164
- const done =
165
- typeof chunk === "function"
166
- ? chunk
167
- : typeof encoding === "function"
168
- ? encoding
169
- : callback;
170
- const buf = takeChunk(chunk, enc);
171
- if (buf && buf.length) chunks.push(buf);
172
-
173
- const body = chunks.length === 0 ? Buffer.alloc(0) : Buffer.concat(chunks);
174
-
175
- if (!shouldRewrite(req, res, body)) {
176
- return typeof done === "function"
177
- ? originalEnd.call(this, body, done)
178
- : originalEnd.call(this, body);
179
- }
180
-
181
- const context = readRobotsContext();
182
- const nextBody = Buffer.from(
183
- appendFrameworkRobots(body.toString("utf8"), context),
184
- "utf8",
185
- );
186
- const tag = `"${crypto.createHash("sha1").update(nextBody).digest("base64url")}"`;
187
-
188
- if (previousEtag && etagMatches(previousEtag, tag)) {
189
- res.statusCode = 304;
190
- res.setHeader("ETag", tag);
191
- res.setHeader("Content-Length", "0");
192
- res.removeHeader("Last-Modified");
193
- return originalEnd.call(this);
194
- }
195
-
196
- const type = String(res.getHeader("content-type") || "");
197
- if (!/^text\/plain\b/i.test(type)) {
198
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
199
- }
200
- res.setHeader("ETag", tag);
201
- res.setHeader("Content-Length", String(nextBody.length));
202
- res.removeHeader("Last-Modified");
203
-
204
- return typeof done === "function"
205
- ? originalEnd.call(this, nextBody, done)
206
- : originalEnd.call(this, nextBody);
207
- };
208
-
209
- next();
210
- };
211
- }
212
-
213
- /**
214
- * @returns {{ brandName: string, paths: string[] }}
215
- */
216
- function readRobotsContext() {
217
- try {
218
- const config = getConfig();
219
- const brand = /** @type {{ name?: string }} */ (config.brand);
220
- return {
221
- brandName: typeof brand?.name === "string" && brand.name ? brand.name : "JSkelet",
222
- paths: frameworkDisallowPaths(config),
223
- };
224
- } catch {
225
- return { brandName: "JSkelet", paths: frameworkDisallowPaths({}) };
226
- }
227
- }
228
-
229
- /**
230
- * @param {import('express').Request} req
231
- * @param {import('express').Response} res
232
- * @param {Buffer} body
233
- * @returns {boolean}
234
- */
235
- function shouldRewrite(req, res, body) {
236
- if (req.method !== "GET") return false;
237
- if (res.headersSent) return false;
238
-
239
- const status = res.statusCode || 200;
240
- if (status < 200 || status >= 300) return false;
241
- if (res.getHeader("content-encoding")) return false;
242
-
243
- const type = String(res.getHeader("content-type") || "");
244
- if (type && !/^text\/(?:plain|html)\b/i.test(type)) return false;
245
-
246
- const sample = body.subarray(0, 64).toString("utf8");
247
- if (/^\s*<(!doctype|html)\b/i.test(sample)) return false;
248
- return true;
249
- }
250
-
251
- /**
252
- * @param {string | string[] | undefined} header
253
- * @param {string} tag
254
- * @returns {boolean}
255
- */
256
- function etagMatches(header, tag) {
257
- const value = Array.isArray(header) ? header.join(",") : String(header);
258
- return value.split(",").some((part) => {
259
- const token = part.trim().replace(/^W\//i, "");
260
- return token === "*" || token === tag;
261
- });
262
- }
263
-
264
- /**
265
- * @param {unknown} chunk
266
- * @param {unknown} encoding
267
- * @returns {Buffer | null}
268
- */
269
- function takeChunk(chunk, encoding) {
270
- if (chunk == null || typeof chunk === "function") return null;
271
- if (Buffer.isBuffer(chunk)) return chunk;
272
- if (chunk instanceof Uint8Array) return Buffer.from(chunk);
273
- if (typeof chunk === "string") {
274
- const enc = typeof encoding === "string" ? encoding : "utf8";
275
- return Buffer.from(chunk, /** @type {BufferEncoding} */ (enc));
276
- }
277
- return null;
278
- }
279
-
280
- /**
281
- * @param {string} text
282
- * @returns {string[]}
283
- */
284
- function collectUserAgents(text) {
285
- /** @type {string[]} */
286
- const agents = [];
287
- const seen = new Set();
288
-
289
- for (const line of text.split(/\r?\n/)) {
290
- const match = /^[ \t]*user-agent[ \t]*:[ \t]*(.*?)[ \t]*$/i.exec(line);
291
- if (!match) continue;
292
- const agent = match[1].trim();
293
- if (!agent || /[\r\n]/.test(agent)) continue;
294
- const key = agent.toLowerCase();
295
- if (seen.has(key)) continue;
296
- seen.add(key);
297
- agents.push(agent);
298
- }
299
-
300
- const star = agents.findIndex((agent) => agent === "*");
301
- if (star === -1) agents.unshift("*");
302
- else if (star > 0) {
303
- agents.splice(star, 1);
304
- agents.unshift("*");
305
- }
306
-
307
- return agents;
308
- }
309
-
310
- /**
311
- * @param {string} raw
312
- * @returns {string | null}
313
- */
314
- function cleanDisallowPath(raw) {
315
- if (typeof raw !== "string") return null;
316
- const pathOnly = raw.split("?")[0].trim();
317
- if (!pathOnly.startsWith("/") || /[\s#]/.test(pathOnly)) return null;
318
- const trimmed = pathOnly.replace(/\/+$/, "");
319
- return trimmed || null;
320
- }
321
-
322
- /**
323
- * @param {string} path
324
- * @returns {boolean}
325
- */
326
- function isReserved(path) {
327
- return RESERVED_PREFIXES.includes(path);
328
- }
329
-
330
- /**
331
- * @param {string} rule
332
- * @param {string} candidate
333
- * @returns {boolean}
334
- */
335
- function coveredBy(rule, candidate) {
336
- const ruleExact = rule.replace(/\/+$/, "");
337
- const candExact = candidate.replace(/\/+$/, "");
338
- if (candExact === ruleExact) return true;
339
- const prefix = rule.endsWith("/") ? rule : `${ruleExact}/`;
340
- return candidate.startsWith(prefix) || `${candExact}/`.startsWith(prefix);
341
- }
1
+ /**
2
+ * Kullanıcının `robots.txt` gövdesinin altına framework uçlarını ekler.
3
+ *
4
+ * Panel, görsel proxy, auth handoff, dev araçları ve fragment parçaları
5
+ * sayfa değildir; bir crawler onları dizinine alırsa indeks parçalı HTML
6
+ * ve yönetim yollarıyla kirlenir. Uygulama dosyayı kendisi yazar (`public/`
7
+ * ya da bir route). Framework yalnızca başarılı metin yanıtının sonuna bir
8
+ * not ve `Disallow` grubu düşer — dosya yoksa uydurulmaz.
9
+ *
10
+ * Google, belirli bir user-agent grubunu `User-agent: *` ile birleştirmez;
11
+ * aynı ajanın ikinci grubunu birleştirir. Bu yüzden alttaki grup, dosyada
12
+ * adı geçen her ajanı tekrarlar. Aksi hâlde yalnızca `Googlebot` grubu olan
13
+ * bir dosyada ek hiç uygulanmazdı.
14
+ *
15
+ * Sıkıştırılmış kopyanın (`Content-Encoding` dolu) üzerine yazılmaz: ek,
16
+ * düz metnin sonuna konur, sıkıştırma dıştaki middleware'de olur.
17
+ */
18
+ import crypto from "node:crypto";
19
+ import { getConfig } from "../../config/index.js";
20
+
21
+ /** İdempotency anahtarı. Marka adı değişse de ikinci kez eklenmez. */
22
+ const NOTE_MARK = "framework endpoints, not for indexing";
23
+
24
+ /** Framework'ün ayırdığı önekler. Özel bir yola taşınsalar da bunlar kalır. */
25
+ const RESERVED_PREFIXES = ["/_jskelet/", "/__jskelet/", "/_fragment/"];
26
+
27
+ /**
28
+ * @param {string} [brandName]
29
+ * @returns {string}
30
+ */
31
+ export function frameworkRobotsNote(brandName = "JSkelet") {
32
+ const name = String(brandName || "JSkelet")
33
+ .replace(/[\r\n]+/g, " ")
34
+ .trim();
35
+ return `# ${name || "JSkelet"} — ${NOTE_MARK}`;
36
+ }
37
+
38
+ /**
39
+ * `robots.txt`'e yazılacak `Disallow` yolları.
40
+ *
41
+ * Üç önek her zaman vardır. Panel, uzak görsel ve handoff yalnızca
42
+ * gerçekten mount edildiklerinde ve öneklerin dışına taşındıklarında eklenir.
43
+ * Dev araçlarının özel yolu yalnızca development'ta yazılır: production'da
44
+ * o yol uygulamanın kendi sayfası olabilir.
45
+ *
46
+ * @param {object} [config]
47
+ * @param {{ dev?: boolean }} [options]
48
+ * @returns {string[]}
49
+ */
50
+ export function frameworkDisallowPaths(config = {}, options = {}) {
51
+ /** @type {string[]} */
52
+ const paths = [...RESERVED_PREFIXES];
53
+ const dev = options.dev ?? process.env.NODE_ENV === "development";
54
+
55
+ /** @type {string[]} */
56
+ const extras = [];
57
+
58
+ const admin = /** @type {{ enabled?: boolean, basePath?: string }} */ (
59
+ config.admin
60
+ );
61
+ if (admin?.enabled && typeof admin.basePath === "string") {
62
+ extras.push(admin.basePath);
63
+ }
64
+
65
+ const images = /** @type {{ remote?: { enabled?: boolean, path?: string } | false } | false} */ (
66
+ config.images
67
+ );
68
+ const remote = images && images !== false ? images.remote : null;
69
+ if (remote && remote !== false && remote.enabled && typeof remote.path === "string") {
70
+ extras.push(remote.path);
71
+ }
72
+
73
+ const auth = /** @type {{ crossSubdomainHandoff?: boolean | { enabled?: boolean, path?: string } }} */ (
74
+ config.auth
75
+ );
76
+ const handoff = auth?.crossSubdomainHandoff;
77
+ const handoffOn =
78
+ handoff === true ||
79
+ (Boolean(handoff) &&
80
+ typeof handoff === "object" &&
81
+ handoff.enabled !== false);
82
+ if (handoffOn && handoff && typeof handoff === "object" && typeof handoff.path === "string") {
83
+ extras.push(handoff.path);
84
+ }
85
+
86
+ const brand = /** @type {{ devBasePath?: string }} */ (config.brand);
87
+ if (dev && typeof brand?.devBasePath === "string") {
88
+ extras.push(brand.devBasePath);
89
+ }
90
+
91
+ for (const raw of extras) {
92
+ const path = cleanDisallowPath(raw);
93
+ if (!path) continue;
94
+ if (paths.some((existing) => coveredBy(existing, path))) continue;
95
+ paths.push(path);
96
+ }
97
+
98
+ return paths;
99
+ }
100
+
101
+ /**
102
+ * @param {string} body
103
+ * @param {{ brandName?: string, paths: string[] }} options
104
+ * @returns {string}
105
+ */
106
+ export function appendFrameworkRobots(body, options) {
107
+ const text = String(body ?? "").replace(/^\uFEFF/, "");
108
+ const paths = options.paths.filter((path) => cleanDisallowPath(path) || isReserved(path));
109
+ if (paths.length === 0 || text.includes(NOTE_MARK)) return text;
110
+
111
+ const agents = collectUserAgents(text);
112
+ const block = [
113
+ frameworkRobotsNote(options.brandName),
114
+ ...agents.map((agent) => `User-agent: ${agent}`),
115
+ ...paths.map((path) => `Disallow: ${path}`),
116
+ "",
117
+ ].join("\n");
118
+
119
+ const base = text.replace(/\s*$/, "");
120
+ return base ? `${base}\n\n${block}` : block;
121
+ }
122
+
123
+ /**
124
+ * @returns {import('express').RequestHandler}
125
+ */
126
+ export function robotsTxtMiddleware() {
127
+ return function robotsTxt(req, res, next) {
128
+ if (req.method !== "GET" && req.method !== "HEAD") {
129
+ next();
130
+ return;
131
+ }
132
+
133
+ const pathname = req.path || "";
134
+ if (pathname !== "/robots.txt") {
135
+ next();
136
+ return;
137
+ }
138
+
139
+ // Handler'ın kendi ETag'i eklenmemiş gövdeye aittir. 304, crawler'ın
140
+ // framework bloğu olmayan kopyayı tutmasına yol açardı.
141
+ const previousEtag = req.headers["if-none-match"];
142
+ delete req.headers["if-none-match"];
143
+ delete req.headers["if-modified-since"];
144
+
145
+ /** @type {Buffer[]} */
146
+ const chunks = [];
147
+ const originalEnd = res.end;
148
+ let finished = false;
149
+
150
+ res.write = function write(chunk, encoding, callback) {
151
+ const enc = typeof encoding === "function" ? undefined : encoding;
152
+ const done = typeof encoding === "function" ? encoding : callback;
153
+ const buf = takeChunk(chunk, enc);
154
+ if (buf && buf.length) chunks.push(buf);
155
+ if (typeof done === "function") done();
156
+ return true;
157
+ };
158
+
159
+ res.end = function end(chunk, encoding, callback) {
160
+ if (finished) return originalEnd.call(this);
161
+ finished = true;
162
+
163
+ const enc = typeof encoding === "function" ? undefined : encoding;
164
+ const done =
165
+ typeof chunk === "function"
166
+ ? chunk
167
+ : typeof encoding === "function"
168
+ ? encoding
169
+ : callback;
170
+ const buf = takeChunk(chunk, enc);
171
+ if (buf && buf.length) chunks.push(buf);
172
+
173
+ const body = chunks.length === 0 ? Buffer.alloc(0) : Buffer.concat(chunks);
174
+
175
+ if (!shouldRewrite(req, res, body)) {
176
+ return typeof done === "function"
177
+ ? originalEnd.call(this, body, done)
178
+ : originalEnd.call(this, body);
179
+ }
180
+
181
+ const context = readRobotsContext();
182
+ const nextBody = Buffer.from(
183
+ appendFrameworkRobots(body.toString("utf8"), context),
184
+ "utf8",
185
+ );
186
+ const tag = `"${crypto.createHash("sha1").update(nextBody).digest("base64url")}"`;
187
+
188
+ if (previousEtag && etagMatches(previousEtag, tag)) {
189
+ res.statusCode = 304;
190
+ res.setHeader("ETag", tag);
191
+ res.setHeader("Content-Length", "0");
192
+ res.removeHeader("Last-Modified");
193
+ return originalEnd.call(this);
194
+ }
195
+
196
+ const type = String(res.getHeader("content-type") || "");
197
+ if (!/^text\/plain\b/i.test(type)) {
198
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
199
+ }
200
+ res.setHeader("ETag", tag);
201
+ res.setHeader("Content-Length", String(nextBody.length));
202
+ res.removeHeader("Last-Modified");
203
+
204
+ return typeof done === "function"
205
+ ? originalEnd.call(this, nextBody, done)
206
+ : originalEnd.call(this, nextBody);
207
+ };
208
+
209
+ next();
210
+ };
211
+ }
212
+
213
+ /**
214
+ * @returns {{ brandName: string, paths: string[] }}
215
+ */
216
+ function readRobotsContext() {
217
+ try {
218
+ const config = getConfig();
219
+ const brand = /** @type {{ name?: string }} */ (config.brand);
220
+ return {
221
+ brandName: typeof brand?.name === "string" && brand.name ? brand.name : "JSkelet",
222
+ paths: frameworkDisallowPaths(config),
223
+ };
224
+ } catch {
225
+ return { brandName: "JSkelet", paths: frameworkDisallowPaths({}) };
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {import('express').Request} req
231
+ * @param {import('express').Response} res
232
+ * @param {Buffer} body
233
+ * @returns {boolean}
234
+ */
235
+ function shouldRewrite(req, res, body) {
236
+ if (req.method !== "GET") return false;
237
+ if (res.headersSent) return false;
238
+
239
+ const status = res.statusCode || 200;
240
+ if (status < 200 || status >= 300) return false;
241
+ if (res.getHeader("content-encoding")) return false;
242
+
243
+ const type = String(res.getHeader("content-type") || "");
244
+ if (type && !/^text\/(?:plain|html)\b/i.test(type)) return false;
245
+
246
+ const sample = body.subarray(0, 64).toString("utf8");
247
+ if (/^\s*<(!doctype|html)\b/i.test(sample)) return false;
248
+ return true;
249
+ }
250
+
251
+ /**
252
+ * @param {string | string[] | undefined} header
253
+ * @param {string} tag
254
+ * @returns {boolean}
255
+ */
256
+ function etagMatches(header, tag) {
257
+ const value = Array.isArray(header) ? header.join(",") : String(header);
258
+ return value.split(",").some((part) => {
259
+ const token = part.trim().replace(/^W\//i, "");
260
+ return token === "*" || token === tag;
261
+ });
262
+ }
263
+
264
+ /**
265
+ * @param {unknown} chunk
266
+ * @param {unknown} encoding
267
+ * @returns {Buffer | null}
268
+ */
269
+ function takeChunk(chunk, encoding) {
270
+ if (chunk == null || typeof chunk === "function") return null;
271
+ if (Buffer.isBuffer(chunk)) return chunk;
272
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk);
273
+ if (typeof chunk === "string") {
274
+ const enc = typeof encoding === "string" ? encoding : "utf8";
275
+ return Buffer.from(chunk, /** @type {BufferEncoding} */ (enc));
276
+ }
277
+ return null;
278
+ }
279
+
280
+ /**
281
+ * @param {string} text
282
+ * @returns {string[]}
283
+ */
284
+ function collectUserAgents(text) {
285
+ /** @type {string[]} */
286
+ const agents = [];
287
+ const seen = new Set();
288
+
289
+ for (const line of text.split(/\r?\n/)) {
290
+ const match = /^[ \t]*user-agent[ \t]*:[ \t]*(.*?)[ \t]*$/i.exec(line);
291
+ if (!match) continue;
292
+ const agent = match[1].trim();
293
+ if (!agent || /[\r\n]/.test(agent)) continue;
294
+ const key = agent.toLowerCase();
295
+ if (seen.has(key)) continue;
296
+ seen.add(key);
297
+ agents.push(agent);
298
+ }
299
+
300
+ const star = agents.findIndex((agent) => agent === "*");
301
+ if (star === -1) agents.unshift("*");
302
+ else if (star > 0) {
303
+ agents.splice(star, 1);
304
+ agents.unshift("*");
305
+ }
306
+
307
+ return agents;
308
+ }
309
+
310
+ /**
311
+ * @param {string} raw
312
+ * @returns {string | null}
313
+ */
314
+ function cleanDisallowPath(raw) {
315
+ if (typeof raw !== "string") return null;
316
+ const pathOnly = raw.split("?")[0].trim();
317
+ if (!pathOnly.startsWith("/") || /[\s#]/.test(pathOnly)) return null;
318
+ const trimmed = pathOnly.replace(/\/+$/, "");
319
+ return trimmed || null;
320
+ }
321
+
322
+ /**
323
+ * @param {string} path
324
+ * @returns {boolean}
325
+ */
326
+ function isReserved(path) {
327
+ return RESERVED_PREFIXES.includes(path);
328
+ }
329
+
330
+ /**
331
+ * @param {string} rule
332
+ * @param {string} candidate
333
+ * @returns {boolean}
334
+ */
335
+ function coveredBy(rule, candidate) {
336
+ const ruleExact = rule.replace(/\/+$/, "");
337
+ const candExact = candidate.replace(/\/+$/, "");
338
+ if (candExact === ruleExact) return true;
339
+ const prefix = rule.endsWith("/") ? rule : `${ruleExact}/`;
340
+ return candidate.startsWith(prefix) || `${candExact}/`.startsWith(prefix);
341
+ }