jskelet 0.7.0 → 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 +19 -0
- package/docs/04-render-ve-sablonlar.md +13 -3
- package/docs/en/04-rendering.md +13 -3
- package/package.json +1 -1
- 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/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
|
|
@@ -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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.7.
|
|
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",
|
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} */
|
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";
|