jskelet 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +127 -0
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +342 -0
- package/bin/jskelet.mjs +104 -0
- package/docs/01-baslangic.md +285 -0
- package/docs/02-mimari.md +287 -0
- package/docs/03-routing.md +437 -0
- package/docs/04-render-ve-sablonlar.md +490 -0
- package/docs/05-islands.md +429 -0
- package/docs/06-cache.md +409 -0
- package/docs/07-yapilandirma.md +673 -0
- package/docs/08-build.md +366 -0
- package/docs/09-dev-araclari.md +302 -0
- package/docs/10-dagitim.md +329 -0
- package/docs/11-tasima.md +352 -0
- package/docs/README.md +82 -0
- package/package.json +97 -0
- package/src/build/build.mjs +138 -0
- package/src/build/ensure-build.mjs +15 -0
- package/src/build/paths.mjs +118 -0
- package/src/build/resolve-peer.mjs +36 -0
- package/src/build/tasks/client.mjs +268 -0
- package/src/build/tasks/css.mjs +124 -0
- package/src/build/tasks/fonts.mjs +146 -0
- package/src/build/tasks/icons.mjs +224 -0
- package/src/build/tasks/images.mjs +244 -0
- package/src/build/tasks/precompress.mjs +78 -0
- package/src/client/devtools/overlay.js +1763 -0
- package/src/client/devtools/report.html +185 -0
- package/src/client/devtools/report.js +712 -0
- package/src/client/dom.js +95 -0
- package/src/client/index.js +26 -0
- package/src/client/registry.js +223 -0
- package/src/client/safe-image.js +91 -0
- package/src/client/store.js +36 -0
- package/src/config/defaults.js +102 -0
- package/src/config/index.js +433 -0
- package/src/config/pattern.js +107 -0
- package/src/dev-server.mjs +383 -0
- package/src/http/control-flow.js +56 -0
- package/src/http/request-cache.js +46 -0
- package/src/index.js +35 -0
- package/src/init.mjs +220 -0
- package/src/log.mjs +332 -0
- package/src/logo.png +0 -0
- package/src/runtime/alias-hooks.mjs +119 -0
- package/src/runtime/register.mjs +4 -0
- package/src/server/assets.js +119 -0
- package/src/server/create-app.js +167 -0
- package/src/server/dev/devtools.js +383 -0
- package/src/server/dev/report.js +351 -0
- package/src/server/head-hints.js +132 -0
- package/src/server/html-cache.js +166 -0
- package/src/server/metadata.js +102 -0
- package/src/server/middleware/compression.js +205 -0
- package/src/server/middleware/dev-gate.js +62 -0
- package/src/server/middleware/headers.js +37 -0
- package/src/server/middleware/redirects.js +32 -0
- package/src/server/middleware/static-precompressed.js +100 -0
- package/src/server/middleware/upstream-proxy.js +141 -0
- package/src/server/prewarm.js +283 -0
- package/src/server/render.js +356 -0
- package/src/server/router.js +121 -0
- package/src/server/status-page.js +164 -0
- package/src/server/upstream-tracking.js +51 -0
- package/src/start.mjs +7 -0
- package/src/templates/layout.ejs +44 -0
- package/src/version.mjs +17 -0
- package/src/views/components/loader.js +85 -0
- package/src/views/helpers/html.js +102 -0
- package/src/views/helpers/tags.js +193 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Bu depo **JSkelet** framework'ünün kaynağıdır: Express 5 + EJS sunucu render,
|
|
4
|
+
vanilla JS island'lar, Tailwind v4 ve süreç belleğinde yaşayan HTML TTL cache.
|
|
5
|
+
React ve TypeScript yok; düz JavaScript + JSDoc.
|
|
6
|
+
|
|
7
|
+
Bir JSkelet **uygulamasında** çalışıyorsan (framework'ün kendisinde değil), aynı
|
|
8
|
+
kuralların uygulama tarafı karşılıkları için [docs/](./docs/README.md) yeterli;
|
|
9
|
+
özellikle 03, 05 ve 07.
|
|
10
|
+
|
|
11
|
+
## Değişiklik yapmadan önce
|
|
12
|
+
|
|
13
|
+
Framework'ün davranışını değiştiren bir iş alıyorsan ilgili belgeyi oku. Bu
|
|
14
|
+
dosyalar kararların **gerekçelerini** taşıyor ve çoğu "iyileştirme" fikri orada
|
|
15
|
+
zaten tartışılmış:
|
|
16
|
+
|
|
17
|
+
| Dokunacağın yer | Önce oku |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `src/server/render.js`, şablonlar | [04-render-ve-sablonlar.md](./docs/04-render-ve-sablonlar.md) |
|
|
20
|
+
| `src/server/html-cache.js`, `route()` | [06-cache.md](./docs/06-cache.md) |
|
|
21
|
+
| `src/server/create-app.js`, middleware | [02-mimari.md](./docs/02-mimari.md) |
|
|
22
|
+
| `src/client/**` | [05-islands.md](./docs/05-islands.md) |
|
|
23
|
+
| `src/build/**` | [08-build.md](./docs/08-build.md) |
|
|
24
|
+
| `src/config/**` | [07-yapilandirma.md](./docs/07-yapilandirma.md) |
|
|
25
|
+
| `src/dev-server.mjs`, `src/server/dev/**` | [09-dev-araclari.md](./docs/09-dev-araclari.md) |
|
|
26
|
+
|
|
27
|
+
## Doğrulama
|
|
28
|
+
|
|
29
|
+
**Lint yeterli, tam build zorunlu değil.**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm run lint
|
|
33
|
+
npm test # desen derleyicisi ve HTML cache için birim testler
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Davranış değiştiren bir iş yaptıysan örneklerden biriyle uçtan uca dene:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm --prefix examples/blog install # ilk seferde
|
|
40
|
+
npm --prefix examples/blog run build
|
|
41
|
+
npm --prefix examples/blog run start # ayrı terminalde
|
|
42
|
+
node examples/blog/smoke.mjs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`examples/blog` bilinçli olarak framework'ün her yüzeyini kullanır (dinamik
|
|
46
|
+
route, tüm config bölümleri, fragment, form, prewarm, RSS/sitemap, dört island).
|
|
47
|
+
Bir şeyi bozduysan smoke testi genelde yakalar.
|
|
48
|
+
|
|
49
|
+
## Mimari kurallar
|
|
50
|
+
|
|
51
|
+
**Framework domain bilgisi taşımaz.** `src/` altında hiçbir yerde uygulamaya
|
|
52
|
+
özel URL, marka adı, metin ya da veri şekli olmaz. Uygulamaya ait mantık
|
|
53
|
+
`hooks` üzerinden gelir (`metadata`, `layoutContext`, `notFound`,
|
|
54
|
+
`prewarmPaths`), görünen adlar `brand` üzerinden.
|
|
55
|
+
|
|
56
|
+
**Yol hesabı tek yerde.** Hiçbir modül `../..` sayarak dizin bulmaz;
|
|
57
|
+
`getConfig().dirs` kullanılır. Framework `node_modules/` içine girdiğinde
|
|
58
|
+
göreli yol sayan her satır bozulur.
|
|
59
|
+
|
|
60
|
+
**Yapılandırma hatası siteyi düşürmez.** Bozuk bir `jskelet.config.mjs`, hata
|
|
61
|
+
veren bir `headers()` ya da fırlatan bir hook uyarı basar ve varsayılana döner.
|
|
62
|
+
|
|
63
|
+
**Build çıktısı olmadan da ayağa kalkar.** `asset()` manifest yoksa hash'siz
|
|
64
|
+
yola döner, `hasAsset()` false olur ve layout etiketi basmaz. `jskelet build`
|
|
65
|
+
unutulduğunda hata değil, stilsiz ama çalışan bir sayfa görülür.
|
|
66
|
+
|
|
67
|
+
**Opsiyonel bağımlılıklar sessizce atlanır.** `sharp`, `postcss`,
|
|
68
|
+
`@phosphor-icons/core` yoksa ilgili build adımı çalışmaz. Peer bağımlılıklar
|
|
69
|
+
**uygulamanın** `node_modules`'ünden çözülmeli: `src/build/resolve-peer.mjs`
|
|
70
|
+
içindeki `importFromApp` / `tryImportFromApp` kullanılır, doğrudan `import
|
|
71
|
+
"postcss"` yazılmaz.
|
|
72
|
+
|
|
73
|
+
**Middleware sırası sözleşmedir.** `src/server/create-app.js` başındaki
|
|
74
|
+
numaralı yorum sırayı ve her konumun gerekçesini anlatır. Sıra değiştirmek
|
|
75
|
+
sessiz bozulmalara yol açar; değiştiriyorsan yorumu da güncelle.
|
|
76
|
+
|
|
77
|
+
**Cache'lenen HTML herkese aynı gider.** Kişiye özel hiçbir şey `route()` ile
|
|
78
|
+
render edilen sayfaya girmez. Tema gibi kararlar client'ta, kullanıcıya özel
|
|
79
|
+
parçalar ayrı ve `no-store` işaretli fragment uçlarında.
|
|
80
|
+
|
|
81
|
+
## Kod stili
|
|
82
|
+
|
|
83
|
+
- **JSDoc zorunlu**: dışa açık her fonksiyonda parametre ve dönüş tipleri.
|
|
84
|
+
- **Yorumlar Türkçe** ve *neden*i anlatır. Kodun ne yaptığını tekrar eden yorum
|
|
85
|
+
yazma; bir kararın gerekçesini, bir takası ya da bir tuzağı yaz.
|
|
86
|
+
- Sunucu ve build tarafı `node:` önekli çekirdek modülleri kullanır.
|
|
87
|
+
- `src/client/**` tarayıcıda çalışır: Node API'si, `process` (build sırasında
|
|
88
|
+
değiştirilen `clientEnv` dışında) ve senkron ağ yok.
|
|
89
|
+
- Yeni bir dışa açık yüzey ekliyorsan `package.json` → `exports` ve
|
|
90
|
+
`src/index.js` / `src/client/index.js` barrel'larını güncelle; belgelerde
|
|
91
|
+
yalnızca `exports` haritasındaki belirteçler kullanılır (`jskelet`,
|
|
92
|
+
`jskelet/client`, `jskelet/html`, `jskelet/tags`).
|
|
93
|
+
|
|
94
|
+
## EJS tuzakları
|
|
95
|
+
|
|
96
|
+
- `include` **async**'tir: `await include('partials/x')` yalnızca şablonun kendi
|
|
97
|
+
gövdesinde çalışır. Bir `forEach` callback'i içinde derleme hatası verir —
|
|
98
|
+
`for` döngüsü kullan.
|
|
99
|
+
- `views/components/**` altındaki her named export otomatik olarak şablon local'i
|
|
100
|
+
olur; import gerekmez. Bileşenler EJS değil, HTML string döndüren
|
|
101
|
+
fonksiyonlardır.
|
|
102
|
+
- Şablona giden her kullanıcı verisi `<%= %>` ile ya da `esc()` üzerinden
|
|
103
|
+
geçmeli; `<%- %>` yalnızca güvenli bildiğin HTML için.
|
|
104
|
+
|
|
105
|
+
## Tailwind
|
|
106
|
+
|
|
107
|
+
Sınıf taraması `styles/globals.css` içindeki `@source` direktiflerine bağlıdır
|
|
108
|
+
ve otomatik tespit `source(none)` ile kapatılmıştır. Sınıf kullanmaya başladığın
|
|
109
|
+
yeni bir dizin varsa oraya bir `@source` satırı eklemek gerekir; yoksa sınıflar
|
|
110
|
+
sessizce çıktıdan düşer.
|
|
111
|
+
|
|
112
|
+
## Örnekleri güncel tut
|
|
113
|
+
|
|
114
|
+
Framework'ün genel yüzeyini değiştirdiysen (`route()` imzası, hook adları,
|
|
115
|
+
config alanları, client API'si) `examples/minimal`, `examples/blog` ve
|
|
116
|
+
`examples/marketing`'i de güncelle. Örnekler belgelerdeki kod parçalarının kaynağı; kaymaları en hızlı
|
|
117
|
+
fark edilen yer orası.
|
|
118
|
+
|
|
119
|
+
## Windows notları
|
|
120
|
+
|
|
121
|
+
Bu depo Windows üzerinde geliştiriliyor ve iki tuzak tekrar tekrar çıkıyor:
|
|
122
|
+
|
|
123
|
+
- `--import` argümanı modül belirteci bekler. `H:\...` gibi mutlak bir yol `h:`
|
|
124
|
+
şemalı URL sanılıp reddedilir; `pathToFileURL(...).href` kullan.
|
|
125
|
+
- `fs.watch` bir dosya yazıldığında komşuları için de olay üretebiliyor. Dev
|
|
126
|
+
sunucusu bu yüzden olayları `mtime` karşılaştırmasıyla eler; watcher mantığını
|
|
127
|
+
değiştirirken bu elemeyi kaldırma.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
While the project is on `0.x`, minor releases may contain breaking changes; each
|
|
7
|
+
one is listed under a **Breaking** heading.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
|
|
14
|
+
a `LICENSE` file, issue and pull request templates, and a CI workflow.
|
|
15
|
+
|
|
16
|
+
## [0.1.0]
|
|
17
|
+
|
|
18
|
+
Initial release.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Express 5 server with EJS rendering: `createApp()`, `startServer()`,
|
|
23
|
+
`route()`, `renderPage()`, `renderView()`, `renderNotFound()`.
|
|
24
|
+
- In-process HTML TTL cache with stale-while-revalidate, plus prewarm at boot.
|
|
25
|
+
- Island runtime with visibility, eager and idle hydration strategies, a small
|
|
26
|
+
cross-island store, and DOM helpers.
|
|
27
|
+
- Configuration through `jskelet.config.mjs`: `brand`, `paths`, `navigation`,
|
|
28
|
+
`icons`, `fonts`, `clientEnv`, `redirects()`, `rewrites()`, `headers()`,
|
|
29
|
+
`cache()` and `hooks`.
|
|
30
|
+
- Build pipeline: fonts, SVG sprite from used icons, Tailwind v4 CSS, esbuild
|
|
31
|
+
bundles with code splitting, webp variants, hashed output and brotli/gzip
|
|
32
|
+
precompression.
|
|
33
|
+
- Dev server with watch build, CSS hot-swap, automatic restart and a devtools
|
|
34
|
+
overlay (requests, errors, upstream calls, cache dump, Web Vitals).
|
|
35
|
+
- CLI: `jskelet dev`, `jskelet build`, `jskelet start`, `jskelet init`.
|
|
36
|
+
- Documentation under `docs/` and three examples: `minimal`, `blog`,
|
|
37
|
+
`marketing`.
|
|
38
|
+
|
|
39
|
+
[Unreleased]: https://github.com/ayberkenis/jskelet/compare/v0.1.0...HEAD
|
|
40
|
+
[0.1.0]: https://github.com/ayberkenis/jskelet/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayberk Enis and JSkelet contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# JSkelet
|
|
2
|
+
|
|
3
|
+
**A framework that feels like no framework** — for sites where SEO and speed are
|
|
4
|
+
the product.
|
|
5
|
+
|
|
6
|
+
JSkelet renders **complete HTML** on an Express 5 server with EJS, adds
|
|
7
|
+
interactivity through vanilla JS **islands**, compiles CSS into a **single
|
|
8
|
+
Tailwind v4 stylesheet**, and instead of ISR keeps an in-process **HTML TTL
|
|
9
|
+
cache** with stale-while-revalidate. No React, no TypeScript — plain JavaScript
|
|
10
|
+
with JSDoc.
|
|
11
|
+
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](./LICENSE)
|
|
14
|
+
|
|
15
|
+
- [Quick start](#quick-start)
|
|
16
|
+
- [What it looks like](#what-it-looks-like)
|
|
17
|
+
- [How it works](#how-it-works)
|
|
18
|
+
- [What you get](#what-you-get)
|
|
19
|
+
- [What it deliberately does not do](#what-it-deliberately-does-not-do)
|
|
20
|
+
- [Project layout](#project-layout)
|
|
21
|
+
- [Configuration](#configuration)
|
|
22
|
+
- [CLI](#cli)
|
|
23
|
+
- [Public API](#public-api)
|
|
24
|
+
- [Deployment](#deployment)
|
|
25
|
+
- [Documentation](#documentation)
|
|
26
|
+
- [Examples](#examples)
|
|
27
|
+
- [Contributing](#contributing)
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
mkdir my-site && cd my-site
|
|
33
|
+
npm init -y && npm pkg set type=module
|
|
34
|
+
npm install jskelet
|
|
35
|
+
npm install -D postcss @tailwindcss/postcss tailwindcss lightningcss
|
|
36
|
+
npx jskelet init
|
|
37
|
+
npx jskelet dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`http://localhost:3000` serves a page that was rendered on the server, stored in
|
|
41
|
+
the HTML cache, and whose island hydrates when it scrolls into view.
|
|
42
|
+
|
|
43
|
+
Requirements:
|
|
44
|
+
|
|
45
|
+
- **Node.js 22 or newer.**
|
|
46
|
+
- Everything else is an **optional peer dependency**: `postcss`,
|
|
47
|
+
`@tailwindcss/postcss`, `tailwindcss` and `lightningcss` for styles,
|
|
48
|
+
`@phosphor-icons/core` for the icon sprite, `sharp` for image optimization. If
|
|
49
|
+
a package is missing, the matching build step is skipped with a warning and
|
|
50
|
+
the site keeps working.
|
|
51
|
+
|
|
52
|
+
## What it looks like
|
|
53
|
+
|
|
54
|
+
A route module receives the app and returns page descriptions. Nothing is
|
|
55
|
+
inferred from the file system — URLs are written out.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// routes/10-pages.mjs
|
|
59
|
+
import { getPost, getPosts } from "../lib/posts.js";
|
|
60
|
+
|
|
61
|
+
export default function register(app, { route, notFound }) {
|
|
62
|
+
app.get("/", route(
|
|
63
|
+
async () => ({
|
|
64
|
+
view: "pages/home",
|
|
65
|
+
metadata: { title: "Home", canonical: "/" },
|
|
66
|
+
data: { posts: getPosts() },
|
|
67
|
+
}),
|
|
68
|
+
{ revalidate: 60 }, // keep this HTML for 60 seconds
|
|
69
|
+
));
|
|
70
|
+
|
|
71
|
+
app.get("/blog/:slug", route(async ({ params }) => {
|
|
72
|
+
const post = getPost(params.slug);
|
|
73
|
+
if (!post) notFound();
|
|
74
|
+
return { view: "pages/blog-post", data: { post } };
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Templates are EJS. Every named export under `views/components/**` becomes a
|
|
80
|
+
template local automatically, so components need no imports — they are plain
|
|
81
|
+
functions returning HTML strings.
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<!-- views/pages/home.ejs -->
|
|
85
|
+
<section class="wrapper">
|
|
86
|
+
<h1 class="text-3xl font-bold">Latest posts</h1>
|
|
87
|
+
<% posts.forEach(function (post) { %>
|
|
88
|
+
<%- postCard({ post }) %>
|
|
89
|
+
<% }); %>
|
|
90
|
+
|
|
91
|
+
<!-- downloaded and wired up once visible -->
|
|
92
|
+
<div data-island="newsletter"></div>
|
|
93
|
+
</section>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Islands are modules with a default export that receives their root element.
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
// client/islands/newsletter.js
|
|
100
|
+
export default function newsletter(el) {
|
|
101
|
+
const form = el.querySelector("form");
|
|
102
|
+
form.addEventListener("submit", async (event) => {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
await fetch("/api/subscribe", { method: "POST", body: new FormData(form) });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## How it works
|
|
110
|
+
|
|
111
|
+
A request goes through a fixed middleware order that is documented in the
|
|
112
|
+
numbered comment at the top of `src/server/create-app.js`; changing that order
|
|
113
|
+
causes silent breakage.
|
|
114
|
+
|
|
115
|
+
1. **Config** (`jskelet.config.mjs`) is loaded once and exposed through
|
|
116
|
+
`getConfig()`. `redirects()`, `rewrites()`, `headers()` and `cache()` follow
|
|
117
|
+
the subset of `next.config` syntax people actually use. A broken config, a
|
|
118
|
+
throwing `headers()` or a failing hook logs a warning and falls back to
|
|
119
|
+
defaults — it never takes the site down.
|
|
120
|
+
2. **Static assets** are served from `public/` with hashed filenames and
|
|
121
|
+
long-lived cache headers, and precompressed `.br` / `.gz` variants are picked
|
|
122
|
+
automatically.
|
|
123
|
+
3. **`route()`** wraps your controller. It builds a cache key, checks the HTML
|
|
124
|
+
TTL cache, and on a miss renders the page. When a cached entry is stale it is
|
|
125
|
+
returned immediately while revalidation runs in the background.
|
|
126
|
+
4. **Render** composes metadata, layout context and your view into one HTML
|
|
127
|
+
document. Hooks (`metadata`, `layoutContext`, `notFound`, `prewarmPaths`)
|
|
128
|
+
are where application knowledge lives — the framework itself carries none.
|
|
129
|
+
5. **Hydration** happens in the browser: the island registry finds
|
|
130
|
+
`data-island` elements and dynamically imports the matching chunk when it
|
|
131
|
+
becomes visible (or eagerly / on idle, if asked).
|
|
132
|
+
|
|
133
|
+
Because cached HTML is shared by every visitor, nothing personalized may appear
|
|
134
|
+
in a page rendered through `route()`. Per-user markup belongs in separate
|
|
135
|
+
fragment endpoints marked `no-store`, and decisions like theme are made on the
|
|
136
|
+
client.
|
|
137
|
+
|
|
138
|
+
## What you get
|
|
139
|
+
|
|
140
|
+
- **Full HTML from the server.** First paint does not wait for JavaScript, and
|
|
141
|
+
crawlers see the complete document because content is never assembled in the
|
|
142
|
+
browser.
|
|
143
|
+
- **Islands.** Interactivity attaches to elements carrying `data-island`.
|
|
144
|
+
Modules are dynamically imported on visibility by default;
|
|
145
|
+
`data-island-eager` and `data-island-idle` pick a different strategy. A small
|
|
146
|
+
store handles sharing state between islands.
|
|
147
|
+
- **HTML TTL cache.** Per-route `revalidate`, stale-while-revalidate on expiry,
|
|
148
|
+
and prewarm that fills the cache at boot so the first visitor is not the one
|
|
149
|
+
who pays for rendering.
|
|
150
|
+
- **Fast navigation.** The `navigation` config section emits Speculation Rules
|
|
151
|
+
to prefetch or prerender links and enables view transitions — without adding
|
|
152
|
+
any client runtime.
|
|
153
|
+
- **Familiar configuration.** `redirects()`, `rewrites()`, `headers()`,
|
|
154
|
+
`cache()`, plus `brand`, `images`, `security` and `hooks` sections.
|
|
155
|
+
- **A real build pipeline.** Fonts, an SVG sprite generated from the icons you
|
|
156
|
+
actually use, Tailwind v4 CSS, esbuild bundles with code splitting, webp
|
|
157
|
+
variants, hashed output and brotli/gzip precompression.
|
|
158
|
+
- **Developer experience.** One command, one terminal: watch build plus server,
|
|
159
|
+
CSS hot-swap, automatic restart, and a devtools overlay on Alt+D showing
|
|
160
|
+
requests, errors, upstream calls, a cache dump and Web Vitals.
|
|
161
|
+
- **Graceful degradation.** Without build output `asset()` returns the unhashed
|
|
162
|
+
path and `hasAsset()` returns false, so forgetting `jskelet build` yields an
|
|
163
|
+
unstyled but working page instead of a crash.
|
|
164
|
+
|
|
165
|
+
## What it deliberately does not do
|
|
166
|
+
|
|
167
|
+
- **No file-system routing.** Paths are written explicitly in route modules.
|
|
168
|
+
- **No streaming or RSC.** A page is flushed as one document; slow sections are
|
|
169
|
+
fetched from separate fragment endpoints.
|
|
170
|
+
- **No targeted cache invalidation.** There is TTL and there is "clear
|
|
171
|
+
everything".
|
|
172
|
+
- **No global state management** beyond the small island store.
|
|
173
|
+
|
|
174
|
+
If you are building an app-shaped interface behind a login — a dashboard, an
|
|
175
|
+
editor, an admin panel — page HTML cannot be cached and this framework is the
|
|
176
|
+
wrong tool. The reasoning and a feature-by-feature comparison with Next.js live
|
|
177
|
+
in [docs/11-tasima.md](./docs/11-tasima.md).
|
|
178
|
+
|
|
179
|
+
## Project layout
|
|
180
|
+
|
|
181
|
+
`jskelet init` scaffolds this shape, and every directory is configurable through
|
|
182
|
+
the `paths` section of the config:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
my-site/
|
|
186
|
+
├── jskelet.config.mjs # config, hooks, headers, redirects
|
|
187
|
+
├── routes/ # loaded in filename order (10-, 20-, …)
|
|
188
|
+
├── views/
|
|
189
|
+
│ ├── pages/ # EJS pages
|
|
190
|
+
│ ├── partials/ # header, footer, …
|
|
191
|
+
│ └── components/ # named exports become template locals
|
|
192
|
+
├── client/
|
|
193
|
+
│ ├── entries/main.js # registers islands, calls start()
|
|
194
|
+
│ └── islands/ # one module per island
|
|
195
|
+
├── styles/globals.css # Tailwind entry with @source directives
|
|
196
|
+
├── lib/ # your data access
|
|
197
|
+
└── public/ # build output plus static files
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Two things bite newcomers:
|
|
201
|
+
|
|
202
|
+
- **Tailwind class scanning follows `@source` directives** in
|
|
203
|
+
`styles/globals.css`, because automatic detection is turned off with
|
|
204
|
+
`source(none)`. A new directory that uses classes needs an `@source` line, or
|
|
205
|
+
its classes silently vanish from the stylesheet.
|
|
206
|
+
- **`include` in EJS is async.** `await include('partials/x')` only works in a
|
|
207
|
+
template's own body; inside a `forEach` callback it is a compile error, so use
|
|
208
|
+
a `for` loop there.
|
|
209
|
+
|
|
210
|
+
## Configuration
|
|
211
|
+
|
|
212
|
+
`jskelet.config.mjs` exports a single object. Every section is optional.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
export default {
|
|
216
|
+
brand: { name: "My Site", lang: "en" },
|
|
217
|
+
icons: { scan: ["views", "client"] },
|
|
218
|
+
|
|
219
|
+
// Speculation Rules plus @view-transition, with no client runtime.
|
|
220
|
+
navigation: { prefetch: "moderate", prerender: "conservative", viewTransition: true },
|
|
221
|
+
|
|
222
|
+
async redirects() {
|
|
223
|
+
return [{ source: "/old", destination: "/new", permanent: true }];
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async headers() {
|
|
227
|
+
return [{ source: "/:path*", headers: [{ key: "X-Frame-Options", value: "DENY" }] }];
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async cache() {
|
|
231
|
+
return {
|
|
232
|
+
html: { "/": 3600, "/pricing": 3600 },
|
|
233
|
+
prewarm: { enabled: true, max: 50, concurrency: 4 },
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
hooks: {
|
|
238
|
+
metadata: () => ({ titleTemplate: "%s · My Site", siteUrl: "https://example.com" }),
|
|
239
|
+
layoutContext: ({ pathname }) => ({ pathname, year: new Date().getFullYear() }),
|
|
240
|
+
prewarmPaths: async () => ["/", "/pricing"],
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The complete reference — every field, default and failure mode — is
|
|
246
|
+
[docs/07-yapilandirma.md](./docs/07-yapilandirma.md).
|
|
247
|
+
|
|
248
|
+
## CLI
|
|
249
|
+
|
|
250
|
+
| Command | What it does |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `jskelet dev` | Watch build plus server, live reload, devtools overlay |
|
|
253
|
+
| `jskelet build` | Production build: fonts → sprite → CSS → JS → images → manifest → precompress |
|
|
254
|
+
| `jskelet start` | Production server; builds first if output is missing |
|
|
255
|
+
| `jskelet init` | Scaffolds a minimal skeleton into the current directory |
|
|
256
|
+
|
|
257
|
+
## Public API
|
|
258
|
+
|
|
259
|
+
Only the specifiers in the `exports` map are supported:
|
|
260
|
+
|
|
261
|
+
| Specifier | Contents |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `jskelet` | `route`, `createApp`, `startServer`, `notFound`, `redirect`, `cache`, `asset`, `getConfig`, HTML cache and prewarm helpers |
|
|
264
|
+
| `jskelet/client` | `register`, `registerAll`, `hydrate`, `start`, `createStore`, DOM helpers |
|
|
265
|
+
| `jskelet/html` | `attrs`, `cn`, `cx`, `esc`, `jsonScript` |
|
|
266
|
+
| `jskelet/tags` | `icon`, `image`, `link`, `preloadImage` |
|
|
267
|
+
|
|
268
|
+
Anything reachable by a deeper path is internal and may change without notice.
|
|
269
|
+
|
|
270
|
+
## Deployment
|
|
271
|
+
|
|
272
|
+
The server is a plain Express 5 app, so anything that can run a Node process
|
|
273
|
+
works: a `Dockerfile` (see `examples/marketing/Dockerfile`), a systemd unit, or
|
|
274
|
+
a PaaS. Run `jskelet build` at image build time, put a reverse proxy in front
|
|
275
|
+
for TLS, and expose a health endpoint (the default dev gate bypass list already
|
|
276
|
+
includes `/api/healthcheck`, so a route there is reachable in every mode).
|
|
277
|
+
Details, including cache sizing
|
|
278
|
+
behind multiple instances, are in [docs/10-dagitim.md](./docs/10-dagitim.md).
|
|
279
|
+
|
|
280
|
+
## Documentation
|
|
281
|
+
|
|
282
|
+
The full reference lives under [docs/](./docs/README.md). It is currently
|
|
283
|
+
written in Turkish; translations are a welcome contribution.
|
|
284
|
+
|
|
285
|
+
| Document | Topic |
|
|
286
|
+
| --- | --- |
|
|
287
|
+
| [01-baslangic](./docs/01-baslangic.md) | Installation, first route, first island, directory layout, CLI |
|
|
288
|
+
| [02-mimari](./docs/02-mimari.md) | Decisions and their reasoning, middleware order |
|
|
289
|
+
| [03-routing](./docs/03-routing.md) | Route modules, controller contract, load order |
|
|
290
|
+
| [04-render-ve-sablonlar](./docs/04-render-ve-sablonlar.md) | Layout, components, helpers, metadata |
|
|
291
|
+
| [05-islands](./docs/05-islands.md) | Island contract, hydration, store, DOM helpers |
|
|
292
|
+
| [06-cache](./docs/06-cache.md) | TTL, stale-while-revalidate, keys, prewarm |
|
|
293
|
+
| [07-yapilandirma](./docs/07-yapilandirma.md) | Complete `jskelet.config.mjs` reference |
|
|
294
|
+
| [08-build](./docs/08-build.md) | Build pipeline, manifest, Tailwind `@source`, sprite |
|
|
295
|
+
| [09-dev-araclari](./docs/09-dev-araclari.md) | Dev workflow, overlay, report page, dev gate |
|
|
296
|
+
| [10-dagitim](./docs/10-dagitim.md) | Production, Docker, reverse proxy, health checks |
|
|
297
|
+
| [11-tasima](./docs/11-tasima.md) | Migrating from Next.js: mapping table and plan |
|
|
298
|
+
|
|
299
|
+
If you work with AI agents, [AGENTS.md](./AGENTS.md) summarizes the rules that
|
|
300
|
+
apply to this repository.
|
|
301
|
+
|
|
302
|
+
## Examples
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
npm --prefix examples/minimal install && npm --prefix examples/minimal run dev
|
|
306
|
+
npm --prefix examples/blog install && npm --prefix examples/blog run dev
|
|
307
|
+
npm --prefix examples/marketing install && npm --prefix examples/marketing run dev
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
- **`examples/minimal`** — two routes, one component, one island. The smallest
|
|
311
|
+
thing that runs.
|
|
312
|
+
- **`examples/blog`** — dynamic routes, tag pages, every config section,
|
|
313
|
+
fragment-loaded tabs, a form, prewarm, RSS and sitemap, four islands. It
|
|
314
|
+
intentionally touches every surface of the framework.
|
|
315
|
+
- **`examples/marketing`** — the framework's own marketing site: comparison
|
|
316
|
+
table, changelog and download pages, long TTLs, prewarm covering every page.
|
|
317
|
+
The byte counts on the page are measured from that site's own build output, the
|
|
318
|
+
version details are read from the installed package, and the latency numbers
|
|
319
|
+
are measured in the browser; there are no invented benchmarks. It is also
|
|
320
|
+
bilingual — English at the root, Turkish under `/tr` — which shows how to build
|
|
321
|
+
a multi-language site on a framework that ships no i18n of its own.
|
|
322
|
+
|
|
323
|
+
With a server running, `node smoke.mjs` inside an example verifies that its
|
|
324
|
+
endpoints respond as expected.
|
|
325
|
+
|
|
326
|
+
## Contributing
|
|
327
|
+
|
|
328
|
+
Bug reports, documentation fixes and pull requests are welcome. Start with
|
|
329
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) for the workflow and local checks, and note
|
|
330
|
+
that participation is covered by our
|
|
331
|
+
[Code of Conduct](./CODE_OF_CONDUCT.md). Security issues should follow
|
|
332
|
+
[SECURITY.md](./SECURITY.md) instead of the public issue tracker.
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
npm install
|
|
336
|
+
npm run lint
|
|
337
|
+
npm test
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
[MIT](./LICENSE) © JSkelet contributors
|
package/bin/jskelet.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* JSkelet CLI.
|
|
4
|
+
*
|
|
5
|
+
* jskelet dev build watch + sunucu, canlı yenileme, dev overlay
|
|
6
|
+
* jskelet build tek seferlik prod build (fontlar, sprite, CSS, JS, görseller)
|
|
7
|
+
* jskelet start prod sunucu (build eksikse önce üretir)
|
|
8
|
+
* jskelet init bulunduğun dizine minimal iskelet kurar
|
|
9
|
+
*
|
|
10
|
+
* Alt komutlar ayrı süreçlerde çalışır. Sebep: `dev` iki uzun ömürlü süreci
|
|
11
|
+
* (build watch + sunucu) yönetiyor ve sunucunun ESM resolve hook'larına
|
|
12
|
+
* (`--import`) ihtiyacı var; bunlar süreç başlarken kurulmak zorunda.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import process from "node:process";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
const SRC = path.resolve(fileURLToPath(import.meta.url), "..", "..", "src");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `--import` bir modül **belirteci** bekler, dosya yolu değil. Windows'ta
|
|
24
|
+
* `H:\...` mutlak yolu `h:` şemalı bir URL sanılıp reddediliyor; file:// URL'e
|
|
25
|
+
* çevirmek her platformda doğru.
|
|
26
|
+
*/
|
|
27
|
+
const REGISTER = pathToFileURL(path.join(SRC, "runtime", "register.mjs")).href;
|
|
28
|
+
|
|
29
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `.env` yalnızca varsa geçilir: `--env-file-if-exists` dosya yokken de bir
|
|
33
|
+
* bildirim satırı basıyor ve bu satır dev çıktısında hata gibi görünüyor.
|
|
34
|
+
* `--import` alias hook'larını kurar; uygulama kodu `@/…` yazabilsin diye.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} file
|
|
37
|
+
* @param {{ env?: Record<string, string>, args?: string[], hooks?: boolean }} [options]
|
|
38
|
+
* @returns {import('node:child_process').ChildProcess}
|
|
39
|
+
*/
|
|
40
|
+
function run(file, options = {}) {
|
|
41
|
+
const args = [
|
|
42
|
+
...(fs.existsSync(path.join(process.cwd(), ".env"))
|
|
43
|
+
? ["--env-file=.env"]
|
|
44
|
+
: []),
|
|
45
|
+
...(options.hooks === false ? [] : ["--import", REGISTER]),
|
|
46
|
+
file,
|
|
47
|
+
...(options.args ?? []),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return spawn(process.execPath, args, {
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: { ...process.env, ...(options.env ?? {}) },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {import('node:child_process').ChildProcess} child */
|
|
57
|
+
function exitWith(child) {
|
|
58
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
switch (command) {
|
|
62
|
+
case "dev": {
|
|
63
|
+
exitWith(run(path.join(SRC, "dev-server.mjs"), { hooks: false, args: rest }));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case "build": {
|
|
68
|
+
exitWith(
|
|
69
|
+
run(path.join(SRC, "build", "build.mjs"), {
|
|
70
|
+
env: { NODE_ENV: process.env.NODE_ENV ?? "production" },
|
|
71
|
+
args: rest,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "start": {
|
|
78
|
+
exitWith(
|
|
79
|
+
run(path.join(SRC, "start.mjs"), {
|
|
80
|
+
env: { NODE_ENV: process.env.NODE_ENV ?? "production" },
|
|
81
|
+
args: rest,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case "init": {
|
|
88
|
+
const { init } = await import("../src/init.mjs");
|
|
89
|
+
await init(process.cwd());
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
default: {
|
|
94
|
+
const known = command ? `bilinmeyen komut: ${command}\n\n` : "";
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`${known}kullanım: jskelet <dev|build|start|init>\n\n` +
|
|
97
|
+
" dev build watch + sunucu (canlı yenileme, dev overlay)\n" +
|
|
98
|
+
" build prod build\n" +
|
|
99
|
+
" start prod sunucu\n" +
|
|
100
|
+
" init bulunduğun dizine minimal iskelet kurar\n",
|
|
101
|
+
);
|
|
102
|
+
process.exit(command ? 1 : 0);
|
|
103
|
+
}
|
|
104
|
+
}
|