jskelet 0.6.3 → 0.6.5
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 +633 -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 +700 -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 +351 -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 +708 -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 +355 -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 +965 -356
- package/src/server/og-raster.mjs +21 -0
- 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 +51 -3
|
@@ -1,341 +1,341 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kullanıcının `robots.txt` gövdesinin altına framework uçlarını ekler.
|
|
3
|
-
*
|
|
4
|
-
* Panel, görsel proxy, auth handoff, dev araçları ve fragment parçaları
|
|
5
|
-
* sayfa değildir; bir crawler onları dizinine alırsa indeks parçalı HTML
|
|
6
|
-
* ve yönetim yollarıyla kirlenir. Uygulama dosyayı kendisi yazar (`public/`
|
|
7
|
-
* ya da bir route). Framework yalnızca başarılı metin yanıtının sonuna bir
|
|
8
|
-
* not ve `Disallow` grubu düşer — dosya yoksa uydurulmaz.
|
|
9
|
-
*
|
|
10
|
-
* Google, belirli bir user-agent grubunu `User-agent: *` ile birleştirmez;
|
|
11
|
-
* aynı ajanın ikinci grubunu birleştirir. Bu yüzden alttaki grup, dosyada
|
|
12
|
-
* adı geçen her ajanı tekrarlar. Aksi hâlde yalnızca `Googlebot` grubu olan
|
|
13
|
-
* bir dosyada ek hiç uygulanmazdı.
|
|
14
|
-
*
|
|
15
|
-
* Sıkıştırılmış kopyanın (`Content-Encoding` dolu) üzerine yazılmaz: ek,
|
|
16
|
-
* düz metnin sonuna konur, sıkıştırma dıştaki middleware'de olur.
|
|
17
|
-
*/
|
|
18
|
-
import crypto from "node:crypto";
|
|
19
|
-
import { getConfig } from "../../config/index.js";
|
|
20
|
-
|
|
21
|
-
/** İdempotency anahtarı. Marka adı değişse de ikinci kez eklenmez. */
|
|
22
|
-
const NOTE_MARK = "framework endpoints, not for indexing";
|
|
23
|
-
|
|
24
|
-
/** Framework'ün ayırdığı önekler. Özel bir yola taşınsalar da bunlar kalır. */
|
|
25
|
-
const RESERVED_PREFIXES = ["/_jskelet/", "/__jskelet/", "/_fragment/"];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} [brandName]
|
|
29
|
-
* @returns {string}
|
|
30
|
-
*/
|
|
31
|
-
export function frameworkRobotsNote(brandName = "JSkelet") {
|
|
32
|
-
const name = String(brandName || "JSkelet")
|
|
33
|
-
.replace(/[\r\n]+/g, " ")
|
|
34
|
-
.trim();
|
|
35
|
-
return `# ${name || "JSkelet"} — ${NOTE_MARK}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* `robots.txt`'e yazılacak `Disallow` yolları.
|
|
40
|
-
*
|
|
41
|
-
* Üç önek her zaman vardır. Panel, uzak görsel ve handoff yalnızca
|
|
42
|
-
* gerçekten mount edildiklerinde ve öneklerin dışına taşındıklarında eklenir.
|
|
43
|
-
* Dev araçlarının özel yolu yalnızca development'ta yazılır: production'da
|
|
44
|
-
* o yol uygulamanın kendi sayfası olabilir.
|
|
45
|
-
*
|
|
46
|
-
* @param {object} [config]
|
|
47
|
-
* @param {{ dev?: boolean }} [options]
|
|
48
|
-
* @returns {string[]}
|
|
49
|
-
*/
|
|
50
|
-
export function frameworkDisallowPaths(config = {}, options = {}) {
|
|
51
|
-
/** @type {string[]} */
|
|
52
|
-
const paths = [...RESERVED_PREFIXES];
|
|
53
|
-
const dev = options.dev ?? process.env.NODE_ENV === "development";
|
|
54
|
-
|
|
55
|
-
/** @type {string[]} */
|
|
56
|
-
const extras = [];
|
|
57
|
-
|
|
58
|
-
const admin = /** @type {{ enabled?: boolean, basePath?: string }} */ (
|
|
59
|
-
config.admin
|
|
60
|
-
);
|
|
61
|
-
if (admin?.enabled && typeof admin.basePath === "string") {
|
|
62
|
-
extras.push(admin.basePath);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const images = /** @type {{ remote?: { enabled?: boolean, path?: string } | false } | false} */ (
|
|
66
|
-
config.images
|
|
67
|
-
);
|
|
68
|
-
const remote = images && images !== false ? images.remote : null;
|
|
69
|
-
if (remote && remote !== false && remote.enabled && typeof remote.path === "string") {
|
|
70
|
-
extras.push(remote.path);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const auth = /** @type {{ crossSubdomainHandoff?: boolean | { enabled?: boolean, path?: string } }} */ (
|
|
74
|
-
config.auth
|
|
75
|
-
);
|
|
76
|
-
const handoff = auth?.crossSubdomainHandoff;
|
|
77
|
-
const handoffOn =
|
|
78
|
-
handoff === true ||
|
|
79
|
-
(Boolean(handoff) &&
|
|
80
|
-
typeof handoff === "object" &&
|
|
81
|
-
handoff.enabled !== false);
|
|
82
|
-
if (handoffOn && handoff && typeof handoff === "object" && typeof handoff.path === "string") {
|
|
83
|
-
extras.push(handoff.path);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const brand = /** @type {{ devBasePath?: string }} */ (config.brand);
|
|
87
|
-
if (dev && typeof brand?.devBasePath === "string") {
|
|
88
|
-
extras.push(brand.devBasePath);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
for (const raw of extras) {
|
|
92
|
-
const path = cleanDisallowPath(raw);
|
|
93
|
-
if (!path) continue;
|
|
94
|
-
if (paths.some((existing) => coveredBy(existing, path))) continue;
|
|
95
|
-
paths.push(path);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return paths;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @param {string} body
|
|
103
|
-
* @param {{ brandName?: string, paths: string[] }} options
|
|
104
|
-
* @returns {string}
|
|
105
|
-
*/
|
|
106
|
-
export function appendFrameworkRobots(body, options) {
|
|
107
|
-
const text = String(body ?? "").replace(/^\uFEFF/, "");
|
|
108
|
-
const paths = options.paths.filter((path) => cleanDisallowPath(path) || isReserved(path));
|
|
109
|
-
if (paths.length === 0 || text.includes(NOTE_MARK)) return text;
|
|
110
|
-
|
|
111
|
-
const agents = collectUserAgents(text);
|
|
112
|
-
const block = [
|
|
113
|
-
frameworkRobotsNote(options.brandName),
|
|
114
|
-
...agents.map((agent) => `User-agent: ${agent}`),
|
|
115
|
-
...paths.map((path) => `Disallow: ${path}`),
|
|
116
|
-
"",
|
|
117
|
-
].join("\n");
|
|
118
|
-
|
|
119
|
-
const base = text.replace(/\s*$/, "");
|
|
120
|
-
return base ? `${base}\n\n${block}` : block;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @returns {import('express').RequestHandler}
|
|
125
|
-
*/
|
|
126
|
-
export function robotsTxtMiddleware() {
|
|
127
|
-
return function robotsTxt(req, res, next) {
|
|
128
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
129
|
-
next();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const pathname = req.path || "";
|
|
134
|
-
if (pathname !== "/robots.txt") {
|
|
135
|
-
next();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Handler'ın kendi ETag'i eklenmemiş gövdeye aittir. 304, crawler'ın
|
|
140
|
-
// framework bloğu olmayan kopyayı tutmasına yol açardı.
|
|
141
|
-
const previousEtag = req.headers["if-none-match"];
|
|
142
|
-
delete req.headers["if-none-match"];
|
|
143
|
-
delete req.headers["if-modified-since"];
|
|
144
|
-
|
|
145
|
-
/** @type {Buffer[]} */
|
|
146
|
-
const chunks = [];
|
|
147
|
-
const originalEnd = res.end;
|
|
148
|
-
let finished = false;
|
|
149
|
-
|
|
150
|
-
res.write = function write(chunk, encoding, callback) {
|
|
151
|
-
const enc = typeof encoding === "function" ? undefined : encoding;
|
|
152
|
-
const done = typeof encoding === "function" ? encoding : callback;
|
|
153
|
-
const buf = takeChunk(chunk, enc);
|
|
154
|
-
if (buf && buf.length) chunks.push(buf);
|
|
155
|
-
if (typeof done === "function") done();
|
|
156
|
-
return true;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
res.end = function end(chunk, encoding, callback) {
|
|
160
|
-
if (finished) return originalEnd.call(this);
|
|
161
|
-
finished = true;
|
|
162
|
-
|
|
163
|
-
const enc = typeof encoding === "function" ? undefined : encoding;
|
|
164
|
-
const done =
|
|
165
|
-
typeof chunk === "function"
|
|
166
|
-
? chunk
|
|
167
|
-
: typeof encoding === "function"
|
|
168
|
-
? encoding
|
|
169
|
-
: callback;
|
|
170
|
-
const buf = takeChunk(chunk, enc);
|
|
171
|
-
if (buf && buf.length) chunks.push(buf);
|
|
172
|
-
|
|
173
|
-
const body = chunks.length === 0 ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
174
|
-
|
|
175
|
-
if (!shouldRewrite(req, res, body)) {
|
|
176
|
-
return typeof done === "function"
|
|
177
|
-
? originalEnd.call(this, body, done)
|
|
178
|
-
: originalEnd.call(this, body);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const context = readRobotsContext();
|
|
182
|
-
const nextBody = Buffer.from(
|
|
183
|
-
appendFrameworkRobots(body.toString("utf8"), context),
|
|
184
|
-
"utf8",
|
|
185
|
-
);
|
|
186
|
-
const tag = `"${crypto.createHash("sha1").update(nextBody).digest("base64url")}"`;
|
|
187
|
-
|
|
188
|
-
if (previousEtag && etagMatches(previousEtag, tag)) {
|
|
189
|
-
res.statusCode = 304;
|
|
190
|
-
res.setHeader("ETag", tag);
|
|
191
|
-
res.setHeader("Content-Length", "0");
|
|
192
|
-
res.removeHeader("Last-Modified");
|
|
193
|
-
return originalEnd.call(this);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const type = String(res.getHeader("content-type") || "");
|
|
197
|
-
if (!/^text\/plain\b/i.test(type)) {
|
|
198
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
199
|
-
}
|
|
200
|
-
res.setHeader("ETag", tag);
|
|
201
|
-
res.setHeader("Content-Length", String(nextBody.length));
|
|
202
|
-
res.removeHeader("Last-Modified");
|
|
203
|
-
|
|
204
|
-
return typeof done === "function"
|
|
205
|
-
? originalEnd.call(this, nextBody, done)
|
|
206
|
-
: originalEnd.call(this, nextBody);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
next();
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @returns {{ brandName: string, paths: string[] }}
|
|
215
|
-
*/
|
|
216
|
-
function readRobotsContext() {
|
|
217
|
-
try {
|
|
218
|
-
const config = getConfig();
|
|
219
|
-
const brand = /** @type {{ name?: string }} */ (config.brand);
|
|
220
|
-
return {
|
|
221
|
-
brandName: typeof brand?.name === "string" && brand.name ? brand.name : "JSkelet",
|
|
222
|
-
paths: frameworkDisallowPaths(config),
|
|
223
|
-
};
|
|
224
|
-
} catch {
|
|
225
|
-
return { brandName: "JSkelet", paths: frameworkDisallowPaths({}) };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* @param {import('express').Request} req
|
|
231
|
-
* @param {import('express').Response} res
|
|
232
|
-
* @param {Buffer} body
|
|
233
|
-
* @returns {boolean}
|
|
234
|
-
*/
|
|
235
|
-
function shouldRewrite(req, res, body) {
|
|
236
|
-
if (req.method !== "GET") return false;
|
|
237
|
-
if (res.headersSent) return false;
|
|
238
|
-
|
|
239
|
-
const status = res.statusCode || 200;
|
|
240
|
-
if (status < 200 || status >= 300) return false;
|
|
241
|
-
if (res.getHeader("content-encoding")) return false;
|
|
242
|
-
|
|
243
|
-
const type = String(res.getHeader("content-type") || "");
|
|
244
|
-
if (type && !/^text\/(?:plain|html)\b/i.test(type)) return false;
|
|
245
|
-
|
|
246
|
-
const sample = body.subarray(0, 64).toString("utf8");
|
|
247
|
-
if (/^\s*<(!doctype|html)\b/i.test(sample)) return false;
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* @param {string | string[] | undefined} header
|
|
253
|
-
* @param {string} tag
|
|
254
|
-
* @returns {boolean}
|
|
255
|
-
*/
|
|
256
|
-
function etagMatches(header, tag) {
|
|
257
|
-
const value = Array.isArray(header) ? header.join(",") : String(header);
|
|
258
|
-
return value.split(",").some((part) => {
|
|
259
|
-
const token = part.trim().replace(/^W\//i, "");
|
|
260
|
-
return token === "*" || token === tag;
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* @param {unknown} chunk
|
|
266
|
-
* @param {unknown} encoding
|
|
267
|
-
* @returns {Buffer | null}
|
|
268
|
-
*/
|
|
269
|
-
function takeChunk(chunk, encoding) {
|
|
270
|
-
if (chunk == null || typeof chunk === "function") return null;
|
|
271
|
-
if (Buffer.isBuffer(chunk)) return chunk;
|
|
272
|
-
if (chunk instanceof Uint8Array) return Buffer.from(chunk);
|
|
273
|
-
if (typeof chunk === "string") {
|
|
274
|
-
const enc = typeof encoding === "string" ? encoding : "utf8";
|
|
275
|
-
return Buffer.from(chunk, /** @type {BufferEncoding} */ (enc));
|
|
276
|
-
}
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* @param {string} text
|
|
282
|
-
* @returns {string[]}
|
|
283
|
-
*/
|
|
284
|
-
function collectUserAgents(text) {
|
|
285
|
-
/** @type {string[]} */
|
|
286
|
-
const agents = [];
|
|
287
|
-
const seen = new Set();
|
|
288
|
-
|
|
289
|
-
for (const line of text.split(/\r?\n/)) {
|
|
290
|
-
const match = /^[ \t]*user-agent[ \t]*:[ \t]*(.*?)[ \t]*$/i.exec(line);
|
|
291
|
-
if (!match) continue;
|
|
292
|
-
const agent = match[1].trim();
|
|
293
|
-
if (!agent || /[\r\n]/.test(agent)) continue;
|
|
294
|
-
const key = agent.toLowerCase();
|
|
295
|
-
if (seen.has(key)) continue;
|
|
296
|
-
seen.add(key);
|
|
297
|
-
agents.push(agent);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const star = agents.findIndex((agent) => agent === "*");
|
|
301
|
-
if (star === -1) agents.unshift("*");
|
|
302
|
-
else if (star > 0) {
|
|
303
|
-
agents.splice(star, 1);
|
|
304
|
-
agents.unshift("*");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return agents;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* @param {string} raw
|
|
312
|
-
* @returns {string | null}
|
|
313
|
-
*/
|
|
314
|
-
function cleanDisallowPath(raw) {
|
|
315
|
-
if (typeof raw !== "string") return null;
|
|
316
|
-
const pathOnly = raw.split("?")[0].trim();
|
|
317
|
-
if (!pathOnly.startsWith("/") || /[\s#]/.test(pathOnly)) return null;
|
|
318
|
-
const trimmed = pathOnly.replace(/\/+$/, "");
|
|
319
|
-
return trimmed || null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @param {string} path
|
|
324
|
-
* @returns {boolean}
|
|
325
|
-
*/
|
|
326
|
-
function isReserved(path) {
|
|
327
|
-
return RESERVED_PREFIXES.includes(path);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* @param {string} rule
|
|
332
|
-
* @param {string} candidate
|
|
333
|
-
* @returns {boolean}
|
|
334
|
-
*/
|
|
335
|
-
function coveredBy(rule, candidate) {
|
|
336
|
-
const ruleExact = rule.replace(/\/+$/, "");
|
|
337
|
-
const candExact = candidate.replace(/\/+$/, "");
|
|
338
|
-
if (candExact === ruleExact) return true;
|
|
339
|
-
const prefix = rule.endsWith("/") ? rule : `${ruleExact}/`;
|
|
340
|
-
return candidate.startsWith(prefix) || `${candExact}/`.startsWith(prefix);
|
|
341
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Kullanıcının `robots.txt` gövdesinin altına framework uçlarını ekler.
|
|
3
|
+
*
|
|
4
|
+
* Panel, görsel proxy, auth handoff, dev araçları ve fragment parçaları
|
|
5
|
+
* sayfa değildir; bir crawler onları dizinine alırsa indeks parçalı HTML
|
|
6
|
+
* ve yönetim yollarıyla kirlenir. Uygulama dosyayı kendisi yazar (`public/`
|
|
7
|
+
* ya da bir route). Framework yalnızca başarılı metin yanıtının sonuna bir
|
|
8
|
+
* not ve `Disallow` grubu düşer — dosya yoksa uydurulmaz.
|
|
9
|
+
*
|
|
10
|
+
* Google, belirli bir user-agent grubunu `User-agent: *` ile birleştirmez;
|
|
11
|
+
* aynı ajanın ikinci grubunu birleştirir. Bu yüzden alttaki grup, dosyada
|
|
12
|
+
* adı geçen her ajanı tekrarlar. Aksi hâlde yalnızca `Googlebot` grubu olan
|
|
13
|
+
* bir dosyada ek hiç uygulanmazdı.
|
|
14
|
+
*
|
|
15
|
+
* Sıkıştırılmış kopyanın (`Content-Encoding` dolu) üzerine yazılmaz: ek,
|
|
16
|
+
* düz metnin sonuna konur, sıkıştırma dıştaki middleware'de olur.
|
|
17
|
+
*/
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import { getConfig } from "../../config/index.js";
|
|
20
|
+
|
|
21
|
+
/** İdempotency anahtarı. Marka adı değişse de ikinci kez eklenmez. */
|
|
22
|
+
const NOTE_MARK = "framework endpoints, not for indexing";
|
|
23
|
+
|
|
24
|
+
/** Framework'ün ayırdığı önekler. Özel bir yola taşınsalar da bunlar kalır. */
|
|
25
|
+
const RESERVED_PREFIXES = ["/_jskelet/", "/__jskelet/", "/_fragment/"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} [brandName]
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function frameworkRobotsNote(brandName = "JSkelet") {
|
|
32
|
+
const name = String(brandName || "JSkelet")
|
|
33
|
+
.replace(/[\r\n]+/g, " ")
|
|
34
|
+
.trim();
|
|
35
|
+
return `# ${name || "JSkelet"} — ${NOTE_MARK}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `robots.txt`'e yazılacak `Disallow` yolları.
|
|
40
|
+
*
|
|
41
|
+
* Üç önek her zaman vardır. Panel, uzak görsel ve handoff yalnızca
|
|
42
|
+
* gerçekten mount edildiklerinde ve öneklerin dışına taşındıklarında eklenir.
|
|
43
|
+
* Dev araçlarının özel yolu yalnızca development'ta yazılır: production'da
|
|
44
|
+
* o yol uygulamanın kendi sayfası olabilir.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} [config]
|
|
47
|
+
* @param {{ dev?: boolean }} [options]
|
|
48
|
+
* @returns {string[]}
|
|
49
|
+
*/
|
|
50
|
+
export function frameworkDisallowPaths(config = {}, options = {}) {
|
|
51
|
+
/** @type {string[]} */
|
|
52
|
+
const paths = [...RESERVED_PREFIXES];
|
|
53
|
+
const dev = options.dev ?? process.env.NODE_ENV === "development";
|
|
54
|
+
|
|
55
|
+
/** @type {string[]} */
|
|
56
|
+
const extras = [];
|
|
57
|
+
|
|
58
|
+
const admin = /** @type {{ enabled?: boolean, basePath?: string }} */ (
|
|
59
|
+
config.admin
|
|
60
|
+
);
|
|
61
|
+
if (admin?.enabled && typeof admin.basePath === "string") {
|
|
62
|
+
extras.push(admin.basePath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const images = /** @type {{ remote?: { enabled?: boolean, path?: string } | false } | false} */ (
|
|
66
|
+
config.images
|
|
67
|
+
);
|
|
68
|
+
const remote = images && images !== false ? images.remote : null;
|
|
69
|
+
if (remote && remote !== false && remote.enabled && typeof remote.path === "string") {
|
|
70
|
+
extras.push(remote.path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const auth = /** @type {{ crossSubdomainHandoff?: boolean | { enabled?: boolean, path?: string } }} */ (
|
|
74
|
+
config.auth
|
|
75
|
+
);
|
|
76
|
+
const handoff = auth?.crossSubdomainHandoff;
|
|
77
|
+
const handoffOn =
|
|
78
|
+
handoff === true ||
|
|
79
|
+
(Boolean(handoff) &&
|
|
80
|
+
typeof handoff === "object" &&
|
|
81
|
+
handoff.enabled !== false);
|
|
82
|
+
if (handoffOn && handoff && typeof handoff === "object" && typeof handoff.path === "string") {
|
|
83
|
+
extras.push(handoff.path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const brand = /** @type {{ devBasePath?: string }} */ (config.brand);
|
|
87
|
+
if (dev && typeof brand?.devBasePath === "string") {
|
|
88
|
+
extras.push(brand.devBasePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const raw of extras) {
|
|
92
|
+
const path = cleanDisallowPath(raw);
|
|
93
|
+
if (!path) continue;
|
|
94
|
+
if (paths.some((existing) => coveredBy(existing, path))) continue;
|
|
95
|
+
paths.push(path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return paths;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} body
|
|
103
|
+
* @param {{ brandName?: string, paths: string[] }} options
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function appendFrameworkRobots(body, options) {
|
|
107
|
+
const text = String(body ?? "").replace(/^\uFEFF/, "");
|
|
108
|
+
const paths = options.paths.filter((path) => cleanDisallowPath(path) || isReserved(path));
|
|
109
|
+
if (paths.length === 0 || text.includes(NOTE_MARK)) return text;
|
|
110
|
+
|
|
111
|
+
const agents = collectUserAgents(text);
|
|
112
|
+
const block = [
|
|
113
|
+
frameworkRobotsNote(options.brandName),
|
|
114
|
+
...agents.map((agent) => `User-agent: ${agent}`),
|
|
115
|
+
...paths.map((path) => `Disallow: ${path}`),
|
|
116
|
+
"",
|
|
117
|
+
].join("\n");
|
|
118
|
+
|
|
119
|
+
const base = text.replace(/\s*$/, "");
|
|
120
|
+
return base ? `${base}\n\n${block}` : block;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @returns {import('express').RequestHandler}
|
|
125
|
+
*/
|
|
126
|
+
export function robotsTxtMiddleware() {
|
|
127
|
+
return function robotsTxt(req, res, next) {
|
|
128
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
129
|
+
next();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pathname = req.path || "";
|
|
134
|
+
if (pathname !== "/robots.txt") {
|
|
135
|
+
next();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handler'ın kendi ETag'i eklenmemiş gövdeye aittir. 304, crawler'ın
|
|
140
|
+
// framework bloğu olmayan kopyayı tutmasına yol açardı.
|
|
141
|
+
const previousEtag = req.headers["if-none-match"];
|
|
142
|
+
delete req.headers["if-none-match"];
|
|
143
|
+
delete req.headers["if-modified-since"];
|
|
144
|
+
|
|
145
|
+
/** @type {Buffer[]} */
|
|
146
|
+
const chunks = [];
|
|
147
|
+
const originalEnd = res.end;
|
|
148
|
+
let finished = false;
|
|
149
|
+
|
|
150
|
+
res.write = function write(chunk, encoding, callback) {
|
|
151
|
+
const enc = typeof encoding === "function" ? undefined : encoding;
|
|
152
|
+
const done = typeof encoding === "function" ? encoding : callback;
|
|
153
|
+
const buf = takeChunk(chunk, enc);
|
|
154
|
+
if (buf && buf.length) chunks.push(buf);
|
|
155
|
+
if (typeof done === "function") done();
|
|
156
|
+
return true;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
res.end = function end(chunk, encoding, callback) {
|
|
160
|
+
if (finished) return originalEnd.call(this);
|
|
161
|
+
finished = true;
|
|
162
|
+
|
|
163
|
+
const enc = typeof encoding === "function" ? undefined : encoding;
|
|
164
|
+
const done =
|
|
165
|
+
typeof chunk === "function"
|
|
166
|
+
? chunk
|
|
167
|
+
: typeof encoding === "function"
|
|
168
|
+
? encoding
|
|
169
|
+
: callback;
|
|
170
|
+
const buf = takeChunk(chunk, enc);
|
|
171
|
+
if (buf && buf.length) chunks.push(buf);
|
|
172
|
+
|
|
173
|
+
const body = chunks.length === 0 ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
174
|
+
|
|
175
|
+
if (!shouldRewrite(req, res, body)) {
|
|
176
|
+
return typeof done === "function"
|
|
177
|
+
? originalEnd.call(this, body, done)
|
|
178
|
+
: originalEnd.call(this, body);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const context = readRobotsContext();
|
|
182
|
+
const nextBody = Buffer.from(
|
|
183
|
+
appendFrameworkRobots(body.toString("utf8"), context),
|
|
184
|
+
"utf8",
|
|
185
|
+
);
|
|
186
|
+
const tag = `"${crypto.createHash("sha1").update(nextBody).digest("base64url")}"`;
|
|
187
|
+
|
|
188
|
+
if (previousEtag && etagMatches(previousEtag, tag)) {
|
|
189
|
+
res.statusCode = 304;
|
|
190
|
+
res.setHeader("ETag", tag);
|
|
191
|
+
res.setHeader("Content-Length", "0");
|
|
192
|
+
res.removeHeader("Last-Modified");
|
|
193
|
+
return originalEnd.call(this);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const type = String(res.getHeader("content-type") || "");
|
|
197
|
+
if (!/^text\/plain\b/i.test(type)) {
|
|
198
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
199
|
+
}
|
|
200
|
+
res.setHeader("ETag", tag);
|
|
201
|
+
res.setHeader("Content-Length", String(nextBody.length));
|
|
202
|
+
res.removeHeader("Last-Modified");
|
|
203
|
+
|
|
204
|
+
return typeof done === "function"
|
|
205
|
+
? originalEnd.call(this, nextBody, done)
|
|
206
|
+
: originalEnd.call(this, nextBody);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
next();
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @returns {{ brandName: string, paths: string[] }}
|
|
215
|
+
*/
|
|
216
|
+
function readRobotsContext() {
|
|
217
|
+
try {
|
|
218
|
+
const config = getConfig();
|
|
219
|
+
const brand = /** @type {{ name?: string }} */ (config.brand);
|
|
220
|
+
return {
|
|
221
|
+
brandName: typeof brand?.name === "string" && brand.name ? brand.name : "JSkelet",
|
|
222
|
+
paths: frameworkDisallowPaths(config),
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return { brandName: "JSkelet", paths: frameworkDisallowPaths({}) };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {import('express').Request} req
|
|
231
|
+
* @param {import('express').Response} res
|
|
232
|
+
* @param {Buffer} body
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
function shouldRewrite(req, res, body) {
|
|
236
|
+
if (req.method !== "GET") return false;
|
|
237
|
+
if (res.headersSent) return false;
|
|
238
|
+
|
|
239
|
+
const status = res.statusCode || 200;
|
|
240
|
+
if (status < 200 || status >= 300) return false;
|
|
241
|
+
if (res.getHeader("content-encoding")) return false;
|
|
242
|
+
|
|
243
|
+
const type = String(res.getHeader("content-type") || "");
|
|
244
|
+
if (type && !/^text\/(?:plain|html)\b/i.test(type)) return false;
|
|
245
|
+
|
|
246
|
+
const sample = body.subarray(0, 64).toString("utf8");
|
|
247
|
+
if (/^\s*<(!doctype|html)\b/i.test(sample)) return false;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {string | string[] | undefined} header
|
|
253
|
+
* @param {string} tag
|
|
254
|
+
* @returns {boolean}
|
|
255
|
+
*/
|
|
256
|
+
function etagMatches(header, tag) {
|
|
257
|
+
const value = Array.isArray(header) ? header.join(",") : String(header);
|
|
258
|
+
return value.split(",").some((part) => {
|
|
259
|
+
const token = part.trim().replace(/^W\//i, "");
|
|
260
|
+
return token === "*" || token === tag;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {unknown} chunk
|
|
266
|
+
* @param {unknown} encoding
|
|
267
|
+
* @returns {Buffer | null}
|
|
268
|
+
*/
|
|
269
|
+
function takeChunk(chunk, encoding) {
|
|
270
|
+
if (chunk == null || typeof chunk === "function") return null;
|
|
271
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
272
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk);
|
|
273
|
+
if (typeof chunk === "string") {
|
|
274
|
+
const enc = typeof encoding === "string" ? encoding : "utf8";
|
|
275
|
+
return Buffer.from(chunk, /** @type {BufferEncoding} */ (enc));
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {string} text
|
|
282
|
+
* @returns {string[]}
|
|
283
|
+
*/
|
|
284
|
+
function collectUserAgents(text) {
|
|
285
|
+
/** @type {string[]} */
|
|
286
|
+
const agents = [];
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
|
|
289
|
+
for (const line of text.split(/\r?\n/)) {
|
|
290
|
+
const match = /^[ \t]*user-agent[ \t]*:[ \t]*(.*?)[ \t]*$/i.exec(line);
|
|
291
|
+
if (!match) continue;
|
|
292
|
+
const agent = match[1].trim();
|
|
293
|
+
if (!agent || /[\r\n]/.test(agent)) continue;
|
|
294
|
+
const key = agent.toLowerCase();
|
|
295
|
+
if (seen.has(key)) continue;
|
|
296
|
+
seen.add(key);
|
|
297
|
+
agents.push(agent);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const star = agents.findIndex((agent) => agent === "*");
|
|
301
|
+
if (star === -1) agents.unshift("*");
|
|
302
|
+
else if (star > 0) {
|
|
303
|
+
agents.splice(star, 1);
|
|
304
|
+
agents.unshift("*");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return agents;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {string} raw
|
|
312
|
+
* @returns {string | null}
|
|
313
|
+
*/
|
|
314
|
+
function cleanDisallowPath(raw) {
|
|
315
|
+
if (typeof raw !== "string") return null;
|
|
316
|
+
const pathOnly = raw.split("?")[0].trim();
|
|
317
|
+
if (!pathOnly.startsWith("/") || /[\s#]/.test(pathOnly)) return null;
|
|
318
|
+
const trimmed = pathOnly.replace(/\/+$/, "");
|
|
319
|
+
return trimmed || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {string} path
|
|
324
|
+
* @returns {boolean}
|
|
325
|
+
*/
|
|
326
|
+
function isReserved(path) {
|
|
327
|
+
return RESERVED_PREFIXES.includes(path);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} rule
|
|
332
|
+
* @param {string} candidate
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
function coveredBy(rule, candidate) {
|
|
336
|
+
const ruleExact = rule.replace(/\/+$/, "");
|
|
337
|
+
const candExact = candidate.replace(/\/+$/, "");
|
|
338
|
+
if (candExact === ruleExact) return true;
|
|
339
|
+
const prefix = rule.endsWith("/") ? rule : `${ruleExact}/`;
|
|
340
|
+
return candidate.startsWith(prefix) || `${candExact}/`.startsWith(prefix);
|
|
341
|
+
}
|