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.
Files changed (72) hide show
  1. package/AGENTS.md +127 -0
  2. package/CHANGELOG.md +40 -0
  3. package/LICENSE +21 -0
  4. package/README.md +342 -0
  5. package/bin/jskelet.mjs +104 -0
  6. package/docs/01-baslangic.md +285 -0
  7. package/docs/02-mimari.md +287 -0
  8. package/docs/03-routing.md +437 -0
  9. package/docs/04-render-ve-sablonlar.md +490 -0
  10. package/docs/05-islands.md +429 -0
  11. package/docs/06-cache.md +409 -0
  12. package/docs/07-yapilandirma.md +673 -0
  13. package/docs/08-build.md +366 -0
  14. package/docs/09-dev-araclari.md +302 -0
  15. package/docs/10-dagitim.md +329 -0
  16. package/docs/11-tasima.md +352 -0
  17. package/docs/README.md +82 -0
  18. package/package.json +97 -0
  19. package/src/build/build.mjs +138 -0
  20. package/src/build/ensure-build.mjs +15 -0
  21. package/src/build/paths.mjs +118 -0
  22. package/src/build/resolve-peer.mjs +36 -0
  23. package/src/build/tasks/client.mjs +268 -0
  24. package/src/build/tasks/css.mjs +124 -0
  25. package/src/build/tasks/fonts.mjs +146 -0
  26. package/src/build/tasks/icons.mjs +224 -0
  27. package/src/build/tasks/images.mjs +244 -0
  28. package/src/build/tasks/precompress.mjs +78 -0
  29. package/src/client/devtools/overlay.js +1763 -0
  30. package/src/client/devtools/report.html +185 -0
  31. package/src/client/devtools/report.js +712 -0
  32. package/src/client/dom.js +95 -0
  33. package/src/client/index.js +26 -0
  34. package/src/client/registry.js +223 -0
  35. package/src/client/safe-image.js +91 -0
  36. package/src/client/store.js +36 -0
  37. package/src/config/defaults.js +102 -0
  38. package/src/config/index.js +433 -0
  39. package/src/config/pattern.js +107 -0
  40. package/src/dev-server.mjs +383 -0
  41. package/src/http/control-flow.js +56 -0
  42. package/src/http/request-cache.js +46 -0
  43. package/src/index.js +35 -0
  44. package/src/init.mjs +220 -0
  45. package/src/log.mjs +332 -0
  46. package/src/logo.png +0 -0
  47. package/src/runtime/alias-hooks.mjs +119 -0
  48. package/src/runtime/register.mjs +4 -0
  49. package/src/server/assets.js +119 -0
  50. package/src/server/create-app.js +167 -0
  51. package/src/server/dev/devtools.js +383 -0
  52. package/src/server/dev/report.js +351 -0
  53. package/src/server/head-hints.js +132 -0
  54. package/src/server/html-cache.js +166 -0
  55. package/src/server/metadata.js +102 -0
  56. package/src/server/middleware/compression.js +205 -0
  57. package/src/server/middleware/dev-gate.js +62 -0
  58. package/src/server/middleware/headers.js +37 -0
  59. package/src/server/middleware/redirects.js +32 -0
  60. package/src/server/middleware/static-precompressed.js +100 -0
  61. package/src/server/middleware/upstream-proxy.js +141 -0
  62. package/src/server/prewarm.js +283 -0
  63. package/src/server/render.js +356 -0
  64. package/src/server/router.js +121 -0
  65. package/src/server/status-page.js +164 -0
  66. package/src/server/upstream-tracking.js +51 -0
  67. package/src/start.mjs +7 -0
  68. package/src/templates/layout.ejs +44 -0
  69. package/src/version.mjs +17 -0
  70. package/src/views/components/loader.js +85 -0
  71. package/src/views/helpers/html.js +102 -0
  72. 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
+ [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
13
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./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
@@ -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
+ }