jskelet 0.5.5 → 0.6.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 (149) hide show
  1. package/AGENTS.md +18 -13
  2. package/CHANGELOG.md +95 -0
  3. package/README.md +9 -7
  4. package/bin/jskelet.mjs +24 -10
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +4 -3
  7. package/docs/03-routing.md +11 -6
  8. package/docs/04-render-ve-sablonlar.md +35 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/07-yapilandirma.md +40 -18
  11. package/docs/08-build.md +15 -9
  12. package/docs/09-dev-araclari.md +5 -1
  13. package/docs/10-dagitim.md +6 -1
  14. package/docs/11-tasima.md +51 -17
  15. package/docs/12-panel-ve-oturum.md +10 -4
  16. package/docs/README.md +7 -5
  17. package/docs/en/01-getting-started.md +4 -3
  18. package/docs/en/02-architecture.md +5 -5
  19. package/docs/en/03-routing.md +12 -7
  20. package/docs/en/04-rendering.md +47 -59
  21. package/docs/en/05-islands.md +13 -8
  22. package/docs/en/07-configuration.md +40 -20
  23. package/docs/en/08-build.md +16 -10
  24. package/docs/en/09-dev-tools.md +6 -1
  25. package/docs/en/10-deployment.md +6 -1
  26. package/docs/en/11-migration.md +51 -16
  27. package/docs/en/12-dashboards-and-sessions.md +9 -4
  28. package/docs/en/README.md +7 -5
  29. package/package.json +49 -14
  30. package/src/build/tasks/client.mjs +91 -10
  31. package/src/build/tasks/icons.mjs +11 -1
  32. package/src/client/index.js +2 -2
  33. package/src/compile/codegen.js +4 -0
  34. package/src/compile/compile-all.js +12 -21
  35. package/src/compile/expr.js +5 -0
  36. package/src/compile/parse.js +64 -8
  37. package/src/compile/resolve.js +3 -0
  38. package/src/config/defaults.js +12 -2
  39. package/src/config/index.js +1 -1
  40. package/src/dev-server.mjs +26 -3
  41. package/src/http/cookies-entry.js +1 -0
  42. package/src/http/cookies.js +18 -0
  43. package/src/logo.png +0 -0
  44. package/src/migrate/apply.mjs +262 -0
  45. package/src/migrate/babel.mjs +79 -0
  46. package/src/migrate/classify.mjs +155 -0
  47. package/src/migrate/config.mjs +126 -0
  48. package/src/migrate/fs-walk.mjs +191 -0
  49. package/src/migrate/parse.mjs +26 -0
  50. package/src/migrate/scan.mjs +177 -0
  51. package/src/migrate/transform/expr-source.mjs +168 -0
  52. package/src/migrate/transform/island.mjs +67 -0
  53. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  54. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  55. package/src/migrate/transform/page-split.mjs +435 -0
  56. package/src/migrate/write.mjs +81 -0
  57. package/src/migrate.mjs +171 -0
  58. package/src/server/auth/handoff.js +94 -11
  59. package/src/server/create-app.js +28 -10
  60. package/src/server/ejs-adapter.js +59 -0
  61. package/src/server/image-optimizer.js +94 -26
  62. package/src/server/port-guard.js +255 -0
  63. package/src/server/render.js +27 -9
  64. package/src/server/status-page.js +105 -4
  65. package/src/start.mjs +18 -3
  66. package/src/templates/layout.ejs +8 -28
  67. package/src/templates/layout.jsk +30 -0
  68. package/src/templates/layout.render.js +41 -0
  69. package/src/views/helpers/tags.js +86 -3
  70. package/types/build/resolve-peer.d.mts +13 -0
  71. package/types/client/dom.d.ts +55 -0
  72. package/types/client/form.d.ts +19 -0
  73. package/types/client/index.d.ts +20 -0
  74. package/types/client/registry.d.ts +53 -0
  75. package/types/client/safe-image.d.ts +19 -0
  76. package/types/client/shared-cookie.d.ts +82 -0
  77. package/types/client/store.d.ts +18 -0
  78. package/types/client/swap.d.ts +46 -0
  79. package/types/compile/codegen.d.ts +32 -0
  80. package/types/compile/compile-all.d.ts +42 -0
  81. package/types/compile/errors.d.ts +30 -0
  82. package/types/compile/expr.d.ts +67 -0
  83. package/types/compile/index.d.ts +10 -0
  84. package/types/compile/parse.d.ts +82 -0
  85. package/types/compile/resolve.d.ts +46 -0
  86. package/types/compile/scan-exports.d.ts +9 -0
  87. package/types/config/defaults.d.ts +449 -0
  88. package/types/config/index.d.ts +299 -0
  89. package/types/config/pattern.d.ts +38 -0
  90. package/types/http/control-flow.d.ts +45 -0
  91. package/types/http/cookies-entry.d.ts +5 -0
  92. package/types/http/cookies.d.ts +113 -0
  93. package/types/http/request-cache.d.ts +13 -0
  94. package/types/http/request-context.d.ts +67 -0
  95. package/types/http/shared-cookie.d.ts +73 -0
  96. package/types/index.d.ts +30 -0
  97. package/types/log.d.mts +153 -0
  98. package/types/server/admin/actions.d.ts +16 -0
  99. package/types/server/admin/auth.d.ts +52 -0
  100. package/types/server/admin/event-log.d.ts +38 -0
  101. package/types/server/admin/gate.d.ts +43 -0
  102. package/types/server/admin/inventory.d.ts +40 -0
  103. package/types/server/admin/mount.d.ts +6 -0
  104. package/types/server/admin/router.d.ts +6 -0
  105. package/types/server/admin/snapshot.d.ts +6 -0
  106. package/types/server/assets.d.ts +47 -0
  107. package/types/server/auth/handoff.d.ts +12 -0
  108. package/types/server/cache-deps.d.ts +16 -0
  109. package/types/server/cache-vary.d.ts +30 -0
  110. package/types/server/cloudflare.d.ts +163 -0
  111. package/types/server/create-app.d.ts +25 -0
  112. package/types/server/data-cache.d.ts +116 -0
  113. package/types/server/dev/devtools.d.ts +44 -0
  114. package/types/server/dev/report.d.ts +229 -0
  115. package/types/server/dev/socket.d.ts +17 -0
  116. package/types/server/dev/version-check.d.mts +15 -0
  117. package/types/server/ejs-adapter.d.ts +11 -0
  118. package/types/server/head-hints.d.ts +40 -0
  119. package/types/server/html-cache.d.ts +173 -0
  120. package/types/server/image-optimizer.d.ts +68 -0
  121. package/types/server/logs/access-middleware.d.ts +7 -0
  122. package/types/server/logs/file-sink.d.ts +17 -0
  123. package/types/server/logs/pipeline.d.ts +37 -0
  124. package/types/server/logs/s3-put.d.ts +85 -0
  125. package/types/server/logs/s3-sink.d.ts +26 -0
  126. package/types/server/metadata.d.ts +38 -0
  127. package/types/server/middleware/compression.d.ts +17 -0
  128. package/types/server/middleware/csrf.d.ts +4 -0
  129. package/types/server/middleware/dev-gate.d.ts +2 -0
  130. package/types/server/middleware/headers.d.ts +2 -0
  131. package/types/server/middleware/redirects.d.ts +2 -0
  132. package/types/server/middleware/static-precompressed.d.ts +5 -0
  133. package/types/server/middleware/trailing-slash.d.ts +11 -0
  134. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  135. package/types/server/og-image.d.ts +149 -0
  136. package/types/server/port-guard.d.ts +50 -0
  137. package/types/server/prewarm.d.ts +128 -0
  138. package/types/server/redis.d.ts +163 -0
  139. package/types/server/render.d.ts +101 -0
  140. package/types/server/router.d.ts +5 -0
  141. package/types/server/status-page.d.ts +24 -0
  142. package/types/server/upstream-limiter.d.ts +123 -0
  143. package/types/server/upstream-tracking.d.ts +42 -0
  144. package/types/shared/cookie-domain.d.ts +29 -0
  145. package/types/templates/layout.render.d.ts +7 -0
  146. package/types/version.d.mts +10 -0
  147. package/types/views/components/loader.d.ts +5 -0
  148. package/types/views/helpers/html.d.ts +39 -0
  149. package/types/views/helpers/tags.d.ts +127 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Dinlenecek port doluysa başlamadan önce net bir hata verilir; `--murder`
3
+ * verilmişse o porttaki dinleyici süreçler öldürülüp bağlama denenir.
4
+ *
5
+ * Windows: `netstat` + `taskkill`. Unix: `lsof` (yoksa `ss`) + `process.kill`.
6
+ * Bu süreçlerin PID'lerini okumak için yerleşik bir Node API'si yok.
7
+ */
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import process from "node:process";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ /**
15
+ * @param {number} ms
16
+ * @returns {Promise<void>}
17
+ */
18
+ function sleep(ms) {
19
+ return new Promise((resolve) => setTimeout(resolve, ms));
20
+ }
21
+
22
+ /**
23
+ * `netstat -ano -p tcp` çıktısından LISTENING PID'lerini çıkarır.
24
+ * `3000` ile `30001` karışmasın diye porttan sonra rakam olmamalı.
25
+ *
26
+ * @param {string} stdout
27
+ * @param {number} port
28
+ * @returns {number[]}
29
+ */
30
+ export function parseWindowsNetstat(stdout, port) {
31
+ const re = new RegExp(
32
+ String.raw`TCP\s+\S*:${port}(?!\d)\s+\S+\s+LISTENING\s+(\d+)`,
33
+ "gi",
34
+ );
35
+ /** @type {number[]} */
36
+ const pids = [];
37
+ for (const match of stdout.matchAll(re)) {
38
+ pids.push(Number(match[1]));
39
+ }
40
+ return pids;
41
+ }
42
+
43
+ /**
44
+ * `lsof -t` çıktısı: satır başına bir PID.
45
+ *
46
+ * @param {string} stdout
47
+ * @returns {number[]}
48
+ */
49
+ export function parseLsofPids(stdout) {
50
+ return stdout
51
+ .split(/\r?\n/)
52
+ .map((line) => Number(line.trim()))
53
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
54
+ }
55
+
56
+ /**
57
+ * `ss -H -lptn` satırlarından `pid=` değerlerini alır.
58
+ *
59
+ * @param {string} stdout
60
+ * @returns {number[]}
61
+ */
62
+ export function parseSsPids(stdout) {
63
+ /** @type {number[]} */
64
+ const pids = [];
65
+ for (const match of stdout.matchAll(/pid=(\d+)/g)) {
66
+ pids.push(Number(match[1]));
67
+ }
68
+ return pids;
69
+ }
70
+
71
+ /**
72
+ * Bu süreç ve ebeveyni öldürülmesin: `--murder` yenilemede kendi CLI'sını
73
+ * ya da henüz dinlemeyen kendisini vurmasın.
74
+ *
75
+ * @param {number} pid
76
+ * @returns {boolean}
77
+ */
78
+ function isSafeToKill(pid) {
79
+ return pid > 0 && pid !== process.pid && pid !== process.ppid;
80
+ }
81
+
82
+ /**
83
+ * @param {number} port
84
+ * @returns {Promise<number[]>}
85
+ */
86
+ async function listWindows(port) {
87
+ const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], {
88
+ windowsHide: true,
89
+ maxBuffer: 10 * 1024 * 1024,
90
+ });
91
+ return parseWindowsNetstat(stdout, port);
92
+ }
93
+
94
+ /**
95
+ * @param {number} port
96
+ * @returns {Promise<number[]>}
97
+ */
98
+ async function listUnix(port) {
99
+ try {
100
+ const { stdout } = await execFileAsync(
101
+ "lsof",
102
+ ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
103
+ { maxBuffer: 1024 * 1024 },
104
+ );
105
+ return parseLsofPids(stdout);
106
+ } catch (error) {
107
+ // lsof yoksa (exit 1 = dinleyen yok; ENOENT = komut yok) ss'e düş.
108
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") {
109
+ return listUnixSs(port);
110
+ }
111
+ const status = /** @type {{ status?: number | null }} */ (error).status;
112
+ if (status === 1) return [];
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {number} port
119
+ * @returns {Promise<number[]>}
120
+ */
121
+ async function listUnixSs(port) {
122
+ try {
123
+ const { stdout } = await execFileAsync(
124
+ "ss",
125
+ ["-H", "-lptn", `sport = :${port}`],
126
+ { maxBuffer: 1024 * 1024 },
127
+ );
128
+ return parseSsPids(stdout);
129
+ } catch (error) {
130
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") {
131
+ return [];
132
+ }
133
+ const status = /** @type {{ status?: number | null }} */ (error).status;
134
+ if (status === 1) return [];
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Portu dinleyen yabancı süreçlerin PID listesi.
141
+ *
142
+ * @param {number} port
143
+ * @returns {Promise<number[]>}
144
+ */
145
+ export async function listListeningPids(port) {
146
+ const raw =
147
+ process.platform === "win32"
148
+ ? await listWindows(port)
149
+ : await listUnix(port);
150
+ return [...new Set(raw)].filter(isSafeToKill);
151
+ }
152
+
153
+ /**
154
+ * @param {number} pid
155
+ * @returns {Promise<void>}
156
+ */
157
+ async function killPid(pid) {
158
+ if (process.platform === "win32") {
159
+ try {
160
+ await execFileAsync("taskkill", ["/PID", String(pid), "/F"], {
161
+ windowsHide: true,
162
+ });
163
+ } catch (error) {
164
+ // Süreç zaten gitmiş olabilir.
165
+ const status = /** @type {{ status?: number | null }} */ (error).status;
166
+ if (status === 128 || status === 1) return;
167
+ throw error;
168
+ }
169
+ return;
170
+ }
171
+
172
+ try {
173
+ process.kill(pid, "SIGTERM");
174
+ } catch (error) {
175
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === "ESRCH") return;
176
+ throw error;
177
+ }
178
+
179
+ await sleep(200);
180
+ try {
181
+ process.kill(pid, 0);
182
+ process.kill(pid, "SIGKILL");
183
+ } catch {
184
+ // ESRCH: SIGTERM yetmiş.
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Porttaki dinleyicileri öldürür ve soketin boşalmasını kısa süre bekler.
190
+ *
191
+ * @param {number} port
192
+ * @returns {Promise<number[]>} öldürülen PID'ler
193
+ */
194
+ export async function murderPort(port) {
195
+ const pids = await listListeningPids(port);
196
+ for (const pid of pids) {
197
+ await killPid(pid);
198
+ }
199
+
200
+ const deadline = Date.now() + 3000;
201
+ while (Date.now() < deadline) {
202
+ const still = await listListeningPids(port);
203
+ if (still.length === 0) return pids;
204
+ await sleep(50);
205
+ }
206
+
207
+ return pids;
208
+ }
209
+
210
+ /**
211
+ * Port doluysa ya net hata fırlatır ya da `--murder` ile temizler.
212
+ *
213
+ * Dinleyici listesi alınamazsa (araç yok / yetki) sessizce geçer; asıl bağlama
214
+ * `EADDRINUSE` ile yine düşer.
215
+ *
216
+ * @param {number} port
217
+ * @param {{ murder?: boolean }} [options]
218
+ * @returns {Promise<void>}
219
+ */
220
+ export async function ensurePortFree(port, options = {}) {
221
+ const murder = options.murder === true;
222
+
223
+ /** @type {number[]} */
224
+ let pids;
225
+ try {
226
+ pids = await listListeningPids(port);
227
+ } catch {
228
+ return;
229
+ }
230
+
231
+ if (pids.length === 0) return;
232
+
233
+ if (!murder) {
234
+ const error = new Error(
235
+ `port ${port} is already in use (pid ${pids.join(", ")}). ` +
236
+ `Pass --murder to kill it and start, or set PORT to another value.`,
237
+ );
238
+ /** @type {NodeJS.ErrnoException} */ (error).code = "EADDRINUSE";
239
+ throw error;
240
+ }
241
+
242
+ console.error(
243
+ `[jskelet] port ${port} in use by pid ${pids.join(", ")} — killing (--murder)`,
244
+ );
245
+ await murderPort(port);
246
+
247
+ const still = await listListeningPids(port);
248
+ if (still.length > 0) {
249
+ const error = new Error(
250
+ `port ${port} still in use after --murder (pid ${still.join(", ")})`,
251
+ );
252
+ /** @type {NodeJS.ErrnoException} */ (error).code = "EADDRINUSE";
253
+ throw error;
254
+ }
255
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * EJS render katmanı + HTML TTL cache (ISR ikamesi).
2
+ * Render katmanı + HTML TTL cache (ISR ikamesi).
3
+ * Varsayılan şablon yolu derlenmiş `.jsk`; EJS opsiyonel legacy peer.
3
4
  *
4
5
  * Controller sözleşmesi:
5
6
  * async (ctx) => { view, data?, metadata?, status?, revalidate?, head?,
@@ -16,10 +17,9 @@ import path from "node:path";
16
17
  import process from "node:process";
17
18
  import fs from "node:fs";
18
19
  import { pathToFileURL } from "node:url";
19
- import ejs from "ejs";
20
20
  import { withHtmlCache } from "./html-cache.js";
21
21
  import { buildVaryPrefix } from "./cache-vary.js";
22
- import { getConfig, hook } from "../config/index.js";
22
+ import { getConfig, hook, FRAMEWORK_ROOT } from "../config/index.js";
23
23
  import { matchPattern } from "../config/pattern.js";
24
24
  import { encodeText, negotiateEncoding } from "./middleware/compression.js";
25
25
  import { navigationHints, preconnectHints } from "./head-hints.js";
@@ -48,15 +48,16 @@ import {
48
48
  getComponentDirs,
49
49
  getViewRoots,
50
50
  } from "../compile/index.js";
51
+ import { renderEjsFile } from "./ejs-adapter.js";
51
52
 
52
53
  const isDev = process.env.NODE_ENV === "development";
53
54
 
54
55
  /**
55
- * Şablonlara otomatik geçen yardımcılar ve EJS ayarları. Bileşen taraması
56
- * dosya sistemine dokunduğu için bir kez yapılır; config yüklenmeden
56
+ * Şablonlara otomatik geçen yardımcılar ve (legacy) EJS ayarları. Bileşen
57
+ * taraması dosya sistemine dokunduğu için bir kez yapılır; config yüklenmeden
57
58
  * hesaplanamaz, bu yüzden ilk render'da kurulur.
58
59
  *
59
- * @type {{ helpers: Record<string, unknown>, options: ejs.Options,
60
+ * @type {{ helpers: Record<string, unknown>, options: Record<string, unknown>,
60
61
  * viewsDir: string, viewRoots: string[], layout: string,
61
62
  * compiled: Map<string, (data: object, helpers: object) => string>,
62
63
  * layoutRender: ((data: object, helpers: object) => string) | null
@@ -89,6 +90,9 @@ async function getEngine() {
89
90
  Icon: (props) => tags.icon(props),
90
91
  CsrfField: () => tags.csrfField(),
91
92
  PreloadImage: (props) => tags.preloadImage(props),
93
+ Stylesheets: (props) => tags.stylesheets(props),
94
+ BodyScripts: (props) => tags.bodyScripts(props),
95
+ JsonLd: (props) => tags.jsonLd(props),
92
96
  };
93
97
 
94
98
  // camelCase JS bileşenlerini PascalCase etiket adıyla da erişilebilir yap.
@@ -99,7 +103,21 @@ async function getEngine() {
99
103
  if (!(pascal in helpers)) helpers[pascal] = value;
100
104
  }
101
105
 
102
- const { compiled, layoutRender } = await loadCompiledTemplates(config, helpers);
106
+ const { compiled, layoutRender: appLayoutRender } = await loadCompiledTemplates(
107
+ config,
108
+ helpers,
109
+ );
110
+
111
+ // Yalnızca framework varsayılan layout'una düşüldüğünde hazır modülü kullan;
112
+ // uygulamanın kendi layout.jsk'si derlenmemişse sessizce framework'e kayma.
113
+ let layoutRender = appLayoutRender;
114
+ if (!layoutRender) {
115
+ const fwLayout = path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
116
+ if (path.resolve(config.layout) === path.resolve(fwLayout)) {
117
+ const fw = await import("../templates/layout.render.js");
118
+ if (typeof fw.render === "function") layoutRender = fw.render;
119
+ }
120
+ }
103
121
 
104
122
  engine = {
105
123
  viewsDir,
@@ -212,7 +230,7 @@ export async function renderView(view, data = {}) {
212
230
  const file =
213
231
  findEjsView(viewRoots.length ? viewRoots : [viewsDir], view) ??
214
232
  path.join(viewsDir, `${view}.ejs`);
215
- return ejs.renderFile(file, { ...helpers, ...data }, options);
233
+ return renderEjsFile(file, { ...helpers, ...data }, options);
216
234
  }
217
235
 
218
236
  /**
@@ -277,7 +295,7 @@ export async function renderPage(page) {
277
295
  );
278
296
  }
279
297
 
280
- return ejs.renderFile(layout, locals, options);
298
+ return renderEjsFile(layout, locals, options);
281
299
  }
282
300
 
283
301
  /**
@@ -5,11 +5,12 @@
5
5
  * "Internal Server Error" çıktısını görmesi istenmiyor; bu yüzden framework
6
6
  * şablonsuz, tek dosyada duran minimal bir HTML üretir.
7
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.
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).
11
12
  *
12
- * Ezme yolları (öncelik sırasıyla):
13
+ * Ezme yolları (öncelik sırasıyla; development 5xx hariç):
13
14
  * 1. `hooks.notFound()` — yalnızca 404 için, geriye dönük uyumluluk.
14
15
  * 2. `hooks.error({ status })` — tüm durumlar için; sayfa tanımı ya da
15
16
  * doğrudan HTML string döner.
@@ -23,11 +24,19 @@ import { esc } from "../views/helpers/html.js";
23
24
  * hata sayfasının kendisi patlarsa ziyaretçi boş yanıt görür, bu yüzden her
24
25
  * başarısızlık gömülü HTML'e düşer.
25
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
+ *
26
31
  * @param {number} status
27
32
  * @param {{ error?: unknown }} [options]
28
33
  * @returns {Promise<string>}
29
34
  */
30
35
  export async function renderStatusPage(status, options = {}) {
36
+ if (process.env.NODE_ENV === "development" && status >= 500) {
37
+ return developmentErrorPage(status, options.error);
38
+ }
39
+
31
40
  const page =
32
41
  (status === 404 ? await hook("notFound", null) : null) ??
33
42
  (await hook("error", null, { status, error: options.error }));
@@ -162,3 +171,95 @@ p { margin: 0.375rem 0 0; color: var(--muted); }
162
171
  </body>
163
172
  </html>`;
164
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
+ }
package/src/start.mjs CHANGED
@@ -1,7 +1,22 @@
1
1
  /**
2
2
  * Prod sunucu girişi. Build çıktısı yoksa önce üretir, sonra dinlemeye başlar.
3
+ *
4
+ * `jskelet start --murder` → dolu porttaki dinleyiciyi öldürüp başlar.
5
+ * Port kontrolü build'den önce: doluysa pahalı üretimi hiç başlatma.
3
6
  */
4
- import "./build/ensure-build.mjs";
5
- import { startServer } from "./server/create-app.js";
7
+ import process from "node:process";
8
+ import { ensurePortFree } from "./server/port-guard.js";
6
9
 
7
- await startServer();
10
+ const port = Number(process.env.PORT ?? 3000);
11
+ const murder = process.argv.includes("--murder");
12
+
13
+ try {
14
+ await ensurePortFree(port, { murder });
15
+ await import("./build/ensure-build.mjs");
16
+ const { startServer } = await import("./server/create-app.js");
17
+ await startServer({ murder, port });
18
+ } catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ process.stderr.write(`${message}\n`);
21
+ process.exit(1);
22
+ }
@@ -1,9 +1,10 @@
1
1
  <%#
2
- JSkelet varsayılan layout'u.
2
+ JSkelet varsayılan layout'unun legacy EJS kopyası.
3
3
 
4
- Uygulama `views/layout.ejs` oluşturduğu anda bu dosya devre dışı kalır;
5
- amacı yeni bir projenin tek route ile çalışabilmesi. Kopyalayıp başlangıç
6
- noktası olarak kullanın.
4
+ Yeni uygulamalar `jskelet/layout` (.jsk) kullanır. Bu dosya yalnızca EJS
5
+ peer'i kurulu eski şablonlar için `jskelet/layout/ejs` üzerinden erişilir.
6
+ Uygulama `views/layout.jsk` veya `views/layout.ejs` oluşturduğu anda
7
+ framework layout'u devre dışı kalır.
7
8
 
8
9
  Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
9
10
  body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
@@ -18,33 +19,12 @@
18
19
  <%# Kaynak ipuçları en başta: preconnect ve LCP preload'ını geciktirmek
19
20
  doğrudan LCP'ye yazılır. Sayfaya özel head buradan gelir. %>
20
21
  <%- extraHead %>
21
- <%# Global sheet + controller `styles: [...]` sayfa sheet'leri.
22
- data-jskelet-css: dev hot-swap doğru link'i bulsun diye. %>
23
- <% if (hasAsset('app.css')) { %>
24
- <link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
25
- <% } %>
26
- <% styles.forEach(function (sheet) { %>
27
- <% if (hasAsset(sheet)) { %>
28
- <link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
29
- <% } %>
30
- <% }); %>
22
+ <%- stylesheets({ styles }) %>
31
23
  <%- headMeta %>
32
- <% structuredData.forEach(function (item) { %>
33
- <script type="application/ld+json"><%- jsonScript(item) %></script>
34
- <% }); %>
24
+ <%- jsonLd({ items: structuredData }) %>
35
25
  </head>
36
26
  <body class="<%= bodyClass %>">
37
27
  <%- body %>
38
- <%# Island runtime; entry yoksa sayfa yine tam çalışır (sadece JS'siz). %>
39
- <% if (hasAsset('main.js')) { %>
40
- <script type="module" src="<%= asset('main.js') %>"></script>
41
- <% } %>
42
- <% entries.forEach(function (entry) { %>
43
- <script type="module" src="<%= asset(entry) %>"></script>
44
- <% }); %>
45
- <%# Dev overlay: prod build'de yok, yalnızca `jskelet dev` sunucusundan. %>
46
- <% if (devtools) { %>
47
- <script type="module" src="<%= devBasePath %>/overlay.js"></script>
48
- <% } %>
28
+ <%- bodyScripts({ entries, devtools, devBasePath }) %>
49
29
  </body>
50
30
  </html>
@@ -0,0 +1,30 @@
1
+ {#
2
+ JSkelet varsayılan layout'u.
3
+
4
+ Uygulama `views/layout.jsk` (veya legacy `views/layout.ejs`) oluşturduğu
5
+ anda bu dosya devre dışı kalır; amacı yeni bir projenin tek route ile
6
+ çalışabilmesi. Kopyalayıp başlangıç noktası olarak kullanın.
7
+
8
+ Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
9
+ body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
10
+ asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
11
+ `hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
12
+
13
+ Stylesheets / JsonLd / BodyScripts: `.jsk` ifade dilinde fonksiyon çağrısı
14
+ olmadığı için asset döngüleri bu yerleşik etiketlere taşındı.
15
+ #}
16
+ <!DOCTYPE html>
17
+ <html :lang="lang">
18
+ <head>
19
+ <meta charset="utf-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1">
21
+ {{{ extraHead }}}
22
+ <Stylesheets :styles="styles" />
23
+ {{{ headMeta }}}
24
+ <JsonLd :items="structuredData" />
25
+ </head>
26
+ <body :class="bodyClass">
27
+ {{{ body }}}
28
+ <BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
29
+ </body>
30
+ </html>
@@ -0,0 +1,41 @@
1
+ /** @generated by scripts/compile-framework-layout.mjs — do not edit */
2
+ /**
3
+ * @param {Record<string, unknown>} data
4
+ * @param {Record<string, any>} helpers
5
+ * @returns {string}
6
+ */
7
+ export function render(data, helpers) {
8
+ let s = "";
9
+ s += "\r\n";
10
+ s += "<!DOCTYPE html>";
11
+ s += "\r\n";
12
+ s += "<html" + helpers.attrs({"lang": data.lang}) + ">";
13
+ s += "\r\n ";
14
+ s += "<head>";
15
+ s += "\r\n ";
16
+ s += "<meta charset=\"utf-8\">";
17
+ s += "\r\n ";
18
+ s += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
19
+ s += "\r\n ";
20
+ s += (data.extraHead ?? "");
21
+ s += "\r\n ";
22
+ s += (helpers.Stylesheets({ "styles": data.styles }) ?? "");
23
+ s += "\r\n ";
24
+ s += (data.headMeta ?? "");
25
+ s += "\r\n ";
26
+ s += (helpers.JsonLd({ "items": data.structuredData }) ?? "");
27
+ s += "\r\n ";
28
+ s += "</head>";
29
+ s += "\r\n ";
30
+ s += "<body" + helpers.attrs({"class": data.bodyClass}) + ">";
31
+ s += "\r\n ";
32
+ s += (data.body ?? "");
33
+ s += "\r\n ";
34
+ s += (helpers.BodyScripts({ "entries": data.entries, "devtools": data.devtools, "devBasePath": data.devBasePath }) ?? "");
35
+ s += "\r\n ";
36
+ s += "</body>";
37
+ s += "\r\n";
38
+ s += "</html>";
39
+ s += "\r\n";
40
+ return s;
41
+ }