jskelet 0.5.5 → 0.6.1
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 +19 -15
- package/CHANGELOG.md +165 -15
- package/README.md +16 -21
- package/bin/jskelet.mjs +23 -9
- package/docs/01-baslangic.md +4 -3
- package/docs/02-mimari.md +10 -4
- package/docs/03-routing.md +14 -7
- package/docs/04-render-ve-sablonlar.md +60 -43
- package/docs/05-islands.md +12 -8
- package/docs/06-cache.md +18 -7
- package/docs/07-yapilandirma.md +69 -27
- package/docs/08-build.md +15 -9
- package/docs/09-dev-araclari.md +22 -8
- package/docs/10-dagitim.md +14 -13
- package/docs/11-tasima.md +51 -17
- package/docs/12-panel-ve-oturum.md +10 -4
- package/docs/README.md +10 -33
- package/docs/en/01-getting-started.md +4 -3
- package/docs/en/02-architecture.md +12 -6
- package/docs/en/03-routing.md +15 -8
- package/docs/en/04-rendering.md +71 -59
- package/docs/en/05-islands.md +13 -8
- package/docs/en/06-caching.md +21 -7
- package/docs/en/07-configuration.md +69 -29
- package/docs/en/08-build.md +16 -10
- package/docs/en/09-dev-tools.md +24 -8
- package/docs/en/10-deployment.md +14 -14
- package/docs/en/11-migration.md +51 -16
- package/docs/en/12-dashboards-and-sessions.md +9 -4
- package/docs/en/README.md +10 -35
- package/package.json +48 -13
- 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 +48 -5
- package/src/config/index.js +138 -27
- 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 +37 -10
- package/src/server/ejs-adapter.js +59 -0
- package/src/server/html-cache.js +178 -32
- package/src/server/image-optimizer.js +94 -26
- package/src/server/middleware/dev-gate.js +21 -8
- package/src/server/middleware/robots-txt.js +341 -0
- package/src/server/port-guard.js +255 -0
- package/src/server/prewarm.js +137 -51
- package/src/server/render.js +30 -10
- 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 +477 -0
- package/types/config/index.d.ts +304 -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 +207 -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/robots-txt.d.ts +33 -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 +131 -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
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları.
|
|
3
|
-
* Hepsi HTML string döndürür;
|
|
3
|
+
* Hepsi HTML string döndürür; şablonlarda PascalCase etiket veya
|
|
4
|
+
* (legacy EJS) `<%- %>` ile basılır.
|
|
4
5
|
*/
|
|
5
|
-
import { attrs, esc, cn } from "./html.js";
|
|
6
|
-
import {
|
|
6
|
+
import { attrs, esc, cn, jsonScript } from "./html.js";
|
|
7
|
+
import {
|
|
8
|
+
asset,
|
|
9
|
+
hasAsset,
|
|
10
|
+
getSpriteIds,
|
|
11
|
+
optimizedImage,
|
|
12
|
+
} from "../../server/assets.js";
|
|
7
13
|
import {
|
|
8
14
|
parseAllowedRemoteUrl,
|
|
9
15
|
remoteImageUrl,
|
|
@@ -290,3 +296,80 @@ export function toKebab(name) {
|
|
|
290
296
|
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
291
297
|
.toLowerCase();
|
|
292
298
|
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Layout `<head>` stylesheet'leri. `hasAsset` false olanları basmaz —
|
|
302
|
+
* build yokken 404 link'i istememek için. `.jsk` ifade dilinde `asset()` /
|
|
303
|
+
* `hasAsset()` çağrılamadığı için layout bu etiketi kullanır.
|
|
304
|
+
*
|
|
305
|
+
* @param {{ styles?: string[] }} [props]
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
export function stylesheets(props = {}) {
|
|
309
|
+
const styles = Array.isArray(props.styles) ? props.styles : [];
|
|
310
|
+
/** @type {string[]} */
|
|
311
|
+
const parts = [];
|
|
312
|
+
|
|
313
|
+
if (hasAsset("app.css")) {
|
|
314
|
+
parts.push(
|
|
315
|
+
`<link rel="stylesheet" href="${esc(asset("app.css"))}" data-jskelet-css="app.css">`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const sheet of styles) {
|
|
320
|
+
if (!sheet || !hasAsset(sheet)) continue;
|
|
321
|
+
parts.push(
|
|
322
|
+
`<link rel="stylesheet" href="${esc(asset(sheet))}" data-jskelet-css="${esc(sheet)}">`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return parts.join("\n ");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Layout gövde sonu script'leri: `main.js`, controller `entries`, isteğe
|
|
331
|
+
* bağlı dev overlay.
|
|
332
|
+
*
|
|
333
|
+
* @param {{ entries?: string[], devtools?: boolean, devBasePath?: string }} [props]
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
export function bodyScripts(props = {}) {
|
|
337
|
+
const entries = Array.isArray(props.entries) ? props.entries : [];
|
|
338
|
+
const devBasePath = props.devBasePath ?? "/__jskelet/dev";
|
|
339
|
+
/** @type {string[]} */
|
|
340
|
+
const parts = [];
|
|
341
|
+
|
|
342
|
+
if (hasAsset("main.js")) {
|
|
343
|
+
parts.push(`<script type="module" src="${esc(asset("main.js"))}"></script>`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
if (!entry) continue;
|
|
348
|
+
parts.push(`<script type="module" src="${esc(asset(entry))}"></script>`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (props.devtools) {
|
|
352
|
+
parts.push(
|
|
353
|
+
`<script type="module" src="${esc(devBasePath)}/overlay.js"></script>`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return parts.join("\n ");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* JSON-LD script etiketleri. `structuredData` dizisindeki her öğe için bir
|
|
362
|
+
* `<script type="application/ld+json">`.
|
|
363
|
+
*
|
|
364
|
+
* @param {{ items?: unknown[] }} [props]
|
|
365
|
+
* @returns {string}
|
|
366
|
+
*/
|
|
367
|
+
export function jsonLd(props = {}) {
|
|
368
|
+
const items = Array.isArray(props.items) ? props.items : [];
|
|
369
|
+
return items
|
|
370
|
+
.map(
|
|
371
|
+
(item) =>
|
|
372
|
+
`<script type="application/ld+json">${jsonScript(item)}</script>`,
|
|
373
|
+
)
|
|
374
|
+
.join("\n ");
|
|
375
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} root Uygulama kökü.
|
|
3
|
+
* @param {string} specifier
|
|
4
|
+
* @returns {Promise<any>}
|
|
5
|
+
* @throws Paket bulunamazsa; zorunlu bağımlılıklar için.
|
|
6
|
+
*/
|
|
7
|
+
export declare function importFromApp(root: string, specifier: string): Promise<any>;
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} root
|
|
10
|
+
* @param {string} specifier
|
|
11
|
+
* @returns {Promise<any | null>} Paket yoksa `null`; adım atlanabilsin diye.
|
|
12
|
+
*/
|
|
13
|
+
export declare function tryImportFromApp(root: string, specifier: string): Promise<any | null>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Island'ların paylaştığı küçük DOM yardımcıları. */
|
|
2
|
+
/**
|
|
3
|
+
* @param {ParentNode} root
|
|
4
|
+
* @param {string} selector
|
|
5
|
+
* @returns {HTMLElement | null}
|
|
6
|
+
*/
|
|
7
|
+
export declare function qs(root: ParentNode, selector: string): HTMLElement | null;
|
|
8
|
+
/**
|
|
9
|
+
* @param {ParentNode} root
|
|
10
|
+
* @param {string} selector
|
|
11
|
+
* @returns {HTMLElement[]}
|
|
12
|
+
*/
|
|
13
|
+
export declare function qsa(root: ParentNode, selector: string): HTMLElement[];
|
|
14
|
+
/**
|
|
15
|
+
* Otomatik temizlenebilir listener.
|
|
16
|
+
* @param {EventTarget} target
|
|
17
|
+
* @param {string} type
|
|
18
|
+
* @param {EventListenerOrEventListenerObject} handler
|
|
19
|
+
* @param {AddEventListenerOptions | boolean} [options]
|
|
20
|
+
* @returns {() => void}
|
|
21
|
+
*/
|
|
22
|
+
export declare function on(target: EventTarget, type: string, handler: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean): () => void;
|
|
23
|
+
/**
|
|
24
|
+
* `data-*` üzerinden delege edilmiş click.
|
|
25
|
+
* @param {HTMLElement} root
|
|
26
|
+
* @param {string} selector
|
|
27
|
+
* @param {(event: MouseEvent, target: HTMLElement) => void} handler
|
|
28
|
+
* @returns {() => void}
|
|
29
|
+
*/
|
|
30
|
+
export declare function onClick(root: HTMLElement, selector: string, handler: (event: MouseEvent, target: HTMLElement) => void): () => void;
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} ms
|
|
33
|
+
* @param {(...args: any[]) => void} fn
|
|
34
|
+
* @returns {(...args: any[]) => void}
|
|
35
|
+
*/
|
|
36
|
+
export declare function debounce(ms: number, fn: (...args: any[]) => void): (...args: any[]) => void;
|
|
37
|
+
/**
|
|
38
|
+
* requestAnimationFrame ile birleştirilmiş çağrı.
|
|
39
|
+
* @param {() => void} fn
|
|
40
|
+
* @returns {() => void}
|
|
41
|
+
*/
|
|
42
|
+
export declare function raf(fn: () => void): () => void;
|
|
43
|
+
/**
|
|
44
|
+
* @param {HTMLElement} element
|
|
45
|
+
* @param {string} name
|
|
46
|
+
* @param {boolean} active
|
|
47
|
+
*/
|
|
48
|
+
export declare function toggleClass(element: HTMLElement, name: string, active: boolean): void;
|
|
49
|
+
/**
|
|
50
|
+
* Overlay portal hedefi: layout'ta `<div id="jskelet-overlays">` varsa
|
|
51
|
+
* modal/drawer içeriği oraya taşınır, yoksa `body`ye. Portal, `overflow`
|
|
52
|
+
* ya da `transform` taşıyan bir ata elementin `position: fixed` overlay'i
|
|
53
|
+
* kırpmasını engeller.
|
|
54
|
+
*/
|
|
55
|
+
export declare function getOverlayRoot(): HTMLElement;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tek bir formu bağlar.
|
|
3
|
+
*
|
|
4
|
+
* `data-target` verilirse başarılı yanıtın gövdesi o bölgeye takılır; yoksa
|
|
5
|
+
* yanıt formun kendisini değiştirir (doğrulama hatalarının geldiği yol).
|
|
6
|
+
*
|
|
7
|
+
* @param {HTMLFormElement} form
|
|
8
|
+
* @returns {() => void} Dinleyiciyi söken fonksiyon.
|
|
9
|
+
*/
|
|
10
|
+
export declare function enhanceForm(form: HTMLFormElement): () => void;
|
|
11
|
+
/**
|
|
12
|
+
* `data-enhance` taşıyan formları tek bir delege dinleyiciyle bağlar.
|
|
13
|
+
* `submit` olayı köpürdüğü için sonradan DOM'a giren (fragment ile gelen)
|
|
14
|
+
* formlar da kapsanır ve her takastan sonra yeniden bağlama gerekmez.
|
|
15
|
+
*
|
|
16
|
+
* @param {ParentNode} [root]
|
|
17
|
+
* @returns {() => void}
|
|
18
|
+
*/
|
|
19
|
+
export declare function startForms(root?: ParentNode): () => void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSkelet island runtime'ının tarayıcı tarafı API'si.
|
|
3
|
+
*
|
|
4
|
+
* `client/entries/main.js` (veya `main.ts`) içinde:
|
|
5
|
+
*
|
|
6
|
+
* import { registerAll, start } from "jskelet/client";
|
|
7
|
+
*
|
|
8
|
+
* registerAll({
|
|
9
|
+
* counter: () => import("../islands/counter.ts"),
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* start();
|
|
13
|
+
*/
|
|
14
|
+
export { register, registerAll, hydrate, unmount, observeDocument, start, } from "./registry.js";
|
|
15
|
+
export { swap, startSwapLinks } from "./swap.js";
|
|
16
|
+
export { enhanceForm, startForms } from "./form.js";
|
|
17
|
+
export { createStore } from "./store.js";
|
|
18
|
+
export { debounce, on, onClick, qs, qsa, raf, toggleClass, getOverlayRoot, } from "./dom.js";
|
|
19
|
+
export { startSafeImages } from "./safe-image.js";
|
|
20
|
+
export { clearSharedCookie, consumeWindowNameHandoff, createHandoffUrl, handoffViaWindowName, readCookie, resolveSharedCookieDomain, SHARED_COOKIE_WARN_BYTES, writeSharedCookie, } from "./shared-cookie.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Island registry.
|
|
3
|
+
*
|
|
4
|
+
* Sunucu HTML'i tamdır; JS yalnızca davranış ekler. Bir element
|
|
5
|
+
* `data-island="ad"` taşıdığında ilgili modül dinamik import edilir ve
|
|
6
|
+
* `mount(element, props)` çağrılır.
|
|
7
|
+
*
|
|
8
|
+
* Hidrasyon **varsayılan olarak görünürlüğe bağlıdır**: her island bir
|
|
9
|
+
* IntersectionObserver'a verilir, ekranda olanlar zaten ilk gözlemde tetiklenir,
|
|
10
|
+
* ekran dışındakiler kaydırılana kadar hiç indirilmez. Ana sayfadaki grafik
|
|
11
|
+
* kütüphanesi gibi ağır modüller böylece ilk yükten tamamen çıkar.
|
|
12
|
+
*
|
|
13
|
+
* `data-island-eager` görünürlükten bağımsız, hemen bağlanmayı zorlar
|
|
14
|
+
* (header, çerez bandı gibi global davranışlar için).
|
|
15
|
+
*
|
|
16
|
+
* `data-island-idle` görünür olsa bile sayfa sakinleşene (load + boş zaman)
|
|
17
|
+
* kadar bekletir. İlk ekranda görünen ama kritik olmayan ağır modüller
|
|
18
|
+
* (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
|
|
19
|
+
*
|
|
20
|
+
* `data-island-props` JSON ise parse edilip props olarak geçer.
|
|
21
|
+
*/
|
|
22
|
+
export type MountFn = (element: HTMLElement, props: object) => void | (() => void);
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} name
|
|
25
|
+
* @param {() => Promise<{ mount: MountFn }>} loader
|
|
26
|
+
*/
|
|
27
|
+
export declare function register(name: string, loader: () => Promise<{
|
|
28
|
+
mount: MountFn;
|
|
29
|
+
}>): void;
|
|
30
|
+
/** @param {Record<string, () => Promise<{ mount: MountFn }>>} entries */
|
|
31
|
+
export declare function registerAll(entries: Record<string, () => Promise<{
|
|
32
|
+
mount: MountFn;
|
|
33
|
+
}>>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Bir alt ağaçtaki island'ları söker.
|
|
36
|
+
*
|
|
37
|
+
* Fragment takasında çağrılması zorunlu: `innerHTML` ile değiştirilen bir
|
|
38
|
+
* bölgenin island'ları DOM'dan çıkar ama `document`/`window` üzerine
|
|
39
|
+
* kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder. Birkaç
|
|
40
|
+
* takastan sonra aynı olay birden fazla kez işlenmeye başlar.
|
|
41
|
+
*
|
|
42
|
+
* Sökülen element yeniden bağlanabilir hâle gelir: `mounted` kaydı da
|
|
43
|
+
* temizlenir, böylece aynı düğüm tekrar DOM'a girerse `hydrate()` onu
|
|
44
|
+
* yeniden görür.
|
|
45
|
+
*
|
|
46
|
+
* @param {ParentNode} [root] Kökün kendisi de island olabilir.
|
|
47
|
+
*/
|
|
48
|
+
export declare function unmount(root?: ParentNode): void;
|
|
49
|
+
/** @param {ParentNode} [root] */
|
|
50
|
+
export declare function hydrate(root?: ParentNode): void;
|
|
51
|
+
/** Sonradan DOM'a eklenen island'ları da yakalar (infinite scroll, portal). */
|
|
52
|
+
export declare function observeDocument(): MutationObserver;
|
|
53
|
+
export declare function start(): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yüklenemeyen görseller için tek bir belge dinleyicisi.
|
|
3
|
+
*
|
|
4
|
+
* Bu iş bilinçli olarak **island değil**. Görsel ağırlıklı bir sayfada 80+
|
|
5
|
+
* `<img>` olabiliyor ve her birine ayrı island bağlamak (gözlemci + dinamik
|
|
6
|
+
* import + mount) sırf hata ihtimali için ciddi bir hidrasyon yükü. `error`
|
|
7
|
+
* olayı kabarmaz ama yakalama fazında görülebilir, bu yüzden tek dinleyici
|
|
8
|
+
* hepsini karşılar ve sonradan DOM'a eklenen görseller de kendiliğinden
|
|
9
|
+
* kapsanır.
|
|
10
|
+
*
|
|
11
|
+
* Kullanım: görsele `data-safe-image` ekleyin. Kendi hata görünümünüzü
|
|
12
|
+
* vermek için görseli `data-safe-image-host` taşıyan bir sarmalayıcıya alın
|
|
13
|
+
* ve içine `<template data-safe-image-fallback>` koyun — framework hiçbir
|
|
14
|
+
* stil dayatmaz, yalnızca değiştirme işini yapar.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
19
|
+
export declare function startSafeImages(): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export { SHARED_COOKIE_WARN_BYTES, resolveSharedCookieDomain, stripHostPort, } from "../shared/cookie-domain.js";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} name
|
|
4
|
+
* @returns {string | null}
|
|
5
|
+
*/
|
|
6
|
+
export declare function readCookie(name: string): string | null;
|
|
7
|
+
export type ClientSharedCookieResult = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
domain: string | null;
|
|
10
|
+
handoff: boolean;
|
|
11
|
+
reason?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} ClientSharedCookieResult
|
|
15
|
+
* @property {boolean} ok
|
|
16
|
+
* @property {string | null} domain
|
|
17
|
+
* @property {boolean} handoff
|
|
18
|
+
* @property {string} [reason]
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} name
|
|
22
|
+
* @param {string} value
|
|
23
|
+
* @param {{ roots?: string[], maxAge?: number, path?: string, sameSite?: string,
|
|
24
|
+
* secure?: boolean }} [options]
|
|
25
|
+
* @returns {ClientSharedCookieResult}
|
|
26
|
+
*/
|
|
27
|
+
export declare function writeSharedCookie(name: string, value: string, options?: {
|
|
28
|
+
roots?: string[];
|
|
29
|
+
maxAge?: number;
|
|
30
|
+
path?: string;
|
|
31
|
+
sameSite?: string;
|
|
32
|
+
secure?: boolean;
|
|
33
|
+
}): ClientSharedCookieResult;
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @param {{ roots?: string[], path?: string, secure?: boolean, sameSite?: string }} [options]
|
|
37
|
+
* @returns {ClientSharedCookieResult}
|
|
38
|
+
*/
|
|
39
|
+
export declare function clearSharedCookie(name: string, options?: {
|
|
40
|
+
roots?: string[];
|
|
41
|
+
path?: string;
|
|
42
|
+
secure?: boolean;
|
|
43
|
+
sameSite?: string;
|
|
44
|
+
}): ClientSharedCookieResult;
|
|
45
|
+
/**
|
|
46
|
+
* Handoff bileti ister; dönen URL'ye gidilir (`?handoff=` taşır).
|
|
47
|
+
*
|
|
48
|
+
* @param {{ name: string, value: string, next: string }} body
|
|
49
|
+
* @param {{ path?: string }} [options]
|
|
50
|
+
* @returns {Promise<string | null>}
|
|
51
|
+
*/
|
|
52
|
+
export declare function createHandoffUrl(body: {
|
|
53
|
+
name: string;
|
|
54
|
+
value: string;
|
|
55
|
+
next: string;
|
|
56
|
+
}, options?: {
|
|
57
|
+
path?: string;
|
|
58
|
+
}): Promise<string | null>;
|
|
59
|
+
/**
|
|
60
|
+
* first-party `window.name` köprüsü — cookie Domain reddedilince yedek.
|
|
61
|
+
* Kaynak sayfada hedefe gitmeden önce çağırın; hedefte
|
|
62
|
+
* `consumeWindowNameHandoff` okur.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} targetUrl
|
|
65
|
+
* @param {{ name: string, value: string, maxAge?: number }} cookie
|
|
66
|
+
* @returns {void}
|
|
67
|
+
*/
|
|
68
|
+
export declare function handoffViaWindowName(targetUrl: string, cookie: {
|
|
69
|
+
name: string;
|
|
70
|
+
value: string;
|
|
71
|
+
maxAge?: number;
|
|
72
|
+
}): void;
|
|
73
|
+
/**
|
|
74
|
+
* Hedef host'ta `window.name` içindeki handoff'u host-only cookie olarak yazar.
|
|
75
|
+
*
|
|
76
|
+
* @param {{ write?: typeof writeSharedCookie }} [options]
|
|
77
|
+
* Varsayılan: Domain denemeden host-only `document.cookie`.
|
|
78
|
+
* @returns {boolean} Bir şey yazıldı mı.
|
|
79
|
+
*/
|
|
80
|
+
export declare function consumeWindowNameHandoff(options?: {
|
|
81
|
+
write?: typeof writeSharedCookie;
|
|
82
|
+
}): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Context yerine kullanılan minimal pub/sub store.
|
|
3
|
+
* `useSyncExternalStore` köprüsünün yerini alır: doğrudan `subscribe`.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @param {T} initial
|
|
7
|
+
* @returns {{
|
|
8
|
+
* get: () => T,
|
|
9
|
+
* set: (next: T | ((prev: T) => T)) => void,
|
|
10
|
+
* subscribe: (listener: (value: T) => void) => () => void,
|
|
11
|
+
* }}
|
|
12
|
+
* @template T
|
|
13
|
+
*/
|
|
14
|
+
export declare function createStore<T>(initial: T): {
|
|
15
|
+
get: () => T;
|
|
16
|
+
set: (next: T | ((prev: T) => T)) => void;
|
|
17
|
+
subscribe: (listener: (value: T) => void) => () => void;
|
|
18
|
+
};
|