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.
- package/AGENTS.md +18 -13
- package/CHANGELOG.md +95 -0
- package/README.md +9 -7
- package/bin/jskelet.mjs +24 -10
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +4 -3
- package/docs/03-routing.md +11 -6
- package/docs/04-render-ve-sablonlar.md +35 -43
- package/docs/05-islands.md +12 -8
- package/docs/07-yapilandirma.md +40 -18
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +5 -1
- package/docs/10-dagitim.md +6 -1
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +7 -5
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +5 -5
- package/docs/en/03-routing.md +12 -7
- package/docs/en/04-rendering.md +47 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/07-configuration.md +40 -20
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +6 -1
- package/docs/en/10-deployment.md +6 -1
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +7 -5
- package/package.json +49 -14
- package/src/build/tasks/client.mjs +91 -10
- package/src/build/tasks/icons.mjs +11 -1
- package/src/client/index.js +2 -2
- package/src/compile/codegen.js +4 -0
- package/src/compile/compile-all.js +12 -21
- package/src/compile/expr.js +5 -0
- package/src/compile/parse.js +64 -8
- package/src/compile/resolve.js +3 -0
- package/src/config/defaults.js +12 -2
- package/src/config/index.js +1 -1
- package/src/dev-server.mjs +26 -3
- package/src/http/cookies-entry.js +1 -0
- package/src/http/cookies.js +18 -0
- package/src/logo.png +0 -0
- package/src/migrate/apply.mjs +262 -0
- package/src/migrate/babel.mjs +79 -0
- package/src/migrate/classify.mjs +155 -0
- package/src/migrate/config.mjs +126 -0
- package/src/migrate/fs-walk.mjs +191 -0
- package/src/migrate/parse.mjs +26 -0
- package/src/migrate/scan.mjs +177 -0
- package/src/migrate/transform/expr-source.mjs +168 -0
- package/src/migrate/transform/island.mjs +67 -0
- package/src/migrate/transform/jsx-to-component.mjs +302 -0
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
- package/src/migrate/transform/page-split.mjs +435 -0
- package/src/migrate/write.mjs +81 -0
- package/src/migrate.mjs +171 -0
- package/src/server/auth/handoff.js +94 -11
- package/src/server/create-app.js +28 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/image-optimizer.js +94 -26
- package/src/server/port-guard.js +255 -0
- package/src/server/render.js +27 -9
- package/src/server/status-page.js +105 -4
- package/src/start.mjs +18 -3
- package/src/templates/layout.ejs +8 -28
- package/src/templates/layout.jsk +30 -0
- package/src/templates/layout.render.js +41 -0
- package/src/views/helpers/tags.js +86 -3
- package/types/build/resolve-peer.d.mts +13 -0
- package/types/client/dom.d.ts +55 -0
- package/types/client/form.d.ts +19 -0
- package/types/client/index.d.ts +20 -0
- package/types/client/registry.d.ts +53 -0
- package/types/client/safe-image.d.ts +19 -0
- package/types/client/shared-cookie.d.ts +82 -0
- package/types/client/store.d.ts +18 -0
- package/types/client/swap.d.ts +46 -0
- package/types/compile/codegen.d.ts +32 -0
- package/types/compile/compile-all.d.ts +42 -0
- package/types/compile/errors.d.ts +30 -0
- package/types/compile/expr.d.ts +67 -0
- package/types/compile/index.d.ts +10 -0
- package/types/compile/parse.d.ts +82 -0
- package/types/compile/resolve.d.ts +46 -0
- package/types/compile/scan-exports.d.ts +9 -0
- package/types/config/defaults.d.ts +449 -0
- package/types/config/index.d.ts +299 -0
- package/types/config/pattern.d.ts +38 -0
- package/types/http/control-flow.d.ts +45 -0
- package/types/http/cookies-entry.d.ts +5 -0
- package/types/http/cookies.d.ts +113 -0
- package/types/http/request-cache.d.ts +13 -0
- package/types/http/request-context.d.ts +67 -0
- package/types/http/shared-cookie.d.ts +73 -0
- package/types/index.d.ts +30 -0
- package/types/log.d.mts +153 -0
- package/types/server/admin/actions.d.ts +16 -0
- package/types/server/admin/auth.d.ts +52 -0
- package/types/server/admin/event-log.d.ts +38 -0
- package/types/server/admin/gate.d.ts +43 -0
- package/types/server/admin/inventory.d.ts +40 -0
- package/types/server/admin/mount.d.ts +6 -0
- package/types/server/admin/router.d.ts +6 -0
- package/types/server/admin/snapshot.d.ts +6 -0
- package/types/server/assets.d.ts +47 -0
- package/types/server/auth/handoff.d.ts +12 -0
- package/types/server/cache-deps.d.ts +16 -0
- package/types/server/cache-vary.d.ts +30 -0
- package/types/server/cloudflare.d.ts +163 -0
- package/types/server/create-app.d.ts +25 -0
- package/types/server/data-cache.d.ts +116 -0
- package/types/server/dev/devtools.d.ts +44 -0
- package/types/server/dev/report.d.ts +229 -0
- package/types/server/dev/socket.d.ts +17 -0
- package/types/server/dev/version-check.d.mts +15 -0
- package/types/server/ejs-adapter.d.ts +11 -0
- package/types/server/head-hints.d.ts +40 -0
- package/types/server/html-cache.d.ts +173 -0
- package/types/server/image-optimizer.d.ts +68 -0
- package/types/server/logs/access-middleware.d.ts +7 -0
- package/types/server/logs/file-sink.d.ts +17 -0
- package/types/server/logs/pipeline.d.ts +37 -0
- package/types/server/logs/s3-put.d.ts +85 -0
- package/types/server/logs/s3-sink.d.ts +26 -0
- package/types/server/metadata.d.ts +38 -0
- package/types/server/middleware/compression.d.ts +17 -0
- package/types/server/middleware/csrf.d.ts +4 -0
- package/types/server/middleware/dev-gate.d.ts +2 -0
- package/types/server/middleware/headers.d.ts +2 -0
- package/types/server/middleware/redirects.d.ts +2 -0
- package/types/server/middleware/static-precompressed.d.ts +5 -0
- package/types/server/middleware/trailing-slash.d.ts +11 -0
- package/types/server/middleware/upstream-proxy.d.ts +21 -0
- package/types/server/og-image.d.ts +149 -0
- package/types/server/port-guard.d.ts +50 -0
- package/types/server/prewarm.d.ts +128 -0
- package/types/server/redis.d.ts +163 -0
- package/types/server/render.d.ts +101 -0
- package/types/server/router.d.ts +5 -0
- package/types/server/status-page.d.ts +24 -0
- package/types/server/upstream-limiter.d.ts +123 -0
- package/types/server/upstream-tracking.d.ts +42 -0
- package/types/shared/cookie-domain.d.ts +29 -0
- package/types/templates/layout.render.d.ts +7 -0
- package/types/version.d.mts +10 -0
- package/types/views/components/loader.d.ts +5 -0
- package/types/views/helpers/html.d.ts +39 -0
- 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
|
+
}
|
package/src/server/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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 "
|
|
5
|
-
import {
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { ensurePortFree } from "./server/port-guard.js";
|
|
6
9
|
|
|
7
|
-
|
|
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
|
+
}
|
package/src/templates/layout.ejs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
JSkelet varsayılan layout'
|
|
2
|
+
JSkelet varsayılan layout'unun legacy EJS kopyası.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|