jskelet 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/AGENTS.md +136 -136
  2. package/CHANGELOG.md +620 -596
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +130 -130
  5. package/docs/01-baslangic.md +291 -291
  6. package/docs/02-mimari.md +310 -309
  7. package/docs/03-routing.md +515 -515
  8. package/docs/04-render-ve-sablonlar.md +661 -661
  9. package/docs/05-islands.md +486 -486
  10. package/docs/06-cache.md +1443 -1423
  11. package/docs/07-yapilandirma.md +12 -6
  12. package/docs/08-build.md +429 -428
  13. package/docs/09-dev-araclari.md +364 -364
  14. package/docs/10-dagitim.md +338 -338
  15. package/docs/12-panel-ve-oturum.md +478 -478
  16. package/docs/README.md +83 -83
  17. package/docs/en/01-getting-started.md +298 -298
  18. package/docs/en/02-architecture.md +329 -328
  19. package/docs/en/03-routing.md +531 -531
  20. package/docs/en/04-rendering.md +669 -669
  21. package/docs/en/05-islands.md +497 -497
  22. package/docs/en/06-caching.md +1453 -1431
  23. package/docs/en/07-configuration.md +1219 -1214
  24. package/docs/en/08-build.md +447 -446
  25. package/docs/en/09-dev-tools.md +373 -373
  26. package/docs/en/10-deployment.md +340 -340
  27. package/docs/en/11-migration.md +398 -398
  28. package/docs/en/12-dashboards-and-sessions.md +488 -488
  29. package/docs/en/README.md +87 -87
  30. package/package.json +137 -137
  31. package/src/build/ensure-build.mjs +19 -19
  32. package/src/build/paths.mjs +153 -153
  33. package/src/build/resolve-peer.mjs +36 -36
  34. package/src/build/tasks/client.mjs +349 -349
  35. package/src/build/tasks/css.mjs +235 -235
  36. package/src/build/tasks/fonts.mjs +146 -146
  37. package/src/build/tasks/icons.mjs +357 -357
  38. package/src/build/tasks/images.mjs +244 -244
  39. package/src/build/tasks/precompress.mjs +78 -78
  40. package/src/build/tasks/templates.mjs +20 -20
  41. package/src/client/admin/i18n.js +764 -764
  42. package/src/client/admin/login.html +74 -74
  43. package/src/client/admin/panel.css +809 -809
  44. package/src/client/admin/panel.html +495 -495
  45. package/src/client/admin/panel.js +1251 -1251
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +745 -745
  48. package/src/client/devtools/seo.js +628 -628
  49. package/src/client/dom.js +95 -95
  50. package/src/client/form.js +192 -192
  51. package/src/client/index.js +45 -45
  52. package/src/client/registry.js +305 -305
  53. package/src/client/safe-image.js +91 -91
  54. package/src/client/shared-cookie.js +225 -225
  55. package/src/client/store.js +36 -36
  56. package/src/client/swap.js +188 -188
  57. package/src/compile/codegen.js +336 -336
  58. package/src/compile/compile-all.js +149 -149
  59. package/src/compile/errors.js +66 -66
  60. package/src/compile/expr.js +409 -409
  61. package/src/compile/index.js +17 -17
  62. package/src/compile/parse.js +541 -541
  63. package/src/compile/resolve.js +211 -211
  64. package/src/compile/scan-exports.js +51 -51
  65. package/src/config/defaults.js +17 -1
  66. package/src/config/index.js +13 -0
  67. package/src/config/pattern.js +107 -107
  68. package/src/generate.mjs +163 -163
  69. package/src/http/control-flow.js +71 -71
  70. package/src/http/cookies-entry.js +21 -21
  71. package/src/http/cookies.js +277 -277
  72. package/src/http/request-cache.js +46 -46
  73. package/src/http/request-context.js +165 -165
  74. package/src/http/shared-cookie.js +178 -178
  75. package/src/index.js +101 -101
  76. package/src/init.mjs +230 -230
  77. package/src/migrate/apply.mjs +262 -262
  78. package/src/migrate/babel.mjs +79 -79
  79. package/src/migrate/classify.mjs +155 -155
  80. package/src/migrate/config.mjs +126 -126
  81. package/src/migrate/fs-walk.mjs +191 -191
  82. package/src/migrate/parse.mjs +26 -26
  83. package/src/migrate/scan.mjs +177 -177
  84. package/src/migrate/transform/expr-source.mjs +168 -168
  85. package/src/migrate/transform/island.mjs +67 -67
  86. package/src/migrate/transform/jsx-to-component.mjs +302 -302
  87. package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
  88. package/src/migrate/transform/page-split.mjs +435 -435
  89. package/src/migrate/write.mjs +81 -81
  90. package/src/migrate.mjs +171 -171
  91. package/src/runtime/alias-hooks.mjs +119 -119
  92. package/src/runtime/register.mjs +4 -4
  93. package/src/server/admin/actions.js +229 -229
  94. package/src/server/admin/auth.js +125 -125
  95. package/src/server/admin/event-log.js +151 -151
  96. package/src/server/admin/gate.js +209 -209
  97. package/src/server/admin/inventory.js +188 -188
  98. package/src/server/admin/mount.js +56 -56
  99. package/src/server/admin/router.js +216 -216
  100. package/src/server/admin/snapshot.js +241 -241
  101. package/src/server/assets.js +147 -147
  102. package/src/server/auth/handoff.js +309 -309
  103. package/src/server/cache-blob.js +70 -0
  104. package/src/server/cache-deps.js +42 -42
  105. package/src/server/cache-vary.js +113 -113
  106. package/src/server/cloudflare.js +607 -607
  107. package/src/server/create-app.js +366 -366
  108. package/src/server/data-cache.js +553 -462
  109. package/src/server/dev/report.js +485 -485
  110. package/src/server/dev/socket.js +170 -170
  111. package/src/server/dev/version-check.mjs +139 -139
  112. package/src/server/disk-cache.js +233 -0
  113. package/src/server/ejs-adapter.js +59 -59
  114. package/src/server/html-cache.js +1196 -1122
  115. package/src/server/image-optimizer.js +500 -407
  116. package/src/server/logs/access-middleware.js +66 -66
  117. package/src/server/logs/file-sink.js +193 -66
  118. package/src/server/logs/pipeline.js +165 -158
  119. package/src/server/logs/s3-put.js +214 -214
  120. package/src/server/logs/s3-sink.js +112 -112
  121. package/src/server/metadata.js +102 -102
  122. package/src/server/middleware/compression.js +205 -205
  123. package/src/server/middleware/csrf.js +134 -134
  124. package/src/server/middleware/dev-gate.js +75 -75
  125. package/src/server/middleware/headers.js +37 -37
  126. package/src/server/middleware/redirects.js +32 -32
  127. package/src/server/middleware/robots-txt.js +341 -341
  128. package/src/server/middleware/static-precompressed.js +121 -100
  129. package/src/server/middleware/trailing-slash.js +53 -53
  130. package/src/server/middleware/upstream-proxy.js +141 -141
  131. package/src/server/og-image.js +356 -356
  132. package/src/server/port-guard.js +255 -255
  133. package/src/server/prewarm.js +1082 -1058
  134. package/src/server/redis.js +588 -569
  135. package/src/server/render.js +4 -4
  136. package/src/server/router.js +157 -157
  137. package/src/server/status-page.js +265 -265
  138. package/src/server/upstream-limiter.js +376 -376
  139. package/src/server/upstream-tracking.js +166 -166
  140. package/src/shared/cookie-domain.js +66 -66
  141. package/src/start.mjs +22 -22
  142. package/src/templates/layout.ejs +30 -30
  143. package/src/templates/layout.jsk +30 -30
  144. package/src/version.mjs +31 -31
  145. package/src/views/components/loader.js +101 -101
  146. package/src/views/helpers/html.js +102 -102
  147. package/src/views/helpers/tags.js +375 -375
  148. package/types/config/defaults.d.ts +15 -1
  149. package/types/config/index.d.ts +8 -0
  150. package/types/server/cache-blob.d.ts +13 -0
  151. package/types/server/data-cache.d.ts +9 -0
  152. package/types/server/disk-cache.d.ts +36 -0
  153. package/types/server/html-cache.d.ts +26 -3
  154. package/types/server/logs/file-sink.d.ts +16 -5
  155. package/types/server/redis.d.ts +2 -1
@@ -1,100 +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 { negotiateEncoding } from "./compression.js";
14
- import { IMMUTABLE_CACHE } from "../../config/defaults.js";
15
-
16
- /** @type {Record<string, string>} */
17
- const CONTENT_TYPES = {
18
- ".css": "text/css; charset=utf-8",
19
- ".js": "text/javascript; charset=utf-8",
20
- ".mjs": "text/javascript; charset=utf-8",
21
- ".svg": "image/svg+xml",
22
- ".json": "application/json; charset=utf-8",
23
- ".xml": "application/xml; charset=utf-8",
24
- ".txt": "text/plain; charset=utf-8",
25
- ".map": "application/json; charset=utf-8",
26
- };
27
-
28
- /**
29
- * @param {string} publicDir
30
- * @returns {import('express').RequestHandler}
31
- */
32
- export function staticPrecompressed(publicDir) {
33
- const root = path.resolve(publicDir);
34
-
35
- return (req, res, next) => {
36
- if (req.method !== "GET" && req.method !== "HEAD") {
37
- next();
38
- return;
39
- }
40
-
41
- const type = CONTENT_TYPES[path.extname(req.path).toLowerCase()];
42
- if (!type) {
43
- next();
44
- return;
45
- }
46
-
47
- const encoding = negotiateEncoding(req.headers["accept-encoding"]);
48
- if (!encoding) {
49
- next();
50
- return;
51
- }
52
-
53
- let decoded;
54
- try {
55
- decoded = decodeURIComponent(req.path);
56
- } catch {
57
- next();
58
- return;
59
- }
60
-
61
- const target = path.resolve(root, `.${decoded}`);
62
-
63
- // Path traversal koruması: çözülen yol public/ dışına çıkamaz.
64
- if (target !== root && !target.startsWith(root + path.sep)) {
65
- next();
66
- return;
67
- }
68
-
69
- const file = `${target}${encoding === "br" ? ".br" : ".gz"}`;
70
-
71
- let stat;
72
- try {
73
- stat = fs.statSync(file);
74
- } catch {
75
- next();
76
- return;
77
- }
78
-
79
- if (!stat.isFile()) {
80
- next();
81
- return;
82
- }
83
-
84
- res.setHeader("Content-Type", type);
85
- res.setHeader("Content-Encoding", encoding);
86
- res.setHeader("Content-Length", String(stat.size));
87
- res.setHeader("Vary", "Accept-Encoding");
88
-
89
- if (req.path.startsWith("/assets/")) {
90
- res.setHeader("Cache-Control", IMMUTABLE_CACHE);
91
- }
92
-
93
- if (req.method === "HEAD") {
94
- res.end();
95
- return;
96
- }
97
-
98
- fs.createReadStream(file).on("error", () => res.end()).pipe(res);
99
- };
100
- }
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
+ }