jskelet 0.5.5 → 0.6.1

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 (156) hide show
  1. package/AGENTS.md +19 -15
  2. package/CHANGELOG.md +165 -15
  3. package/README.md +16 -21
  4. package/bin/jskelet.mjs +23 -9
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +10 -4
  7. package/docs/03-routing.md +14 -7
  8. package/docs/04-render-ve-sablonlar.md +60 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/06-cache.md +18 -7
  11. package/docs/07-yapilandirma.md +69 -27
  12. package/docs/08-build.md +15 -9
  13. package/docs/09-dev-araclari.md +22 -8
  14. package/docs/10-dagitim.md +14 -13
  15. package/docs/11-tasima.md +51 -17
  16. package/docs/12-panel-ve-oturum.md +10 -4
  17. package/docs/README.md +10 -33
  18. package/docs/en/01-getting-started.md +4 -3
  19. package/docs/en/02-architecture.md +12 -6
  20. package/docs/en/03-routing.md +15 -8
  21. package/docs/en/04-rendering.md +71 -59
  22. package/docs/en/05-islands.md +13 -8
  23. package/docs/en/06-caching.md +21 -7
  24. package/docs/en/07-configuration.md +69 -29
  25. package/docs/en/08-build.md +16 -10
  26. package/docs/en/09-dev-tools.md +24 -8
  27. package/docs/en/10-deployment.md +14 -14
  28. package/docs/en/11-migration.md +51 -16
  29. package/docs/en/12-dashboards-and-sessions.md +9 -4
  30. package/docs/en/README.md +10 -35
  31. package/package.json +48 -13
  32. package/src/build/tasks/client.mjs +91 -10
  33. package/src/build/tasks/icons.mjs +11 -1
  34. package/src/client/index.js +2 -2
  35. package/src/compile/codegen.js +4 -0
  36. package/src/compile/compile-all.js +12 -21
  37. package/src/compile/expr.js +5 -0
  38. package/src/compile/parse.js +64 -8
  39. package/src/compile/resolve.js +3 -0
  40. package/src/config/defaults.js +48 -5
  41. package/src/config/index.js +138 -27
  42. package/src/dev-server.mjs +26 -3
  43. package/src/http/cookies-entry.js +1 -0
  44. package/src/http/cookies.js +18 -0
  45. package/src/logo.png +0 -0
  46. package/src/migrate/apply.mjs +262 -0
  47. package/src/migrate/babel.mjs +79 -0
  48. package/src/migrate/classify.mjs +155 -0
  49. package/src/migrate/config.mjs +126 -0
  50. package/src/migrate/fs-walk.mjs +191 -0
  51. package/src/migrate/parse.mjs +26 -0
  52. package/src/migrate/scan.mjs +177 -0
  53. package/src/migrate/transform/expr-source.mjs +168 -0
  54. package/src/migrate/transform/island.mjs +67 -0
  55. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  56. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  57. package/src/migrate/transform/page-split.mjs +435 -0
  58. package/src/migrate/write.mjs +81 -0
  59. package/src/migrate.mjs +171 -0
  60. package/src/server/auth/handoff.js +94 -11
  61. package/src/server/create-app.js +37 -10
  62. package/src/server/ejs-adapter.js +59 -0
  63. package/src/server/html-cache.js +178 -32
  64. package/src/server/image-optimizer.js +94 -26
  65. package/src/server/middleware/dev-gate.js +21 -8
  66. package/src/server/middleware/robots-txt.js +341 -0
  67. package/src/server/port-guard.js +255 -0
  68. package/src/server/prewarm.js +137 -51
  69. package/src/server/render.js +30 -10
  70. package/src/server/status-page.js +105 -4
  71. package/src/start.mjs +18 -3
  72. package/src/templates/layout.ejs +8 -28
  73. package/src/templates/layout.jsk +30 -0
  74. package/src/templates/layout.render.js +41 -0
  75. package/src/views/helpers/tags.js +86 -3
  76. package/types/build/resolve-peer.d.mts +13 -0
  77. package/types/client/dom.d.ts +55 -0
  78. package/types/client/form.d.ts +19 -0
  79. package/types/client/index.d.ts +20 -0
  80. package/types/client/registry.d.ts +53 -0
  81. package/types/client/safe-image.d.ts +19 -0
  82. package/types/client/shared-cookie.d.ts +82 -0
  83. package/types/client/store.d.ts +18 -0
  84. package/types/client/swap.d.ts +46 -0
  85. package/types/compile/codegen.d.ts +32 -0
  86. package/types/compile/compile-all.d.ts +42 -0
  87. package/types/compile/errors.d.ts +30 -0
  88. package/types/compile/expr.d.ts +67 -0
  89. package/types/compile/index.d.ts +10 -0
  90. package/types/compile/parse.d.ts +82 -0
  91. package/types/compile/resolve.d.ts +46 -0
  92. package/types/compile/scan-exports.d.ts +9 -0
  93. package/types/config/defaults.d.ts +477 -0
  94. package/types/config/index.d.ts +304 -0
  95. package/types/config/pattern.d.ts +38 -0
  96. package/types/http/control-flow.d.ts +45 -0
  97. package/types/http/cookies-entry.d.ts +5 -0
  98. package/types/http/cookies.d.ts +113 -0
  99. package/types/http/request-cache.d.ts +13 -0
  100. package/types/http/request-context.d.ts +67 -0
  101. package/types/http/shared-cookie.d.ts +73 -0
  102. package/types/index.d.ts +30 -0
  103. package/types/log.d.mts +153 -0
  104. package/types/server/admin/actions.d.ts +16 -0
  105. package/types/server/admin/auth.d.ts +52 -0
  106. package/types/server/admin/event-log.d.ts +38 -0
  107. package/types/server/admin/gate.d.ts +43 -0
  108. package/types/server/admin/inventory.d.ts +40 -0
  109. package/types/server/admin/mount.d.ts +6 -0
  110. package/types/server/admin/router.d.ts +6 -0
  111. package/types/server/admin/snapshot.d.ts +6 -0
  112. package/types/server/assets.d.ts +47 -0
  113. package/types/server/auth/handoff.d.ts +12 -0
  114. package/types/server/cache-deps.d.ts +16 -0
  115. package/types/server/cache-vary.d.ts +30 -0
  116. package/types/server/cloudflare.d.ts +163 -0
  117. package/types/server/create-app.d.ts +25 -0
  118. package/types/server/data-cache.d.ts +116 -0
  119. package/types/server/dev/devtools.d.ts +44 -0
  120. package/types/server/dev/report.d.ts +229 -0
  121. package/types/server/dev/socket.d.ts +17 -0
  122. package/types/server/dev/version-check.d.mts +15 -0
  123. package/types/server/ejs-adapter.d.ts +11 -0
  124. package/types/server/head-hints.d.ts +40 -0
  125. package/types/server/html-cache.d.ts +207 -0
  126. package/types/server/image-optimizer.d.ts +68 -0
  127. package/types/server/logs/access-middleware.d.ts +7 -0
  128. package/types/server/logs/file-sink.d.ts +17 -0
  129. package/types/server/logs/pipeline.d.ts +37 -0
  130. package/types/server/logs/s3-put.d.ts +85 -0
  131. package/types/server/logs/s3-sink.d.ts +26 -0
  132. package/types/server/metadata.d.ts +38 -0
  133. package/types/server/middleware/compression.d.ts +17 -0
  134. package/types/server/middleware/csrf.d.ts +4 -0
  135. package/types/server/middleware/dev-gate.d.ts +2 -0
  136. package/types/server/middleware/headers.d.ts +2 -0
  137. package/types/server/middleware/redirects.d.ts +2 -0
  138. package/types/server/middleware/robots-txt.d.ts +33 -0
  139. package/types/server/middleware/static-precompressed.d.ts +5 -0
  140. package/types/server/middleware/trailing-slash.d.ts +11 -0
  141. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  142. package/types/server/og-image.d.ts +149 -0
  143. package/types/server/port-guard.d.ts +50 -0
  144. package/types/server/prewarm.d.ts +131 -0
  145. package/types/server/redis.d.ts +163 -0
  146. package/types/server/render.d.ts +101 -0
  147. package/types/server/router.d.ts +5 -0
  148. package/types/server/status-page.d.ts +24 -0
  149. package/types/server/upstream-limiter.d.ts +123 -0
  150. package/types/server/upstream-tracking.d.ts +42 -0
  151. package/types/shared/cookie-domain.d.ts +29 -0
  152. package/types/templates/layout.render.d.ts +7 -0
  153. package/types/version.d.mts +10 -0
  154. package/types/views/components/loader.d.ts +5 -0
  155. package/types/views/helpers/html.d.ts +39 -0
  156. package/types/views/helpers/tags.d.ts +127 -0
@@ -19,7 +19,7 @@ import {
19
19
  * @param {{ quiet?: boolean }} [options]
20
20
  * @returns {Promise<{ count: number, manifest: Record<string, string>, outDir: string }>}
21
21
  */
22
- export async function compileAll(config, options = {}) {
22
+ export async function compileAll(config, _options = {}) {
23
23
  const outDir = path.join(config.dirs.generated, "templates");
24
24
  const viewRoots = getViewRoots(config);
25
25
  const jskFiles = discoverJskFiles(viewRoots);
@@ -32,7 +32,7 @@ export async function compileAll(config, options = {}) {
32
32
 
33
33
  /** @type {Record<string, string>} */
34
34
  const manifest = {};
35
- /** @type {Map<string, { code: string, includes: string[] }>} */
35
+ /** @type {Map<string, { code: string, includes: string[], includeEntries: { id: string, index: number }[], source: string, relative: string }>} */
36
36
  const compiled = new Map();
37
37
 
38
38
  for (const [viewId, file] of jskFiles) {
@@ -47,34 +47,25 @@ export async function compileAll(config, options = {}) {
47
47
  throw error;
48
48
  }
49
49
 
50
- const { code, includes, components: usedComponents } = codegen(ast, {
50
+ const { code, includes, includeEntries } = codegen(ast, {
51
51
  viewId,
52
52
  file: relative,
53
- knownComponents: null,
53
+ knownComponents,
54
54
  });
55
55
 
56
- for (const name of usedComponents) {
57
- if (knownComponents.has(name)) continue;
58
- const camel = name.charAt(0).toLowerCase() + name.slice(1);
59
- if (knownComponents.has(camel)) continue;
60
- if (!options.quiet) {
61
- console.warn(`[jsk] ${relative}: component "${name}" was not found at compile time`);
62
- }
63
- }
64
-
65
- compiled.set(viewId, { code, includes });
56
+ compiled.set(viewId, { code, includes, includeEntries, source, relative });
66
57
  }
67
58
 
68
59
  // Include hedefleri mevcut mu?
69
- for (const [viewId, { includes }] of compiled) {
70
- for (const id of includes) {
60
+ for (const [, { includeEntries, source, relative }] of compiled) {
61
+ for (const { id, index } of includeEntries) {
71
62
  const normalized = normalizeIncludeId(id);
72
63
  if (!compiled.has(normalized) && !jskFiles.has(normalized)) {
73
- const file = jskFiles.get(viewId);
74
- const relative = file
75
- ? path.relative(config.root, file).split(path.sep).join("/")
76
- : viewId;
77
- throw new CompileError(`Unknown include "${id}"`, { file: relative });
64
+ throw new CompileError(`Unknown include "${id}"`, {
65
+ file: relative,
66
+ source,
67
+ index,
68
+ });
78
69
  }
79
70
  }
80
71
  }
@@ -278,6 +278,11 @@ class ExprParser {
278
278
  let node = this.parsePrimary();
279
279
  for (;;) {
280
280
  this.skipWs();
281
+ if (this.peek() === "(") {
282
+ throw this.error(
283
+ "Function calls are not allowed in .jsk; move logic to a JS component or controller",
284
+ );
285
+ }
281
286
  if (this.match("?.")) {
282
287
  this.skipWs();
283
288
  if (this.peek() === "[") {
@@ -64,6 +64,8 @@ class TemplateParser {
64
64
  this.source = source;
65
65
  this.file = file;
66
66
  this.i = 0;
67
+ /** @type {string[]} Açık {#if}/{#each} kapanışları — EOF'ta öncelikli hata. */
68
+ this.blockStack = [];
67
69
  }
68
70
 
69
71
  /**
@@ -121,6 +123,13 @@ class TemplateParser {
121
123
  break;
122
124
  }
123
125
 
126
+ // `<!DOCTYPE …>` / `<![CDATA[…]]>` — etiket değil; metin olarak bas.
127
+ // İlerletmezsek `<` + `!` dalı parseText'te de takılıp sonsuz döngü olur.
128
+ if (this.startsWith("<!")) {
129
+ nodes.push(this.parseDeclaration());
130
+ continue;
131
+ }
132
+
124
133
  if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
125
134
  nodes.push(this.parseTag());
126
135
  continue;
@@ -131,6 +140,35 @@ class TemplateParser {
131
140
  return nodes;
132
141
  }
133
142
 
143
+ /**
144
+ * `<!DOCTYPE html>` gibi bildirimler. Kapanış `>` ilk tırnak dışı geçiş.
145
+ * @returns {TextNode}
146
+ */
147
+ parseDeclaration() {
148
+ const start = this.i;
149
+ this.i += 2; // <!
150
+ let quote = null;
151
+ while (!this.eof()) {
152
+ const ch = this.peek();
153
+ if (quote) {
154
+ if (ch === quote) quote = null;
155
+ this.i += 1;
156
+ continue;
157
+ }
158
+ if (ch === '"' || ch === "'") {
159
+ quote = ch;
160
+ this.i += 1;
161
+ continue;
162
+ }
163
+ if (ch === ">") {
164
+ this.i += 1;
165
+ break;
166
+ }
167
+ this.i += 1;
168
+ }
169
+ return { type: "Text", value: this.source.slice(start, this.i) };
170
+ }
171
+
134
172
  skipComment() {
135
173
  const end = this.source.indexOf("-->", this.i + 4);
136
174
  if (end === -1) throw this.error("Unterminated HTML comment");
@@ -217,13 +255,19 @@ class TemplateParser {
217
255
  });
218
256
  this.i = closeBrace + 1;
219
257
 
220
- const consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
258
+ this.blockStack.push("{/if}");
259
+ /** @type {AstNode[]} */
260
+ let consequent;
221
261
  /** @type {AstNode[]} */
222
262
  let alternate = [];
223
-
224
- if (this.startsWith("{#else}")) {
225
- this.i += "{#else}".length;
226
- alternate = this.parseNodesUntil(["{/if}"]);
263
+ try {
264
+ consequent = this.parseNodesUntil(["{#else}", "{/if}"]);
265
+ if (this.startsWith("{#else}")) {
266
+ this.i += "{#else}".length;
267
+ alternate = this.parseNodesUntil(["{/if}"]);
268
+ }
269
+ } finally {
270
+ this.blockStack.pop();
227
271
  }
228
272
 
229
273
  if (!this.startsWith("{/if}")) {
@@ -263,6 +307,10 @@ class TemplateParser {
263
307
  continue;
264
308
  }
265
309
  if (this.startsWith("</")) break;
310
+ if (this.startsWith("<!")) {
311
+ nodes.push(this.parseDeclaration());
312
+ continue;
313
+ }
266
314
  if (this.peek() === "<" && /[A-Za-z]/.test(this.peek(1))) {
267
315
  nodes.push(this.parseTag());
268
316
  continue;
@@ -308,7 +356,14 @@ class TemplateParser {
308
356
  templateIndex: this.i,
309
357
  });
310
358
  this.i = closeBrace + 1;
311
- const children = this.parseNodesUntil(["{/each}"]);
359
+ this.blockStack.push("{/each}");
360
+ /** @type {AstNode[]} */
361
+ let children;
362
+ try {
363
+ children = this.parseNodesUntil(["{/each}"]);
364
+ } finally {
365
+ this.blockStack.pop();
366
+ }
312
367
  if (!this.startsWith("{/each}")) {
313
368
  throw this.error("Expected {/each}", this.i);
314
369
  }
@@ -369,9 +424,10 @@ class TemplateParser {
369
424
  let children = [];
370
425
  if (!selfClosing && !voidEl) {
371
426
  children = this.parseNodesUntil([`</${name}>`]);
372
- // Also allow parseNodes style — check closing tag
373
427
  if (!this.startsWith(`</${name}>`)) {
374
- // try case-sensitive exact
428
+ if (this.eof() && this.blockStack.length) {
429
+ throw this.error(`Expected ${this.blockStack.at(-1)}`, this.i);
430
+ }
375
431
  throw this.error(`Expected </${name}>`, this.i);
376
432
  }
377
433
  this.i += `</${name}>`.length;
@@ -131,6 +131,9 @@ export function collectKnownComponents(componentDirs, jskFiles) {
131
131
  "Icon",
132
132
  "CsrfField",
133
133
  "PreloadImage",
134
+ "Stylesheets",
135
+ "BodyScripts",
136
+ "JsonLd",
134
137
  ]);
135
138
 
136
139
  /** @type {Map<string, string>} export adı → göreli yol */
@@ -106,18 +106,34 @@ export const DEFAULT_PREWARM_ON_VISIT = {
106
106
  /** Sayfa başına üstten alta en fazla kaç link kuyruğa alınır. */
107
107
  perPage: 20,
108
108
  /**
109
- * Paralel işçi; `null` → klasik prewarm ile aynı varsayılan
110
- * (prod 4 / dev 1) `startPrewarm` içinde çözülür.
109
+ * Paralel işçi. `null` yalnızca kapalı modda durur; açıkken tavan
110
+ * (`ON_VISIT_CONCURRENCY_CEILING`) uygulanır. Klasik prewarm'daki
111
+ * prod 4 burada geçerli değil — onVisit sürekli çalışır.
111
112
  * @type {number | null}
112
113
  */
113
114
  concurrency: null,
114
115
  /**
115
- * Saniyedeki istek tavanı; `null` → klasik ile aynı (prod 0 / dev 4).
116
+ * Saniyedeki istek tavanı. `null` yalnızca kapalı modda durur; açıkken
117
+ * `0` (sınırsız) dahil her şey `ON_VISIT_RPS_CEILING` ile kesilir.
116
118
  * @type {number | null}
117
119
  */
118
120
  rps: null,
119
121
  };
120
122
 
123
+ /**
124
+ * onVisit sürekli tur olduğu için klasik prewarm'daki "sınırsız" burada yok.
125
+ * Config daha yükseğini yazsa da çözümlenen değer bu tavanları geçemez.
126
+ */
127
+ export const ON_VISIT_PER_PAGE_CEILING = 20;
128
+ export const ON_VISIT_RPS_CEILING = 2;
129
+ export const ON_VISIT_CONCURRENCY_CEILING = 2;
130
+
131
+ /**
132
+ * Bekleyen onVisit yolları. Bir sayfa düzinelerce link basınca kuyruk
133
+ * birikmesin; taşan link bu turda alınmaz.
134
+ */
135
+ export const ON_VISIT_QUEUE_MAX = 64;
136
+
121
137
  /** `onVisit` açıkken `cache().prewarm` kökünde yasak olan klasik alanlar. */
122
138
  export const CLASSIC_PREWARM_KEYS = [
123
139
  "enabled",
@@ -136,9 +152,26 @@ export const CLASSIC_PREWARM_KEYS = [
136
152
  * HTML önbelleğinin girdi sınırı. 500 girdi ortalama bir sayfa boyutunda
137
153
  * yaklaşık 100-200 MB tutar; uzun kuyruklu siteler bunu yükseltmek yerine
138
154
  * veri önbelleğine yaslanmalı (bkz. `DEFAULT_DATA_CACHE`).
155
+ *
156
+ * Config daha yükseğini istese de girdi sayısı `HTML_CACHE_MAX_ENTRIES_CEILING`
157
+ * değerini geçemez. Asıl bellek freni bayt bütçesidir: şişman sayfa ve
158
+ * `vary.host` kopyası sayı tavanının altında da RSS'i şişirir.
139
159
  */
140
160
  export const DEFAULT_HTML_CACHE_MAX_ENTRIES = 500;
141
161
 
162
+ /** `cache().maxEntries` için sert tavan. Üstü uyarıyla bu değere çekilir. */
163
+ export const HTML_CACHE_MAX_ENTRIES_CEILING = 800;
164
+
165
+ /**
166
+ * Süreç içi HTML string + sıkıştırılmış gövde tavanı (256 MB).
167
+ * `install()` bunu uygular; config yükseltemez. Tek sayfa bütçeden büyükse
168
+ * saklanmaz, yanıt yine gider.
169
+ */
170
+ export const HTML_CACHE_BYTE_BUDGET = 256 * 1024 * 1024;
171
+
172
+ /** `cache().data.maxEntries` için sert tavan. Uzun kuyruk burada durur, HTML'de değil. */
173
+ export const DATA_CACHE_MAX_ENTRIES_CEILING = 20_000;
174
+
142
175
  /**
143
176
  * `notFound()` geçici bir upstream hatasına denk geldiğinde sayfanın kaç kez
144
177
  * daha denenmesi gerektiği.
@@ -467,9 +500,19 @@ export const DEFAULT_BRAND = {
467
500
  */
468
501
  export const DEFAULT_AUTH = {
469
502
  /**
470
- * `true` veya `{ ttlSeconds?, path?, maxValueBytes? }`.
503
+ * `true` veya `{ allowedCookieNames, ttlSeconds?, path?, maxValueBytes?,
504
+ * maxPendingTickets?, maxMintsPerIpPerMinute? }`.
471
505
  * Açıkken `POST /_jskelet/auth/handoff` ve `?handoff=` redeem middleware'i.
472
- * @type {boolean | { enabled?: boolean, ttlSeconds?: number, path?: string, maxValueBytes?: number }}
506
+ * Mint için `allowedCookieNames` dolu olmalı (aksi halde 400).
507
+ * @type {boolean | {
508
+ * enabled?: boolean,
509
+ * allowedCookieNames?: string[],
510
+ * ttlSeconds?: number,
511
+ * path?: string,
512
+ * maxValueBytes?: number,
513
+ * maxPendingTickets?: number,
514
+ * maxMintsPerIpPerMinute?: number,
515
+ * }}
473
516
  */
474
517
  crossSubdomainHandoff: false,
475
518
  };
@@ -37,10 +37,15 @@ import {
37
37
  DEFAULT_AUTH,
38
38
  DEFAULT_BRAND,
39
39
  DEFAULT_CLOUDFLARE,
40
+ DATA_CACHE_MAX_ENTRIES_CEILING,
40
41
  DEFAULT_DATA_CACHE,
41
42
  DEFAULT_DEV_GATE_BYPASS,
42
43
  DEFAULT_DIRS,
43
44
  DEFAULT_HTML_CACHE_MAX_ENTRIES,
45
+ HTML_CACHE_MAX_ENTRIES_CEILING,
46
+ ON_VISIT_CONCURRENCY_CEILING,
47
+ ON_VISIT_PER_PAGE_CEILING,
48
+ ON_VISIT_RPS_CEILING,
44
49
  DEFAULT_IMAGES,
45
50
  DEFAULT_LOGS,
46
51
  DEFAULT_NAVIGATION,
@@ -134,6 +139,8 @@ const CONFIG_FILE = "jskelet.config.mjs";
134
139
  * @property {string[] | null} routes Açık route modülü listesi.
135
140
  * @property {boolean} trailingSlash URL'ler `/` ile bitsin mi (Next `trailingSlash`).
136
141
  * @property {{ extensions: Set<string>, prefixes: string[] }} static
142
+ * @property {boolean} devGate `DEV_TOKEN` tek başına siteyi kilitlemez; gate
143
+ * ancak bu bayrak veya `DEV_GATE=1` ile açılır.
137
144
  * @property {string[]} devGateBypass
138
145
  * @property {string[]} preconnect
139
146
  * @property {NavigationConfig} navigation
@@ -348,6 +355,22 @@ function normalizeUpstream(raw) {
348
355
  };
349
356
  }
350
357
 
358
+ /**
359
+ * Dev gate. `DEV_TOKEN` ortamda durması siteyi kilitlemez: paylaşılan bir
360
+ * task tanımı production'a da aynı değişkeni taşır ve herkese 404 olur.
361
+ * Gate ancak `devGate: true` ya da `DEV_GATE=1` ile açılır. `DEV_GATE=0`
362
+ * config'teki açığı da kapatır.
363
+ *
364
+ * @param {Record<string, any>} source
365
+ * @returns {boolean}
366
+ */
367
+ function normalizeDevGate(source) {
368
+ const env = process.env.DEV_GATE;
369
+ if (env === "0" || env === "false") return false;
370
+ if (env === "1" || env === "true") return true;
371
+ return source.devGate === true;
372
+ }
373
+
351
374
  /**
352
375
  * Yönetim paneli. `enabled` yalnızca açıkça `true` verildiğinde ya da
353
376
  * `JSKELET_ADMIN` ortam değişkeni ayarlandığında açılır: paneli yanlışlıkla
@@ -701,16 +724,24 @@ function normalizeCache(raw) {
701
724
  const prewarm = normalizePrewarm(raw?.prewarm);
702
725
  const queryRules = normalizeQueryRules(raw?.query);
703
726
  const maxEntries = Number(raw?.maxEntries);
727
+ let htmlMaxEntries =
728
+ Number.isFinite(maxEntries) && maxEntries > 0
729
+ ? Math.floor(maxEntries)
730
+ : DEFAULT_HTML_CACHE_MAX_ENTRIES;
731
+ if (htmlMaxEntries > HTML_CACHE_MAX_ENTRIES_CEILING) {
732
+ console.warn(
733
+ `[config] cache().maxEntries ${htmlMaxEntries} exceeds the ceiling of ` +
734
+ `${HTML_CACHE_MAX_ENTRIES_CEILING}; using ${HTML_CACHE_MAX_ENTRIES_CEILING}`,
735
+ );
736
+ htmlMaxEntries = HTML_CACHE_MAX_ENTRIES_CEILING;
737
+ }
704
738
 
705
739
  return {
706
740
  html,
707
741
  cacheQuery: queryRules,
708
742
  cacheVary: normalizeVary(raw?.vary),
709
- htmlMaxEntries:
710
- Number.isFinite(maxEntries) && maxEntries > 0
711
- ? Math.floor(maxEntries)
712
- : DEFAULT_HTML_CACHE_MAX_ENTRIES,
713
- data: { ...DEFAULT_DATA_CACHE, ...(raw?.data ?? {}) },
743
+ htmlMaxEntries,
744
+ data: normalizeDataCache(raw?.data),
714
745
  // Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
715
746
  // bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
716
747
  trackUpstream: raw?.trackUpstream !== false,
@@ -732,14 +763,110 @@ function normalizeCache(raw) {
732
763
  };
733
764
  }
734
765
 
766
+ /**
767
+ * İstenen pozitif tamsayı tavanı aşıyorsa uyarı basıp tavana çeker.
768
+ *
769
+ * @param {string} field
770
+ * @param {number} requested
771
+ * @param {number} ceiling
772
+ * @returns {number}
773
+ */
774
+ function clampCeiling(field, requested, ceiling) {
775
+ if (requested <= ceiling) return requested;
776
+ console.warn(
777
+ `[config] ${field} ${requested} exceeds the ceiling of ${ceiling}; using ${ceiling}`,
778
+ );
779
+ return ceiling;
780
+ }
781
+
782
+ /**
783
+ * Veri önbelleği JSON tutar; sınır HTML'den yüksek olabilir ama sonsuz değil.
784
+ *
785
+ * @param {unknown} raw
786
+ * @returns {{ maxEntries: number, staleFactor: number }}
787
+ */
788
+ function normalizeDataCache(raw) {
789
+ const source =
790
+ raw && typeof raw === "object" && !Array.isArray(raw)
791
+ ? /** @type {Record<string, unknown>} */ (raw)
792
+ : {};
793
+ const requested = Number(source.maxEntries);
794
+ let maxEntries =
795
+ Number.isFinite(requested) && requested > 0
796
+ ? Math.floor(requested)
797
+ : DEFAULT_DATA_CACHE.maxEntries;
798
+ maxEntries = clampCeiling(
799
+ "cache().data.maxEntries",
800
+ maxEntries,
801
+ DATA_CACHE_MAX_ENTRIES_CEILING,
802
+ );
803
+
804
+ const stale = Number(source.staleFactor);
805
+ return {
806
+ maxEntries,
807
+ staleFactor:
808
+ Number.isFinite(stale) && stale >= 0 ? stale : DEFAULT_DATA_CACHE.staleFactor,
809
+ };
810
+ }
811
+
812
+ /**
813
+ * onVisit sürekli çalışır. Klasik turdaki `rps: 0` (sınırsız) burada her
814
+ * ziyaretçide yeniden crawl demek; boş, `0` ve tavanın üstü 2'ye çekilir.
815
+ *
816
+ * @param {Record<string, unknown>} source
817
+ * @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
818
+ */
819
+ function resolveOnVisitLimits(source) {
820
+ const perPageRaw = Number(source.perPage);
821
+ const perPage = clampCeiling(
822
+ "cache().prewarm.onVisit.perPage",
823
+ Number.isFinite(perPageRaw) && perPageRaw > 0
824
+ ? Math.floor(perPageRaw)
825
+ : DEFAULT_PREWARM_ON_VISIT.perPage,
826
+ ON_VISIT_PER_PAGE_CEILING,
827
+ );
828
+
829
+ const concurrencyRaw = Number(source.concurrency);
830
+ const concurrency = clampCeiling(
831
+ "cache().prewarm.onVisit.concurrency",
832
+ Number.isFinite(concurrencyRaw) && concurrencyRaw > 0
833
+ ? Math.floor(concurrencyRaw)
834
+ : ON_VISIT_CONCURRENCY_CEILING,
835
+ ON_VISIT_CONCURRENCY_CEILING,
836
+ );
837
+
838
+ const rpsRaw = Number(source.rps);
839
+ let rps = ON_VISIT_RPS_CEILING;
840
+ if (source.rps != null && source.rps !== "") {
841
+ if (!Number.isFinite(rpsRaw) || rpsRaw <= 0) {
842
+ console.warn(
843
+ `[config] cache().prewarm.onVisit.rps ${source.rps} is not a positive rate; ` +
844
+ `using ${ON_VISIT_RPS_CEILING}`,
845
+ );
846
+ } else {
847
+ rps = clampCeiling(
848
+ "cache().prewarm.onVisit.rps",
849
+ rpsRaw,
850
+ ON_VISIT_RPS_CEILING,
851
+ );
852
+ }
853
+ }
854
+
855
+ return {
856
+ ...DEFAULT_PREWARM_ON_VISIT,
857
+ enabled: source.enabled !== false,
858
+ perPage,
859
+ concurrency,
860
+ rps,
861
+ };
862
+ }
863
+
735
864
  /**
736
865
  * @param {unknown} raw
737
866
  * @returns {{ enabled: boolean } & typeof DEFAULT_PREWARM_ON_VISIT}
738
867
  */
739
868
  function normalizeOnVisit(raw) {
740
- if (raw === true) {
741
- return { ...DEFAULT_PREWARM_ON_VISIT, enabled: true };
742
- }
869
+ if (raw === true) return resolveOnVisitLimits({});
743
870
 
744
871
  if (raw == null || raw === false) {
745
872
  return { ...DEFAULT_PREWARM_ON_VISIT, enabled: false };
@@ -751,24 +878,7 @@ function normalizeOnVisit(raw) {
751
878
  );
752
879
  }
753
880
 
754
- const source = /** @type {Record<string, unknown>} */ (raw);
755
- const perPage = Number(source.perPage);
756
- const concurrency = Number(source.concurrency);
757
- const rps = Number(source.rps);
758
-
759
- return {
760
- ...DEFAULT_PREWARM_ON_VISIT,
761
- enabled: source.enabled !== false,
762
- perPage:
763
- Number.isFinite(perPage) && perPage > 0
764
- ? Math.floor(perPage)
765
- : DEFAULT_PREWARM_ON_VISIT.perPage,
766
- concurrency:
767
- Number.isFinite(concurrency) && concurrency > 0
768
- ? Math.floor(concurrency)
769
- : null,
770
- rps: Number.isFinite(rps) && rps >= 0 ? rps : null,
771
- };
881
+ return resolveOnVisitLimits(/** @type {Record<string, unknown>} */ (raw));
772
882
  }
773
883
 
774
884
  /**
@@ -1138,7 +1248,7 @@ function resolveLayout(dirs, override) {
1138
1248
  const appLayout = path.join(dirs.views, "layout.ejs");
1139
1249
  if (fs.existsSync(appLayout)) return appLayout;
1140
1250
 
1141
- return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.ejs");
1251
+ return path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
1142
1252
  }
1143
1253
 
1144
1254
  /**
@@ -1255,6 +1365,7 @@ export async function loadConfig(options = {}) {
1255
1365
  extensions: new Set(source.static?.extensions ?? DEFAULT_STATIC.extensions),
1256
1366
  prefixes: source.static?.prefixes ?? DEFAULT_STATIC.prefixes,
1257
1367
  },
1368
+ devGate: normalizeDevGate(source),
1258
1369
  devGateBypass: source.devGateBypass ?? DEFAULT_DEV_GATE_BYPASS,
1259
1370
  preconnect: source.preconnect ?? [],
1260
1371
  navigation: normalizeNavigation(source.navigation, brand),
@@ -138,6 +138,18 @@ function escapeRegExp(value) {
138
138
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
139
139
  }
140
140
 
141
+ /**
142
+ * Bekleyen hata kutusunu hemen bas. Alt süreç exit/close olduğunda 60 ms
143
+ * zamanlayıcıya güvenilmez: parent `shutdown` ile ölürse satır hiç görünmez.
144
+ */
145
+ function flushErrorNow() {
146
+ if (errorTimer) {
147
+ clearTimeout(errorTimer);
148
+ errorTimer = null;
149
+ }
150
+ flushError();
151
+ }
152
+
141
153
  /**
142
154
  * @param {string} line
143
155
  * @param {boolean} isError
@@ -174,8 +186,9 @@ function onServerLine(line, isError) {
174
186
 
175
187
  if (!line.trim()) return;
176
188
 
177
- // Hata gövdesi ve yığın satırları kutuya toplanır.
178
- if (isError || /^\s+at\s/.test(line) || /Error\b/.test(line)) {
189
+ // Yığın / Error: kutuya. Düz stderr (port dolu vb.) anında — süreç hemen
190
+ // exit 1 ile öldüğü için gecikmeli flush'a kalırsa satır kayboluyor.
191
+ if (/^\s+at\s/.test(line) || /Error\b/.test(line)) {
179
192
  errorBuffer.push(
180
193
  line.replace(/^\[(?:uncaughtException|unhandledRejection)\]\s*/, ""),
181
194
  );
@@ -183,6 +196,11 @@ function onServerLine(line, isError) {
183
196
  return;
184
197
  }
185
198
 
199
+ if (isError) {
200
+ log.error(line.trim());
201
+ return;
202
+ }
203
+
186
204
  log.event({ scope: "server", message: line });
187
205
  }
188
206
 
@@ -205,9 +223,12 @@ function run(args, label, passthrough) {
205
223
  passthrough ? onBuildLine(line) : onServerLine(line, true),
206
224
  );
207
225
 
208
- child.on("exit", (code) => {
226
+ // `close`: stdio boşaldıktan sonra. `exit` ile yarışınca port hatası
227
+ // stderr'de kalıp "server exited" dışında bir şey görünmüyordu.
228
+ child.on("close", (code) => {
209
229
  if (code === 0 || code === null) return;
210
230
  if (child[STOPPING]) return; // yeniden başlatma: beklenen çıkış
231
+ if (!passthrough) flushErrorNow();
211
232
  log.error(`${label} exited (code ${code})`);
212
233
  shutdown(code);
213
234
  });
@@ -249,6 +270,7 @@ let server = null;
249
270
 
250
271
  // `--import` modül belirteci bekler: Windows'ta `H:\…` yolu `h:` şemalı URL
251
272
  // sanılıp reddediliyor, bu yüzden file:// URL'e çevrilir.
273
+ // `--murder` sunucu sürecine iletilir: dolu portta dinleyen öldürülüp başlanır.
252
274
  const SERVER_ARGS = [
253
275
  // `.env` yoksa bayrak hiç geçilmez: `--env-file-if-exists` dosya yokken bir
254
276
  // bildirim satırı basıyor ve bu satır hata kutusuna dönüşüyordu.
@@ -256,6 +278,7 @@ const SERVER_ARGS = [
256
278
  "--import",
257
279
  pathToFileURL(path.join(SRC, "runtime", "register.mjs")).href,
258
280
  path.join(SRC, "start.mjs"),
281
+ ...(process.argv.includes("--murder") ? ["--murder"] : []),
259
282
  ];
260
283
 
261
284
  function startServerProcess() {
@@ -4,6 +4,7 @@
4
4
  export {
5
5
  clearCookie,
6
6
  getSignedCookie,
7
+ isValidCookieName,
7
8
  parseCookies,
8
9
  randomToken,
9
10
  safeEqual,
@@ -148,6 +148,17 @@ export function parseCookies(req) {
148
148
  * @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
149
149
  */
150
150
 
151
+ /**
152
+ * RFC 6265 cookie-name (token). `;`, boşluk, CRLF gibi karakterler
153
+ * Set-Cookie enjeksiyonuna yol açardı — reddedilir.
154
+ *
155
+ * @param {unknown} name
156
+ * @returns {boolean}
157
+ */
158
+ export function isValidCookieName(name) {
159
+ return typeof name === "string" && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
160
+ }
161
+
151
162
  /**
152
163
  * Varsayılanlar bilinçli olarak kısıtlayıcı: `HttpOnly` ile JS okuyamaz,
153
164
  * `SameSite=Lax` ile çapraz site POST'larında gönderilmez (CSRF'nin büyük
@@ -159,6 +170,13 @@ export function parseCookies(req) {
159
170
  * @returns {string}
160
171
  */
161
172
  export function serializeCookie(name, value, options = {}) {
173
+ if (!isValidCookieName(name)) {
174
+ throw new Error(
175
+ `[cookies] invalid cookie name ${JSON.stringify(name)}; ` +
176
+ "names must be RFC 6265 tokens (no spaces, semicolons, or control chars)",
177
+ );
178
+ }
179
+
162
180
  const parts = [`${name}=${encodeURIComponent(value)}`];
163
181
 
164
182
  parts.push(`Path=${options.path ?? "/"}`);
package/src/logo.png CHANGED
Binary file