jskelet 0.6.17 → 0.7.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/CHANGELOG.md +23 -0
- package/docs/04-render-ve-sablonlar.md +13 -3
- package/docs/09-dev-araclari.md +8 -0
- package/docs/en/04-rendering.md +13 -3
- package/docs/en/09-dev-tools.md +8 -0
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +155 -2
- package/src/compile/codegen.js +70 -4
- package/src/compile/compile-all.js +10 -1
- package/src/compile/index.js +6 -1
- package/src/server/assets.js +51 -22
- package/src/server/dev/devtools.js +47 -1
- package/src/server/render.js +20 -9
- package/src/templates/layout.jsk +4 -4
- package/src/templates/layout.render.js +2 -21
- package/src/views/components/loader.js +11 -6
- package/src/views/helpers/html.js +13 -5
- package/src/views/helpers/tags.js +21 -4
- package/types/compile/codegen.d.ts +9 -4
- package/types/compile/compile-all.d.ts +3 -1
- package/types/compile/index.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@ one is listed under a **Breaking** heading.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `.jsk` codegen merges adjacent static HTML into fewer `+=` ops and drops
|
|
14
|
+
template indentation (newline-only whitespace). A lone space between
|
|
15
|
+
inline siblings is kept; `pre` / `textarea` / `script` / `style` / `code`
|
|
16
|
+
preserve whitespace. Framework default `layout.render.js` regenerated.
|
|
17
|
+
- `.jsk` page layout no longer copies the full helpers object into `data`
|
|
18
|
+
each render (EJS layouts still receive helpers as locals).
|
|
19
|
+
- `esc()` skips the regex replace when the string has no special characters;
|
|
20
|
+
`attrs()` builds the attribute string without an intermediate array.
|
|
21
|
+
- Compiled templates and JS components load with parallel `import` on first
|
|
22
|
+
render; production skips the `.jsk` mtime scan when a template manifest
|
|
23
|
+
already exists.
|
|
24
|
+
- `icon()` caches the sprite URL in production; `Image` `fill` without an
|
|
25
|
+
extra `class` skips `twMerge`; `optimizedImage()` materializes CDN-prefixed
|
|
26
|
+
variant URLs once at load time.
|
|
27
|
+
- `scripts/measure-ssr.mjs` microbench for fixture `renderView` /
|
|
28
|
+
`renderPage` and live cache MISS/HIT timings against a running app.
|
|
29
|
+
|
|
11
30
|
### Fixed
|
|
12
31
|
|
|
13
32
|
- S3 PutObject uses path-style URLs for bucket names that contain dots
|
|
@@ -17,6 +36,10 @@ one is listed under a **Breaking** heading.
|
|
|
17
36
|
|
|
18
37
|
### Added
|
|
19
38
|
|
|
39
|
+
- Dev tools **Server** tab can clear the HTML cache for the open page, every
|
|
40
|
+
HTML entry, or the data cache. Live stats include HTML/data entry counts;
|
|
41
|
+
new endpoints under `devBasePath`: `POST /cache/clear-html`,
|
|
42
|
+
`/cache/clear-data`, and `/cache/invalidate`.
|
|
20
43
|
- On-demand HTML cache via `cache().prewarm: false`: visited pages still
|
|
21
44
|
write to L1 and Redis/disk L2 with the usual `html` TTLs, but classic
|
|
22
45
|
prewarm, `onVisit` link warming, and early-expiry HTTP refresh stay off
|
|
@@ -73,6 +73,12 @@ controller data → import edilmiş render(data, helpers) → HTML
|
|
|
73
73
|
Atama, object literal ve rastgele fonksiyon çağrısı yok — mantık controller
|
|
74
74
|
veya JS bileşende kalır.
|
|
75
75
|
|
|
76
|
+
Derleyici bitişik statik HTML’i tek literal’de birleştirir ve **şablon
|
|
77
|
+
girintisini** düşürür: satır sonu içeren salt-whitespace metin düğümleri
|
|
78
|
+
çıktıya girmez; etiketler arasında yalnızca boşluk/tab varsa tek boşluk
|
|
79
|
+
kalır (inline aralık). `pre`, `textarea`, `script`, `style` ve `code`
|
|
80
|
+
içinde whitespace olduğu gibi korunur.
|
|
81
|
+
|
|
76
82
|
#### Şablon mu, bileşen mi?
|
|
77
83
|
|
|
78
84
|
EJS’den geçerken sınırı erken çizmek işe yarar:
|
|
@@ -199,11 +205,15 @@ Dikkat edilecek noktalar:
|
|
|
199
205
|
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
200
206
|
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
|
201
207
|
| `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
|
|
202
|
-
| `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
|
|
203
|
-
| html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
204
|
-
| `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
|
|
208
|
+
| `asset`, `hasAsset` | fonksiyon | Manifest erişimi (EJS local; `.jsk`’de ikinci `helpers` argümanı / yerleşik etiketler) |
|
|
209
|
+
| html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` — aynı ayrım |
|
|
210
|
+
| `views/components/**` export'ları | fonksiyon | Otomatik kayıt (PascalCase etiket veya EJS local) |
|
|
205
211
|
| `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
|
|
206
212
|
|
|
213
|
+
`.jsk` layout `(data, helpers)` aldığı için framework helpers nesnesini her
|
|
214
|
+
render’da `data`’ya kopyalamaz; EJS layout hâlâ bare helper adlarını local
|
|
215
|
+
olarak alır.
|
|
216
|
+
|
|
207
217
|
`pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
|
|
208
218
|
sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
|
|
209
219
|
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -208,6 +208,11 @@ Gösterdikleri:
|
|
|
208
208
|
bloke süresi.
|
|
209
209
|
- **Prewarm:** ısıtma turunun ilerlemesi; panelden elle tetiklenebilir, tek tek
|
|
210
210
|
yollar tekrar denenebilir.
|
|
211
|
+
- **Önbellek (Server sekmesi):** HTML ve veri girdisi sayıları; **Clear this
|
|
212
|
+
page** açık yolun bütün varyantlarını sert düşürüp sayfayı yeniler, **Clear
|
|
213
|
+
all HTML** / **Clear data cache** süreç önbelleğini boşaltır (onay ister).
|
|
214
|
+
Aynı işlemler admin panelindeki `html:invalidate` / `html:clear` /
|
|
215
|
+
`data:clear` ile paylaşılır.
|
|
211
216
|
- **Süreç:** pid, Node sürümü, uptime, RSS ve heap kullanımı.
|
|
212
217
|
- **Sürüm:** kurulu JSkelet sürümü ve npm'deki `latest` ile karşılaştırması.
|
|
213
218
|
Yeni bir sürüm varsa **Server** sekmesinde `update` rozeti ve yükseltme
|
|
@@ -297,6 +302,9 @@ Rapor katmanı yalnızca development'ta yüklenir, üretim çıktısına hiç gi
|
|
|
297
302
|
| `/report/clear` | POST | Sayfa ölçümlerini ve sunucu API kayıtlarını sıfırlar |
|
|
298
303
|
| `/prewarm` | POST | Isıtmayı elle tetikler. Gövdede `paths` varsa yalnızca o yollar; ısıtma zaten çalışıyorsa 409. |
|
|
299
304
|
| `/clear` | POST | İstek ve hata günlüklerini sıfırlar |
|
|
305
|
+
| `/cache/clear-html` | POST | Tüm HTML önbelleğini boşaltır; `{ cleared }` döner |
|
|
306
|
+
| `/cache/clear-data` | POST | Veri önbelleğini boşaltır. Gövdede isteğe bağlı `prefix`; `{ cleared }` döner |
|
|
307
|
+
| `/cache/invalidate` | POST | Gövdede `path` (zorunlu, `/` ile başlamalı) ve isteğe bağlı `hard` (varsayılan `true`). Etkilenen girdi sayısıyla `{ count }` döner |
|
|
300
308
|
|
|
301
309
|
Bu uçların tamamı `mountDevtools()` tarafından yalnızca
|
|
302
310
|
`NODE_ENV=development` iken bağlanır; prod sürecine dinamik import sayesinde
|
package/docs/en/04-rendering.md
CHANGED
|
@@ -74,6 +74,12 @@ The expression language is intentionally small (access, compare, ternary,
|
|
|
74
74
|
`.length`). No assignments, object literals, or arbitrary calls — keep logic in
|
|
75
75
|
controllers or JS components.
|
|
76
76
|
|
|
77
|
+
The compiler merges adjacent static HTML into fewer `+=` ops and strips
|
|
78
|
+
**template indentation**: whitespace-only text nodes that contain a newline are
|
|
79
|
+
dropped; a space/tab-only gap between siblings stays as a single space (inline
|
|
80
|
+
separation). Inside `pre`, `textarea`, `script`, `style`, and `code`, whitespace
|
|
81
|
+
is left unchanged.
|
|
82
|
+
|
|
77
83
|
#### Template or component?
|
|
78
84
|
|
|
79
85
|
When moving off EJS, draw the line early:
|
|
@@ -198,11 +204,15 @@ Points to watch:
|
|
|
198
204
|
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
199
205
|
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
|
200
206
|
| `devBasePath` | `string` | `brand.devBasePath`, defaults to `/__jskelet/dev` |
|
|
201
|
-
| `asset`, `hasAsset` | function | Manifest access |
|
|
202
|
-
| html/tags helpers | function | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
203
|
-
| exports of `views/components/**` | function | Automatic registration |
|
|
207
|
+
| `asset`, `hasAsset` | function | Manifest access (EJS local; in `.jsk` the second `helpers` argument / built-in tags) |
|
|
208
|
+
| html/tags helpers | function | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` — same split |
|
|
209
|
+
| exports of `views/components/**` | function | Automatic registration (PascalCase tags or EJS locals) |
|
|
204
210
|
| every field returned by `hooks.layoutContext()` | — | Becomes a local directly |
|
|
205
211
|
|
|
212
|
+
Because a `.jsk` layout receives `(data, helpers)`, the framework does not copy
|
|
213
|
+
the helpers object into `data` on every render; EJS layouts still get bare
|
|
214
|
+
helper names as locals.
|
|
215
|
+
|
|
206
216
|
The empty default for `pathname` is deliberate: writing `"/"` leads to the kind
|
|
207
217
|
of bug where every page thinks it is the home page and renders the logo as an
|
|
208
218
|
`<h1>`.
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -213,6 +213,11 @@ What it shows:
|
|
|
213
213
|
blocking time.
|
|
214
214
|
- **Prewarm:** the progress of the warming round; it can be triggered manually
|
|
215
215
|
from the panel, and individual paths can be retried.
|
|
216
|
+
- **Cache (Server tab):** HTML and data entry counts; **Clear this page**
|
|
217
|
+
hard-drops every variant of the open path and reloads, **Clear all HTML** /
|
|
218
|
+
**Clear data cache** empty the in-process caches (with a confirm). Same
|
|
219
|
+
core APIs as the admin panel's `html:invalidate` / `html:clear` /
|
|
220
|
+
`data:clear`.
|
|
216
221
|
- **Process:** pid, Node version, uptime, RSS and heap usage.
|
|
217
222
|
- **Version:** the installed JSkelet version compared against the `latest` tag
|
|
218
223
|
on npm. When a newer release exists, the **Server** tab grows an `update` chip
|
|
@@ -302,6 +307,9 @@ Under `brand.devBasePath` (default `/__jskelet/dev`):
|
|
|
302
307
|
| `/report/clear` | POST | Resets page measurements and server API records |
|
|
303
308
|
| `/prewarm` | POST | Triggers warming manually. If the body has `paths`, only those paths; 409 if warming is already running. |
|
|
304
309
|
| `/clear` | POST | Resets the request and error logs |
|
|
310
|
+
| `/cache/clear-html` | POST | Clears the entire HTML cache; returns `{ cleared }` |
|
|
311
|
+
| `/cache/clear-data` | POST | Clears the data cache. Optional `prefix` in the body; returns `{ cleared }` |
|
|
312
|
+
| `/cache/invalidate` | POST | Body: required `path` (must start with `/`) and optional `hard` (default `true`). Returns `{ count }` for affected entries |
|
|
305
313
|
|
|
306
314
|
All of these endpoints are mounted by `mountDevtools()` only when
|
|
307
315
|
`NODE_ENV=development`; thanks to the dynamic import, nothing is loaded into the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk SSR (optional EJS peer), vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -861,6 +861,93 @@ async function clearAll() {
|
|
|
861
861
|
pollServer();
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
/** Son önbellek eyleminin kısa geri bildirimi; sonraki poll'da silinmez. */
|
|
865
|
+
let cacheNotice = null;
|
|
866
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
867
|
+
let cacheNoticeTimer = null;
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* @param {string} message
|
|
871
|
+
*/
|
|
872
|
+
function setCacheNotice(message) {
|
|
873
|
+
cacheNotice = message;
|
|
874
|
+
if (cacheNoticeTimer) clearTimeout(cacheNoticeTimer);
|
|
875
|
+
cacheNoticeTimer = setTimeout(() => {
|
|
876
|
+
cacheNotice = null;
|
|
877
|
+
cacheNoticeTimer = null;
|
|
878
|
+
lastBody = "";
|
|
879
|
+
render();
|
|
880
|
+
}, 4000);
|
|
881
|
+
lastBody = "";
|
|
882
|
+
render();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Bu sekmenin HTML önbelleğini düşürür ve sayfayı yeniler — aksi hâlde
|
|
887
|
+
* tarayıcıdaki DOM eski kalır, soğuk render görülmez.
|
|
888
|
+
*/
|
|
889
|
+
async function clearThisPageCache() {
|
|
890
|
+
const path = location.pathname || "/";
|
|
891
|
+
try {
|
|
892
|
+
await fetch(`${BASE}/cache/invalidate`, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: { "content-type": "application/json" },
|
|
895
|
+
body: JSON.stringify({ path, hard: true }),
|
|
896
|
+
});
|
|
897
|
+
} catch {
|
|
898
|
+
setCacheNotice("Could not reach the server to clear this page.");
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
saveState();
|
|
902
|
+
location.reload();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function clearHtmlCacheAll() {
|
|
906
|
+
if (
|
|
907
|
+
!confirm(
|
|
908
|
+
"Clear the entire HTML cache? Every page goes cold until the next render or prewarm.",
|
|
909
|
+
)
|
|
910
|
+
) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
const response = await fetch(`${BASE}/cache/clear-html`, { method: "POST" });
|
|
916
|
+
const body = await response.json().catch(() => ({}));
|
|
917
|
+
setCacheNotice(
|
|
918
|
+
`HTML cache cleared (${typeof body.cleared === "number" ? body.cleared : "?"} entries).`,
|
|
919
|
+
);
|
|
920
|
+
} catch {
|
|
921
|
+
setCacheNotice("Could not clear the HTML cache.");
|
|
922
|
+
}
|
|
923
|
+
pollServer();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function clearDataCacheAll() {
|
|
927
|
+
if (
|
|
928
|
+
!confirm(
|
|
929
|
+
"Clear every data-cache entry? Upstream traffic will spike on the next renders.",
|
|
930
|
+
)
|
|
931
|
+
) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
const response = await fetch(`${BASE}/cache/clear-data`, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: { "content-type": "application/json" },
|
|
939
|
+
body: JSON.stringify({}),
|
|
940
|
+
});
|
|
941
|
+
const body = await response.json().catch(() => ({}));
|
|
942
|
+
setCacheNotice(
|
|
943
|
+
`Data cache cleared (${typeof body.cleared === "number" ? body.cleared : "?"} entries).`,
|
|
944
|
+
);
|
|
945
|
+
} catch {
|
|
946
|
+
setCacheNotice("Could not clear the data cache.");
|
|
947
|
+
}
|
|
948
|
+
pollServer();
|
|
949
|
+
}
|
|
950
|
+
|
|
864
951
|
/* ---------------------------------------------------------------- arayüz */
|
|
865
952
|
|
|
866
953
|
const CSS = `
|
|
@@ -1162,6 +1249,7 @@ h4 .head-action { float: inline-end; text-transform: none; letter-spacing: 0; }
|
|
|
1162
1249
|
.status.warn { background: rgba(251,191,36,.14); color: var(--mid); }
|
|
1163
1250
|
.status.bad { background: rgba(251,113,133,.14); color: var(--bad); }
|
|
1164
1251
|
.hint { color: var(--muted); font-size: 10.5px; }
|
|
1252
|
+
.hint.block { display: block; margin: 0 0 8px; line-height: 1.45; }
|
|
1165
1253
|
.row .good { color: var(--good); } .row .mid { color: var(--mid); } .row .bad { color: var(--bad); }
|
|
1166
1254
|
.row.clickable { cursor: pointer; grid-template-columns: auto auto 1fr auto auto; }
|
|
1167
1255
|
.row.clickable.selected { border-color: rgba(255,255,255,.22); background: var(--surface-hover); }
|
|
@@ -1200,9 +1288,14 @@ h4 .head-action { float: inline-end; text-transform: none; letter-spacing: 0; }
|
|
|
1200
1288
|
.btn.mini { display: inline-flex; align-items: center; gap: 6px; padding: 4px 9px; font-size: 11px; }
|
|
1201
1289
|
.btn.mini svg { width: 12px; height: 12px; }
|
|
1202
1290
|
.btn[disabled] { opacity: .5; cursor: default; }
|
|
1203
|
-
.toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; }
|
|
1291
|
+
.toolbar { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; margin-bottom: 10px; }
|
|
1292
|
+
.toolbar.start { justify-content: flex-start; }
|
|
1204
1293
|
.btn.danger { color: #ffc9d2; border-color: rgba(251,113,133,.28); background: rgba(251,113,133,.1); }
|
|
1205
1294
|
.btn.danger:hover { color: #fff; border-color: rgba(251,113,133,.55); background: rgba(251,113,133,.22); }
|
|
1295
|
+
.notice {
|
|
1296
|
+
margin: 0 0 10px; padding: 8px 10px; border-radius: 8px; font-size: 12px; line-height: 1.4;
|
|
1297
|
+
color: #b9f2dc; background: rgba(52,211,153,.1); border: 1px solid rgba(52,211,153,.28);
|
|
1298
|
+
}
|
|
1206
1299
|
`;
|
|
1207
1300
|
|
|
1208
1301
|
let shadow = null;
|
|
@@ -2022,6 +2115,14 @@ function serverTab() {
|
|
|
2022
2115
|
{ label: "Avg. render", value: ms(average), tone: grade(average, [150, 500]) },
|
|
2023
2116
|
{ label: "Slowest", value: ms(slowest), tone: grade(slowest, [300, 1000]) },
|
|
2024
2117
|
{ label: "Restart", value: String(restarts), tone: offline ? "mid" : "" },
|
|
2118
|
+
{
|
|
2119
|
+
label: "HTML cache",
|
|
2120
|
+
value: String(serverStats.cache?.html ?? 0),
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
label: "Data cache",
|
|
2124
|
+
value: String(serverStats.cache?.data ?? 0),
|
|
2125
|
+
},
|
|
2025
2126
|
...(serverStats.prewarm?.total
|
|
2026
2127
|
? [
|
|
2027
2128
|
{
|
|
@@ -2033,6 +2134,7 @@ function serverTab() {
|
|
|
2033
2134
|
]
|
|
2034
2135
|
: []),
|
|
2035
2136
|
])}
|
|
2137
|
+
${cacheSection()}
|
|
2036
2138
|
${upstreamSection()}
|
|
2037
2139
|
<h4>Recent requests</h4>
|
|
2038
2140
|
${
|
|
@@ -2052,6 +2154,42 @@ function serverTab() {
|
|
|
2052
2154
|
`;
|
|
2053
2155
|
}
|
|
2054
2156
|
|
|
2157
|
+
/**
|
|
2158
|
+
* HTML / veri önbelleğini panelden boşaltma. Admin panelindeki aynı işlemlerin
|
|
2159
|
+
* kısa yolu; "bu sayfa" mevcut `location.pathname` için sert düşürme yapar.
|
|
2160
|
+
*
|
|
2161
|
+
* @returns {string}
|
|
2162
|
+
*/
|
|
2163
|
+
function cacheSection() {
|
|
2164
|
+
const path = location.pathname || "/";
|
|
2165
|
+
const html = serverStats?.cache?.html ?? 0;
|
|
2166
|
+
const data = serverStats?.cache?.data ?? 0;
|
|
2167
|
+
|
|
2168
|
+
return `
|
|
2169
|
+
<h4>Cache</h4>
|
|
2170
|
+
${
|
|
2171
|
+
cacheNotice
|
|
2172
|
+
? `<div class="notice">${escapeHtml(cacheNotice)}</div>`
|
|
2173
|
+
: ""
|
|
2174
|
+
}
|
|
2175
|
+
<p class="hint block">
|
|
2176
|
+
In-process HTML (${html}) and data (${data}) entries.
|
|
2177
|
+
Clearing this page drops every variant of <code>${escapeHtml(path)}</code> and reloads.
|
|
2178
|
+
</p>
|
|
2179
|
+
<div class="toolbar start">
|
|
2180
|
+
<button class="btn mini" data-action="cache-page" title="Hard-drop HTML for ${escapeHtml(path)}">
|
|
2181
|
+
${TRASH_ICON} Clear this page
|
|
2182
|
+
</button>
|
|
2183
|
+
<button class="btn mini danger" data-action="cache-html" title="Clear every HTML cache entry">
|
|
2184
|
+
${TRASH_ICON} Clear all HTML
|
|
2185
|
+
</button>
|
|
2186
|
+
<button class="btn mini danger" data-action="cache-data" title="Clear every data-cache entry">
|
|
2187
|
+
${TRASH_ICON} Clear data cache
|
|
2188
|
+
</button>
|
|
2189
|
+
</div>
|
|
2190
|
+
`;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2055
2193
|
/**
|
|
2056
2194
|
* Upstream hız freninin durumu. Fren kapalıysa (varsayılan) hiçbir şey
|
|
2057
2195
|
* basılmaz; açıkken 429 fırtınasında hızın nereye indiğini görmek, ayarı
|
|
@@ -2224,7 +2362,7 @@ const ABOUT = {
|
|
|
2224
2362
|
<div class="step"><span class="n">1</span><span><b>Route</b><span><code>server/routes/</code> matches the URL and hands the request to the right controller.</span></span></div>
|
|
2225
2363
|
<div class="step"><span class="n">2</span><span><b>Controller</b><span><code>server/controllers/</code> gathers the data it needs from the <code>lib/</code> services and prepares the model for the template.</span></span></div>
|
|
2226
2364
|
<div class="step"><span class="n">3</span><span><b>Render</b><span><code>server/render.js</code> runs the EJS template and prints manifest entries and head hints into the layout.</span></span></div>
|
|
2227
|
-
<div class="step"><span class="n">4</span><span><b>HTML cache</b><span><code>server/html-cache.js</code> stores the output; if the same URL is requested again the template never runs. The
|
|
2365
|
+
<div class="step"><span class="n">4</span><span><b>HTML cache</b><span><code>server/html-cache.js</code> stores the output; if the same URL is requested again the template never runs. The Server tab shows entry counts and can clear this page, all HTML, or the data cache.</span></span></div>
|
|
2228
2366
|
<div class="step"><span class="n">5</span><span><b>Response</b><span>Compression and cache headers are decided in <code>server/middleware/</code>; responses like event streams skip compression.</span></span></div>
|
|
2229
2367
|
</div>
|
|
2230
2368
|
<p><strong>Prewarming:</strong> <code>server/prewarm.js</code> visits the most travelled
|
|
@@ -2734,6 +2872,21 @@ function bind(root) {
|
|
|
2734
2872
|
}
|
|
2735
2873
|
|
|
2736
2874
|
if (target.dataset.action === "clear") clearAll();
|
|
2875
|
+
|
|
2876
|
+
if (target.dataset.action === "cache-page") {
|
|
2877
|
+
void clearThisPageCache();
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
if (target.dataset.action === "cache-html") {
|
|
2882
|
+
void clearHtmlCacheAll();
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
if (target.dataset.action === "cache-data") {
|
|
2887
|
+
void clearDataCacheAll();
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2737
2890
|
});
|
|
2738
2891
|
}
|
|
2739
2892
|
|
package/src/compile/codegen.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AST → ESM kaynak metni. `eval` / `new Function` yok; çıktı diskteki
|
|
3
3
|
* `.mjs` dosyasına yazılır ve runtime'da normal `import` ile yüklenir.
|
|
4
|
+
*
|
|
5
|
+
* Statik HTML parçaları bitişik `s +=` yerine tek literal'de birleşir;
|
|
6
|
+
* şablon girintisi (satır sonu içeren salt-whitespace) derlemede düşer.
|
|
7
|
+
* `pre` / `textarea` / `script` / `style` / `code` içinde whitespace korunur.
|
|
4
8
|
*/
|
|
5
9
|
import { codegenExpr } from "./expr.js";
|
|
6
10
|
import { CompileError } from "./errors.js";
|
|
@@ -10,6 +14,9 @@ import { CompileError } from "./errors.js";
|
|
|
10
14
|
* @typedef {import('./parse.js').AttrNode} AttrNode
|
|
11
15
|
*/
|
|
12
16
|
|
|
17
|
+
/** İçinde girinti sıkıştırması yapılmayan etiketler. */
|
|
18
|
+
const PRESERVE_WS = new Set(["pre", "textarea", "script", "style", "code"]);
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* @param {AstNode[]} ast
|
|
15
22
|
* @param {{ viewId: string, file?: string, knownComponents?: Set<string> }} options
|
|
@@ -49,6 +56,21 @@ export function toPascalCase(name) {
|
|
|
49
56
|
.join("");
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Şablon girintisini düşür: satır sonu içeren salt-whitespace → boş;
|
|
61
|
+
* yalnızca boşluk/tab → tek boşluk (inline aralık). Koruma alanı dışında.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} value
|
|
64
|
+
* @param {boolean} preserve
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function collapseTemplateWhitespace(value, preserve) {
|
|
68
|
+
if (preserve || !value) return value;
|
|
69
|
+
if (!/^[\t\n\r\f\v ]+$/.test(value)) return value;
|
|
70
|
+
if (/[\n\r]/.test(value)) return "";
|
|
71
|
+
return " ";
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
class CodegenContext {
|
|
53
75
|
/**
|
|
54
76
|
* @param {{ viewId: string, file?: string, knownComponents?: Set<string> }} options
|
|
@@ -71,6 +93,10 @@ class CodegenContext {
|
|
|
71
93
|
this.locals = new Set();
|
|
72
94
|
/** Çocuk gövdesinde `s` yerine kullanılacak değişken. */
|
|
73
95
|
this.outVar = "s";
|
|
96
|
+
/** Bitişik statik parçalar — flush edilene kadar birikir. */
|
|
97
|
+
this.staticBuf = "";
|
|
98
|
+
/** `pre` vb. iç içe derinliği. */
|
|
99
|
+
this.preserveWs = 0;
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
/**
|
|
@@ -81,9 +107,28 @@ class CodegenContext {
|
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
/**
|
|
110
|
+
* Birikmiş statik HTML'i tek `+=` olarak yazar.
|
|
111
|
+
* @returns {void}
|
|
112
|
+
*/
|
|
113
|
+
flushStatic() {
|
|
114
|
+
if (!this.staticBuf) return;
|
|
115
|
+
this.line(`${this.outVar} += ${JSON.stringify(this.staticBuf)};`);
|
|
116
|
+
this.staticBuf = "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} text
|
|
121
|
+
*/
|
|
122
|
+
appendStatic(text) {
|
|
123
|
+
if (text) this.staticBuf += text;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Dinamik ifade — önce statik tamponu boşaltır.
|
|
84
128
|
* @param {string} exprJs
|
|
85
129
|
*/
|
|
86
130
|
append(exprJs) {
|
|
131
|
+
this.flushStatic();
|
|
87
132
|
this.line(`${this.outVar} += ${exprJs};`);
|
|
88
133
|
}
|
|
89
134
|
|
|
@@ -105,9 +150,14 @@ class CodegenContext {
|
|
|
105
150
|
*/
|
|
106
151
|
emit(node) {
|
|
107
152
|
switch (node.type) {
|
|
108
|
-
case "Text":
|
|
109
|
-
|
|
153
|
+
case "Text": {
|
|
154
|
+
const value = collapseTemplateWhitespace(
|
|
155
|
+
node.value,
|
|
156
|
+
this.preserveWs > 0,
|
|
157
|
+
);
|
|
158
|
+
if (value) this.appendStatic(value);
|
|
110
159
|
break;
|
|
160
|
+
}
|
|
111
161
|
case "Interp": {
|
|
112
162
|
const expr = codegenExpr(node.expr, this.locals);
|
|
113
163
|
if (node.raw) this.append(`(${expr} ?? "")`);
|
|
@@ -139,17 +189,21 @@ class CodegenContext {
|
|
|
139
189
|
*/
|
|
140
190
|
emitElement(node) {
|
|
141
191
|
const bound = node.attrs.some((a) => a.bound);
|
|
192
|
+
const preserve = PRESERVE_WS.has(node.name.toLowerCase());
|
|
193
|
+
|
|
142
194
|
if (bound) {
|
|
143
195
|
this.append(
|
|
144
196
|
`"<${node.name}" + helpers.attrs({${this.attrsObject(node.attrs)}}) + ">"`,
|
|
145
197
|
);
|
|
146
198
|
} else {
|
|
147
|
-
this.
|
|
199
|
+
this.appendStatic(`<${node.name}${staticAttrString(node.attrs)}>`);
|
|
148
200
|
}
|
|
149
201
|
|
|
150
202
|
if (!node.selfClosing) {
|
|
203
|
+
if (preserve) this.preserveWs += 1;
|
|
151
204
|
this.emitNodes(node.children);
|
|
152
|
-
this.
|
|
205
|
+
if (preserve) this.preserveWs -= 1;
|
|
206
|
+
this.appendStatic(`</${node.name}>`);
|
|
153
207
|
}
|
|
154
208
|
}
|
|
155
209
|
|
|
@@ -186,13 +240,18 @@ class CodegenContext {
|
|
|
186
240
|
|
|
187
241
|
if (node.children.length) {
|
|
188
242
|
const tmp = this.nextTemp();
|
|
243
|
+
this.flushStatic();
|
|
189
244
|
this.line(`{`);
|
|
190
245
|
this.indent += 1;
|
|
191
246
|
this.line(`let ${tmp} = "";`);
|
|
192
247
|
const prevOut = this.outVar;
|
|
248
|
+
const prevStatic = this.staticBuf;
|
|
193
249
|
this.outVar = tmp;
|
|
250
|
+
this.staticBuf = "";
|
|
194
251
|
this.emitNodes(node.children);
|
|
252
|
+
this.flushStatic();
|
|
195
253
|
this.outVar = prevOut;
|
|
254
|
+
this.staticBuf = prevStatic;
|
|
196
255
|
const propsWithChildren = props
|
|
197
256
|
? `${props}, children: ${tmp}`
|
|
198
257
|
: `children: ${tmp}`;
|
|
@@ -209,14 +268,17 @@ class CodegenContext {
|
|
|
209
268
|
*/
|
|
210
269
|
emitIf(node) {
|
|
211
270
|
const test = codegenExpr(node.test, this.locals);
|
|
271
|
+
this.flushStatic();
|
|
212
272
|
this.line(`if (${test}) {`);
|
|
213
273
|
this.indent += 1;
|
|
214
274
|
this.emitNodes(node.consequent);
|
|
275
|
+
this.flushStatic();
|
|
215
276
|
this.indent -= 1;
|
|
216
277
|
if (node.alternate.length) {
|
|
217
278
|
this.line(`} else {`);
|
|
218
279
|
this.indent += 1;
|
|
219
280
|
this.emitNodes(node.alternate);
|
|
281
|
+
this.flushStatic();
|
|
220
282
|
this.indent -= 1;
|
|
221
283
|
}
|
|
222
284
|
this.line(`}`);
|
|
@@ -228,6 +290,7 @@ class CodegenContext {
|
|
|
228
290
|
emitEach(node) {
|
|
229
291
|
const list = codegenExpr(node.list, this.locals);
|
|
230
292
|
const idx = node.indexName ?? `__i${++this.temp}`;
|
|
293
|
+
this.flushStatic();
|
|
231
294
|
this.line(`{`);
|
|
232
295
|
this.indent += 1;
|
|
233
296
|
this.line(`const __list = ${list} ?? [];`);
|
|
@@ -239,6 +302,7 @@ class CodegenContext {
|
|
|
239
302
|
this.locals.add(idx);
|
|
240
303
|
if (node.indexName) this.locals.add(node.indexName);
|
|
241
304
|
this.emitNodes(node.children);
|
|
305
|
+
this.flushStatic();
|
|
242
306
|
this.locals = prev;
|
|
243
307
|
this.indent -= 1;
|
|
244
308
|
this.line(`}`);
|
|
@@ -259,6 +323,8 @@ class CodegenContext {
|
|
|
259
323
|
|
|
260
324
|
/** @returns {string} */
|
|
261
325
|
finish() {
|
|
326
|
+
this.flushStatic();
|
|
327
|
+
|
|
262
328
|
const imports = [...this.includes]
|
|
263
329
|
.map((id) => {
|
|
264
330
|
const alias = includeAlias(id);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
6
7
|
import { parseTemplate } from "./parse.js";
|
|
7
8
|
import { codegen, normalizeIncludeId } from "./codegen.js";
|
|
8
9
|
import { CompileError } from "./errors.js";
|
|
@@ -97,7 +98,9 @@ export async function compileAll(config, _options = {}) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/**
|
|
100
|
-
* Manifest yoksa veya herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
101
|
+
* Manifest yoksa veya (dev'de) herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
102
|
+
* Production'da manifest varsa mtime taraması atlanır — `jskelet build` CI
|
|
103
|
+
* güvencesi; ilk istekte yüzlerce `statSync` cold start'ı şişirmesin.
|
|
101
104
|
* Dev sunucu restart'ında build watch kaçırmış olsa bile şablonlar güncel kalır.
|
|
102
105
|
*
|
|
103
106
|
* @param {{ root: string, dirs: Record<string, string> }} config
|
|
@@ -112,6 +115,12 @@ export async function ensureTemplatesCompiled(config) {
|
|
|
112
115
|
|
|
113
116
|
const outDir = path.join(config.dirs.generated, "templates");
|
|
114
117
|
const manifestPath = path.join(outDir, "manifest.json");
|
|
118
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
119
|
+
|
|
120
|
+
if (fs.existsSync(manifestPath) && isProd) {
|
|
121
|
+
return { count: jskFiles.size, rebuilt: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
let needs = !fs.existsSync(manifestPath);
|
|
116
125
|
|
|
117
126
|
if (!needs) {
|
package/src/compile/index.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
export { CompileError, indexToLocation } from "./errors.js";
|
|
5
5
|
export { parseExpr, codegenExpr } from "./expr.js";
|
|
6
6
|
export { parseTemplate, isComponentTag } from "./parse.js";
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
codegen,
|
|
9
|
+
toPascalCase,
|
|
10
|
+
normalizeIncludeId,
|
|
11
|
+
collapseTemplateWhitespace,
|
|
12
|
+
} from "./codegen.js";
|
|
8
13
|
export {
|
|
9
14
|
getViewRoots,
|
|
10
15
|
getComponentDirs,
|
package/src/server/assets.js
CHANGED
|
@@ -151,6 +151,55 @@ export function getBuildId() {
|
|
|
151
151
|
/** @type {Record<string, OptimizedImage> | null} */
|
|
152
152
|
let images = null;
|
|
153
153
|
|
|
154
|
+
/** Prefix uygulanmış kopya — her `optimizedImage` çağrısında map/spread yok. */
|
|
155
|
+
/** @type {Record<string, OptimizedImage> | null} */
|
|
156
|
+
let imagesPrefixed = null;
|
|
157
|
+
|
|
158
|
+
/** @type {string | null} */
|
|
159
|
+
let imagesPrefix = null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `images.json` yükler; CDN prefix varsa varyant URL'lerini bir kez materialize eder.
|
|
163
|
+
* @returns {Record<string, OptimizedImage>}
|
|
164
|
+
*/
|
|
165
|
+
function loadImages() {
|
|
166
|
+
const prefix = assetPrefix();
|
|
167
|
+
if (!isDev && images && imagesPrefix === prefix) {
|
|
168
|
+
return imagesPrefixed ?? images;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @type {Record<string, OptimizedImage>} */
|
|
172
|
+
let raw = {};
|
|
173
|
+
try {
|
|
174
|
+
const file = path.join(getConfig().dirs.generated, "images.json");
|
|
175
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8")).images ?? {};
|
|
176
|
+
} catch {
|
|
177
|
+
raw = {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
images = raw;
|
|
181
|
+
imagesPrefix = prefix;
|
|
182
|
+
|
|
183
|
+
if (!prefix) {
|
|
184
|
+
imagesPrefixed = raw;
|
|
185
|
+
return raw;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @type {Record<string, OptimizedImage>} */
|
|
189
|
+
const prefixed = {};
|
|
190
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
191
|
+
prefixed[key] = {
|
|
192
|
+
...entry,
|
|
193
|
+
variants: entry.variants.map((variant) => ({
|
|
194
|
+
...variant,
|
|
195
|
+
url: applyAssetPrefix(prefix, variant.url),
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
imagesPrefixed = prefixed;
|
|
200
|
+
return prefixed;
|
|
201
|
+
}
|
|
202
|
+
|
|
154
203
|
/**
|
|
155
204
|
* `build/tasks/images.mjs` çıktısı: kaynak yolundan webp varyantlarına.
|
|
156
205
|
* Build çalışmadıysa boş kalır ve görseller orijinalleriyle servis edilir.
|
|
@@ -159,29 +208,9 @@ let images = null;
|
|
|
159
208
|
* @returns {OptimizedImage | undefined}
|
|
160
209
|
*/
|
|
161
210
|
export function optimizedImage(src) {
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const file = path.join(getConfig().dirs.generated, "images.json");
|
|
165
|
-
images = JSON.parse(fs.readFileSync(file, "utf8")).images ?? {};
|
|
166
|
-
} catch {
|
|
167
|
-
images = {};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
211
|
+
const table = loadImages();
|
|
171
212
|
if (!src) return undefined;
|
|
172
|
-
|
|
173
|
-
if (!entry) return undefined;
|
|
174
|
-
|
|
175
|
-
const prefix = assetPrefix();
|
|
176
|
-
if (!prefix) return entry;
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
...entry,
|
|
180
|
-
variants: entry.variants.map((variant) => ({
|
|
181
|
-
...variant,
|
|
182
|
-
url: applyAssetPrefix(prefix, variant.url),
|
|
183
|
-
})),
|
|
184
|
-
};
|
|
213
|
+
return table[src];
|
|
185
214
|
}
|
|
186
215
|
|
|
187
216
|
/** @type {{ url: string, ids: Set<string> } | null} */
|
|
@@ -15,7 +15,12 @@ import * as log from "../../log.mjs";
|
|
|
15
15
|
import { FRAMEWORK_ROOT, getConfig } from "../../config/index.js";
|
|
16
16
|
import { getRequestContext } from "../../http/request-context.js";
|
|
17
17
|
import { prewarm, prewarmProgress } from "../prewarm.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
clearHtmlCache,
|
|
20
|
+
getHtmlCacheSize,
|
|
21
|
+
invalidateHtmlCache,
|
|
22
|
+
} from "../html-cache.js";
|
|
23
|
+
import { clearDataCache, getDataCacheSize } from "../data-cache.js";
|
|
19
24
|
import {
|
|
20
25
|
buildReport,
|
|
21
26
|
clearPageReports,
|
|
@@ -325,6 +330,12 @@ function statsPayload() {
|
|
|
325
330
|
version: versionStatus(),
|
|
326
331
|
memory: { rss: usage.rss, heapUsed: usage.heapUsed },
|
|
327
332
|
prewarm: { ...prewarmProgress },
|
|
333
|
+
// Overlay Server sekmesindeki önbellek sayaçları; her kalp atışında
|
|
334
|
+
// güncellenir ki temizleme sonrası yenilemeye gerek kalmasın.
|
|
335
|
+
cache: {
|
|
336
|
+
html: getHtmlCacheSize(),
|
|
337
|
+
data: getDataCacheSize(),
|
|
338
|
+
},
|
|
328
339
|
requests: requests.slice(-25).reverse(),
|
|
329
340
|
errors: errors.slice(-25).reverse(),
|
|
330
341
|
// SSR dış çağrıları: overlay API sekmesi hangi sayfanın hangi
|
|
@@ -557,6 +568,41 @@ function router() {
|
|
|
557
568
|
res.json({ ok: true });
|
|
558
569
|
});
|
|
559
570
|
|
|
571
|
+
// Panelden HTML / veri önbelleğini elle boşaltma. Admin panelindeki
|
|
572
|
+
// `html:clear` / `html:invalidate` / `data:clear` ile aynı çekirdek API;
|
|
573
|
+
// yalnızca development'ta ve CSRF'siz — overlay zaten aynı origin'den gelir.
|
|
574
|
+
api.post("/cache/clear-html", (req, res) => {
|
|
575
|
+
const cleared = getHtmlCacheSize();
|
|
576
|
+
clearHtmlCache();
|
|
577
|
+
pushStats();
|
|
578
|
+
res.json({ ok: true, cleared });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
api.post("/cache/clear-data", express.json({ limit: "8kb" }), (req, res) => {
|
|
582
|
+
const prefix =
|
|
583
|
+
typeof req.body?.prefix === "string" && req.body.prefix
|
|
584
|
+
? req.body.prefix
|
|
585
|
+
: undefined;
|
|
586
|
+
const cleared = clearDataCache(prefix);
|
|
587
|
+
pushStats();
|
|
588
|
+
res.json({ ok: true, cleared });
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
api.post("/cache/invalidate", express.json({ limit: "8kb" }), (req, res) => {
|
|
592
|
+
const target = String(req.body?.path ?? "");
|
|
593
|
+
if (!target.startsWith("/")) {
|
|
594
|
+
res.status(400).json({ ok: false, reason: "path" });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Dev panelinde "bu sayfayı sil" soğuk render bekler; yumuşak bayatlatma
|
|
599
|
+
// eski HTML'i servis etmeye devam eder ve etkisi görünmez kalır.
|
|
600
|
+
const hard = req.body?.hard !== false;
|
|
601
|
+
const count = invalidateHtmlCache(target, { hard });
|
|
602
|
+
pushStats();
|
|
603
|
+
res.json({ ok: true, count, path: target, hard });
|
|
604
|
+
});
|
|
605
|
+
|
|
560
606
|
return api;
|
|
561
607
|
}
|
|
562
608
|
|
package/src/server/render.js
CHANGED
|
@@ -161,11 +161,21 @@ async function loadCompiledTemplates(config, helpers) {
|
|
|
161
161
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
162
162
|
const bust = isDev ? `?t=${Date.now()}` : "";
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
// Paralel import — sıralı await cold start'ı şişiriyordu.
|
|
165
|
+
const entries = await Promise.all(
|
|
166
|
+
Object.entries(manifest).map(async ([viewId, rel]) => {
|
|
167
|
+
const file = path.join(templatesDir, rel);
|
|
168
|
+
try {
|
|
169
|
+
const mod = await import(pathToFileURL(file).href + bust);
|
|
170
|
+
return /** @type {const} */ ([viewId, mod]);
|
|
171
|
+
} catch {
|
|
172
|
+
return /** @type {const} */ ([viewId, null]);
|
|
173
|
+
}
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
for (const [viewId, mod] of entries) {
|
|
178
|
+
if (!mod || typeof mod.render !== "function") continue;
|
|
169
179
|
compiled.set(viewId, mod.render);
|
|
170
180
|
|
|
171
181
|
// `components/*.jsk` → helpers.PascalName
|
|
@@ -259,8 +269,9 @@ export async function renderPage(page) {
|
|
|
259
269
|
hook("layoutContext", {}, { pathname: page.pathname ?? "", metadata }),
|
|
260
270
|
]);
|
|
261
271
|
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
// Layout local'leri. `.jsk` `(data, helpers)` aldığı için helpers'ı data'ya
|
|
273
|
+
// kopyalamak gereksiz allocation; EJS layout hâlâ bare helper adları bekler.
|
|
274
|
+
const pageLocals = {
|
|
264
275
|
...context,
|
|
265
276
|
metadata,
|
|
266
277
|
// Boş varsayılan bilinçli: "/" yazmak her sayfayı ana sayfa sanıp
|
|
@@ -286,7 +297,7 @@ export async function renderPage(page) {
|
|
|
286
297
|
};
|
|
287
298
|
|
|
288
299
|
if (layoutRender) {
|
|
289
|
-
return layoutRender(
|
|
300
|
+
return layoutRender(pageLocals, helpers);
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
if (layout.endsWith(".jsk")) {
|
|
@@ -296,7 +307,7 @@ export async function renderPage(page) {
|
|
|
296
307
|
);
|
|
297
308
|
}
|
|
298
309
|
|
|
299
|
-
return renderEjsFile(layout,
|
|
310
|
+
return renderEjsFile(layout, { ...helpers, ...pageLocals }, options);
|
|
300
311
|
}
|
|
301
312
|
|
|
302
313
|
/**
|
package/src/templates/layout.jsk
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
anda bu dosya devre dışı kalır; amacı yeni bir projenin tek route ile
|
|
6
6
|
çalışabilmesi. Kopyalayıp başlangıç noktası olarak kullanın.
|
|
7
7
|
|
|
8
|
-
Kullanılabilir
|
|
9
|
-
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath
|
|
10
|
-
|
|
11
|
-
`hooks.layoutContext()`
|
|
8
|
+
Kullanılabilir data alanları: metadata, headMeta, extraHead, structuredData,
|
|
9
|
+
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath.
|
|
10
|
+
Yardımcılar ikinci `helpers` argümanında (PascalCase etiketler /
|
|
11
|
+
helpers.esc). `hooks.layoutContext()` alanları data'ya eklenir.
|
|
12
12
|
|
|
13
13
|
Stylesheets / JsonLd / BodyScripts: `.jsk` ifade dilinde fonksiyon çağrısı
|
|
14
14
|
olmadığı için asset döngüleri bu yerleşik etiketlere taşındı.
|
|
@@ -6,36 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export function render(data, helpers) {
|
|
8
8
|
let s = "";
|
|
9
|
-
s += "\r\n";
|
|
10
9
|
s += "<!DOCTYPE html>";
|
|
11
|
-
s += "\r\n";
|
|
12
10
|
s += "<html" + helpers.attrs({"lang": data.lang}) + ">";
|
|
13
|
-
s += "\
|
|
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 ";
|
|
11
|
+
s += "<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
|
|
20
12
|
s += (data.extraHead ?? "");
|
|
21
|
-
s += "\r\n ";
|
|
22
13
|
s += (helpers.Stylesheets({ "styles": data.styles }) ?? "");
|
|
23
|
-
s += "\r\n ";
|
|
24
14
|
s += (data.headMeta ?? "");
|
|
25
|
-
s += "\r\n ";
|
|
26
15
|
s += (helpers.JsonLd({ "items": data.structuredData }) ?? "");
|
|
27
|
-
s += "\r\n ";
|
|
28
16
|
s += "</head>";
|
|
29
|
-
s += "\r\n ";
|
|
30
17
|
s += "<body" + helpers.attrs({"class": data.bodyClass}) + ">";
|
|
31
|
-
s += "\r\n ";
|
|
32
18
|
s += (data.body ?? "");
|
|
33
|
-
s += "\r\n ";
|
|
34
19
|
s += (helpers.BodyScripts({ "entries": data.entries, "devtools": data.devtools, "devBasePath": data.devBasePath }) ?? "");
|
|
35
|
-
s += "
|
|
36
|
-
s += "</body>";
|
|
37
|
-
s += "\r\n";
|
|
38
|
-
s += "</html>";
|
|
39
|
-
s += "\r\n";
|
|
20
|
+
s += "</body></html>";
|
|
40
21
|
return s;
|
|
41
22
|
}
|
|
@@ -43,7 +43,8 @@ function collect(dir, out = []) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Modülleri paralel import eder; export uygulama sırası dosya listesiyle
|
|
47
|
+
* aynı kalır (barrel önce, sonra sıralı bileşenler).
|
|
47
48
|
*
|
|
48
49
|
* @param {string} dir
|
|
49
50
|
* @param {Record<string, unknown>} components
|
|
@@ -59,12 +60,16 @@ async function loadDir(dir, components, origin) {
|
|
|
59
60
|
...collect(dir).sort(),
|
|
60
61
|
];
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const loaded = await Promise.all(
|
|
64
|
+
files.map(async (file) => {
|
|
65
|
+
const isBarrel = path.basename(file) === BARREL;
|
|
66
|
+
const fileId = isBarrel ? BARREL : file.split(path.sep).join("/");
|
|
67
|
+
const module = await import(pathToFileURL(file).href);
|
|
68
|
+
return { fileId, module };
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
67
71
|
|
|
72
|
+
for (const { fileId, module } of loaded) {
|
|
68
73
|
for (const [name, value] of Object.entries(module)) {
|
|
69
74
|
if (name === "default") continue;
|
|
70
75
|
|
|
@@ -9,6 +9,11 @@ const ESCAPE_MAP = {
|
|
|
9
9
|
"'": "'",
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/** Kaçış gerektiren karakter var mı — yoksa `replace` callback'ine girilmez. */
|
|
13
|
+
const NEEDS_ESCAPE = /[&<>"']/;
|
|
14
|
+
/** Tüm eşleşmeleri değiştirmek için ayrı global kopya (`test` lastIndex'i kirletmesin). */
|
|
15
|
+
const ESCAPE_ALL = /[&<>"']/g;
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Metin içeriği ve attribute değerleri için kaçış.
|
|
14
19
|
* @param {unknown} value
|
|
@@ -16,7 +21,9 @@ const ESCAPE_MAP = {
|
|
|
16
21
|
*/
|
|
17
22
|
export function esc(value) {
|
|
18
23
|
if (value == null || value === false) return "";
|
|
19
|
-
|
|
24
|
+
const str = typeof value === "string" ? value : String(value);
|
|
25
|
+
if (!NEEDS_ESCAPE.test(str)) return str;
|
|
26
|
+
return str.replace(ESCAPE_ALL, (char) => ESCAPE_MAP[char]);
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
@@ -41,16 +48,17 @@ export function jsonScript(value) {
|
|
|
41
48
|
* @returns {string}
|
|
42
49
|
*/
|
|
43
50
|
export function attrs(attrs) {
|
|
44
|
-
|
|
51
|
+
// Dizi + join yerine doğrudan birikim: tipik etiket 2–6 attribute.
|
|
52
|
+
let out = "";
|
|
45
53
|
for (const [key, value] of Object.entries(attrs)) {
|
|
46
54
|
if (value == null || value === false) continue;
|
|
47
55
|
if (value === true) {
|
|
48
|
-
|
|
56
|
+
out += ` ${key}`;
|
|
49
57
|
continue;
|
|
50
58
|
}
|
|
51
|
-
|
|
59
|
+
out += ` ${key}="${esc(value)}"`;
|
|
52
60
|
}
|
|
53
|
-
return
|
|
61
|
+
return out;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
@@ -24,6 +24,12 @@ const isDev = process.env.NODE_ENV === "development";
|
|
|
24
24
|
/** Aynı eksik ikon için tek uyarı. */
|
|
25
25
|
const warnedIcons = new Set();
|
|
26
26
|
|
|
27
|
+
/** `fill` Image varsayılan sınıfları — `cn`/`twMerge` olmadan sabit. */
|
|
28
|
+
const IMAGE_FILL_CLASS = "absolute inset-0 h-full w-full object-cover";
|
|
29
|
+
|
|
30
|
+
/** Sprite URL process ömründe bir kez çözülür (hash değişince process yenilenir). */
|
|
31
|
+
let cachedSpritePath = /** @type {string | null} */ (null);
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* `SeoLink` karşılığı: title otomatik doldurulur.
|
|
29
35
|
* @param {{ href: string, text?: string, html?: string, class?: string,
|
|
@@ -101,14 +107,19 @@ export function image(props) {
|
|
|
101
107
|
(optimized && optimized.variants.length > 1) ||
|
|
102
108
|
(remote && remote.srcset);
|
|
103
109
|
|
|
110
|
+
// `fill` + ek sınıf yoksa twMerge'e girme; ek sınıf varsa çakışma çözümü gerekir.
|
|
111
|
+
const fillClass = fill
|
|
112
|
+
? className
|
|
113
|
+
? cn(IMAGE_FILL_CLASS, className)
|
|
114
|
+
: IMAGE_FILL_CLASS
|
|
115
|
+
: className;
|
|
116
|
+
|
|
104
117
|
const attributes = attrs({
|
|
105
118
|
src: largest?.url ?? remote?.src ?? src,
|
|
106
119
|
alt: alt ?? "",
|
|
107
120
|
width: fill ? undefined : (width ?? optimized?.width),
|
|
108
121
|
height: fill ? undefined : (height ?? optimized?.height),
|
|
109
|
-
class:
|
|
110
|
-
? cn("absolute inset-0 h-full w-full object-cover", className)
|
|
111
|
-
: className,
|
|
122
|
+
class: fillClass,
|
|
112
123
|
sizes: responsive
|
|
113
124
|
? (sizes ?? (optimized ? defaultSizes(optimized) : remote?.sizes))
|
|
114
125
|
: sizes,
|
|
@@ -213,7 +224,13 @@ export function icon(props) {
|
|
|
213
224
|
attrs: extra = {},
|
|
214
225
|
} = props;
|
|
215
226
|
|
|
216
|
-
|
|
227
|
+
// Prod'da hash deploy ile birlikte değişir (process yenilenir); dev'de
|
|
228
|
+
// watch rebuild sprite yolunu güncelleyebildiği için her seferinde çözülür.
|
|
229
|
+
let spritePath = cachedSpritePath;
|
|
230
|
+
if (spritePath == null || isDev) {
|
|
231
|
+
spritePath = asset("sprite.svg", { origin: "document" });
|
|
232
|
+
if (!isDev) cachedSpritePath = spritePath;
|
|
233
|
+
}
|
|
217
234
|
const id = `${toKebab(name)}-${weight}`;
|
|
218
235
|
|
|
219
236
|
if (isDev && !warnedIcons.has(id)) {
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export type AstNode = import('./parse.js').AstNode;
|
|
2
2
|
export type AttrNode = import('./parse.js').AttrNode;
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {import('./parse.js').AstNode} AstNode
|
|
5
|
-
* @typedef {import('./parse.js').AttrNode} AttrNode
|
|
6
|
-
*/
|
|
7
3
|
/**
|
|
8
4
|
* @param {AstNode[]} ast
|
|
9
5
|
* @param {{ viewId: string, file?: string, knownComponents?: Set<string> }} options
|
|
@@ -30,3 +26,12 @@ export declare function normalizeIncludeId(includePath: string): string;
|
|
|
30
26
|
* @returns {string}
|
|
31
27
|
*/
|
|
32
28
|
export declare function toPascalCase(name: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Şablon girintisini düşür: satır sonu içeren salt-whitespace → boş;
|
|
31
|
+
* yalnızca boşluk/tab → tek boşluk (inline aralık). Koruma alanı dışında.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} value
|
|
34
|
+
* @param {boolean} preserve
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export declare function collapseTemplateWhitespace(value: string, preserve: boolean): string;
|
|
@@ -12,7 +12,9 @@ export declare function compileAll(config: {
|
|
|
12
12
|
outDir: string;
|
|
13
13
|
}>;
|
|
14
14
|
/**
|
|
15
|
-
* Manifest yoksa veya herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
15
|
+
* Manifest yoksa veya (dev'de) herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
16
|
+
* Production'da manifest varsa mtime taraması atlanır — `jskelet build` CI
|
|
17
|
+
* güvencesi; ilk istekte yüzlerce `statSync` cold start'ı şişirmesin.
|
|
16
18
|
* Dev sunucu restart'ında build watch kaçırmış olsa bile şablonlar güncel kalır.
|
|
17
19
|
*
|
|
18
20
|
* @param {{ root: string, dirs: Record<string, string> }} config
|
package/types/compile/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export { CompileError, indexToLocation } from "./errors.js";
|
|
5
5
|
export { parseExpr, codegenExpr } from "./expr.js";
|
|
6
6
|
export { parseTemplate, isComponentTag } from "./parse.js";
|
|
7
|
-
export { codegen, toPascalCase, normalizeIncludeId } from "./codegen.js";
|
|
7
|
+
export { codegen, toPascalCase, normalizeIncludeId, collapseTemplateWhitespace, } from "./codegen.js";
|
|
8
8
|
export { getViewRoots, getComponentDirs, discoverJskFiles, componentNameFromViewId, collectKnownComponents, toComponentTag, } from "./resolve.js";
|
|
9
9
|
export { scanNamedExports } from "./scan-exports.js";
|
|
10
10
|
export { compileAll, compileSource, ensureTemplatesCompiled } from "./compile-all.js";
|