jskelet 0.4.1 → 0.4.3

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
@@ -24,9 +24,19 @@ one is listed under a **Breaking** heading.
24
24
  query time and a default 24h lookback became `1d` plus network delay — Free
25
25
  zones reject anything wider than one day. Both ends are now pinned from the
26
26
  same clock (`datetime_leq` included).
27
+ - Missing `/assets/*` responses no longer keep the long-lived `immutable`
28
+ Cache-Control that `headersMiddleware` stamps for static prefixes. A deploy
29
+ race (prune-before-write) could 404 a hashed CSS URL for a moment; a CDN then
30
+ cached that HTML 404 for a year and browsers refused it as a stylesheet
31
+ (`MIME type 'text/html'`). Catch-all and `notFound` handlers now set
32
+ `Cache-Control: no-store`. CSS and sprite builds write the new file before
33
+ pruning older hashes so the same content hash never has a gap.
27
34
 
28
35
  ### Added
29
36
 
37
+ - Marketing homepage ops storyboard: Redis L2, `/_jskelet/admin` panel mock and
38
+ Cloudflare purge flow, with tabbed visual scenes animated by the vanilla
39
+ `motion` API (Framer Motion’s non-React package) via an `ops-story` island.
30
40
  - VS Code / Cursor extension skeleton under `extensions/vscode-jsk`: `.jsk`
31
41
  language id, TextMate highlighting (`{{ }}` / `{#if}` / `{#each}` /
32
42
  components), language config, and snippets. Install from that folder or
@@ -47,7 +57,8 @@ one is listed under a **Breaking** heading.
47
57
  - Feature-first conventions: `paths.features` / `paths.shared`, multi-root
48
58
  views and components, `features/<name>/index.js` route registration after
49
59
  `routes/`. CLI: `jskelet generate feature|page|island`. `jskelet init`
50
- scaffolds `.jsk` pages.
60
+ scaffolds a feature-first `.jsk` skeleton (`features/home/` with route,
61
+ page, component and island; global `views/pages/not-found.jsk`).
51
62
  - Template compile step in `jskelet build`; icon scan and Tailwind docs cover
52
63
  `.jsk` / `features` / `shared`. Bench: `node scripts/bench-templates.mjs`.
53
64
  - Top-level `logs` config for persistent sinks: daily NDJSON files
@@ -138,6 +149,19 @@ one is listed under a **Breaking** heading.
138
149
 
139
150
  ### Changed
140
151
 
152
+ - README rewritten for the current surface: build-time `.jsk` as the default
153
+ template story (EJS still supported), feature-first `init` examples, `mount`
154
+ island contract, path-based `invalidateHtmlCache` (replacing the outdated
155
+ “no targeted invalidation” claim), Redis / admin / data-cache callouts, and
156
+ bilingual doc links under `docs/` and `docs/en/`.
157
+ - Marketing compare live latency demo now measures two same-sized fragments
158
+ (cached vs `no-store`) with an explicit 80 ms simulated upstream inside the
159
+ shared producer — a hit skips that wait so the gap is visible even when RTT
160
+ dominates the wall clock. The island prints transferred bytes, Server-Timing
161
+ `produce` duration, and a View Source section contrasts `__NEXT_DATA__`
162
+ payload tax with plain JSkelet HTML. The measured-weight block also shows an
163
+ estimated Next.js App Router first-load breakdown beside this site’s real
164
+ gzip totals (clearly labelled estimate, not a build from this repo).
141
165
  - Duplicate component named exports (or the same PascalCase tag in two files)
142
166
  now **fail** at build and at server startup instead of warning and letting
143
167
  the second definition win. Overwriting `components/index.js` barrel exports
package/README.md CHANGED
@@ -3,11 +3,12 @@
3
3
  **A framework that feels like no framework** — for sites where SEO and speed are
4
4
  the product.
5
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.
6
+ JSkelet renders **complete HTML** on an Express 5 server from build-time
7
+ **`.jsk` templates** (EJS still works), adds interactivity through vanilla JS
8
+ **islands**, compiles CSS into a **single Tailwind v4 stylesheet**, and instead
9
+ of ISR keeps an in-process **HTML TTL cache** with stale-while-revalidate — plus
10
+ optional Redis sharing and path-based invalidation. No React, no TypeScript —
11
+ plain JavaScript with JSDoc.
11
12
 
12
13
  [![npm version](https://img.shields.io/npm/v/jskelet)](https://www.npmjs.com/package/jskelet)
13
14
  [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
@@ -46,19 +47,17 @@ Requirements:
46
47
  - **Node.js 22 or newer.**
47
48
  - Everything else is an **optional peer dependency**: `postcss`,
48
49
  `@tailwindcss/postcss`, `tailwindcss` and `lightningcss` for styles,
49
- `@phosphor-icons/core` for the icon sprite, `sharp` for image optimization. If
50
- a package is missing, the matching build step is skipped with a warning and
51
- the site keeps working.
50
+ `@phosphor-icons/core` for the icon sprite, `sharp` for image optimization,
51
+ `ioredis` for the shared Redis cache tier. If a package is missing, the
52
+ matching step is skipped with a warning and the site keeps working.
52
53
 
53
54
  ## What it looks like
54
55
 
55
- A route module receives the app and returns page descriptions. Nothing is
56
- inferred from the file system — URLs are written out.
56
+ A feature (or route) module receives the app and registers URLs explicitly.
57
+ Nothing is inferred from the file system.
57
58
 
58
59
  ```js
59
- // routes/10-pages.mjs
60
- import { getPost, getPosts } from "../lib/posts.js";
61
-
60
+ // features/home/index.js
62
61
  export default function register(app, { route, notFound }) {
63
62
  app.get("/", route(
64
63
  async () => ({
@@ -66,7 +65,7 @@ export default function register(app, { route, notFound }) {
66
65
  metadata: { title: "Home", canonical: "/" },
67
66
  data: { posts: getPosts() },
68
67
  }),
69
- { revalidate: 60 }, // keep this HTML for 60 seconds
68
+ { revalidate: 60 },
70
69
  ));
71
70
 
72
71
  app.get("/blog/:slug", route(async ({ params }) => {
@@ -77,28 +76,28 @@ export default function register(app, { route, notFound }) {
77
76
  }
78
77
  ```
79
78
 
80
- Templates are EJS. Every named export under `views/components/**` becomes a
81
- template local automatically, so components need no imports — they are plain
82
- functions returning HTML strings.
79
+ Templates are `.jsk`: compiled to ESM at build time (no request-time parse or
80
+ `eval`). Named exports under `views/components/**` become PascalCase tags —
81
+ plain functions that return HTML strings.
83
82
 
84
83
  ```html
85
- <!-- views/pages/home.ejs -->
84
+ <!-- features/home/views/pages/home.jsk -->
86
85
  <section class="wrapper">
87
86
  <h1 class="text-3xl font-bold">Latest posts</h1>
88
- <% posts.forEach(function (post) { %>
89
- <%- postCard({ post }) %>
90
- <% }); %>
87
+ {#each posts as post}
88
+ <PostCard :post="post" />
89
+ {/each}
91
90
 
92
- <!-- downloaded and wired up once visible -->
93
91
  <div data-island="newsletter"></div>
94
92
  </section>
95
93
  ```
96
94
 
97
- Islands are modules with a default export that receives their root element.
95
+ Islands export a named `mount(element, props)` and are registered from the
96
+ client entry as dynamic imports.
98
97
 
99
98
  ```js
100
- // client/islands/newsletter.js
101
- export default function newsletter(el) {
99
+ // features/home/client/newsletter.js
100
+ export function mount(el) {
102
101
  const form = el.querySelector("form");
103
102
  form.addEventListener("submit", async (event) => {
104
103
  event.preventDefault();
@@ -114,20 +113,22 @@ numbered comment at the top of `src/server/create-app.js`; changing that order
114
113
  causes silent breakage.
115
114
 
116
115
  1. **Config** (`jskelet.config.mjs`) is loaded once and exposed through
117
- `getConfig()`. `redirects()`, `rewrites()`, `headers()` and `cache()` follow
118
- the subset of `next.config` syntax people actually use. A broken config, a
119
- throwing `headers()` or a failing hook logs a warning and falls back to
120
- defaults — it never takes the site down.
121
- 2. **Static assets** are served from `public/` with hashed filenames and
122
- long-lived cache headers, and precompressed `.br` / `.gz` variants are picked
116
+ `getConfig()`. `redirects()`, `rewrites()`, `headers()`, `cache()` and
117
+ `admin()` follow the subset of `next.config` syntax people actually use. A
118
+ broken config, a throwing `headers()` or a failing hook logs a warning and
119
+ falls back to defaults — it never takes the site down.
120
+ 2. **Build** turns `.jsk` into modules under `.jskelet/templates/`, bundles
121
+ islands, compiles Tailwind, hashes assets and optionally precompresses them.
122
+ 3. **Static assets** are served from `public/` with hashed filenames and
123
+ long-lived cache headers; precompressed `.br` / `.gz` variants are picked
123
124
  automatically.
124
- 3. **`route()`** wraps your controller. It builds a cache key, checks the HTML
125
+ 4. **`route()`** wraps your controller. It builds a cache key, checks the HTML
125
126
  TTL cache, and on a miss renders the page. When a cached entry is stale it is
126
127
  returned immediately while revalidation runs in the background.
127
- 4. **Render** composes metadata, layout context and your view into one HTML
128
+ 5. **Render** composes metadata, layout context and your view into one HTML
128
129
  document. Hooks (`metadata`, `layoutContext`, `notFound`, `prewarmPaths`)
129
130
  are where application knowledge lives — the framework itself carries none.
130
- 5. **Hydration** happens in the browser: the island registry finds
131
+ 6. **Hydration** happens in the browser: the island registry finds
131
132
  `data-island` elements and dynamically imports the matching chunk when it
132
133
  becomes visible (or eagerly / on idle, if asked).
133
134
 
@@ -141,21 +142,35 @@ client.
141
142
  - **Full HTML from the server.** First paint does not wait for JavaScript, and
142
143
  crawlers see the complete document because content is never assembled in the
143
144
  browser.
145
+ - **Build-time `.jsk` templates.** Declarative HTML-like syntax compiled to ESM
146
+ before the server starts; EJS remains supported where both exist, `.jsk`
147
+ wins. A VS Code / Cursor extension under `extensions/vscode-jsk` covers
148
+ highlighting and snippets.
149
+ - **Feature-first layout.** `features/<name>/` co-locates routes, views,
150
+ components and islands; `jskelet generate feature|page|island` scaffolds the
151
+ next slice. URLs stay explicit.
144
152
  - **Islands.** Interactivity attaches to elements carrying `data-island`.
145
153
  Modules are dynamically imported on visibility by default;
146
154
  `data-island-eager` and `data-island-idle` pick a different strategy. A small
147
155
  store handles sharing state between islands.
148
156
  - **HTML TTL cache.** Per-route `revalidate`, stale-while-revalidate on expiry,
149
- and prewarm that fills the cache at boot so the first visitor is not the one
150
- who pays for rendering.
157
+ query allowlists, dependency tracking from `withDataCache`, and prewarm that
158
+ fills the cache at boot. `invalidateHtmlCache()` stales a path, pattern or
159
+ RegExp without flushing everything.
160
+ - **Optional Redis tier.** With `ioredis`, replicas share HTML/data and
161
+ broadcast invalidation over pub/sub so a webhook reaches every process.
162
+ - **Admin panel.** Opt-in at `/_jskelet/admin` (`admin()` or `JSKELET_ADMIN=1`):
163
+ cache inventory, targeted purge, Cloudflare CDN controls, live logs, routes
164
+ and system meters — password printed once per process start.
151
165
  - **Fast navigation.** The `navigation` config section emits Speculation Rules
152
166
  to prefetch or prerender links and enables view transitions — without adding
153
167
  any client runtime.
154
168
  - **Familiar configuration.** `redirects()`, `rewrites()`, `headers()`,
155
- `cache()`, plus `brand`, `images`, `security` and `hooks` sections.
156
- - **A real build pipeline.** Fonts, an SVG sprite generated from the icons you
157
- actually use, Tailwind v4 CSS, esbuild bundles with code splitting, webp
158
- variants, hashed output and brotli/gzip precompression.
169
+ `cache()`, `admin()`, plus `brand`, `images`, `security`, `logs`,
170
+ `trailingSlash` and `hooks`.
171
+ - **A real build pipeline.** Fonts, an SVG sprite from the icons you use,
172
+ Tailwind v4 CSS, esbuild bundles with code splitting, webp variants, hashed
173
+ output and brotli/gzip precompression.
159
174
  - **Developer experience.** One command, one terminal: watch build plus server,
160
175
  CSS hot-swap, automatic restart, and a devtools overlay on Alt+D showing
161
176
  requests, errors, upstream calls, a cache dump and Web Vitals.
@@ -168,8 +183,9 @@ client.
168
183
  - **No file-system routing.** Paths are written explicitly in route modules.
169
184
  - **No streaming or RSC.** A page is flushed as one document; slow sections are
170
185
  fetched from separate fragment endpoints.
171
- - **No targeted cache invalidation.** There is TTL and there is "clear
172
- everything".
186
+ - **No Next.js-style cache tags.** Invalidation is by path, pattern or RegExp
187
+ (`invalidateHtmlCache`), plus data-cache dependency tracking — not arbitrary
188
+ tag graphs.
173
189
  - **No global state management** beyond the small island store.
174
190
 
175
191
  An app-shaped interface behind a login — a dashboard, an editor, an admin panel
@@ -177,10 +193,12 @@ An app-shaped interface behind a login — a dashboard, an editor, an admin pane
177
193
  framework. It is supported rather than recommended: `route(fn, { private: true })`
178
194
  keeps per-visitor pages out of the cache, and signed cookies, CSRF, fragment
179
195
  endpoints and region swapping cover the rest
180
- ([docs/12-panel-ve-oturum.md](./docs/12-panel-ve-oturum.md)). Live data
181
- transport is deliberately left to you; pick SSE, WebSocket or polling yourself.
182
- A feature-by-feature comparison with Next.js is in
183
- [docs/11-tasima.md](./docs/11-tasima.md).
196
+ ([docs/12-panel-ve-oturum.md](./docs/12-panel-ve-oturum.md) /
197
+ [docs/en/12-dashboards-and-sessions.md](./docs/en/12-dashboards-and-sessions.md)).
198
+ Live data transport is deliberately left to you; pick SSE, WebSocket or polling
199
+ yourself. A feature-by-feature comparison with Next.js is in
200
+ [docs/11-tasima.md](./docs/11-tasima.md) /
201
+ [docs/en/11-migration.md](./docs/en/11-migration.md).
184
202
 
185
203
  ## Project layout
186
204
 
@@ -189,18 +207,20 @@ the `paths` section of the config:
189
207
 
190
208
  ```
191
209
  my-site/
192
- ├── jskelet.config.mjs # config, hooks, headers, redirects
193
- ├── routes/ # loaded in filename order (10-, 20-, …)
194
- ├── views/
195
- │ ├── pages/ # EJS pages
196
- │ ├── partials/ # header, footer, …
197
- │ └── components/ # named exports become template locals
198
- ├── client/
199
- │ ├── entries/main.js # registers islands, calls start()
200
- │ └── islands/ # one module per island
201
- ├── styles/globals.css # Tailwind entry with @source directives
202
- ├── lib/ # your data access
203
- └── public/ # build output plus static files
210
+ ├── jskelet.config.mjs # config, hooks, headers, redirects
211
+ ├── features/ # feature-first slices (optional but default in init)
212
+ │ └── home/
213
+ │ ├── index.js # register(app, api) — URLs stay explicit
214
+ │ ├── views/pages/ # .jsk pages for this feature
215
+ │ ├── views/components/
216
+ │ ├── client/ # islands; register from client/entries
217
+ │ └── server/
218
+ ├── routes/ # optional; loaded before features (10-, 20-, …)
219
+ ├── views/ # shared / app-wide pages (e.g. 404)
220
+ ├── shared/ # cross-feature server/views/client
221
+ ├── client/entries/main.js # registers islands, calls start()
222
+ ├── styles/globals.css # Tailwind entry with @source directives
223
+ └── public/ # build output plus static files
204
224
  ```
205
225
 
206
226
  Two things bite newcomers:
@@ -209,9 +229,8 @@ Two things bite newcomers:
209
229
  `styles/globals.css`, because automatic detection is turned off with
210
230
  `source(none)`. A new directory that uses classes needs an `@source` line, or
211
231
  its classes silently vanish from the stylesheet.
212
- - **`include` in EJS is async.** `await include('partials/x')` only works in a
213
- template's own body; inside a `forEach` callback it is a compile error, so use
214
- a `for` loop there.
232
+ - **`.jsk` expression language is intentionally narrow.** Formatting and object
233
+ literals belong in JS components (`views/components/**`), not in the template.
215
234
 
216
235
  ## Configuration
217
236
 
@@ -220,7 +239,7 @@ Two things bite newcomers:
220
239
  ```js
221
240
  export default {
222
241
  brand: { name: "My Site", lang: "en" },
223
- icons: { scan: ["views", "client"] },
242
+ icons: { scan: ["views", "features", "client"] },
224
243
 
225
244
  // Speculation Rules plus @view-transition, with no client runtime.
226
245
  navigation: { prefetch: "moderate", prerender: "conservative", viewTransition: true },
@@ -236,10 +255,17 @@ export default {
236
255
  async cache() {
237
256
  return {
238
257
  html: { "/": 3600, "/pricing": 3600 },
258
+ query: { "/search": ["q"] }, // only these params enter the cache key
239
259
  prewarm: { enabled: true, max: 50, concurrency: 4 },
260
+ // redis: { enabled: true, url: process.env.REDIS_URL },
240
261
  };
241
262
  },
242
263
 
264
+ // Opt-in production panel at /_jskelet/admin (password in the server log).
265
+ async admin() {
266
+ return { enabled: false };
267
+ },
268
+
243
269
  hooks: {
244
270
  metadata: () => ({ titleTemplate: "%s · My Site", siteUrl: "https://example.com" }),
245
271
  layoutContext: ({ pathname }) => ({ pathname, year: new Date().getFullYear() }),
@@ -249,16 +275,18 @@ export default {
249
275
  ```
250
276
 
251
277
  The complete reference — every field, default and failure mode — is
252
- [docs/07-yapilandirma.md](./docs/07-yapilandirma.md).
278
+ [docs/07-yapilandirma.md](./docs/07-yapilandirma.md) /
279
+ [docs/en/07-configuration.md](./docs/en/07-configuration.md).
253
280
 
254
281
  ## CLI
255
282
 
256
283
  | Command | What it does |
257
284
  | --- | --- |
258
285
  | `jskelet dev` | Watch build plus server, live reload, devtools overlay |
259
- | `jskelet build` | Production build: fonts → sprite → CSS → JS → images → manifest → precompress |
286
+ | `jskelet build` | Production build: templates → fonts → sprite → CSS → JS → images → manifest → precompress |
260
287
  | `jskelet start` | Production server; builds first if output is missing |
261
- | `jskelet init` | Scaffolds a minimal skeleton into the current directory |
288
+ | `jskelet init` | Scaffolds a feature-first `.jsk` skeleton into the current directory |
289
+ | `jskelet generate` | Scaffolds a `feature` / `page` / `island` |
262
290
 
263
291
  ## Public API
264
292
 
@@ -266,7 +294,7 @@ Only the specifiers in the `exports` map are supported:
266
294
 
267
295
  | Specifier | Contents |
268
296
  | --- | --- |
269
- | `jskelet` | `route`, `fragment`, `createApp`, `startServer`, `notFound`, `redirect`, `seeOther`, `cache`, `asset`, `getConfig`, cookie helpers, HTML cache and prewarm helpers |
297
+ | `jskelet` | `route`, `fragment`, `createApp`, `startServer`, `notFound`, `redirect`, `seeOther`, `cache`, `asset`, `getConfig`, cookie helpers, HTML/data cache and prewarm helpers, Redis/Cloudflare status and purge helpers |
270
298
  | `jskelet/client` | `register`, `registerAll`, `hydrate`, `unmount`, `start`, `swap`, `startForms`, `createStore`, DOM helpers |
271
299
  | `jskelet/html` | `attrs`, `cn`, `cx`, `esc`, `jsonScript` |
272
300
  | `jskelet/tags` | `icon`, `image`, `link`, `preloadImage`, `csrfField` |
@@ -281,28 +309,29 @@ works: a `Dockerfile` (see `examples/marketing/Dockerfile`), a systemd unit, or
281
309
  a PaaS. Run `jskelet build` at image build time, put a reverse proxy in front
282
310
  for TLS, and expose a health endpoint (the default dev gate bypass list already
283
311
  includes `/api/healthcheck`, so a route there is reachable in every mode).
284
- Details, including cache sizing
285
- behind multiple instances, are in [docs/10-dagitim.md](./docs/10-dagitim.md).
312
+ Details, including cache sizing behind multiple instances and the optional
313
+ Redis tier, are in [docs/10-dagitim.md](./docs/10-dagitim.md) /
314
+ [docs/en/10-deployment.md](./docs/en/10-deployment.md).
286
315
 
287
316
  ## Documentation
288
317
 
289
- The full reference lives under [docs/](./docs/README.md). It is currently
290
- written in Turkish; translations are a welcome contribution.
318
+ Full reference in Turkish under [docs/](./docs/README.md) and in English under
319
+ [docs/en/](./docs/en/README.md). Both editions are kept in sync.
291
320
 
292
- | Document | Topic |
321
+ | Document (TR / EN) | Topic |
293
322
  | --- | --- |
294
- | [01-baslangic](./docs/01-baslangic.md) | Installation, first route, first island, directory layout, CLI |
295
- | [02-mimari](./docs/02-mimari.md) | Decisions and their reasoning, middleware order |
296
- | [03-routing](./docs/03-routing.md) | Route modules, controller contract, load order |
297
- | [04-render-ve-sablonlar](./docs/04-render-ve-sablonlar.md) | Layout, components, helpers, metadata |
298
- | [05-islands](./docs/05-islands.md) | Island contract, hydration, store, DOM helpers |
299
- | [06-cache](./docs/06-cache.md) | TTL, stale-while-revalidate, keys, prewarm |
300
- | [07-yapilandirma](./docs/07-yapilandirma.md) | Complete `jskelet.config.mjs` reference |
301
- | [08-build](./docs/08-build.md) | Build pipeline, manifest, Tailwind `@source`, sprite |
302
- | [09-dev-araclari](./docs/09-dev-araclari.md) | Dev workflow, overlay, report page, dev gate |
303
- | [10-dagitim](./docs/10-dagitim.md) | Production, Docker, reverse proxy, health checks |
304
- | [11-tasima](./docs/11-tasima.md) | Migrating from Next.js: mapping table and plan |
305
- | [12-panel-ve-oturum](./docs/12-panel-ve-oturum.md) | Per-visitor pages: `private: true`, sessions, CSRF, fragments, swapping |
323
+ | [01-baslangic](./docs/01-baslangic.md) / [getting-started](./docs/en/01-getting-started.md) | Installation, first route, first island, directory layout, CLI |
324
+ | [02-mimari](./docs/02-mimari.md) / [architecture](./docs/en/02-architecture.md) | Decisions and their reasoning, middleware order |
325
+ | [03-routing](./docs/03-routing.md) / [routing](./docs/en/03-routing.md) | Route modules, controller contract, load order |
326
+ | [04-render](./docs/04-render-ve-sablonlar.md) / [rendering](./docs/en/04-rendering.md) | `.jsk` / EJS, layout, components, helpers, metadata |
327
+ | [05-islands](./docs/05-islands.md) / [islands](./docs/en/05-islands.md) | Island contract, hydration, store, DOM helpers |
328
+ | [06-cache](./docs/06-cache.md) / [caching](./docs/en/06-caching.md) | TTL, SWR, keys, prewarm, Redis, invalidation, admin |
329
+ | [07-yapilandirma](./docs/07-yapilandirma.md) / [configuration](./docs/en/07-configuration.md) | Complete `jskelet.config.mjs` reference |
330
+ | [08-build](./docs/08-build.md) / [build](./docs/en/08-build.md) | Build pipeline, manifest, Tailwind `@source`, sprite |
331
+ | [09-dev](./docs/09-dev-araclari.md) / [dev-tools](./docs/en/09-dev-tools.md) | Dev workflow, overlay, report page, dev gate |
332
+ | [10-dagitim](./docs/10-dagitim.md) / [deployment](./docs/en/10-deployment.md) | Production, Docker, reverse proxy, health checks |
333
+ | [11-tasima](./docs/11-tasima.md) / [migration](./docs/en/11-migration.md) | Migrating from Next.js: mapping table and plan |
334
+ | [12-panel](./docs/12-panel-ve-oturum.md) / [dashboards](./docs/en/12-dashboards-and-sessions.md) | Per-visitor pages: `private: true`, sessions, CSRF, fragments |
306
335
 
307
336
  If you work with AI agents, [AGENTS.md](./AGENTS.md) summarizes the rules that
308
337
  apply to this repository.
@@ -316,8 +345,8 @@ npm --prefix examples/marketing install && npm --prefix examples/marketing run d
316
345
  npm --prefix examples/dashboard install && npm --prefix examples/dashboard run dev
317
346
  ```
318
347
 
319
- - **`examples/minimal`** — two routes, one component, one island. The smallest
320
- thing that runs.
348
+ - **`examples/minimal`** — two routes, one component, one island, plus a
349
+ co-located `features/demo` slice. The smallest thing that runs.
321
350
  - **`examples/blog`** — dynamic routes, tag pages, every config section,
322
351
  fragment-loaded tabs, a form, prewarm, RSS and sitemap, four islands. It
323
352
  intentionally touches every surface of the framework.
package/bin/jskelet.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * JSkelet CLI.
4
4
  *
@@ -2,7 +2,7 @@
2
2
 
3
3
  Bu belge JSkelet'i sıfırdan çalıştırmayı anlatır: paket kurulumu, `jskelet init`
4
4
  ile iskeletin oluşturulması, ilk route ve ilk island'ın yazılması, oluşan dizin
5
- yapısının ne anlama geldiği ve CLI'ın dört komutu. Sonunda tarayıcıda sunucuda
5
+ yapısının ne anlama geldiği ve CLI komutları. Sonunda tarayıcıda sunucuda
6
6
  render edilmiş, önbelleğe alınmış ve island'ı görünürlükte hidre olan bir sayfa
7
7
  olacak. Kararların *nedenleri* için [02-mimari.md](./02-mimari.md)'ye, buradaki
8
8
  her config alanının tam referansı için
@@ -56,21 +56,24 @@ tamamlar, atlanan dosyaların sayısını uyarı olarak basar. Amaç, "kurulumu
56
56
  yaptım ama hiçbir şey çalışmıyor" aşamasını tamamen atlamak — `jskelet dev`
57
57
  hemen ardından çalışır.
58
58
 
59
- Oluşturulan dosyalar:
59
+ Oluşturulan dosyalar (feature-first + `.jsk`):
60
60
 
61
61
  ```
62
- jskelet.config.mjs config: brand, preconnect, cache(), hooks
63
- routes/10-pages.mjs "/" route'u
64
- views/pages/home.ejs ana sayfa şablonu
65
- views/pages/not-found.ejs 404 şablonu
66
- views/components/button.js örnek bileşen (HTML string döndüren fonksiyon)
67
- client/entries/main.js island bootstrap'ı
68
- client/islands/counter.js örnek island
69
- styles/globals.css Tailwind girişi + @source direktifleri
70
- jsconfig.json checkJs + "@/*" alias'ı
71
- .gitignore node_modules/, .jskelet/, public/assets/, .env
62
+ jskelet.config.mjs config: brand, preconnect, cache(), hooks
63
+ features/home/index.js "/" route'u
64
+ features/home/views/pages/home.jsk ana sayfa şablonu
65
+ features/home/views/components/button.js örnek bileşen (<Button />)
66
+ features/home/client/counter.js örnek island
67
+ features/home/server/.gitkeep
68
+ views/pages/not-found.jsk uygulama geneli 404
69
+ client/entries/main.js island bootstrap'ı
70
+ styles/globals.css Tailwind girişi + @source direktifleri
71
+ jsconfig.json checkJs + "@/*" alias'ı
72
+ .gitignore node_modules/, .jskelet/, public/assets/, .env
72
73
  ```
73
74
 
75
+ Büyümek için: `npx jskelet generate feature <name>` (veya `page` / `island`).
76
+
74
77
  Sonra:
75
78
 
76
79
  ```bash
@@ -88,44 +91,44 @@ ile ezilebilir. Aşağıdaki değerler varsayılanlardır (`src/config/defaults.
88
91
 
89
92
  | Dizin | Varsayılan | İçeriği |
90
93
  | --- | --- | --- |
91
- | `views` | `views` | EJS layout, sayfalar ve bileşenler |
94
+ | `views` | `views` | Uygulama geneli layout, sayfalar ve bileşenler |
95
+ | `features` | `features` | Feature dilimleri (`<name>/{server,views,client}`) |
96
+ | `shared` | `shared` | Feature'lar arası paylaşılan server/views/client |
92
97
  | `public` | `public` | Statik dosyalar; build çıktısı da buraya yazılır |
93
98
  | `client` | `client` | Island runtime kaynakları ve entry'ler |
94
- | `routes` | `routes` | Route modülleri |
99
+ | `routes` | `routes` | Route modülleri (feature'lardan önce yüklenir) |
95
100
  | `styles` | `styles/globals.css` | Tailwind/PostCSS giriş **dosyası** |
96
- | `generated` | `.jskelet` | Build ara çıktıları: `manifest.json`, `metafile.json`, `images.json` |
101
+ | `generated` | `.jskelet` | Build ara çıktıları: `manifest.json`, `metafile.json`, `images.json`, `templates/` |
97
102
 
98
103
  Bunlara ek olarak framework iki yolu her zaman türetir ve ayrı ayar kabul
99
104
  etmez: `public/assets` (hash'li build çıktısı) ve `public/fonts` (self-host
100
105
  fontlar).
101
106
 
102
- Tipik bir proje:
107
+ Tipik bir proje (`jskelet init` çıktısına yakın):
103
108
 
104
109
  ```
105
110
  benim-sitem/
106
111
  ├── jskelet.config.mjs
107
112
  ├── jsconfig.json
108
- ├── routes/
109
- │ ├── 10-pages.mjs
110
- │ └── 90-catch-all.mjs
113
+ ├── features/
114
+ │ └── home/
115
+ │ ├── index.js
116
+ │ ├── server/
117
+ │ ├── views/
118
+ │ │ ├── pages/home.jsk
119
+ │ │ └── components/button.js
120
+ │ └── client/counter.js
111
121
  ├── views/
112
- │ ├── layout.ejs
113
- │ ├── pages/
114
- │ │ ├── home.ejs
115
- │ │ └── not-found.ejs
116
- │ └── components/
117
- │ └── card.js
122
+ │ └── pages/not-found.jsk
118
123
  ├── client/
119
- │ ├── entries/
120
- │ │ └── main.js
121
- │ └── islands/
122
- │ └── counter.js
124
+ │ └── entries/main.js
123
125
  ├── styles/
124
126
  │ └── globals.css
125
127
  ├── public/
126
128
  │ └── (statik dosyalar; build → public/assets)
127
129
  └── .jskelet/
128
- └── manifest.json
130
+ ├── manifest.json
131
+ └── templates/
129
132
  ```
130
133
 
131
134
  ## İlk route
@@ -135,7 +138,7 @@ kendi yollarını `app.get(...)` ile açıkça yazar. Modül sözleşmesi: defau
135
138
  export ya da `register` adlı named export, `(app, api)` imzasıyla.
136
139
 
137
140
  ```js
138
- // routes/10-pages.mjs
141
+ // features/home/index.js
139
142
  export default function register(app, { route }) {
140
143
  app.get(
141
144
  "/",
@@ -143,7 +146,7 @@ export default function register(app, { route }) {
143
146
  async () => ({
144
147
  view: "pages/home",
145
148
  metadata: { title: "Ana sayfa" },
146
- data: { heading: "JSkelet çalışıyor", items: ["Bir", "İki"] },
149
+ data: { message: "JSkelet çalışıyor" },
147
150
  }),
148
151
  { revalidate: 60 },
149
152
  ),
@@ -157,25 +160,26 @@ yapmak zorunda kalmaz. `route()` controller'ı sarar: HTML cache'i,
157
160
  notFound/redirect kontrol akışı, sıkıştırma ve `X-JSkelet-Cache` başlığı ondan
158
161
  gelir. Controller'ın tek işi bir sayfa tanımı döndürmektir.
159
162
 
160
- Dosya adındaki `10-` öneki yükleme sırasını belirler. `routes/` alfabetik
161
- tarandığı için `/:slug` gibi yakalayıcı route'ları daha yüksek numaralı bir
162
- dosyaya koymalısınız; aksi hâlde `/hakkinda` bir slug sanılır. Ayrıntı:
163
- [03-routing.md](./03-routing.md).
163
+ `routes/` kullanıyorsanız dosya adındaki `10-` öneki yükleme sırasını belirler;
164
+ `/:slug` gibi yakalayıcı route'ları daha yüksek numaralı bir dosyaya koyun.
165
+ Feature `index.js` dosyaları `routes/` tarandıktan sonra alfabetik eklenir.
166
+ Ayrıntı: [03-routing.md](./03-routing.md).
164
167
 
165
- Şablon tarafı düz EJS:
168
+ Şablon tarafı `.jsk` (build-time derlenir):
166
169
 
167
- ```ejs
168
- <%# views/pages/home.ejs %>
170
+ ```html
171
+ {# features/home/views/pages/home.jsk #}
169
172
  <section class="wrapper">
170
- <h1 class="text-3xl font-bold"><%= heading %></h1>
171
- <%- list({ items }) %>
172
- <div data-island="counter" data-island-props='{"start":5}'></div>
173
+ <h1>{{ metadata.title }}</h1>
174
+ <p>{{ message }}</p>
175
+ <Button text="Örnek bileşen" />
176
+ <div data-island="counter" data-island-props='{"start":0}'></div>
173
177
  </section>
174
178
  ```
175
179
 
176
- `list` burada `views/components/list.js` içinde tanımlı bir fonksiyondur ve
177
- import edilmemiştir: `views/components/**` altındaki her named export otomatik
178
- olarak şablon local'i olur ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
180
+ `Button`, `features/home/views/components/button.js` içindeki `button` named
181
+ export'undan gelir — PascalCase etiket; import gerekmez
182
+ ([04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md)).
179
183
 
180
184
  ## İlk island
181
185
 
@@ -193,7 +197,7 @@ iki parçadan oluşur.
193
197
  verir.
194
198
 
195
199
  ```js
196
- // client/islands/counter.js
200
+ // features/home/client/counter.js
197
201
  /**
198
202
  * @param {HTMLElement} element
199
203
  * @param {{ start?: number }} props
@@ -225,7 +229,7 @@ runtime'ı başlatır.
225
229
  import { registerAll, start } from "jskelet/client";
226
230
 
227
231
  registerAll({
228
- counter: () => import("../islands/counter.js"),
232
+ counter: () => import("../../features/home/client/counter.js"),
229
233
  });
230
234
 
231
235
  start();
@@ -239,7 +243,7 @@ haritayı büyütmek ilk yükü büyütmez. Hidrasyon stratejileri
239
243
 
240
244
  ## CLI komutları
241
245
 
242
- `bin/jskelet.mjs` dört alt komut sunar. Her biri ayrı bir Node sürecinde
246
+ `bin/jskelet.mjs` şu alt komutları sunar. Her biri ayrı bir Node sürecinde
243
247
  çalışır; sebebi `dev`in iki uzun ömürlü süreci yönetmesi ve sunucunun ESM
244
248
  resolve hook'larına (`--import`) süreç başlangıcında ihtiyaç duyması.
245
249
 
@@ -248,7 +252,8 @@ resolve hook'larına (`--import`) süreç başlangıcında ihtiyaç duyması.
248
252
  | `jskelet dev` | Build watch + sunucu, tek terminalde. Canlı yenileme, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
249
253
  | `jskelet build` | Tek seferlik prod build: fontlar → ikon sprite → CSS → client JS → görseller → manifest → precompress. `NODE_ENV` verilmemişse `production`. |
250
254
  | `jskelet start` | Prod sunucu. Build çıktısı yoksa önce üretir. `NODE_ENV` verilmemişse `production`. |
251
- | `jskelet init` | Bulunduğun dizine minimal iskelet kurar; var olan dosyalara dokunmaz. |
255
+ | `jskelet init` | Bulunduğun dizine feature-first `.jsk` iskeleti kurar; var olan dosyalara dokunmaz. |
256
+ | `jskelet generate` | `feature` / `page` / `island` iskeleti üretir. |
252
257
 
253
258
  Bilinmeyen bir komut ya da argümansız çağrı kullanım metnini basar.
254
259
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  This document explains how to get JSkelet running from scratch: installing the
4
4
  package, scaffolding the skeleton with `jskelet init`, writing your first route
5
- and your first island, what the resulting directory layout means, and the CLI's
6
- four commands. By the end you will have a page in the browser that is rendered
5
+ and your first island, what the resulting directory layout means, and the CLI
6
+ commands. By the end you will have a page in the browser that is rendered
7
7
  on the server, cached, and whose island hydrates on visibility. For the
8
8
  *reasons* behind the decisions see
9
9
  [02-architecture.md](./02-architecture.md), and for the full reference of every
@@ -58,21 +58,24 @@ what is missing and prints the number of skipped files as a warning. The goal is
58
58
  to skip the "I installed it but nothing works" stage entirely — `jskelet dev`
59
59
  runs right afterwards.
60
60
 
61
- The files it creates:
61
+ The files it creates (feature-first + `.jsk`):
62
62
 
63
63
  ```
64
- jskelet.config.mjs config: brand, preconnect, cache(), hooks
65
- routes/10-pages.mjs the "/" route
66
- views/pages/home.ejs home page template
67
- views/pages/not-found.ejs 404 template
68
- views/components/button.js example component (a function returning an HTML string)
69
- client/entries/main.js island bootstrap
70
- client/islands/counter.js example island
71
- styles/globals.css Tailwind entry + @source directives
72
- jsconfig.json checkJs + the "@/*" alias
73
- .gitignore node_modules/, .jskelet/, public/assets/, .env
64
+ jskelet.config.mjs config: brand, preconnect, cache(), hooks
65
+ features/home/index.js the "/" route
66
+ features/home/views/pages/home.jsk home page template
67
+ features/home/views/components/button.js example component (<Button />)
68
+ features/home/client/counter.js example island
69
+ features/home/server/.gitkeep
70
+ views/pages/not-found.jsk app-wide 404
71
+ client/entries/main.js island bootstrap
72
+ styles/globals.css Tailwind entry + @source directives
73
+ jsconfig.json checkJs + the "@/*" alias
74
+ .gitignore node_modules/, .jskelet/, public/assets/, .env
74
75
  ```
75
76
 
77
+ To grow: `npx jskelet generate feature <name>` (or `page` / `island`).
78
+
76
79
  Then:
77
80
 
78
81
  ```bash
@@ -92,44 +95,44 @@ None of the directory names are fixed; all of them can be overridden via
92
95
 
93
96
  | Directory | Default | Contents |
94
97
  | --- | --- | --- |
95
- | `views` | `views` | EJS layout, pages and components |
98
+ | `views` | `views` | App-wide layout, pages and components |
99
+ | `features` | `features` | Feature slices (`<name>/{server,views,client}`) |
100
+ | `shared` | `shared` | Cross-feature server/views/client |
96
101
  | `public` | `public` | Static files; build output is written here too |
97
102
  | `client` | `client` | Island runtime sources and entries |
98
- | `routes` | `routes` | Route modules |
103
+ | `routes` | `routes` | Route modules (loaded before features) |
99
104
  | `styles` | `styles/globals.css` | Tailwind/PostCSS entry **file** |
100
- | `generated` | `.jskelet` | Intermediate build output: `manifest.json`, `metafile.json`, `images.json` |
105
+ | `generated` | `.jskelet` | Intermediate build output: `manifest.json`, `metafile.json`, `images.json`, `templates/` |
101
106
 
102
107
  In addition to these the framework always derives two paths and accepts no
103
108
  separate setting for them: `public/assets` (hashed build output) and
104
109
  `public/fonts` (self-hosted fonts).
105
110
 
106
- A typical project:
111
+ A typical project (close to what `jskelet init` writes):
107
112
 
108
113
  ```
109
114
  my-site/
110
115
  ├── jskelet.config.mjs
111
116
  ├── jsconfig.json
112
- ├── routes/
113
- │ ├── 10-pages.mjs
114
- │ └── 90-catch-all.mjs
117
+ ├── features/
118
+ │ └── home/
119
+ │ ├── index.js
120
+ │ ├── server/
121
+ │ ├── views/
122
+ │ │ ├── pages/home.jsk
123
+ │ │ └── components/button.js
124
+ │ └── client/counter.js
115
125
  ├── views/
116
- │ ├── layout.ejs
117
- │ ├── pages/
118
- │ │ ├── home.ejs
119
- │ │ └── not-found.ejs
120
- │ └── components/
121
- │ └── card.js
126
+ │ └── pages/not-found.jsk
122
127
  ├── client/
123
- │ ├── entries/
124
- │ │ └── main.js
125
- │ └── islands/
126
- │ └── counter.js
128
+ │ └── entries/main.js
127
129
  ├── styles/
128
130
  │ └── globals.css
129
131
  ├── public/
130
132
  │ └── (static files; build → public/assets)
131
133
  └── .jskelet/
132
- └── manifest.json
134
+ ├── manifest.json
135
+ └── templates/
133
136
  ```
134
137
 
135
138
  ## Your first route
@@ -140,7 +143,7 @@ a default export or a named export called `register`, with the signature
140
143
  `(app, api)`.
141
144
 
142
145
  ```js
143
- // routes/10-pages.mjs
146
+ // features/home/index.js
144
147
  export default function register(app, { route }) {
145
148
  app.get(
146
149
  "/",
@@ -148,7 +151,7 @@ export default function register(app, { route }) {
148
151
  async () => ({
149
152
  view: "pages/home",
150
153
  metadata: { title: "Home" },
151
- data: { heading: "JSkelet is running", items: ["One", "Two"] },
154
+ data: { message: "JSkelet is running" },
152
155
  }),
153
156
  { revalidate: 60 },
154
157
  ),
@@ -163,25 +166,26 @@ HTML cache, the notFound/redirect control flow, compression and the
163
166
  `X-JSkelet-Cache` header all come from it. The controller's only job is to
164
167
  return a page definition.
165
168
 
166
- The `10-` prefix in the file name determines the load order. Because `routes/`
167
- is scanned alphabetically, you should put catch-all routes such as `/:slug` in a
168
- file with a higher number; otherwise `/about` will be mistaken for a slug.
169
- Details: [03-routing.md](./03-routing.md).
169
+ If you use `routes/`, the `10-` prefix in the file name determines load order;
170
+ put catch-alls such as `/:slug` in a higher-numbered file. Feature `index.js`
171
+ files are appended alphabetically after the `routes/` scan. Details:
172
+ [03-routing.md](./03-routing.md).
170
173
 
171
- The template side is plain EJS:
174
+ The template side is `.jsk` (compiled at build time):
172
175
 
173
- ```ejs
174
- <%# views/pages/home.ejs %>
176
+ ```html
177
+ {# features/home/views/pages/home.jsk #}
175
178
  <section class="wrapper">
176
- <h1 class="text-3xl font-bold"><%= heading %></h1>
177
- <%- list({ items }) %>
178
- <div data-island="counter" data-island-props='{"start":5}'></div>
179
+ <h1>{{ metadata.title }}</h1>
180
+ <p>{{ message }}</p>
181
+ <Button text="Example component" />
182
+ <div data-island="counter" data-island-props='{"start":0}'></div>
179
183
  </section>
180
184
  ```
181
185
 
182
- Here `list` is a function defined in `views/components/list.js` and it has not
183
- been imported: every named export under `views/components/**` automatically
184
- becomes a template local ([04-rendering.md](./04-rendering.md)).
186
+ `Button` comes from the `button` named export in
187
+ `features/home/views/components/button.js` — PascalCase tag, no import
188
+ ([04-rendering.md](./04-rendering.md)).
185
189
 
186
190
  ## Your first island
187
191
 
@@ -199,7 +203,7 @@ carried as JSON inside `data-island-props`.
199
203
  `mount(element, props)`.
200
204
 
201
205
  ```js
202
- // client/islands/counter.js
206
+ // features/home/client/counter.js
203
207
  /**
204
208
  * @param {HTMLElement} element
205
209
  * @param {{ start?: number }} props
@@ -231,7 +235,7 @@ import and starts the runtime.
231
235
  import { registerAll, start } from "jskelet/client";
232
236
 
233
237
  registerAll({
234
- counter: () => import("../islands/counter.js"),
238
+ counter: () => import("../../features/home/client/counter.js"),
235
239
  });
236
240
 
237
241
  start();
@@ -245,7 +249,7 @@ runtime API are in [05-islands.md](./05-islands.md).
245
249
 
246
250
  ## CLI commands
247
251
 
248
- `bin/jskelet.mjs` offers four subcommands. Each runs in a separate Node process;
252
+ `bin/jskelet.mjs` offers these subcommands. Each runs in a separate Node process;
249
253
  the reason is that `dev` manages two long-lived processes and the server needs
250
254
  ESM resolve hooks (`--import`) at process start.
251
255
 
@@ -254,7 +258,8 @@ ESM resolve hooks (`--import`) at process start.
254
258
  | `jskelet dev` | Build watch + server, in a single terminal. Live reload, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
255
259
  | `jskelet build` | One-shot prod build: fonts → icon sprite → CSS → client JS → images → manifest → precompress. `production` if `NODE_ENV` is not given. |
256
260
  | `jskelet start` | Prod server. If there is no build output it produces it first. `production` if `NODE_ENV` is not given. |
257
- | `jskelet init` | Installs a minimal skeleton into the current directory; leaves existing files alone. |
261
+ | `jskelet init` | Installs a feature-first `.jsk` skeleton into the current directory; leaves existing files alone. |
262
+ | `jskelet generate` | Scaffolds a `feature` / `page` / `island`. |
258
263
 
259
264
  An unknown command, or a call with no arguments, prints the usage text.
260
265
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jskelet",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -131,13 +131,23 @@ export function patchManifest(key, url) {
131
131
  /**
132
132
  * Eski hash'li çıktıları temizler.
133
133
  *
134
+ * Yeni dosya **önce** yazılmalı, prune sonra gelmeli: aynı içerik aynı hash'i
135
+ * üretir ve önce silmek `/assets/app.<hash>.css` için kısa bir 404 penceresi
136
+ * açar. CDN o 404'ü `immutable` ile saklarsa (eski headersMiddleware
137
+ * davranışı) tarayıcı bir yıl boyunca stilsiz kalır.
138
+ *
134
139
  * @param {string[]} prefixes
140
+ * @param {{ keep?: string[] }} [options] Korunacak dosya adları (ör. yeni
141
+ * yazılan `app.<hash>.css`); `.br` / `.gz` sonekleri de eşleşir.
135
142
  */
136
- export function pruneAssets(prefixes) {
143
+ export function pruneAssets(prefixes, { keep = [] } = {}) {
137
144
  if (!fs.existsSync(paths.assets)) return;
145
+
138
146
  for (const file of fs.readdirSync(paths.assets)) {
139
- if (prefixes.some((prefix) => file.startsWith(prefix))) {
140
- fs.rmSync(path.join(paths.assets, file), { force: true });
147
+ if (!prefixes.some((prefix) => file.startsWith(prefix))) continue;
148
+ if (keep.some((name) => file === name || file.startsWith(`${name}.`))) {
149
+ continue;
141
150
  }
151
+ fs.rmSync(path.join(paths.assets, file), { force: true });
142
152
  }
143
153
  }
@@ -64,9 +64,11 @@ export async function buildCss(config, { watch = false } = {}) {
64
64
 
65
65
  const run = async () => {
66
66
  const started = Date.now();
67
- pruneAssets(["app."]);
68
67
  const css = await compile(input);
68
+ // Önce yaz, sonra eski hash'leri sil — aynı hash'e düşen içerikte 404
69
+ // penceresi olmasın (CDN immutable zehirlenmesi).
69
70
  const url = writeAsset("app.css", css);
71
+ pruneAssets(["app."], { keep: [path.basename(url)] });
70
72
  return { url, bytes: Buffer.byteLength(css), elapsed: Date.now() - started };
71
73
  };
72
74
 
@@ -192,8 +192,6 @@ export async function buildIconSprite(config) {
192
192
  config.icons?.scan ?? ["views", "client", "routes", "lib", "features", "shared"]
193
193
  ).map((dir) => path.resolve(config.root, dir));
194
194
 
195
- pruneAssets(["sprite."]);
196
-
197
195
  const used = [...scanUsedIcons(scanDirs)].sort();
198
196
  const symbols = [];
199
197
  const missing = [];
@@ -214,6 +212,7 @@ export async function buildIconSprite(config) {
214
212
 
215
213
  const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">${symbols.join("")}</svg>`;
216
214
  const url = writeAsset("sprite.svg", sprite);
215
+ pruneAssets(["sprite."], { keep: [path.basename(url)] });
217
216
 
218
217
  log.detail(`${symbols.length} symbols`);
219
218
  if (missing.length) {
package/src/init.mjs CHANGED
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * `jskelet init` — bulunduğun dizine çalışan bir minimum iskelet kurar.
3
3
  *
4
- * Var olan dosyaların üzerine yazmaz: komutu ikinci kez çalıştırmak yalnızca
5
- * eksikleri tamamlar. Amaç, "kurulum yaptım ama hiçbir şey çalışmıyor"
6
- * aşamasını tamamen atlamak — `jskelet dev` hemen ardından çalışır.
4
+ * Varsayılan düzen feature-first'tir: sayfa, bileşen ve island
5
+ * `features/<name>/` altında toplanır; URL kaydı yine açıkça yazılır.
6
+ * Şablonlar `.jsk`. Var olan dosyaların üzerine yazmaz: komutu ikinci kez
7
+ * çalıştırmak yalnızca eksikleri tamamlar. Amaç, "kurulum yaptım ama hiçbir
8
+ * şey çalışmıyor" aşamasını tamamen atlamak — `jskelet dev` hemen ardından
9
+ * çalışır.
7
10
  */
8
11
  import fs from "node:fs";
9
12
  import path from "node:path";
@@ -55,13 +58,12 @@ export default {
55
58
  };
56
59
  `,
57
60
 
58
- "routes/10-pages.mjs": `/**
59
- * Route module. The default export receives \`(app, api)\`; \`api.route()\` wraps
60
- * the controller with the HTML cache, the notFound/redirect flow and
61
- * compression.
61
+ "features/home/index.js": `/**
62
+ * Feature route registration. Explicit paths only — no filesystem URL routing.
63
+ * Loaded after \`routes/\` (alphabetically among features).
62
64
  *
63
- * The numeric prefix in the file name sets load order: catch-all routes
64
- * (like "/:slug") belong to a higher number.
65
+ * @param {import('express').Express} app
66
+ * @param {{ route: Function }} api
65
67
  */
66
68
  export default function register(app, { route }) {
67
69
  app.get(
@@ -78,25 +80,19 @@ export default function register(app, { route }) {
78
80
  }
79
81
  `,
80
82
 
81
- "views/pages/home.jsk": `<section class="wrapper">
83
+ "features/home/views/pages/home.jsk": `<section class="wrapper">
82
84
  <h1>{{ metadata.title }}</h1>
83
85
  <p>{{ message }}</p>
86
+ <Button text="Example component" />
84
87
  <div data-island="counter" data-island-props='{"start":0}'></div>
85
88
  </section>
86
89
  `,
87
90
 
88
- "views/pages/not-found.jsk": `<section class="wrapper">
89
- <h1>404</h1>
90
- <p>The page you are looking for was not found.</p>
91
- <p><Link href="/" text="Back to home" /></p>
92
- </section>
93
- `,
94
-
95
- "views/components/button.js": `import { attrs, esc } from "jskelet/html";
91
+ "features/home/views/components/button.js": `import { attrs, esc } from "jskelet/html";
96
92
 
97
93
  /**
98
- * Every named export under \`views/components/**\` is usable directly in
99
- * templates: \`<Button text="Save" />\` in \`.jsk\` or \`<%- button({ text }) %>\` in EJS.
94
+ * Named exports under \`views/components/**\` (including feature views) become
95
+ * PascalCase tags in \`.jsk\`: \`<Button text="Save" />\`.
100
96
  *
101
97
  * @param {{ text: string, href?: string, class?: string }} props
102
98
  * @returns {string}
@@ -107,22 +103,10 @@ export function button({ text, href, class: className }) {
107
103
  }
108
104
  `,
109
105
 
110
- "client/entries/main.js": `import { registerAll, start } from "jskelet/client";
111
-
112
- /**
113
- * Island registry. Values are dynamic imports: a module is downloaded only if
114
- * that island is actually on the page and becomes visible.
115
- */
116
- registerAll({
117
- counter: () => import("../islands/counter.js"),
118
- });
119
-
120
- start();
121
- `,
122
-
123
- "client/islands/counter.js": `/**
106
+ "features/home/client/counter.js": `/**
124
107
  * Island contract: a named export called \`mount(element, props)\`.
125
- * The returned function, if any, is reserved for cleanup.
108
+ * Register it from \`client/entries/main.js\`. The returned function, if any,
109
+ * is reserved for cleanup.
126
110
  *
127
111
  * @param {HTMLElement} element
128
112
  * @param {{ start?: number }} props
@@ -145,6 +129,28 @@ export function mount(element, props) {
145
129
  paint();
146
130
  element.append(button);
147
131
  }
132
+ `,
133
+
134
+ "features/home/server/.gitkeep": "",
135
+
136
+ "views/pages/not-found.jsk": `<section class="wrapper">
137
+ <h1>404</h1>
138
+ <p>The page you are looking for was not found.</p>
139
+ <p><Link href="/" text="Back to home" /></p>
140
+ </section>
141
+ `,
142
+
143
+ "client/entries/main.js": `import { registerAll, start } from "jskelet/client";
144
+
145
+ /**
146
+ * Island registry. Values are dynamic imports: a module is downloaded only if
147
+ * that island is actually on the page and becomes visible.
148
+ */
149
+ registerAll({
150
+ counter: () => import("../../features/home/client/counter.js"),
151
+ });
152
+
153
+ start();
148
154
  `,
149
155
 
150
156
  "styles/globals.css": `@import "tailwindcss" source(none);
@@ -220,4 +226,5 @@ export async function init(root) {
220
226
 
221
227
  log.line("");
222
228
  log.line("next step: npx jskelet dev");
229
+ log.line("grow with: npx jskelet generate feature <name>");
223
230
  }
@@ -148,7 +148,10 @@ export async function createApp(options = {}) {
148
148
 
149
149
  app.use(async (req, res, next) => {
150
150
  try {
151
- res.status(404).type("html").send(await renderNotFound());
151
+ // headersMiddleware `/assets/*` için immutable basmış olabilir; eksik bir
152
+ // hash'li dosyanın 404'ü CDN'de bir yıl zehirlenmesin.
153
+ res.status(404).setHeader("Cache-Control", "no-store");
154
+ res.type("html").send(await renderNotFound());
152
155
  } catch (error) {
153
156
  next(error);
154
157
  }
@@ -168,7 +171,8 @@ export async function createApp(options = {}) {
168
171
  }
169
172
 
170
173
  if (isNotFoundError(error)) {
171
- res.status(404).type("html").send(await renderNotFound());
174
+ res.status(404).setHeader("Cache-Control", "no-store");
175
+ res.type("html").send(await renderNotFound());
172
176
  return;
173
177
  }
174
178