jskelet 0.4.8 → 0.5.0
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 +4 -0
- package/docs/02-mimari.md +8 -3
- package/docs/03-routing.md +2 -0
- package/docs/04-render-ve-sablonlar.md +11 -4
- package/docs/08-build.md +32 -4
- package/docs/09-dev-araclari.md +1 -1
- package/docs/en/02-architecture.md +8 -2
- package/docs/en/03-routing.md +2 -0
- package/docs/en/04-rendering.md +11 -4
- package/docs/en/08-build.md +37 -8
- package/docs/en/09-dev-tools.md +1 -1
- package/package.json +1 -1
- package/src/build/tasks/css.mjs +122 -13
- package/src/client/devtools/overlay.js +9 -4
- package/src/server/dev/devtools.js +6 -2
- package/src/server/render.js +4 -2
- package/src/templates/layout.ejs +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,10 @@ one is listed under a **Breaking** heading.
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- Route-level stylesheets: put files in `styles/pages/*.css` and load them from
|
|
14
|
+
the controller with `styles: ["home.css"]` (same contract as island
|
|
15
|
+
`entries`). The layout emits them after global `app.css`; dev hot-swaps any
|
|
16
|
+
changed `.css` manifest key without a full reload.
|
|
13
17
|
- Dev overlay Errors tab now lists failed SSR and browser `fetch` calls with
|
|
14
18
|
page path, API URL, optional island name, and expandable response-body
|
|
15
19
|
details (JSON instead of `[object Object]`). Server `console.error` /
|
package/docs/02-mimari.md
CHANGED
|
@@ -144,9 +144,14 @@ geciktirmek doğrudan LCP'ye yazılır.
|
|
|
144
144
|
|
|
145
145
|
Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
|
|
146
146
|
kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında CLS
|
|
147
|
-
0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış
|
|
148
|
-
|
|
149
|
-
zaten `immutable` önbellekten geliyor.
|
|
147
|
+
0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış global
|
|
148
|
+
`app.css`'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci
|
|
149
|
+
ziyarette zaten `immutable` önbellekten geliyor.
|
|
150
|
+
|
|
151
|
+
Sayfaya özel kurallar için `styles/pages/*.css` + controller `styles: [...]`
|
|
152
|
+
eklenebilir ([08-build.md](./08-build.md)); bunlar da render-blocking basılır
|
|
153
|
+
ama yalnızca ilgili sayfada. Tailwind utility'leri global sheet'te kalır —
|
|
154
|
+
sayfa sheet'inde tam `@import "tailwindcss"` utility çıktısını tekrarlar.
|
|
150
155
|
|
|
151
156
|
Aynı mantık ikonlarda da var: her ikon için ayrı istek yerine, build zamanında
|
|
152
157
|
yalnızca kaynakta kullanılan sembollerden bir SVG sprite üretilir. Tüm Phosphor
|
package/docs/03-routing.md
CHANGED
|
@@ -213,6 +213,7 @@ Controller `async (ctx) => sayfa` biçimindedir ve şu alanları döndürebilir:
|
|
|
213
213
|
| `head` | `string` | `""` | `<head>`e olduğu gibi basılacak ham HTML (ör. LCP preload'ı). |
|
|
214
214
|
| `bodyClass` | `string` | `hooks.layoutContext().bodyClass ?? ""` | `<body class="…">`. |
|
|
215
215
|
| `entries` | `string[]` | `[]` | Bu sayfada ek olarak yüklenecek client entry adları: `["chart.js"]`. |
|
|
216
|
+
| `styles` | `string[]` | `[]` | Bu sayfada ek olarak yüklenecek stylesheet adları: `["home.css"]` → `styles/pages/home.css`. |
|
|
216
217
|
|
|
217
218
|
`revalidate` **`route()`'un ikinci argümanıdır**, controller'ın döndürdüğü
|
|
218
219
|
nesnenin alanı değil.
|
|
@@ -239,6 +240,7 @@ app.get(
|
|
|
239
240
|
head: headHints({ href: data.cover }),
|
|
240
241
|
bodyClass: "bg-slate-50",
|
|
241
242
|
entries: ["chart.js"],
|
|
243
|
+
styles: ["markets.css"],
|
|
242
244
|
};
|
|
243
245
|
},
|
|
244
246
|
{ revalidate: 30 },
|
|
@@ -151,8 +151,13 @@ kopyalamaktır.
|
|
|
151
151
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
152
152
|
<%- extraHead %>
|
|
153
153
|
<% if (hasAsset('app.css')) { %>
|
|
154
|
-
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
154
|
+
<link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
|
|
155
155
|
<% } %>
|
|
156
|
+
<% styles.forEach(function (sheet) { %>
|
|
157
|
+
<% if (hasAsset(sheet)) { %>
|
|
158
|
+
<link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
|
|
159
|
+
<% } %>
|
|
160
|
+
<% }); %>
|
|
156
161
|
<%- headMeta %>
|
|
157
162
|
<% structuredData.forEach(function (item) { %>
|
|
158
163
|
<script type="application/ld+json"><%- jsonScript(item) %></script>
|
|
@@ -177,9 +182,10 @@ Dikkat edilecek noktalar:
|
|
|
177
182
|
|
|
178
183
|
- **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
|
|
179
184
|
geciktirmek doğrudan LCP'ye yazılır.
|
|
180
|
-
- **
|
|
181
|
-
[02-mimari.md](./02-mimari.md)'de.
|
|
182
|
-
|
|
185
|
+
- **Global `app.css` render-blocking** ve gerekçesi
|
|
186
|
+
[02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
|
|
187
|
+
sheet'leri de aynı şekilde basılır. Build çalışmadıysa `hasAsset` false olur
|
|
188
|
+
ve etiket hiç basılmaz.
|
|
183
189
|
- **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
|
|
184
190
|
istememesini sağlar.
|
|
185
191
|
- **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
|
|
@@ -196,6 +202,7 @@ Dikkat edilecek noktalar:
|
|
|
196
202
|
| `body` | `string` | Sayfa şablonunun render çıktısı |
|
|
197
203
|
| `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
|
|
198
204
|
| `entries` | `string[]` | controller `entries`; varsayılan `[]` |
|
|
205
|
+
| `styles` | `string[]` | controller `styles`; varsayılan `[]` |
|
|
199
206
|
| `pathname` | `string` | `req.path`; **varsayılan boş string** |
|
|
200
207
|
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
201
208
|
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
package/docs/08-build.md
CHANGED
|
@@ -64,8 +64,13 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
|
|
|
64
64
|
|
|
65
65
|
```ejs
|
|
66
66
|
<% if (hasAsset('app.css')) { %>
|
|
67
|
-
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
67
|
+
<link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
|
|
68
68
|
<% } %>
|
|
69
|
+
<% styles.forEach(function (sheet) { %>
|
|
70
|
+
<% if (hasAsset(sheet)) { %>
|
|
71
|
+
<link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
|
|
72
|
+
<% } %>
|
|
73
|
+
<% }); %>
|
|
69
74
|
```
|
|
70
75
|
|
|
71
76
|
- `asset(name)` manifest'te varsa hash'li URL'i, yoksa `/assets/<name>` döner.
|
|
@@ -85,7 +90,8 @@ Watch turunda yeniden derlenen varlık yeni bir hash'e yazılıp eskisi silinir.
|
|
|
85
90
|
yüzden manifest de güncellenmek zorunda (`patchManifest`): aksi hâlde HTML
|
|
86
91
|
silinmiş dosyayı isteyip 404 alır ve sayfa dev oturumunun kalanında stilsiz ya
|
|
87
92
|
da JS'siz kalır. CSS ve client görevlerinin ikisi de her turda kendi anahtarını
|
|
88
|
-
yamalar; diğer anahtarlar korunur.
|
|
93
|
+
yamalar; diğer anahtarlar korunur. CSS tarafı watch'ta `syncCssManifest` ile
|
|
94
|
+
tüm `.css` anahtarlarını günceller (silinen sayfa sheet'leri de düşer).
|
|
89
95
|
|
|
90
96
|
## CSS — Tailwind v4
|
|
91
97
|
|
|
@@ -100,10 +106,32 @@ minifikasyon → `writeAsset("app.css", …)`.
|
|
|
100
106
|
şekilde yavaşlatıyor.
|
|
101
107
|
- **lightningcss opsiyoneldir:** yoksa Tailwind'in kendi çıktısı kullanılır,
|
|
102
108
|
yalnızca birkaç kB daha büyük olur.
|
|
103
|
-
-
|
|
104
|
-
"critical CSS" üretilmemesinin ölçüm gerekçesi
|
|
109
|
+
- Global çıktı `app.css`'tir ve layout onu her sayfada render-blocking olarak
|
|
110
|
+
yükler. Ayrı bir "critical CSS" üretilmemesinin ölçüm gerekçesi
|
|
105
111
|
[02-mimari.md](./02-mimari.md)'de.
|
|
106
112
|
|
|
113
|
+
### Sayfa stylesheet'leri (`styles/pages/`)
|
|
114
|
+
|
|
115
|
+
Island `entries` ile aynı sözleşme. `styles/pages/*.css` altındaki her dosya
|
|
116
|
+
ayrı bir hash'li varlıktır (`home.css` → `/assets/home.<hash>.css`). Controller
|
|
117
|
+
yalnızca istediği sayfada yükler:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
return {
|
|
121
|
+
view: "pages/home",
|
|
122
|
+
styles: ["home.css"],
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Dizin, `paths.styles` dosyasının yanındaki `pages/` klasörüdür (`styles` taşınırsa
|
|
127
|
+
pages de yanında kalır). Dizin yoksa veya boşsa adım yalnızca global sheet üretir.
|
|
128
|
+
|
|
129
|
+
Sayfa CSS'i sayfaya özel kurallar içindir. Tailwind utility'leri global sheet'te
|
|
130
|
+
kalmalı — dosyada tam `@import "tailwindcss"` utility çıktısını tekrarlar.
|
|
131
|
+
|
|
132
|
+
Layout `app.css`ten sonra `styles` dizisindeki her sheet için
|
|
133
|
+
`<link data-jskelet-css="…">` basar; `hasAsset` false ise etiket yok.
|
|
134
|
+
|
|
107
135
|
### `@source` direktifleri zorunludur
|
|
108
136
|
|
|
109
137
|
Tailwind v4'ün sınıf taraması `globals.css` içindeki `@source` direktiflerine
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -117,7 +117,7 @@ yeniden yazıldığı için değişiklik tespiti manifest üzerinden yapılır.
|
|
|
117
117
|
|
|
118
118
|
| Değişen | Davranış |
|
|
119
119
|
| --- | --- |
|
|
120
|
-
| Yalnızca
|
|
120
|
+
| Yalnızca `.css` anahtarları | **CSS hot-swap:** değişen her stylesheet takas edilir, sayfa yenilenmez. Durum ve kaydırma korunur. |
|
|
121
121
|
| `main.js`, sprite, başka bir varlık ya da birden fazla anahtar | **Tam yenileme** |
|
|
122
122
|
|
|
123
123
|
Her iki durumda önce HTML önbelleği temizlenir: saklanan HTML eski hash'li varlık
|
|
@@ -147,10 +147,16 @@ resource hints (`preconnect`, LCP `preload`) at the **very beginning** of the
|
|
|
147
147
|
No separate "critical CSS" is produced. In measurement, because the inline
|
|
148
148
|
critical CSS did not fully cover the first viewport, the page reflowed once the
|
|
149
149
|
sheet arrived (CLS 0.307 on a list page) and the same ~27 KB was repeated in
|
|
150
|
-
every HTML response. Leaving
|
|
151
|
-
faster and free of CLS; on the second visit it already comes from the
|
|
150
|
+
every HTML response. Leaving the compressed global `app.css` render-blocking is
|
|
151
|
+
both faster and free of CLS; on the second visit it already comes from the
|
|
152
152
|
`immutable` cache.
|
|
153
153
|
|
|
154
|
+
Page-specific rules can use `styles/pages/*.css` plus controller
|
|
155
|
+
`styles: [...]` ([08-build.md](./08-build.md)); those are also render-blocking
|
|
156
|
+
but only on the pages that ask for them. Keep Tailwind utilities in the global
|
|
157
|
+
sheet — a full `@import "tailwindcss"` in a page sheet duplicates utility
|
|
158
|
+
output.
|
|
159
|
+
|
|
154
160
|
The same logic applies to icons: instead of a separate request per icon, an SVG
|
|
155
161
|
sprite is produced at build time from only the symbols actually used in the
|
|
156
162
|
source. Shipping the whole Phosphor set is 1500+ icons, that is several
|
package/docs/en/03-routing.md
CHANGED
|
@@ -216,6 +216,7 @@ fields:
|
|
|
216
216
|
| `head` | `string` | `""` | Raw HTML to be printed into `<head>` as-is (e.g. the LCP preload). |
|
|
217
217
|
| `bodyClass` | `string` | `hooks.layoutContext().bodyClass ?? ""` | `<body class="…">`. |
|
|
218
218
|
| `entries` | `string[]` | `[]` | The names of client entries to be loaded additionally on this page: `["chart.js"]`. |
|
|
219
|
+
| `styles` | `string[]` | `[]` | Extra stylesheets for this page: `["home.css"]` → `styles/pages/home.css`. |
|
|
219
220
|
|
|
220
221
|
`revalidate` is **the second argument of `route()`**, not a field of the object
|
|
221
222
|
the controller returns.
|
|
@@ -242,6 +243,7 @@ app.get(
|
|
|
242
243
|
head: headHints({ href: data.cover }),
|
|
243
244
|
bodyClass: "bg-slate-50",
|
|
244
245
|
entries: ["chart.js"],
|
|
246
|
+
styles: ["markets.css"],
|
|
245
247
|
};
|
|
246
248
|
},
|
|
247
249
|
{ revalidate: 30 },
|
package/docs/en/04-rendering.md
CHANGED
|
@@ -151,8 +151,13 @@ most practical way to move to your own layout is to copy that file to
|
|
|
151
151
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
152
152
|
<%- extraHead %>
|
|
153
153
|
<% if (hasAsset('app.css')) { %>
|
|
154
|
-
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
154
|
+
<link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
|
|
155
155
|
<% } %>
|
|
156
|
+
<% styles.forEach(function (sheet) { %>
|
|
157
|
+
<% if (hasAsset(sheet)) { %>
|
|
158
|
+
<link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
|
|
159
|
+
<% } %>
|
|
160
|
+
<% }); %>
|
|
156
161
|
<%- headMeta %>
|
|
157
162
|
<% structuredData.forEach(function (item) { %>
|
|
158
163
|
<script type="application/ld+json"><%- jsonScript(item) %></script>
|
|
@@ -177,9 +182,10 @@ Points to watch:
|
|
|
177
182
|
|
|
178
183
|
- **`extraHead` comes first.** Delaying resource hints (`preconnect`, LCP
|
|
179
184
|
`preload`) writes straight into LCP.
|
|
180
|
-
- **
|
|
181
|
-
[02-architecture.md](./02-architecture.md).
|
|
182
|
-
|
|
185
|
+
- **Global `app.css` is render-blocking**, with the reasoning in
|
|
186
|
+
[02-architecture.md](./02-architecture.md). Controller `styles: [...]` adds
|
|
187
|
+
page sheets the same way. If the build has not run, `hasAsset` is false and
|
|
188
|
+
the tag is never emitted.
|
|
183
189
|
- **The `hasAsset` checks** keep the page from requesting files that 404 when
|
|
184
190
|
the build is missing.
|
|
185
191
|
- **The devtools script** is emitted only when `NODE_ENV=development`; it does
|
|
@@ -196,6 +202,7 @@ Points to watch:
|
|
|
196
202
|
| `body` | `string` | The render output of the page template |
|
|
197
203
|
| `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
|
|
198
204
|
| `entries` | `string[]` | controller `entries`; defaults to `[]` |
|
|
205
|
+
| `styles` | `string[]` | controller `styles`; defaults to `[]` |
|
|
199
206
|
| `pathname` | `string` | `req.path`; **defaults to the empty string** |
|
|
200
207
|
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
201
208
|
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
package/docs/en/08-build.md
CHANGED
|
@@ -68,8 +68,13 @@ They are passed to templates automatically; in server code,
|
|
|
68
68
|
|
|
69
69
|
```ejs
|
|
70
70
|
<% if (hasAsset('app.css')) { %>
|
|
71
|
-
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
71
|
+
<link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
|
|
72
72
|
<% } %>
|
|
73
|
+
<% styles.forEach(function (sheet) { %>
|
|
74
|
+
<% if (hasAsset(sheet)) { %>
|
|
75
|
+
<link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
|
|
76
|
+
<% } %>
|
|
77
|
+
<% }); %>
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
- `asset(name)` returns the hashed URL if it is in the manifest, otherwise
|
|
@@ -88,10 +93,11 @@ hashes) and once in prod.
|
|
|
88
93
|
### Manifest consistency in watch mode
|
|
89
94
|
|
|
90
95
|
On a watch pass, a recompiled asset is written to a new hash and the old one is
|
|
91
|
-
deleted. That is why the manifest has to be updated too (`patchManifest`
|
|
92
|
-
otherwise the HTML asks for the deleted file, gets a
|
|
93
|
-
unstyled or JS-less for the rest of the dev session.
|
|
94
|
-
tasks
|
|
96
|
+
deleted. That is why the manifest has to be updated too (`patchManifest` /
|
|
97
|
+
CSS `syncCssManifest`): otherwise the HTML asks for the deleted file, gets a
|
|
98
|
+
404, and the page stays unstyled or JS-less for the rest of the dev session.
|
|
99
|
+
Both the CSS and the client tasks refresh their own keys on every pass; the
|
|
100
|
+
other keys are preserved.
|
|
95
101
|
|
|
96
102
|
## CSS — Tailwind v4
|
|
97
103
|
|
|
@@ -106,9 +112,32 @@ The pipeline: PostCSS + `@tailwindcss/postcss` → minification with lightningcs
|
|
|
106
112
|
noticeably.
|
|
107
113
|
- **lightningcss is optional:** without it, Tailwind's own output is used, and it
|
|
108
114
|
is only a few kB bigger.
|
|
109
|
-
- The output is
|
|
110
|
-
measurement-based reasoning for not producing a separate "critical
|
|
111
|
-
[02-architecture.md](./02-architecture.md).
|
|
115
|
+
- The global output is `app.css` and the layout loads it render-blocking on every
|
|
116
|
+
page. The measurement-based reasoning for not producing a separate "critical
|
|
117
|
+
CSS" is in [02-architecture.md](./02-architecture.md).
|
|
118
|
+
|
|
119
|
+
### Page stylesheets (`styles/pages/`)
|
|
120
|
+
|
|
121
|
+
Same contract as island `entries`. Each file under `styles/pages/*.css` becomes
|
|
122
|
+
its own hashed asset (`home.css` → `/assets/home.<hash>.css`). The controller
|
|
123
|
+
loads it only on the pages that need it:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
return {
|
|
127
|
+
view: "pages/home",
|
|
128
|
+
styles: ["home.css"],
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The directory is `pages/` next to the `paths.styles` file (if `styles` moves,
|
|
133
|
+
pages stays beside it). If the directory is missing or empty, the step only
|
|
134
|
+
produces the global sheet.
|
|
135
|
+
|
|
136
|
+
Page CSS is for page-specific rules. Keep Tailwind utilities in the global
|
|
137
|
+
sheet — a full `@import "tailwindcss"` in a page file duplicates utility output.
|
|
138
|
+
|
|
139
|
+
After `app.css`, the layout emits a `<link data-jskelet-css="…">` for each name
|
|
140
|
+
in `styles`; if `hasAsset` is false the tag is omitted.
|
|
112
141
|
|
|
113
142
|
### `@source` directives are mandatory
|
|
114
143
|
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -121,7 +121,7 @@ rewritten on every build round, change detection is done through the manifest.
|
|
|
121
121
|
|
|
122
122
|
| What changed | Behavior |
|
|
123
123
|
| --- | --- |
|
|
124
|
-
| Only
|
|
124
|
+
| Only `.css` keys | **CSS hot-swap:** each changed stylesheet is swapped, the page is not reloaded. State and scroll position are preserved. |
|
|
125
125
|
| `main.js`, the sprite, another asset, or more than one key | **Full reload** |
|
|
126
126
|
|
|
127
127
|
In both cases the HTML cache is cleared first: the stored HTML carries the old
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/build/tasks/css.mjs
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tailwind v4 →
|
|
2
|
+
* Tailwind v4 → global `app.css` + isteğe bağlı sayfa stylesheet'leri.
|
|
3
|
+
*
|
|
4
|
+
* Global sheet her sayfada yüklenir (`paths.styles`). Sayfa sheet'leri
|
|
5
|
+
* `styles/pages/*.css` altındadır ve yalnızca controller `styles: ["home.css"]`
|
|
6
|
+
* bildiren sayfalarda basılır — island `entries` ile aynı sözleşme.
|
|
3
7
|
*
|
|
4
8
|
* Ayrı bir "critical CSS" üretilmez. Ölçümde inline kritik CSS ilk ekranı tam
|
|
5
9
|
* kapsamadığı için sheet gelince sayfa yeniden akıyordu (bir liste sayfasında
|
|
6
10
|
* CLS 0.307) ve aynı ~27 KB her HTML yanıtında tekrar ediyordu. Sıkıştırılmış
|
|
7
|
-
*
|
|
8
|
-
* ziyarette zaten immutable önbellekten geliyor.
|
|
11
|
+
* global sheet'i render-blocking bırakmak hem daha hızlı hem CLS'siz; ikinci
|
|
12
|
+
* ziyarette zaten immutable önbellekten geliyor. Sayfa sheet'leri de
|
|
13
|
+
* render-blocking; yalnızca ilgili sayfada ek maliyet.
|
|
14
|
+
*
|
|
15
|
+
* Tailwind utility'leri global sheet'te kalmalı. Sayfa CSS'inde tam
|
|
16
|
+
* `@import "tailwindcss"` utility çıktısını tekrarlar; sayfaya özel kurallar
|
|
17
|
+
* yeter.
|
|
9
18
|
*
|
|
10
19
|
* Tailwind'in sınıf taraması `globals.css` içindeki `@source` direktiflerine
|
|
11
20
|
* bağlıdır. Otomatik tespit yalnızca stylesheet'in bulunduğu dizini tarar,
|
|
@@ -14,12 +23,12 @@
|
|
|
14
23
|
*/
|
|
15
24
|
import fs from "node:fs";
|
|
16
25
|
import path from "node:path";
|
|
17
|
-
import {
|
|
26
|
+
import { paths, writeAsset, writeManifest } from "../paths.mjs";
|
|
18
27
|
import { importFromApp, tryImportFromApp } from "../resolve-peer.mjs";
|
|
19
28
|
import * as log from "../../log.mjs";
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
|
-
* PostCSS boru hattı bir kez kurulur: Tailwind'in kendi önbelleği plugin
|
|
31
|
+
* PostCSS boru hattı bir kez kurulur: Tailwind'in kendi önbelleği plugin-++
|
|
23
32
|
* örneğinde yaşıyor, her derlemede yeniden oluşturmak watch turlarını
|
|
24
33
|
* belirgin şekilde yavaşlatıyor.
|
|
25
34
|
*
|
|
@@ -53,6 +62,29 @@ async function createCompiler(root) {
|
|
|
53
62
|
};
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
/**
|
|
66
|
+
* `paths.styles` dosyasının yanında `pages/` — config'te ayrı alan yok.
|
|
67
|
+
*
|
|
68
|
+
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function pageStylesDir(config) {
|
|
72
|
+
return path.join(path.dirname(config.dirs.styles), "pages");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} dir
|
|
77
|
+
* @returns {string[]} Mutlak yollar, dosya adına göre sıralı.
|
|
78
|
+
*/
|
|
79
|
+
function listPageStyles(dir) {
|
|
80
|
+
if (!fs.existsSync(dir)) return [];
|
|
81
|
+
return fs
|
|
82
|
+
.readdirSync(dir)
|
|
83
|
+
.filter((name) => name.endsWith(".css"))
|
|
84
|
+
.sort()
|
|
85
|
+
.map((name) => path.join(dir, name));
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
/**
|
|
57
89
|
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
58
90
|
* @param {{ watch?: boolean }} [options]
|
|
@@ -60,27 +92,55 @@ async function createCompiler(root) {
|
|
|
60
92
|
*/
|
|
61
93
|
export async function buildCss(config, { watch = false } = {}) {
|
|
62
94
|
const input = config.dirs.styles;
|
|
95
|
+
const pagesDir = pageStylesDir(config);
|
|
63
96
|
const compile = await createCompiler(config.root);
|
|
64
97
|
|
|
65
98
|
const run = async () => {
|
|
66
99
|
const started = Date.now();
|
|
67
|
-
|
|
100
|
+
/** @type {Record<string, string>} */
|
|
101
|
+
const urls = {};
|
|
102
|
+
let bytes = 0;
|
|
103
|
+
|
|
104
|
+
const appCss = await compile(input);
|
|
105
|
+
const appUrl = writeAsset("app.css", appCss);
|
|
106
|
+
urls["app.css"] = appUrl;
|
|
107
|
+
bytes += Buffer.byteLength(appCss);
|
|
108
|
+
|
|
109
|
+
/** @type {string[]} */
|
|
110
|
+
const keep = [path.basename(appUrl)];
|
|
111
|
+
|
|
112
|
+
for (const file of listPageStyles(pagesDir)) {
|
|
113
|
+
const name = path.basename(file);
|
|
114
|
+
const css = await compile(file);
|
|
115
|
+
const url = writeAsset(name, css);
|
|
116
|
+
urls[name] = url;
|
|
117
|
+
bytes += Buffer.byteLength(css);
|
|
118
|
+
keep.push(path.basename(url));
|
|
119
|
+
}
|
|
120
|
+
|
|
68
121
|
// Önce yaz, sonra eski hash'leri sil — aynı hash'e düşen içerikte 404
|
|
69
|
-
// penceresi olmasın (CDN immutable zehirlenmesi).
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
122
|
+
// penceresi olmasın (CDN immutable zehirlenmesi). Silinen sayfa sheet'leri
|
|
123
|
+
// de burada gider; önek listesi tutmaya gerek yok.
|
|
124
|
+
pruneCssOutputs(keep);
|
|
125
|
+
|
|
126
|
+
return { urls, bytes, elapsed: Date.now() - started };
|
|
73
127
|
};
|
|
74
128
|
|
|
75
129
|
const first = await run();
|
|
76
|
-
|
|
130
|
+
const pageCount = Object.keys(first.urls).length - 1;
|
|
131
|
+
log.detail(
|
|
132
|
+
pageCount > 0
|
|
133
|
+
? `${log.size(first.bytes)}, ${pageCount} page ${pageCount === 1 ? "sheet" : "sheets"}`
|
|
134
|
+
: log.size(first.bytes),
|
|
135
|
+
);
|
|
77
136
|
|
|
78
137
|
if (watch) {
|
|
79
138
|
watchCssSources(config, async () => {
|
|
80
139
|
try {
|
|
81
140
|
const result = await run();
|
|
82
141
|
// Yeni hash manifest'e yazılmazsa HTML silinmiş dosyayı ister.
|
|
83
|
-
|
|
142
|
+
// Silinen sayfa sheet anahtarları da düşsün.
|
|
143
|
+
syncCssManifest(result.urls);
|
|
84
144
|
log.event({
|
|
85
145
|
scope: "css",
|
|
86
146
|
message: "rebuilt",
|
|
@@ -98,12 +158,61 @@ export async function buildCss(config, { watch = false } = {}) {
|
|
|
98
158
|
});
|
|
99
159
|
}
|
|
100
160
|
|
|
101
|
-
return
|
|
161
|
+
return first.urls;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `public/assets/` kökündeki stylesheet çıktıları. `js/` ve `img/` altındakilere
|
|
166
|
+
* dokunulmaz.
|
|
167
|
+
*
|
|
168
|
+
* @param {string[]} keep Yeni yazılan dosya adları (`app.<hash>.css`, …)
|
|
169
|
+
*/
|
|
170
|
+
function pruneCssOutputs(keep) {
|
|
171
|
+
if (!fs.existsSync(paths.assets)) return;
|
|
172
|
+
|
|
173
|
+
for (const file of fs.readdirSync(paths.assets)) {
|
|
174
|
+
if (!/\.css(?:\.(?:br|gz))?$/i.test(file)) continue;
|
|
175
|
+
if (keep.some((name) => file === name || file.startsWith(`${name}.`))) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
fs.rmSync(path.join(paths.assets, file), { force: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Watch turunda CSS anahtarlarını toplu günceller; artık üretilmeyen sayfa
|
|
184
|
+
* sheet'lerini manifest'ten siler.
|
|
185
|
+
*
|
|
186
|
+
* @param {Record<string, string>} urls
|
|
187
|
+
*/
|
|
188
|
+
function syncCssManifest(urls) {
|
|
189
|
+
const file = path.join(paths.generated, "manifest.json");
|
|
190
|
+
/** @type {Record<string, string>} */
|
|
191
|
+
let current = {};
|
|
192
|
+
try {
|
|
193
|
+
current = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
194
|
+
} catch {
|
|
195
|
+
// İlk yazımda oluşur.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @type {Record<string, string>} */
|
|
199
|
+
const next = {};
|
|
200
|
+
for (const [key, value] of Object.entries(current)) {
|
|
201
|
+
if (key.endsWith(".css") && !(key in urls)) continue;
|
|
202
|
+
next[key] = value;
|
|
203
|
+
}
|
|
204
|
+
for (const [key, url] of Object.entries(urls)) {
|
|
205
|
+
next[key] = url;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (JSON.stringify(current) === JSON.stringify(next)) return;
|
|
209
|
+
writeManifest(next);
|
|
102
210
|
}
|
|
103
211
|
|
|
104
212
|
/**
|
|
105
213
|
* Şablon ve island dosyaları da izlenir: Tailwind sınıfları oradan geliyor,
|
|
106
214
|
* yalnızca `styles/` izlemek yeni bir utility yazıldığında rebuild etmez.
|
|
215
|
+
* `styles/pages` zaten `dirname(styles)` altında.
|
|
107
216
|
*
|
|
108
217
|
* @param {import('../../config/index.js').ResolvedConfig} config
|
|
109
218
|
* @param {() => void} onChange
|
|
@@ -630,7 +630,7 @@ function handleServerMessage(payload) {
|
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
if (payload.type === "css") {
|
|
633
|
-
swapStylesheet(payload.href);
|
|
633
|
+
swapStylesheet(payload.href, payload.name);
|
|
634
634
|
return;
|
|
635
635
|
}
|
|
636
636
|
|
|
@@ -664,13 +664,18 @@ function startFallback() {
|
|
|
664
664
|
|
|
665
665
|
/**
|
|
666
666
|
* Yeni sheet yüklenmeden eskisi kaldırılmaz; böylece stilsiz bir kare oluşmaz.
|
|
667
|
+
* `name` varsa `data-jskelet-css` ile doğru link seçilir (sayfa sheet'leri).
|
|
668
|
+
*
|
|
667
669
|
* @param {string} href
|
|
670
|
+
* @param {string} [name] Manifest anahtarı (`app.css`, `home.css`, …)
|
|
668
671
|
*/
|
|
669
|
-
function swapStylesheet(href) {
|
|
670
|
-
const current =
|
|
672
|
+
function swapStylesheet(href, name) {
|
|
673
|
+
const current = name
|
|
674
|
+
? document.querySelector(`link[rel="stylesheet"][data-jskelet-css="${CSS.escape(name)}"]`)
|
|
675
|
+
: document.querySelector('link[rel="stylesheet"]');
|
|
671
676
|
if (!current || current.getAttribute("href") === href) return;
|
|
672
677
|
|
|
673
|
-
const next = current.cloneNode();
|
|
678
|
+
const next = /** @type {HTMLLinkElement} */ (current.cloneNode());
|
|
674
679
|
next.href = href;
|
|
675
680
|
next.addEventListener("load", () => current.remove(), { once: true });
|
|
676
681
|
current.after(next);
|
|
@@ -426,8 +426,12 @@ function watchManifest() {
|
|
|
426
426
|
// sayfa silinmiş dosyayı istemeye devam eder.
|
|
427
427
|
clearHtmlCache();
|
|
428
428
|
|
|
429
|
-
|
|
430
|
-
|
|
429
|
+
// Yalnızca stylesheet anahtarları değiştiyse hot-swap; JS/sprite için
|
|
430
|
+
// tam yenileme gerekir. Birden fazla sayfa sheet'i de desteklenir.
|
|
431
|
+
if (changed.every((key) => key.endsWith(".css"))) {
|
|
432
|
+
for (const name of changed) {
|
|
433
|
+
broadcast({ type: "css", name, href: next[name] });
|
|
434
|
+
}
|
|
431
435
|
return;
|
|
432
436
|
}
|
|
433
437
|
|
package/src/server/render.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Controller sözleşmesi:
|
|
5
5
|
* async (ctx) => { view, data?, metadata?, status?, revalidate?, head?,
|
|
6
|
-
* bodyClass?, entries? }
|
|
6
|
+
* bodyClass?, entries?, styles? }
|
|
7
7
|
* `ctx` → { params, query, pathname, req }
|
|
8
8
|
*
|
|
9
9
|
* `route()` üç kapsamı belirli bir sırayla iç içe kurar:
|
|
@@ -218,7 +218,8 @@ export async function renderView(view, data = {}) {
|
|
|
218
218
|
* Sayfayı layout içinde render eder.
|
|
219
219
|
*
|
|
220
220
|
* @param {{ view: string, data?: object, metadata?: object, head?: string,
|
|
221
|
-
* bodyClass?: string, entries?: string[],
|
|
221
|
+
* bodyClass?: string, entries?: string[], styles?: string[],
|
|
222
|
+
* pathname?: string }} page
|
|
222
223
|
* @returns {Promise<string>}
|
|
223
224
|
*/
|
|
224
225
|
export async function renderPage(page) {
|
|
@@ -258,6 +259,7 @@ export async function renderPage(page) {
|
|
|
258
259
|
(context.extraHead ?? ""),
|
|
259
260
|
bodyClass: page.bodyClass ?? context.bodyClass ?? "",
|
|
260
261
|
entries: page.entries ?? [],
|
|
262
|
+
styles: page.styles ?? [],
|
|
261
263
|
devtools: isDev,
|
|
262
264
|
devBasePath: config.brand.devBasePath,
|
|
263
265
|
body,
|
package/src/templates/layout.ejs
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
noktası olarak kullanın.
|
|
7
7
|
|
|
8
8
|
Kullanılabilir local'ler: metadata, headMeta, extraHead, structuredData,
|
|
9
|
-
body, bodyClass, entries, pathname, lang, devtools, devBasePath,
|
|
10
|
-
hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
|
|
9
|
+
body, bodyClass, entries, styles, pathname, lang, devtools, devBasePath,
|
|
10
|
+
asset, hasAsset + tüm html/tag helper'ları ve views/components/** export'ları.
|
|
11
11
|
`hooks.layoutContext()` döndürdüğü her alan da buraya eklenir.
|
|
12
12
|
-%>
|
|
13
13
|
<!DOCTYPE html>
|
|
@@ -18,10 +18,16 @@
|
|
|
18
18
|
<%# Kaynak ipuçları en başta: preconnect ve LCP preload'ını geciktirmek
|
|
19
19
|
doğrudan LCP'ye yazılır. Sayfaya özel head buradan gelir. %>
|
|
20
20
|
<%- extraHead %>
|
|
21
|
-
<%#
|
|
21
|
+
<%# Global sheet + controller `styles: [...]` sayfa sheet'leri.
|
|
22
|
+
data-jskelet-css: dev hot-swap doğru link'i bulsun diye. %>
|
|
22
23
|
<% if (hasAsset('app.css')) { %>
|
|
23
|
-
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
24
|
+
<link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
|
|
24
25
|
<% } %>
|
|
26
|
+
<% styles.forEach(function (sheet) { %>
|
|
27
|
+
<% if (hasAsset(sheet)) { %>
|
|
28
|
+
<link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
|
|
29
|
+
<% } %>
|
|
30
|
+
<% }); %>
|
|
25
31
|
<%- headMeta %>
|
|
26
32
|
<% structuredData.forEach(function (item) { %>
|
|
27
33
|
<script type="application/ld+json"><%- jsonScript(item) %></script>
|