jskelet 0.6.3 → 0.6.4
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 +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build'de üretilmiş `.br` / `.gz` kopyalarını servis eder.
|
|
3
|
-
*
|
|
4
|
-
* `express.static`ten önce çalışır: istemcinin kabul ettiği bir kopya varsa
|
|
5
|
-
* istek yolunu ona çevirir ve doğru `Content-Type` / `Content-Encoding`
|
|
6
|
-
* başlıklarını kendisi kurar. Böylece hash'li varlıklar istek başına yeniden
|
|
7
|
-
* sıkıştırılmaz ve build'deki kalite 11 brotli kullanılır.
|
|
8
|
-
*
|
|
9
|
-
* Kopya yoksa istek olduğu gibi `express.static`e devredilir.
|
|
10
|
-
*/
|
|
11
|
-
import fs from "node:fs";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import process from "node:process";
|
|
14
|
-
import { negotiateEncoding } from "./compression.js";
|
|
15
|
-
import { IMMUTABLE_CACHE } from "../../config/defaults.js";
|
|
16
|
-
|
|
17
|
-
/** @type {Record<string, string>} */
|
|
18
|
-
const CONTENT_TYPES = {
|
|
19
|
-
".css": "text/css; charset=utf-8",
|
|
20
|
-
".js": "text/javascript; charset=utf-8",
|
|
21
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
22
|
-
".svg": "image/svg+xml",
|
|
23
|
-
".json": "application/json; charset=utf-8",
|
|
24
|
-
".xml": "application/xml; charset=utf-8",
|
|
25
|
-
".txt": "text/plain; charset=utf-8",
|
|
26
|
-
".map": "application/json; charset=utf-8",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {string} publicDir
|
|
31
|
-
* @returns {import('express').RequestHandler}
|
|
32
|
-
*/
|
|
33
|
-
export function staticPrecompressed(publicDir) {
|
|
34
|
-
const root = path.resolve(publicDir);
|
|
35
|
-
// Hash'li `/assets/` süreç ömründe değişmez. Üretimde olumlu ve olumsuz
|
|
36
|
-
// `stat` sonucu bellekte kalır; dev'de watch hash değiştirdiği için yok.
|
|
37
|
-
const cacheStats = process.env.NODE_ENV !== "development";
|
|
38
|
-
/** @type {Map<string, number | null>} yol → bayt, `null` → dosya yok */
|
|
39
|
-
const statCache = new Map();
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {string} file
|
|
43
|
-
* @returns {number | null}
|
|
44
|
-
*/
|
|
45
|
-
function fileSize(file) {
|
|
46
|
-
if (cacheStats && statCache.has(file)) {
|
|
47
|
-
return /** @type {number | null} */ (statCache.get(file));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** @type {number | null} */
|
|
51
|
-
let size = null;
|
|
52
|
-
try {
|
|
53
|
-
const stat = fs.statSync(file);
|
|
54
|
-
if (stat.isFile()) size = stat.size;
|
|
55
|
-
} catch {
|
|
56
|
-
size = null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (cacheStats) statCache.set(file, size);
|
|
60
|
-
return size;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return (req, res, next) => {
|
|
64
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
65
|
-
next();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const type = CONTENT_TYPES[path.extname(req.path).toLowerCase()];
|
|
70
|
-
if (!type) {
|
|
71
|
-
next();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const encoding = negotiateEncoding(req.headers["accept-encoding"]);
|
|
76
|
-
if (!encoding) {
|
|
77
|
-
next();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let decoded;
|
|
82
|
-
try {
|
|
83
|
-
decoded = decodeURIComponent(req.path);
|
|
84
|
-
} catch {
|
|
85
|
-
next();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const target = path.resolve(root, `.${decoded}`);
|
|
90
|
-
|
|
91
|
-
// Path traversal koruması: çözülen yol public/ dışına çıkamaz.
|
|
92
|
-
if (target !== root && !target.startsWith(root + path.sep)) {
|
|
93
|
-
next();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const file = `${target}${encoding === "br" ? ".br" : ".gz"}`;
|
|
98
|
-
const size = fileSize(file);
|
|
99
|
-
|
|
100
|
-
if (size == null) {
|
|
101
|
-
next();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
res.setHeader("Content-Type", type);
|
|
106
|
-
res.setHeader("Content-Encoding", encoding);
|
|
107
|
-
res.setHeader("Content-Length", String(size));
|
|
108
|
-
res.setHeader("Vary", "Accept-Encoding");
|
|
109
|
-
|
|
110
|
-
if (req.path.startsWith("/assets/")) {
|
|
111
|
-
res.setHeader("Cache-Control", IMMUTABLE_CACHE);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (req.method === "HEAD") {
|
|
115
|
-
res.end();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
fs.createReadStream(file).on("error", () => res.end()).pipe(res);
|
|
120
|
-
};
|
|
121
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Build'de üretilmiş `.br` / `.gz` kopyalarını servis eder.
|
|
3
|
+
*
|
|
4
|
+
* `express.static`ten önce çalışır: istemcinin kabul ettiği bir kopya varsa
|
|
5
|
+
* istek yolunu ona çevirir ve doğru `Content-Type` / `Content-Encoding`
|
|
6
|
+
* başlıklarını kendisi kurar. Böylece hash'li varlıklar istek başına yeniden
|
|
7
|
+
* sıkıştırılmaz ve build'deki kalite 11 brotli kullanılır.
|
|
8
|
+
*
|
|
9
|
+
* Kopya yoksa istek olduğu gibi `express.static`e devredilir.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import process from "node:process";
|
|
14
|
+
import { negotiateEncoding } from "./compression.js";
|
|
15
|
+
import { IMMUTABLE_CACHE } from "../../config/defaults.js";
|
|
16
|
+
|
|
17
|
+
/** @type {Record<string, string>} */
|
|
18
|
+
const CONTENT_TYPES = {
|
|
19
|
+
".css": "text/css; charset=utf-8",
|
|
20
|
+
".js": "text/javascript; charset=utf-8",
|
|
21
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
22
|
+
".svg": "image/svg+xml",
|
|
23
|
+
".json": "application/json; charset=utf-8",
|
|
24
|
+
".xml": "application/xml; charset=utf-8",
|
|
25
|
+
".txt": "text/plain; charset=utf-8",
|
|
26
|
+
".map": "application/json; charset=utf-8",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} publicDir
|
|
31
|
+
* @returns {import('express').RequestHandler}
|
|
32
|
+
*/
|
|
33
|
+
export function staticPrecompressed(publicDir) {
|
|
34
|
+
const root = path.resolve(publicDir);
|
|
35
|
+
// Hash'li `/assets/` süreç ömründe değişmez. Üretimde olumlu ve olumsuz
|
|
36
|
+
// `stat` sonucu bellekte kalır; dev'de watch hash değiştirdiği için yok.
|
|
37
|
+
const cacheStats = process.env.NODE_ENV !== "development";
|
|
38
|
+
/** @type {Map<string, number | null>} yol → bayt, `null` → dosya yok */
|
|
39
|
+
const statCache = new Map();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} file
|
|
43
|
+
* @returns {number | null}
|
|
44
|
+
*/
|
|
45
|
+
function fileSize(file) {
|
|
46
|
+
if (cacheStats && statCache.has(file)) {
|
|
47
|
+
return /** @type {number | null} */ (statCache.get(file));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @type {number | null} */
|
|
51
|
+
let size = null;
|
|
52
|
+
try {
|
|
53
|
+
const stat = fs.statSync(file);
|
|
54
|
+
if (stat.isFile()) size = stat.size;
|
|
55
|
+
} catch {
|
|
56
|
+
size = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (cacheStats) statCache.set(file, size);
|
|
60
|
+
return size;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (req, res, next) => {
|
|
64
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
65
|
+
next();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const type = CONTENT_TYPES[path.extname(req.path).toLowerCase()];
|
|
70
|
+
if (!type) {
|
|
71
|
+
next();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const encoding = negotiateEncoding(req.headers["accept-encoding"]);
|
|
76
|
+
if (!encoding) {
|
|
77
|
+
next();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let decoded;
|
|
82
|
+
try {
|
|
83
|
+
decoded = decodeURIComponent(req.path);
|
|
84
|
+
} catch {
|
|
85
|
+
next();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const target = path.resolve(root, `.${decoded}`);
|
|
90
|
+
|
|
91
|
+
// Path traversal koruması: çözülen yol public/ dışına çıkamaz.
|
|
92
|
+
if (target !== root && !target.startsWith(root + path.sep)) {
|
|
93
|
+
next();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const file = `${target}${encoding === "br" ? ".br" : ".gz"}`;
|
|
98
|
+
const size = fileSize(file);
|
|
99
|
+
|
|
100
|
+
if (size == null) {
|
|
101
|
+
next();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
res.setHeader("Content-Type", type);
|
|
106
|
+
res.setHeader("Content-Encoding", encoding);
|
|
107
|
+
res.setHeader("Content-Length", String(size));
|
|
108
|
+
res.setHeader("Vary", "Accept-Encoding");
|
|
109
|
+
|
|
110
|
+
if (req.path.startsWith("/assets/")) {
|
|
111
|
+
res.setHeader("Cache-Control", IMMUTABLE_CACHE);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (req.method === "HEAD") {
|
|
115
|
+
res.end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fs.createReadStream(file).on("error", () => res.end()).pipe(res);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jskelet.config.mjs` → `trailingSlash` uygulayıcısı.
|
|
3
|
-
*
|
|
4
|
-
* Açıkken kanonik URL `/` ile biter: `/hakkinda/` 200 döner, slash'sız
|
|
5
|
-
* istek 308 ile slash'lıya gider. 301 değil 308 — framework'ün diğer kalıcı
|
|
6
|
-
* yönlendirmeleri gibi metodu korur ve eski istemcilerin POST'u GET'e
|
|
7
|
-
* çevirmesini engeller.
|
|
8
|
-
*
|
|
9
|
-
* Kapalıyken (varsayılan) hiçbir şey yapmaz: mevcut sitelerin `/x` ve `/x/`
|
|
10
|
-
* biçimlerini kırmaz.
|
|
11
|
-
*
|
|
12
|
-
* İstisnalar Next ile aynı: uzantılı dosya yolları ve `/.well-known/`.
|
|
13
|
-
* Kök `/` zaten slash ile biter.
|
|
14
|
-
*/
|
|
15
|
-
import { getConfig } from "../../config/index.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Bu yola trailing slash dayatılmamalı mı.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} pathname
|
|
21
|
-
* @returns {boolean}
|
|
22
|
-
*/
|
|
23
|
-
export function skipTrailingSlash(pathname) {
|
|
24
|
-
if (!pathname || pathname === "/") return true;
|
|
25
|
-
if (pathname.startsWith("/.well-known")) return true;
|
|
26
|
-
|
|
27
|
-
// Son segmentte nokta varsa statik dosya sayılır (`/a.b/c` değil,
|
|
28
|
-
// `/foto.png` ve `/assets/app.js` gibi).
|
|
29
|
-
const lastSlash = pathname.lastIndexOf("/");
|
|
30
|
-
const lastSegment = pathname.slice(lastSlash + 1);
|
|
31
|
-
return lastSegment.includes(".");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @returns {import('express').RequestHandler}
|
|
36
|
-
*/
|
|
37
|
-
export function trailingSlash() {
|
|
38
|
-
if (!getConfig().trailingSlash) {
|
|
39
|
-
return (_req, _res, next) => next();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return (req, res, next) => {
|
|
43
|
-
const pathname = req.path ?? "";
|
|
44
|
-
if (pathname.endsWith("/") || skipTrailingSlash(pathname)) {
|
|
45
|
-
next();
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Query string `originalUrl`'den: `req.path` onu düşürür.
|
|
50
|
-
const query = req.originalUrl.slice(pathname.length);
|
|
51
|
-
res.redirect(308, `${pathname}/${query}`);
|
|
52
|
-
};
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet.config.mjs` → `trailingSlash` uygulayıcısı.
|
|
3
|
+
*
|
|
4
|
+
* Açıkken kanonik URL `/` ile biter: `/hakkinda/` 200 döner, slash'sız
|
|
5
|
+
* istek 308 ile slash'lıya gider. 301 değil 308 — framework'ün diğer kalıcı
|
|
6
|
+
* yönlendirmeleri gibi metodu korur ve eski istemcilerin POST'u GET'e
|
|
7
|
+
* çevirmesini engeller.
|
|
8
|
+
*
|
|
9
|
+
* Kapalıyken (varsayılan) hiçbir şey yapmaz: mevcut sitelerin `/x` ve `/x/`
|
|
10
|
+
* biçimlerini kırmaz.
|
|
11
|
+
*
|
|
12
|
+
* İstisnalar Next ile aynı: uzantılı dosya yolları ve `/.well-known/`.
|
|
13
|
+
* Kök `/` zaten slash ile biter.
|
|
14
|
+
*/
|
|
15
|
+
import { getConfig } from "../../config/index.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bu yola trailing slash dayatılmamalı mı.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} pathname
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function skipTrailingSlash(pathname) {
|
|
24
|
+
if (!pathname || pathname === "/") return true;
|
|
25
|
+
if (pathname.startsWith("/.well-known")) return true;
|
|
26
|
+
|
|
27
|
+
// Son segmentte nokta varsa statik dosya sayılır (`/a.b/c` değil,
|
|
28
|
+
// `/foto.png` ve `/assets/app.js` gibi).
|
|
29
|
+
const lastSlash = pathname.lastIndexOf("/");
|
|
30
|
+
const lastSegment = pathname.slice(lastSlash + 1);
|
|
31
|
+
return lastSegment.includes(".");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @returns {import('express').RequestHandler}
|
|
36
|
+
*/
|
|
37
|
+
export function trailingSlash() {
|
|
38
|
+
if (!getConfig().trailingSlash) {
|
|
39
|
+
return (_req, _res, next) => next();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (req, res, next) => {
|
|
43
|
+
const pathname = req.path ?? "";
|
|
44
|
+
if (pathname.endsWith("/") || skipTrailingSlash(pathname)) {
|
|
45
|
+
next();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Query string `originalUrl`'den: `req.path` onu düşürür.
|
|
50
|
+
const query = req.originalUrl.slice(pathname.length);
|
|
51
|
+
res.redirect(308, `${pathname}/${query}`);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jskelet.config.mjs` → `rewrites()` uygulayıcısı ve yeniden kullanılabilir
|
|
3
|
-
* reverse proxy.
|
|
4
|
-
*
|
|
5
|
-
* Harici paket yok; native `fetch` ile stream eden ince bir proxy. `duplex:
|
|
6
|
-
* "half"` gövdeli isteklerde şart, `redirect: "manual"` olmadan upstream'in
|
|
7
|
-
* 302'si burada tüketilir ve tarayıcı hiç görmez.
|
|
8
|
-
*
|
|
9
|
-
* Tipik kullanım: `/api/*` yolunu backend'e taşımak. Bunu tarayıcı
|
|
10
|
-
* same-origin çağırdığı için CORS ve third-party cookie sorunları oluşmaz.
|
|
11
|
-
*/
|
|
12
|
-
import { Readable } from "node:stream";
|
|
13
|
-
import { getConfig } from "../../config/index.js";
|
|
14
|
-
import { fillDestination, matchPattern } from "../../config/pattern.js";
|
|
15
|
-
|
|
16
|
-
/** Hop-by-hop header'lar proxy edilmez. */
|
|
17
|
-
const STRIPPED_REQUEST_HEADERS = new Set([
|
|
18
|
-
"host",
|
|
19
|
-
"connection",
|
|
20
|
-
"content-length",
|
|
21
|
-
"accept-encoding",
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
const STRIPPED_RESPONSE_HEADERS = new Set([
|
|
25
|
-
"content-encoding",
|
|
26
|
-
"content-length",
|
|
27
|
-
"transfer-encoding",
|
|
28
|
-
"connection",
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Rewrite hedefini istek nesnesinde taşımak için; string bir alan adı
|
|
33
|
-
* uygulamanın kendi alanlarıyla çakışabilir.
|
|
34
|
-
*/
|
|
35
|
-
const REWRITE_TARGET = Symbol("jskelet.rewriteTarget");
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @param {import('express').Request} req
|
|
39
|
-
* @returns {Record<string, string>}
|
|
40
|
-
*/
|
|
41
|
-
function forwardHeaders(req) {
|
|
42
|
-
/** @type {Record<string, string>} */
|
|
43
|
-
const out = {};
|
|
44
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
45
|
-
if (STRIPPED_REQUEST_HEADERS.has(key)) continue;
|
|
46
|
-
if (value === undefined) continue;
|
|
47
|
-
out[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Hedefi isteğe göre hesaplayan proxy üretir. `resolveTarget` fırlatırsa
|
|
54
|
-
* istek proxy'lenmez ve zincire devam eder — hedef origin yapılandırılmamış
|
|
55
|
-
* bir kurulumda 500 yerine normal 404 almak daha doğru.
|
|
56
|
-
*
|
|
57
|
-
* @param {(req: import('express').Request) => string} resolveTarget
|
|
58
|
-
* @returns {import('express').RequestHandler}
|
|
59
|
-
*/
|
|
60
|
-
export function createProxy(resolveTarget) {
|
|
61
|
-
return async (req, res, next) => {
|
|
62
|
-
let target;
|
|
63
|
-
try {
|
|
64
|
-
target = resolveTarget(req);
|
|
65
|
-
} catch {
|
|
66
|
-
next();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!target) {
|
|
71
|
-
next();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const upstream = await fetch(target, {
|
|
79
|
-
method: req.method,
|
|
80
|
-
headers: forwardHeaders(req),
|
|
81
|
-
body: hasBody ? Readable.toWeb(req) : undefined,
|
|
82
|
-
duplex: hasBody ? "half" : undefined,
|
|
83
|
-
redirect: "manual",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
res.status(upstream.status);
|
|
87
|
-
upstream.headers.forEach((value, key) => {
|
|
88
|
-
if (STRIPPED_RESPONSE_HEADERS.has(key.toLowerCase())) return;
|
|
89
|
-
res.setHeader(key, value);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (!upstream.body) {
|
|
93
|
-
res.end();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
Readable.fromWeb(upstream.body).pipe(res);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
next(error);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Config'teki `rewrites()` kuralları.
|
|
106
|
-
*
|
|
107
|
-
* Hedef mutlaksa (http/https) istek proxy ile dışa taşınır; göreliyse
|
|
108
|
-
* yalnızca `req.url` değiştirilir ve istek kendi route tablosunda devam eder.
|
|
109
|
-
* İki faz var: `beforeFiles` statik dosyalardan önce, `afterFiles` statik
|
|
110
|
-
* denendikten sonra sayfalardan önce.
|
|
111
|
-
*
|
|
112
|
-
* @param {"beforeFiles" | "afterFiles"} phase
|
|
113
|
-
* @returns {import('express').RequestHandler}
|
|
114
|
-
*/
|
|
115
|
-
export function configRewrites(phase) {
|
|
116
|
-
const rules = getConfig().rewrites.filter((rule) => rule.phase === phase);
|
|
117
|
-
if (!rules.length) return (req, res, next) => next();
|
|
118
|
-
|
|
119
|
-
const proxy = createProxy((req) => req[REWRITE_TARGET]);
|
|
120
|
-
|
|
121
|
-
return (req, res, next) => {
|
|
122
|
-
for (const rule of rules) {
|
|
123
|
-
const params = matchPattern(rule.pattern, req.path);
|
|
124
|
-
if (!params) continue;
|
|
125
|
-
|
|
126
|
-
const query = req.originalUrl.slice(req.path.length);
|
|
127
|
-
const destination = fillDestination(rule.destination, params);
|
|
128
|
-
|
|
129
|
-
if (/^https?:\/\//.test(destination)) {
|
|
130
|
-
req[REWRITE_TARGET] = `${destination}${query}`;
|
|
131
|
-
proxy(req, res, next);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
req.url = `${destination}${query}`;
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
next();
|
|
140
|
-
};
|
|
141
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet.config.mjs` → `rewrites()` uygulayıcısı ve yeniden kullanılabilir
|
|
3
|
+
* reverse proxy.
|
|
4
|
+
*
|
|
5
|
+
* Harici paket yok; native `fetch` ile stream eden ince bir proxy. `duplex:
|
|
6
|
+
* "half"` gövdeli isteklerde şart, `redirect: "manual"` olmadan upstream'in
|
|
7
|
+
* 302'si burada tüketilir ve tarayıcı hiç görmez.
|
|
8
|
+
*
|
|
9
|
+
* Tipik kullanım: `/api/*` yolunu backend'e taşımak. Bunu tarayıcı
|
|
10
|
+
* same-origin çağırdığı için CORS ve third-party cookie sorunları oluşmaz.
|
|
11
|
+
*/
|
|
12
|
+
import { Readable } from "node:stream";
|
|
13
|
+
import { getConfig } from "../../config/index.js";
|
|
14
|
+
import { fillDestination, matchPattern } from "../../config/pattern.js";
|
|
15
|
+
|
|
16
|
+
/** Hop-by-hop header'lar proxy edilmez. */
|
|
17
|
+
const STRIPPED_REQUEST_HEADERS = new Set([
|
|
18
|
+
"host",
|
|
19
|
+
"connection",
|
|
20
|
+
"content-length",
|
|
21
|
+
"accept-encoding",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const STRIPPED_RESPONSE_HEADERS = new Set([
|
|
25
|
+
"content-encoding",
|
|
26
|
+
"content-length",
|
|
27
|
+
"transfer-encoding",
|
|
28
|
+
"connection",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rewrite hedefini istek nesnesinde taşımak için; string bir alan adı
|
|
33
|
+
* uygulamanın kendi alanlarıyla çakışabilir.
|
|
34
|
+
*/
|
|
35
|
+
const REWRITE_TARGET = Symbol("jskelet.rewriteTarget");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {import('express').Request} req
|
|
39
|
+
* @returns {Record<string, string>}
|
|
40
|
+
*/
|
|
41
|
+
function forwardHeaders(req) {
|
|
42
|
+
/** @type {Record<string, string>} */
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
45
|
+
if (STRIPPED_REQUEST_HEADERS.has(key)) continue;
|
|
46
|
+
if (value === undefined) continue;
|
|
47
|
+
out[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hedefi isteğe göre hesaplayan proxy üretir. `resolveTarget` fırlatırsa
|
|
54
|
+
* istek proxy'lenmez ve zincire devam eder — hedef origin yapılandırılmamış
|
|
55
|
+
* bir kurulumda 500 yerine normal 404 almak daha doğru.
|
|
56
|
+
*
|
|
57
|
+
* @param {(req: import('express').Request) => string} resolveTarget
|
|
58
|
+
* @returns {import('express').RequestHandler}
|
|
59
|
+
*/
|
|
60
|
+
export function createProxy(resolveTarget) {
|
|
61
|
+
return async (req, res, next) => {
|
|
62
|
+
let target;
|
|
63
|
+
try {
|
|
64
|
+
target = resolveTarget(req);
|
|
65
|
+
} catch {
|
|
66
|
+
next();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!target) {
|
|
71
|
+
next();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const upstream = await fetch(target, {
|
|
79
|
+
method: req.method,
|
|
80
|
+
headers: forwardHeaders(req),
|
|
81
|
+
body: hasBody ? Readable.toWeb(req) : undefined,
|
|
82
|
+
duplex: hasBody ? "half" : undefined,
|
|
83
|
+
redirect: "manual",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
res.status(upstream.status);
|
|
87
|
+
upstream.headers.forEach((value, key) => {
|
|
88
|
+
if (STRIPPED_RESPONSE_HEADERS.has(key.toLowerCase())) return;
|
|
89
|
+
res.setHeader(key, value);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!upstream.body) {
|
|
93
|
+
res.end();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Readable.fromWeb(upstream.body).pipe(res);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
next(error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Config'teki `rewrites()` kuralları.
|
|
106
|
+
*
|
|
107
|
+
* Hedef mutlaksa (http/https) istek proxy ile dışa taşınır; göreliyse
|
|
108
|
+
* yalnızca `req.url` değiştirilir ve istek kendi route tablosunda devam eder.
|
|
109
|
+
* İki faz var: `beforeFiles` statik dosyalardan önce, `afterFiles` statik
|
|
110
|
+
* denendikten sonra sayfalardan önce.
|
|
111
|
+
*
|
|
112
|
+
* @param {"beforeFiles" | "afterFiles"} phase
|
|
113
|
+
* @returns {import('express').RequestHandler}
|
|
114
|
+
*/
|
|
115
|
+
export function configRewrites(phase) {
|
|
116
|
+
const rules = getConfig().rewrites.filter((rule) => rule.phase === phase);
|
|
117
|
+
if (!rules.length) return (req, res, next) => next();
|
|
118
|
+
|
|
119
|
+
const proxy = createProxy((req) => req[REWRITE_TARGET]);
|
|
120
|
+
|
|
121
|
+
return (req, res, next) => {
|
|
122
|
+
for (const rule of rules) {
|
|
123
|
+
const params = matchPattern(rule.pattern, req.path);
|
|
124
|
+
if (!params) continue;
|
|
125
|
+
|
|
126
|
+
const query = req.originalUrl.slice(req.path.length);
|
|
127
|
+
const destination = fillDestination(rule.destination, params);
|
|
128
|
+
|
|
129
|
+
if (/^https?:\/\//.test(destination)) {
|
|
130
|
+
req[REWRITE_TARGET] = `${destination}${query}`;
|
|
131
|
+
proxy(req, res, next);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
req.url = `${destination}${query}`;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
next();
|
|
140
|
+
};
|
|
141
|
+
}
|