jskelet 0.3.5 → 0.4.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 CHANGED
@@ -27,6 +27,29 @@ one is listed under a **Breaking** heading.
27
27
 
28
28
  ### Added
29
29
 
30
+ - VS Code / Cursor extension skeleton under `extensions/vscode-jsk`: `.jsk`
31
+ language id, TextMate highlighting (`{{ }}` / `{#if}` / `{#each}` /
32
+ components), language config, and snippets. Install from that folder or
33
+ launch **JSK: Extension** from the repo root. Bound attrs on HTML tags
34
+ (`:src="… + '/path'"`) highlight nested single-quoted strings.
35
+ - Compile-time known components are discovered from **named exports** in
36
+ `views/components/**/*.js` (plus `.jsk` component files), not from the file
37
+ basename — so `<SectionHead />` resolves when `sectionHead` lives in
38
+ `ui.js` without a stub re-export. Docs cover the `.jsk` template-vs-component
39
+ boundary and a `{ items, error }` loader / `LoadErrorState` pattern so
40
+ upstream failures are not mistaken for empty data.
41
+ - Build-time `.jsk` templates: declarative HTML-like syntax compiled to ESM
42
+ render modules under `.jskelet/templates/` (no request-time parse, `eval`, or
43
+ `new Function`). Coexists with EJS; compiled `.jsk` wins when both exist.
44
+ Syntax: `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}` / `{#include}`, PascalCase
45
+ components (`:prop` bindings), built-ins `Link` / `Image` / `Icon` /
46
+ `CsrfField` / `PreloadImage`.
47
+ - Feature-first conventions: `paths.features` / `paths.shared`, multi-root
48
+ views and components, `features/<name>/index.js` route registration after
49
+ `routes/`. CLI: `jskelet generate feature|page|island`. `jskelet init`
50
+ scaffolds `.jsk` pages.
51
+ - Template compile step in `jskelet build`; icon scan and Tailwind docs cover
52
+ `.jsk` / `features` / `shared`. Bench: `node scripts/bench-templates.mjs`.
30
53
  - Top-level `logs` config for persistent sinks: daily NDJSON files
31
54
  (`logs.file`) and batched S3 PutObject (`logs.s3`) with embedded SigV4 — no
32
55
  `@aws-sdk` dependency. `kinds` selects `http` / `event` / `error`; `console`
@@ -113,6 +136,17 @@ one is listed under a **Breaking** heading.
113
136
  variant of a path, which is the right default for a webhook but wrong when you
114
137
  want `/list?page=2` gone and `/list?page=3` left hot.
115
138
 
139
+ ### Changed
140
+
141
+ - Duplicate component named exports (or the same PascalCase tag in two files)
142
+ now **fail** at build and at server startup instead of warning and letting
143
+ the second definition win. Overwriting `components/index.js` barrel exports
144
+ remains allowed.
145
+ - `examples/minimal` pages moved to `.jsk`; adds `features/demo` as a
146
+ co-located route + view sample.
147
+ - Marketing compare/FAQ copy no longer claims targeted invalidation is missing;
148
+ it points at `invalidateHtmlCache()` (and Redis pub/sub for multi-instance).
149
+
116
150
  - An adaptive per-host rate limit for upstream calls, `cache().upstream`. It sits
117
151
  in the `fetch` wrapper rather than in the prewarm pass, because what spends the
118
152
  quota is the API call, not the page: one render may make one call or twenty, so
package/bin/jskelet.mjs CHANGED
@@ -1,11 +1,12 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * JSkelet CLI.
4
4
  *
5
5
  * jskelet dev build watch + sunucu, canlı yenileme, dev overlay
6
6
  * jskelet build tek seferlik prod build (fontlar, sprite, CSS, JS, görseller)
7
7
  * jskelet start prod sunucu (build eksikse önce üretir)
8
- * jskelet init bulunduğun dizine minimal iskelet kurar
8
+ * jskelet init bulunduğun dizine minimal iskelet kurar
9
+ * jskelet generate feature / page / island iskeleti
9
10
  *
10
11
  * Alt komutlar ayrı süreçlerde çalışır. Sebep: `dev` iki uzun ömürlü süreci
11
12
  * (build watch + sunucu) yönetiyor ve sunucunun ESM resolve hook'larına
@@ -90,14 +91,26 @@ switch (command) {
90
91
  break;
91
92
  }
92
93
 
94
+ case "generate": {
95
+ const { generate } = await import("../src/generate.mjs");
96
+ try {
97
+ await generate(process.cwd(), rest);
98
+ } catch (error) {
99
+ process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
100
+ process.exit(1);
101
+ }
102
+ break;
103
+ }
104
+
93
105
  default: {
94
106
  const known = command ? `unknown command: ${command}\n\n` : "";
95
107
  process.stderr.write(
96
- `${known}usage: jskelet <dev|build|start|init>\n\n` +
97
- " dev build watch + server (live reload, dev overlay)\n" +
98
- " build production build\n" +
99
- " start production server\n" +
100
- " init scaffold a minimal skeleton in the current directory\n",
108
+ `${known}usage: jskelet <dev|build|start|init|generate>\n\n` +
109
+ " dev build watch + server (live reload, dev overlay)\n" +
110
+ " build production build\n" +
111
+ " start production server\n" +
112
+ " init scaffold a minimal skeleton in the current directory\n" +
113
+ " generate scaffold feature | page | island\n",
101
114
  );
102
115
  process.exit(command ? 1 : 0);
103
116
  }
package/docs/02-mimari.md CHANGED
@@ -25,6 +25,9 @@ JSkelet bu gözlemi mimarinin merkezine alır:
25
25
  olarak, kendi modülüyle, kendi zamanında bağlanır.
26
26
  3. **Sayfa üretimi önbelleklenir.** Aynı HTML'i her istekte yeniden üretmenin
27
27
  anlamı yok; TTL'li bir bellek önbelleği ISR'nin yerini tutar.
28
+ 4. **Şablonlar build-time derlenir (`.jsk`).** İstek anında parse yok; EJS
29
+ legacy olarak yan yana kalır. Feature'lar `features/<name>/{server,views,client}`
30
+ altında toplanabilir — URL kaydı yine açıktır.
28
31
 
29
32
  ## Bir isteğin yolu
30
33
 
@@ -63,9 +63,25 @@ export default {
63
63
  };
64
64
  ```
65
65
 
66
- **2. Liste yoksa `routes/` dizini alfabetik taranır.** Tarama özyinelemelidir
67
- (alt dizinler de dâhil), yalnızca `.js` ve `.mjs` dosyaları alınır ve adı `_`
68
- ile başlayan dosyalar atlanır (`_helpers.js` gibi paylaşılan modüller için).
66
+ **2. Liste yoksa `routes/` dizini alfabetik taranır**, ardından her
67
+ `features/<name>/index.js` (veya `.mjs`) alfabetik eklenir. Tarama
68
+ özyinelemelidir (alt dizinler de dâhil), yalnızca `.js` ve `.mjs` dosyaları
69
+ alınır ve adı `_` ile başlayan dosyalar atlanır (`_helpers.js` gibi paylaşılan
70
+ modüller için).
71
+
72
+ Feature-first düzen zorunlu değildir; bir feature örneği:
73
+
74
+ ```
75
+ features/piyasalar/
76
+ index.js # register(app, api) — URL'ler açıkça yazılır
77
+ server/
78
+ views/pages/… # .jsk veya .ejs
79
+ views/components/
80
+ client/ # island'lar; client/entries'ten registerAll
81
+ ```
82
+
83
+ `jskelet generate feature|page|island` iskelet üretir. Filesystem URL routing
84
+ yoktur.
69
85
 
70
86
  Bu durumda dosya adlarına sayısal önek verin:
71
87
 
@@ -20,17 +20,96 @@ route(controller)
20
20
  │ renderView(page.view, { …data, metadata }), → body
21
21
  │ hooks.layoutContext({ pathname, metadata }), → context
22
22
  │ ])
23
- └─ layout.ejs render → tam HTML
23
+ └─ layout (.jsk derlenmiş veya .ejs) → tam HTML
24
24
  ```
25
25
 
26
26
  Layout bağlamı ve gövde **paralel** üretilir. Sebebi ölçümden geliyor:
27
27
  navigasyon çoğu projede upstream'den geliyor ve gövde render'ıyla sırayla
28
28
  beklemek her sayfaya gereksiz gecikme ekliyor.
29
29
 
30
- ## EJS motoru
30
+ ## `.jsk` — build-time derlenmiş şablonlar
31
31
 
32
- Motor ilk render'da bir kez kurulur; bileşen taraması dosya sistemine
33
- dokunduğu için her istekte yapılamaz ve config yüklenmeden hesaplanamaz.
32
+ Yeni uygulamalarda varsayılan şablon biçimi `.jsk`'dir. Build sırasında
33
+ (`.jskelet/templates/*.mjs`) normal ESM modüllerine çevrilir; **istek anında
34
+ parse / `eval` / `new Function` yoktur**. Production yolu:
35
+
36
+ ```
37
+ controller data → import edilmiş render(data, helpers) → HTML
38
+ ```
39
+
40
+ ### Sözdizimi özeti
41
+
42
+ ```html
43
+ <section class="wrapper">
44
+ <h1>{{ title }}</h1>
45
+ <div>{{{ trustedHtml }}}</div>
46
+
47
+ {#if items.length}
48
+ <List :items="items" />
49
+ {#else}
50
+ <p>Boş</p>
51
+ {/if}
52
+
53
+ {#each items as item, i}
54
+ <li data-i="{{ i }}">{{ item }}</li>
55
+ {/each}
56
+
57
+ <Link href="/" text="Home" />
58
+ <div data-island="counter" data-island-props='{"start":0}'></div>
59
+ </section>
60
+ ```
61
+
62
+ | Özellik | Yazım |
63
+ | --- | --- |
64
+ | Kaçışlı metin | `{{ expr }}` |
65
+ | Ham HTML | `{{{ expr }}}` |
66
+ | Koşul | `{#if expr}` … `{#else}` … `{/if}` |
67
+ | Döngü | `{#each list as item}` veya `as item, i` |
68
+ | Include | `{#include "partials/header"}` (derlenmiş `.jsk`) |
69
+ | Bileşen | PascalCase etiket; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
70
+ | Yerleşikler | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
71
+
72
+ İfade dili kasıtlı olarak dardır (erişim, karşılaştırma, ternary, `.length`).
73
+ Atama, object literal ve rastgele fonksiyon çağrısı yok — mantık controller
74
+ veya JS bileşende kalır.
75
+
76
+ #### Şablon mu, bileşen mi?
77
+
78
+ EJS’den geçerken sınırı erken çizmek işe yarar:
79
+
80
+ | Burada kalsın (`.jsk`) | JS bileşene taşı |
81
+ | --- | --- |
82
+ | Metin, koşul, liste, prop bağlama | Fonksiyon çağrısı, nesne üretimi, biçimlendirme |
83
+ | Yerleşik etiketler (`Link`, `Image`, …) | Birden fazla yardımcıdan HTML birleştirme |
84
+ | Controller’dan gelen hazır veri | Upstream / hata ayırt eden UI (`LoadErrorState`) |
85
+
86
+ Şablonda `format(x)` veya `{ a: 1 }` yazılamıyorsa bu bir eksik değil: o iş
87
+ `views/components/*.js` veya controller’ındır. Karmaşık sayfalar bileşene
88
+ kaçıyorsa ifade dilini genişletmek yerine bileşen sınırını net tutmak tercih
89
+ edilir.
90
+
91
+ ### Editör desteği
92
+
93
+ Repo içinde `extensions/vscode-jsk` VS Code / Cursor uzantısı vardır: sözdizimi
94
+ renklendirme, dil yapılandırması ve snippet'ler. Yerel kurulum:
95
+
96
+ ```bash
97
+ code --install-extension extensions/vscode-jsk
98
+ ```
99
+
100
+ Ayrıntılar uzantı README'sinde.
101
+
102
+ ### EJS ile birlikte yaşam
103
+
104
+ Aynı `view` id için derlenmiş `.jsk` varsa o kullanılır; yoksa `.ejs` dosyası
105
+ EJS ile render edilir. Mevcut uygulamalar değişmeden çalışır. `jskelet init`
106
+ yeni iskeleti `.jsk` ile kurar.
107
+
108
+ ## EJS motoru (legacy)
109
+
110
+ EJS hâlâ desteklenir. Motor ilk render'da bir kez kurulur; bileşen taraması
111
+ dosya sistemine dokunduğu için her istekte yapılamaz ve config yüklenmeden
112
+ hesaplanamaz.
34
113
 
35
114
  Ayarlar:
36
115
 
@@ -52,8 +131,9 @@ normal akışta gerekmez.
52
131
  1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
53
132
  dizininin üst dizinine** göre çözülür: `views` varsayılansa
54
133
  `layout: "views/ozel.ejs"` → `<root>/views/ozel.ejs`.
55
- 2. Verilmemişse `views/layout.ejs` varsa o kullanılır.
56
- 3. O da yoksa framework'ün kendi minimal layout'u kullanılır
134
+ 2. Verilmemişse `views/layout.jsk` (derlenmiş) varsa o kullanılır.
135
+ 3. Yoksa `views/layout.ejs` varsa o kullanılır.
136
+ 4. O da yoksa framework'ün kendi minimal layout'u kullanılır
57
137
  (`node_modules/jskelet/src/templates/layout.ejs`, ayrıca
58
138
  `jskelet/layout` belirteciyle de erişilebilir).
59
139
 
@@ -189,13 +269,18 @@ Kurallar:
189
269
 
190
270
  - Tarama özyinelemelidir; alt dizinler de kapsanır.
191
271
  - `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
272
+ - Compile-time bilinen bileşen listesi **dosya adından değil**, kaynak
273
+ metindeki named export'lardan okunur. `ui.js` içindeki `sectionHead` →
274
+ şablonda `<SectionHead />` (runtime zaten camelCase export'a PascalCase
275
+ alias ekler). Dosya adına göre stub re-export eklemeye gerek yoktur.
192
276
  - `loader.js` ve `index.js` bileşen dosyası sayılmaz.
193
277
  - `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
194
278
  önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
195
279
  bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
196
- - Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
197
- kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
198
- one wins.`
280
+ - Aynı ad (veya aynı PascalCase etiket) iki farklı bileşen dosyasında
281
+ tanımlıysa **uyarı değil hata**: build ve sunucu açılışı
282
+ `Component 'card' is defined twice: …` ile durur. Barrel üzerine yazmak
283
+ bilinçli istisnadır.
199
284
  - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
200
285
  bir proje de çalışır.
201
286
 
package/docs/06-cache.md CHANGED
@@ -380,6 +380,68 @@ export async function apiGet(path) {
380
380
  }
381
381
  ```
382
382
 
383
+ ### Loader sözleşmesi: boş liste ≠ hata
384
+
385
+ `catch → []` (veya `null`) ile yutulan bir upstream hatası, yanlış mapping ile
386
+ aynı görünür: boş UI. Rate limit ve geçici hatalar logda doğru yönde
387
+ işaretlense bile ziyaretçi “veri yok” sanır. Widget loader’ları sessiz
388
+ `[]`’ye gömülmek yerine sonucu ayırsın:
389
+
390
+ ```js
391
+ /**
392
+ * @returns {Promise<{ items: object[], error: Error | null }>}
393
+ */
394
+ export async function loadTickerItems() {
395
+ try {
396
+ const items = await apiGet("/ticker");
397
+ if (!items) {
398
+ return { items: [], error: new Error("Upstream returned no data") };
399
+ }
400
+ return { items, error: null };
401
+ } catch (error) {
402
+ return {
403
+ items: [],
404
+ error: error instanceof Error ? error : new Error(String(error)),
405
+ };
406
+ }
407
+ }
408
+ ```
409
+
410
+ Uygulama tarafında ortak bir `LoadErrorState` bileşeni (veya eşdeğeri) bu
411
+ `error` alanını göstersin; her widget kendi boş hâline düşmesin:
412
+
413
+ ```js
414
+ // views/components/load-error-state.js
415
+ import { esc } from "jskelet/html";
416
+
417
+ /**
418
+ * @param {{ message?: string, title?: string }} props
419
+ * @returns {string}
420
+ */
421
+ export function LoadErrorState({ message, title = "Veri yüklenemedi" }) {
422
+ return `<div role="alert" data-load-error class="…">
423
+ <p>${esc(title)}</p>
424
+ ${message ? `<p>${esc(message)}</p>` : ""}
425
+ </div>`;
426
+ }
427
+ ```
428
+
429
+ ```html
430
+ {#if error}
431
+ <LoadErrorState :message="error.message" />
432
+ {#else if items.length}
433
+ {#each items as item}
434
+ …
435
+ {/each}
436
+ {#else}
437
+ <p>Kayıt yok</p>
438
+ {/if}
439
+ ```
440
+
441
+ Framework markaya özel UI taşımaz; `LoadErrorState` uygulama bileşenidir.
442
+ Önemli olan sözleşme: `{ items, error }` (veya eşdeğeri) ve hata ile “gerçekten
443
+ boş”un şablonda ayrı kolları.
444
+
383
445
  ### Geçici ve kalıcı hata ayrımı
384
446
 
385
447
  | Durum | Sayılır | Sonuç |
@@ -150,12 +150,14 @@ göre çözülür ve içeride mutlak yola çevrilir.
150
150
 
151
151
  | Anahtar | Varsayılan | İçeriği |
152
152
  | --- | --- | --- |
153
- | `views` | `"views"` | EJS layout, sayfalar, bileşenler |
153
+ | `views` | `"views"` | Layout, sayfalar, bileşenler (klasik kök; `.jsk` / `.ejs`) |
154
+ | `features` | `"features"` | Feature-first dilimler (`<name>/{server,views,client}`) |
155
+ | `shared` | `"shared"` | Özellikler arası paylaşılan server/views/client |
154
156
  | `public` | `"public"` | Statik dosyalar; build çıktısı da buraya yazılır |
155
157
  | `client` | `"client"` | Island runtime kaynakları ve entry'ler |
156
158
  | `routes` | `"routes"` | Route modülleri |
157
159
  | `styles` | `"styles/globals.css"` | Tailwind/PostCSS giriş **dosyası** |
158
- | `generated` | `".jskelet"` | `manifest.json`, `metafile.json`, `images.json` |
160
+ | `generated` | `".jskelet"` | `manifest.json`, `templates/`, `metafile.json`, `images.json` |
159
161
 
160
162
  `styles` bir dosya yolu olduğu hâlde aynı çözümlemeden geçer; ayrı bir alan
161
163
  tutmaya değmiyor.
@@ -200,8 +202,8 @@ Layout `.ejs` dosyasının yolu. Verilen değer **views dizininin üst dizinine*
200
202
  göre çözülür, yani varsayılan `views` ile `"views/ozel.ejs"` →
201
203
  `<root>/views/ozel.ejs`.
202
204
 
203
- Verilmezse sırayla: `views/layout.ejs` varsa o, yoksa framework'ün minimal
204
- layout'u. Ayrıntı: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
205
+ Verilmezse sırayla: `views/layout.jsk`, `views/layout.ejs`, yoksa framework'ün
206
+ minimal layout'u. Ayrıntı: [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md).
205
207
 
206
208
  ## `routes`
207
209
 
@@ -467,7 +469,7 @@ Phosphor SVG sprite üretimi.
467
469
 
468
470
  | Değer | Sonuç |
469
471
  | --- | --- |
470
- | `{}` (varsayılan) | Sprite üretilir; taranan dizinler `["views", "client", "routes", "lib"]` |
472
+ | `{}` (varsayılan) | Sprite üretilir; taranan dizinler `["views", "client", "routes", "lib", "features", "shared"]` |
471
473
  | `{ scan: [...] }` | Taranan dizinler değiştirilir |
472
474
  | `false` | Sprite adımı tamamen atlanır |
473
475
 
package/docs/08-build.md CHANGED
@@ -12,6 +12,7 @@ davranışı burada. Çıktının çalışma anında nasıl servis edildiği
12
12
  ## Hat ve sırası
13
13
 
14
14
  ```
15
+ 0. Templates .jsk → .jskelet/templates/*.mjs (her zaman; dosya yoksa no-op)
15
16
  1. Fonts config.fonts varsa
16
17
  2. Icon sprite config.icons !== false ise
17
18
  3. CSS styles giriş dosyası varsa
@@ -21,6 +22,10 @@ davranışı burada. Çıktının çalışma anında nasıl servis edildiği
21
22
  7. Precompress watch değilse
22
23
  ```
23
24
 
25
+ Şablon derlemesi asset taramasından **önce** biter; istek yolunda parse yoktur.
26
+ Tailwind `@source` ve ikon taraması kaynak `.jsk` dosyalarını okur (üretilmiş
27
+ `.mjs` değil).
28
+
24
29
  Sıra rastgele değil:
25
30
 
26
31
  - **CSS ikon sprite'ından sonra gelir.** Sprite bir varlıktır ve sınıf üretmez,
@@ -25,6 +25,9 @@ JSkelet puts this observation at the centre of the architecture:
25
25
  independent "island", with its own module, at its own time.
26
26
  3. **Page production is cached.** There is no point in producing the same HTML
27
27
  again on every request; a memory cache with a TTL takes the place of ISR.
28
+ 4. **Templates are compiled at build time (`.jsk`).** No request-time parsing;
29
+ EJS remains as a legacy path. Features may co-locate under
30
+ `features/<name>/{server,views,client}` — route URLs stay explicit.
28
31
 
29
32
  ## The path of a request
30
33
 
@@ -65,10 +65,25 @@ export default {
65
65
  };
66
66
  ```
67
67
 
68
- **2. If there is no list, the `routes/` directory is scanned alphabetically.**
69
- The scan is recursive (subdirectories included), only `.js` and `.mjs` files are
70
- picked up, and files whose name begins with `_` are skipped (for shared modules
71
- like `_helpers.js`).
68
+ **2. If there is no list, the `routes/` directory is scanned alphabetically**,
69
+ then each `features/<name>/index.js` (or `.mjs`) is appended alphabetically.
70
+ The scan under `routes/` is recursive (subdirectories included), only `.js` and
71
+ `.mjs` files are picked up, and files whose name begins with `_` are skipped
72
+ (for shared modules like `_helpers.js`).
73
+
74
+ Feature-first layout is optional:
75
+
76
+ ```
77
+ features/markets/
78
+ index.js # register(app, api) — URLs are still explicit
79
+ server/
80
+ views/pages/… # .jsk or .ejs
81
+ views/components/
82
+ client/ # islands; register from client/entries
83
+ ```
84
+
85
+ `jskelet generate feature|page|island` scaffolds this. There is no filesystem
86
+ URL routing.
72
87
 
73
88
  In that case, give the file names a numeric prefix:
74
89
 
@@ -21,18 +21,95 @@ route(controller)
21
21
  │ renderView(page.view, { …data, metadata }), → body
22
22
  │ hooks.layoutContext({ pathname, metadata }), → context
23
23
  │ ])
24
- └─ layout.ejs render → full HTML
24
+ └─ layout (.jsk compiled or .ejs) → full HTML
25
25
  ```
26
26
 
27
27
  The layout context and the body are produced **in parallel**. The reason comes
28
28
  from measurement: in most projects navigation comes from upstream, and waiting
29
29
  for it in sequence with the body render adds needless latency to every page.
30
30
 
31
- ## The EJS engine
31
+ ## `.jsk` — build-time compiled templates
32
32
 
33
- The engine is set up once on the first render; the component scan touches the
34
- file system, so it cannot be done on every request and cannot be computed
35
- before the config is loaded.
33
+ New apps default to `.jsk`. At build time they become normal ESM modules under
34
+ `.jskelet/templates/*.mjs`. There is **no request-time parsing, `eval`, or
35
+ `new Function`**. Production path:
36
+
37
+ ```
38
+ controller data → imported render(data, helpers) → HTML
39
+ ```
40
+
41
+ ### Syntax summary
42
+
43
+ ```html
44
+ <section class="wrapper">
45
+ <h1>{{ title }}</h1>
46
+ <div>{{{ trustedHtml }}}</div>
47
+
48
+ {#if items.length}
49
+ <List :items="items" />
50
+ {#else}
51
+ <p>Empty</p>
52
+ {/if}
53
+
54
+ {#each items as item, i}
55
+ <li data-i="{{ i }}">{{ item }}</li>
56
+ {/each}
57
+
58
+ <Link href="/" text="Home" />
59
+ <div data-island="counter" data-island-props='{"start":0}'></div>
60
+ </section>
61
+ ```
62
+
63
+ | Feature | Form |
64
+ | --- | --- |
65
+ | Escaped text | `{{ expr }}` |
66
+ | Raw HTML | `{{{ expr }}}` |
67
+ | Conditional | `{#if expr}` … `{#else}` … `{/if}` |
68
+ | Loop | `{#each list as item}` or `as item, i` |
69
+ | Include | `{#include "partials/header"}` (compiled `.jsk`) |
70
+ | Component | PascalCase tag; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
71
+ | Built-ins | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
72
+
73
+ The expression language is intentionally small (access, compare, ternary,
74
+ `.length`). No assignments, object literals, or arbitrary calls — keep logic in
75
+ controllers or JS components.
76
+
77
+ #### Template or component?
78
+
79
+ When moving off EJS, draw the line early:
80
+
81
+ | Stay in `.jsk` | Move to a JS component |
82
+ | --- | --- |
83
+ | Text, conditionals, lists, prop binding | Function calls, object construction, formatting |
84
+ | Built-in tags (`Link`, `Image`, …) | Composing HTML from several helpers |
85
+ | Ready-made data from the controller | Upstream / error-aware UI (`LoadErrorState`) |
86
+
87
+ If the template cannot write `format(x)` or `{ a: 1 }`, that is intentional: the
88
+ work belongs in `views/components/*.js` or the controller. Prefer a clear
89
+ component boundary over widening the expression language when complex pages
90
+ “escape” into JS.
91
+
92
+ ### Editor support
93
+
94
+ `extensions/vscode-jsk` is a VS Code / Cursor extension in this repo: syntax
95
+ highlighting, language configuration, and snippets. Local install:
96
+
97
+ ```bash
98
+ code --install-extension extensions/vscode-jsk
99
+ ```
100
+
101
+ See the extension README for details.
102
+
103
+ ### Coexistence with EJS
104
+
105
+ If a compiled `.jsk` exists for a view id it wins; otherwise `.ejs` is rendered
106
+ with EJS. Existing apps keep working unchanged. `jskelet init` scaffolds `.jsk`.
107
+
108
+ ## The EJS engine (legacy)
109
+
110
+ EJS remains supported. The engine is set up once on the first render; the
111
+ component scan touches the file system, so it cannot be done on every request
112
+ and cannot be computed before the config is loaded.
36
113
 
37
114
  Settings:
38
115
 
@@ -54,14 +131,15 @@ normal flow because the dev server restarts the process.
54
131
  1. `jskelet.config.mjs` → if `layout` is given, it is used. The path is
55
132
  resolved relative to the **parent directory of the views directory**: if
56
133
  `views` is the default, `layout: "views/custom.ejs"` → `<root>/views/custom.ejs`.
57
- 2. If it is not given and `views/layout.ejs` exists, that is used.
58
- 3. If that does not exist either, the framework's own minimal layout is used
134
+ 2. If it is not given and `views/layout.jsk` exists (compiled), that is used.
135
+ 3. Else if `views/layout.ejs` exists, that is used.
136
+ 4. If that does not exist either, the framework's own minimal layout is used
59
137
  (`node_modules/jskelet/src/templates/layout.ejs`, also reachable through the
60
138
  `jskelet/layout` specifier).
61
139
 
62
- The third option exists so that a new project can work with a single route. The
140
+ These fallbacks exist so that a new project can work with a single route. The
63
141
  most practical way to move to your own layout is to copy that file to
64
- `views/layout.ejs`.
142
+ `views/layout.ejs` or author `views/layout.jsk`.
65
143
 
66
144
  ### The framework's default layout
67
145
 
@@ -192,14 +270,20 @@ Rules:
192
270
 
193
271
  - The scan is recursive; subdirectories are covered too.
194
272
  - `default` exports are ignored — only named exports are registered.
273
+ - The compile-time known-component set is read from **named exports in the
274
+ source**, not from the file basename. `sectionHead` in `ui.js` →
275
+ `<SectionHead />` in the template (runtime already adds a PascalCase alias
276
+ for camelCase exports). You do not need a stub re-export named after the
277
+ file.
195
278
  - `loader.js` and `index.js` do not count as component files.
196
279
  - If `views/components/index.js` exists it is loaded first as a **barrel**,
197
280
  with the lowest priority. Its only purpose is to turn `lib/` re-exports into
198
281
  template locals; the components' own files come later and silently overwrite
199
282
  it.
200
- - If the same name is defined in two different component files a warning is
201
- printed and **the second one wins**: `[components] 'card' is defined twice:
202
- a.js and b.js — the second one wins.`
283
+ - If the same name (or the same PascalCase tag) is defined in two different
284
+ component files, that is an **error, not a warning**: build and server
285
+ startup stop with `Component 'card' is defined twice: …`. Overwriting the
286
+ barrel is the deliberate exception.
203
287
  - If the `views/components` directory does not exist the component registry
204
288
  stays empty; a project that uses no components works fine too.
205
289