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 +25 -1
- package/README.md +114 -85
- package/bin/jskelet.mjs +1 -1
- package/docs/01-baslangic.md +54 -49
- package/docs/en/01-getting-started.md +55 -50
- package/package.json +1 -1
- package/src/build/paths.mjs +13 -3
- package/src/build/tasks/css.mjs +3 -1
- package/src/build/tasks/icons.mjs +1 -2
- package/src/init.mjs +42 -35
- package/src/server/create-app.js +6 -2
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`
|
|
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
|
|
7
|
-
|
|
8
|
-
Tailwind v4 stylesheet**, and instead
|
|
9
|
-
cache** with stale-while-revalidate
|
|
10
|
-
|
|
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
|
[](https://www.npmjs.com/package/jskelet)
|
|
13
14
|
[](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
|
|
50
|
-
|
|
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
|
|
56
|
-
inferred from the file system
|
|
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
|
-
//
|
|
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 },
|
|
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
|
|
81
|
-
|
|
82
|
-
functions
|
|
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.
|
|
84
|
+
<!-- features/home/views/pages/home.jsk -->
|
|
86
85
|
<section class="wrapper">
|
|
87
86
|
<h1 class="text-3xl font-bold">Latest posts</h1>
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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/
|
|
101
|
-
export
|
|
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()
|
|
118
|
-
the subset of `next.config` syntax people actually use. A
|
|
119
|
-
throwing `headers()` or a failing hook logs a warning and
|
|
120
|
-
defaults — it never takes the site down.
|
|
121
|
-
2. **
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
172
|
-
|
|
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)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
193
|
-
├──
|
|
194
|
-
|
|
195
|
-
│
|
|
196
|
-
│
|
|
197
|
-
│
|
|
198
|
-
├── client/
|
|
199
|
-
│
|
|
200
|
-
|
|
201
|
-
├──
|
|
202
|
-
├──
|
|
203
|
-
|
|
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
|
-
-
|
|
213
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
|
298
|
-
| [05-islands](./docs/05-islands.md) | Island contract, hydration, store, DOM helpers |
|
|
299
|
-
| [06-cache](./docs/06-cache.md) | TTL,
|
|
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-
|
|
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-
|
|
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
|
|
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
package/docs/01-baslangic.md
CHANGED
|
@@ -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
|
|
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
|
|
63
|
-
|
|
64
|
-
views/pages/home.
|
|
65
|
-
views/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
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` |
|
|
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
|
-
├──
|
|
109
|
-
│
|
|
110
|
-
│
|
|
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
|
-
│
|
|
113
|
-
│ ├── pages/
|
|
114
|
-
│ │ ├── home.ejs
|
|
115
|
-
│ │ └── not-found.ejs
|
|
116
|
-
│ └── components/
|
|
117
|
-
│ └── card.js
|
|
122
|
+
│ └── pages/not-found.jsk
|
|
118
123
|
├── client/
|
|
119
|
-
│
|
|
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
|
-
|
|
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
|
-
//
|
|
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: {
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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ı
|
|
168
|
+
Şablon tarafı `.jsk` (build-time derlenir):
|
|
166
169
|
|
|
167
|
-
```
|
|
168
|
-
|
|
170
|
+
```html
|
|
171
|
+
{# features/home/views/pages/home.jsk #}
|
|
169
172
|
<section class="wrapper">
|
|
170
|
-
<h1
|
|
171
|
-
|
|
172
|
-
<
|
|
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
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
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/
|
|
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("
|
|
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`
|
|
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
|
|
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
|
|
6
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
views/pages/home.
|
|
67
|
-
views/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
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` |
|
|
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
|
-
├──
|
|
113
|
-
│
|
|
114
|
-
│
|
|
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
|
-
│
|
|
117
|
-
│ ├── pages/
|
|
118
|
-
│ │ ├── home.ejs
|
|
119
|
-
│ │ └── not-found.ejs
|
|
120
|
-
│ └── components/
|
|
121
|
-
│ └── card.js
|
|
126
|
+
│ └── pages/not-found.jsk
|
|
122
127
|
├── client/
|
|
123
|
-
│
|
|
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
|
-
|
|
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
|
-
//
|
|
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: {
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
174
|
+
The template side is `.jsk` (compiled at build time):
|
|
172
175
|
|
|
173
|
-
```
|
|
174
|
-
|
|
176
|
+
```html
|
|
177
|
+
{# features/home/views/pages/home.jsk #}
|
|
175
178
|
<section class="wrapper">
|
|
176
|
-
<h1
|
|
177
|
-
|
|
178
|
-
<
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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/
|
|
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("
|
|
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
|
|
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
|
|
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.
|
|
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",
|
package/src/build/paths.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/build/tasks/css.mjs
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
"
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
*
|
|
64
|
-
*
|
|
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/
|
|
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
|
-
*
|
|
99
|
-
*
|
|
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/
|
|
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,
|
|
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
|
}
|
package/src/server/create-app.js
CHANGED
|
@@ -148,7 +148,10 @@ export async function createApp(options = {}) {
|
|
|
148
148
|
|
|
149
149
|
app.use(async (req, res, next) => {
|
|
150
150
|
try {
|
|
151
|
-
|
|
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).
|
|
174
|
+
res.status(404).setHeader("Cache-Control", "no-store");
|
|
175
|
+
res.type("html").send(await renderNotFound());
|
|
172
176
|
return;
|
|
173
177
|
}
|
|
174
178
|
|