jskelet 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -1,292 +1,292 @@
1
- # 01 — Getting started
2
-
3
- This document explains how to get JSkelet running from scratch: installing the
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
7
- on the server, cached, and whose island hydrates on visibility. For the
8
- *reasons* behind the decisions see
9
- [02-architecture.md](./02-architecture.md), and for the full reference of every
10
- config field mentioned here see
11
- [07-configuration.md](./07-configuration.md).
12
-
13
- ## Requirements
14
-
15
- - **Node.js 22 or newer.** `package.json` → `engines` enforces this. The
16
- framework uses new Node surfaces such as `node:async_hooks`,
17
- `fs.readdirSync(..., { recursive: true })`, `--env-file-if-exists` and
18
- `module.register()` directly.
19
- - If you are going to use Tailwind CSS, the `postcss`, `@tailwindcss/postcss`
20
- and `tailwindcss` packages. These are **optional peer dependencies** of the
21
- framework; if they are not installed the CSS step is skipped and the site
22
- stays unstyled but working (details: [08-build.md](./08-build.md)).
23
-
24
- ## Installation
25
-
26
- ```bash
27
- mkdir my-site && cd my-site
28
- npm init -y
29
- npm pkg set type=module
30
- npm install jskelet
31
- npm install -D postcss @tailwindcss/postcss tailwindcss lightningcss
32
- ```
33
-
34
- `type: "module"` is required: route modules, components and the config file are
35
- loaded as ESM.
36
-
37
- Then add the scripts to `package.json`:
38
-
39
- ```json
40
- {
41
- "scripts": {
42
- "dev": "jskelet dev",
43
- "build": "jskelet build",
44
- "start": "jskelet start"
45
- }
46
- }
47
- ```
48
-
49
- ## `jskelet init`
50
-
51
- ```bash
52
- npx jskelet init
53
- ```
54
-
55
- This command installs a working minimal skeleton into the directory you are in.
56
- It **does not overwrite existing files**: running it a second time only fills in
57
- what is missing and prints the number of skipped files as a warning. The goal is
58
- to skip the "I installed it but nothing works" stage entirely — `jskelet dev`
59
- runs right afterwards.
60
-
61
- The files it creates:
62
-
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
74
- ```
75
-
76
- Then:
77
-
78
- ```bash
79
- npm run dev
80
- ```
81
-
82
- In the terminal you will see a banner, aligned build lines and a `Ready`
83
- summary; `http://localhost:3000` serves the page. A dev overlay bubble sits in
84
- the bottom right corner, opened with `Alt+D`
85
- ([09-dev-tools.md](./09-dev-tools.md)).
86
-
87
- ## Directory layout
88
-
89
- None of the directory names are fixed; all of them can be overridden via
90
- `jskelet.config.mjs` → `paths`. The values below are the defaults
91
- (`src/config/defaults.js`).
92
-
93
- | Directory | Default | Contents |
94
- | --- | --- | --- |
95
- | `views` | `views` | EJS layout, pages and components |
96
- | `public` | `public` | Static files; build output is written here too |
97
- | `client` | `client` | Island runtime sources and entries |
98
- | `routes` | `routes` | Route modules |
99
- | `styles` | `styles/globals.css` | Tailwind/PostCSS entry **file** |
100
- | `generated` | `.jskelet` | Intermediate build output: `manifest.json`, `metafile.json`, `images.json` |
101
-
102
- In addition to these the framework always derives two paths and accepts no
103
- separate setting for them: `public/assets` (hashed build output) and
104
- `public/fonts` (self-hosted fonts).
105
-
106
- A typical project:
107
-
108
- ```
109
- my-site/
110
- ├── jskelet.config.mjs
111
- ├── jsconfig.json
112
- ├── routes/
113
- │ ├── 10-pages.mjs
114
- │ └── 90-catch-all.mjs
115
- ├── views/
116
- │ ├── layout.ejs
117
- │ ├── pages/
118
- │ │ ├── home.ejs
119
- │ │ └── not-found.ejs
120
- │ └── components/
121
- │ └── card.js
122
- ├── client/
123
- │ ├── entries/
124
- │ │ └── main.js
125
- │ └── islands/
126
- │ └── counter.js
127
- ├── styles/
128
- │ └── globals.css
129
- ├── public/
130
- │ └── (static files; build → public/assets)
131
- └── .jskelet/
132
- └── manifest.json
133
- ```
134
-
135
- ## Your first route
136
-
137
- Route modules **do not derive URLs automatically from the file system**; every
138
- module writes its own paths explicitly with `app.get(...)`. The module contract:
139
- a default export or a named export called `register`, with the signature
140
- `(app, api)`.
141
-
142
- ```js
143
- // routes/10-pages.mjs
144
- export default function register(app, { route }) {
145
- app.get(
146
- "/",
147
- route(
148
- async () => ({
149
- view: "pages/home",
150
- metadata: { title: "Home" },
151
- data: { heading: "JSkelet is running", items: ["One", "Two"] },
152
- }),
153
- { revalidate: 60 },
154
- ),
155
- );
156
- }
157
- ```
158
-
159
- The `api` object comes with `route`, `renderView`, `renderPage`, `notFound`,
160
- `redirect` and `permanentRedirect` ready to use, so route files don't have to
161
- import them one by one from the framework. `route()` wraps the controller: the
162
- HTML cache, the notFound/redirect control flow, compression and the
163
- `X-JSkelet-Cache` header all come from it. The controller's only job is to
164
- return a page definition.
165
-
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).
170
-
171
- The template side is plain EJS:
172
-
173
- ```ejs
174
- <%# views/pages/home.ejs %>
175
- <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
- </section>
180
- ```
181
-
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)).
185
-
186
- ## Your first island
187
-
188
- An island is a small module that adds behaviour to the HTML the server
189
- produced. The contract has two parts.
190
-
191
- **1. A marker in the template:** give an element `data-island="ad"`. Props are
192
- carried as JSON inside `data-island-props`.
193
-
194
- ```ejs
195
- <div data-island="counter" data-island-props='{"start":5}'></div>
196
- ```
197
-
198
- **2. A `mount` in the module:** the island provides a named export called
199
- `mount(element, props)`.
200
-
201
- ```js
202
- // client/islands/counter.js
203
- /**
204
- * @param {HTMLElement} element
205
- * @param {{ start?: number }} props
206
- */
207
- export function mount(element, props) {
208
- let value = props.start ?? 0;
209
-
210
- const button = document.createElement("button");
211
- button.type = "button";
212
-
213
- const paint = () => {
214
- button.textContent = `Clicks: ${value}`;
215
- };
216
-
217
- button.addEventListener("click", () => {
218
- value += 1;
219
- paint();
220
- });
221
-
222
- paint();
223
- element.append(button);
224
- }
225
- ```
226
-
227
- **3. Registration:** `client/entries/main.js` maps the island name to a dynamic
228
- import and starts the runtime.
229
-
230
- ```js
231
- import { registerAll, start } from "jskelet/client";
232
-
233
- registerAll({
234
- counter: () => import("../islands/counter.js"),
235
- });
236
-
237
- start();
238
- ```
239
-
240
- It is critical that the values are dynamic imports: the module is downloaded
241
- only if that island actually exists on the page **and** when the element becomes
242
- visible. In other words, growing this map does not grow the initial payload.
243
- Hydration strategies (`data-island-eager`, `data-island-idle`) and the complete
244
- runtime API are in [05-islands.md](./05-islands.md).
245
-
246
- ## CLI commands
247
-
248
- `bin/jskelet.mjs` offers four subcommands. Each runs in a separate Node process;
249
- the reason is that `dev` manages two long-lived processes and the server needs
250
- ESM resolve hooks (`--import`) at process start.
251
-
252
- | Command | What it does |
253
- | --- | --- |
254
- | `jskelet dev` | Build watch + server, in a single terminal. Live reload, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
255
- | `jskelet build` | One-shot prod build: fonts → icon sprite → CSS → client JS → images → manifest → precompress. `production` if `NODE_ENV` is not given. |
256
- | `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. |
258
-
259
- An unknown command, or a call with no arguments, prints the usage text.
260
-
261
- Every command runs with two Node flags:
262
-
263
- - `--env-file=.env` — passed only if the file really exists; otherwise no flag
264
- is added and no warning is printed.
265
- - `--import <register.mjs>` — installs the ESM hooks that resolve the
266
- `compilerOptions.paths` aliases (`@/lib/x`) from `jsconfig.json` /
267
- `tsconfig.json` and extensionless relative imports (`./cache` →
268
- `./cache.js`). (`jskelet dev` installs these hooks in its own child
269
- processes, not in the outer process.)
270
-
271
- ## Import paths
272
-
273
- The `package.json` → `exports` map defines the stable surface. In examples, use
274
- only these specifiers:
275
-
276
- | Specifier | Contents |
277
- | --- | --- |
278
- | `jskelet` | Server API: `route`, `renderPage`, `renderView`, `renderNotFound`, `createApp`, `startServer`, `notFound`, `redirect`, `permanentRedirect`, `cache`, `withRequestCache`, `reportUpstreamFailure`, `asset`, `hasAsset`, `optimizedImage`, `getSpriteIds`, `headHints`, `renderHeadMeta`, HTML cache functions, `prewarm`, `createProxy`, `getConfig`, `loadConfig` and the html/tag helpers |
279
- | `jskelet/server` | The same module as `jskelet` (an alias for readability) |
280
- | `jskelet/client` | Browser runtime: `register`, `registerAll`, `hydrate`, `observeDocument`, `start`, `createStore`, DOM helpers, `startSafeImages` |
281
- | `jskelet/html` | `esc`, `attrs`, `cx`, `cn`, `jsonScript` |
282
- | `jskelet/tags` | `link`, `image`, `icon`, `preloadImage`, `toKebab` |
283
- | `jskelet/log` | Console output helpers (`banner`, `event`, `task`, `size`, `ms`, …) |
284
- | `jskelet/register` | Alias + extension hooks via `node --import jskelet/register` |
285
- | `jskelet/layout` | The path to the framework's default `layout.ejs` file |
286
-
287
- ## What's next
288
-
289
- - Why it works this way: [02-architecture.md](./02-architecture.md)
290
- - More routes and catch-all patterns: [03-routing.md](./03-routing.md)
291
- - Taking over the layout, and metadata: [04-rendering.md](./04-rendering.md)
292
- - Tuning the cache: [06-caching.md](./06-caching.md)
1
+ # 01 — Getting started
2
+
3
+ This document explains how to get JSkelet running from scratch: installing the
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
7
+ on the server, cached, and whose island hydrates on visibility. For the
8
+ *reasons* behind the decisions see
9
+ [02-architecture.md](./02-architecture.md), and for the full reference of every
10
+ config field mentioned here see
11
+ [07-configuration.md](./07-configuration.md).
12
+
13
+ ## Requirements
14
+
15
+ - **Node.js 22 or newer.** `package.json` → `engines` enforces this. The
16
+ framework uses new Node surfaces such as `node:async_hooks`,
17
+ `fs.readdirSync(..., { recursive: true })`, `--env-file-if-exists` and
18
+ `module.register()` directly.
19
+ - If you are going to use Tailwind CSS, the `postcss`, `@tailwindcss/postcss`
20
+ and `tailwindcss` packages. These are **optional peer dependencies** of the
21
+ framework; if they are not installed the CSS step is skipped and the site
22
+ stays unstyled but working (details: [08-build.md](./08-build.md)).
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ mkdir my-site && cd my-site
28
+ npm init -y
29
+ npm pkg set type=module
30
+ npm install jskelet
31
+ npm install -D postcss @tailwindcss/postcss tailwindcss lightningcss
32
+ ```
33
+
34
+ `type: "module"` is required: route modules, components and the config file are
35
+ loaded as ESM.
36
+
37
+ Then add the scripts to `package.json`:
38
+
39
+ ```json
40
+ {
41
+ "scripts": {
42
+ "dev": "jskelet dev",
43
+ "build": "jskelet build",
44
+ "start": "jskelet start"
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## `jskelet init`
50
+
51
+ ```bash
52
+ npx jskelet init
53
+ ```
54
+
55
+ This command installs a working minimal skeleton into the directory you are in.
56
+ It **does not overwrite existing files**: running it a second time only fills in
57
+ what is missing and prints the number of skipped files as a warning. The goal is
58
+ to skip the "I installed it but nothing works" stage entirely — `jskelet dev`
59
+ runs right afterwards.
60
+
61
+ The files it creates:
62
+
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
74
+ ```
75
+
76
+ Then:
77
+
78
+ ```bash
79
+ npm run dev
80
+ ```
81
+
82
+ In the terminal you will see a banner, aligned build lines and a `Ready`
83
+ summary; `http://localhost:3000` serves the page. A dev overlay bubble sits in
84
+ the bottom right corner, opened with `Alt+D`
85
+ ([09-dev-tools.md](./09-dev-tools.md)).
86
+
87
+ ## Directory layout
88
+
89
+ None of the directory names are fixed; all of them can be overridden via
90
+ `jskelet.config.mjs` → `paths`. The values below are the defaults
91
+ (`src/config/defaults.js`).
92
+
93
+ | Directory | Default | Contents |
94
+ | --- | --- | --- |
95
+ | `views` | `views` | EJS layout, pages and components |
96
+ | `public` | `public` | Static files; build output is written here too |
97
+ | `client` | `client` | Island runtime sources and entries |
98
+ | `routes` | `routes` | Route modules |
99
+ | `styles` | `styles/globals.css` | Tailwind/PostCSS entry **file** |
100
+ | `generated` | `.jskelet` | Intermediate build output: `manifest.json`, `metafile.json`, `images.json` |
101
+
102
+ In addition to these the framework always derives two paths and accepts no
103
+ separate setting for them: `public/assets` (hashed build output) and
104
+ `public/fonts` (self-hosted fonts).
105
+
106
+ A typical project:
107
+
108
+ ```
109
+ my-site/
110
+ ├── jskelet.config.mjs
111
+ ├── jsconfig.json
112
+ ├── routes/
113
+ │ ├── 10-pages.mjs
114
+ │ └── 90-catch-all.mjs
115
+ ├── views/
116
+ │ ├── layout.ejs
117
+ │ ├── pages/
118
+ │ │ ├── home.ejs
119
+ │ │ └── not-found.ejs
120
+ │ └── components/
121
+ │ └── card.js
122
+ ├── client/
123
+ │ ├── entries/
124
+ │ │ └── main.js
125
+ │ └── islands/
126
+ │ └── counter.js
127
+ ├── styles/
128
+ │ └── globals.css
129
+ ├── public/
130
+ │ └── (static files; build → public/assets)
131
+ └── .jskelet/
132
+ └── manifest.json
133
+ ```
134
+
135
+ ## Your first route
136
+
137
+ Route modules **do not derive URLs automatically from the file system**; every
138
+ module writes its own paths explicitly with `app.get(...)`. The module contract:
139
+ a default export or a named export called `register`, with the signature
140
+ `(app, api)`.
141
+
142
+ ```js
143
+ // routes/10-pages.mjs
144
+ export default function register(app, { route }) {
145
+ app.get(
146
+ "/",
147
+ route(
148
+ async () => ({
149
+ view: "pages/home",
150
+ metadata: { title: "Home" },
151
+ data: { heading: "JSkelet is running", items: ["One", "Two"] },
152
+ }),
153
+ { revalidate: 60 },
154
+ ),
155
+ );
156
+ }
157
+ ```
158
+
159
+ The `api` object comes with `route`, `renderView`, `renderPage`, `notFound`,
160
+ `redirect` and `permanentRedirect` ready to use, so route files don't have to
161
+ import them one by one from the framework. `route()` wraps the controller: the
162
+ HTML cache, the notFound/redirect control flow, compression and the
163
+ `X-JSkelet-Cache` header all come from it. The controller's only job is to
164
+ return a page definition.
165
+
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).
170
+
171
+ The template side is plain EJS:
172
+
173
+ ```ejs
174
+ <%# views/pages/home.ejs %>
175
+ <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
+ </section>
180
+ ```
181
+
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)).
185
+
186
+ ## Your first island
187
+
188
+ An island is a small module that adds behaviour to the HTML the server
189
+ produced. The contract has two parts.
190
+
191
+ **1. A marker in the template:** give an element `data-island="ad"`. Props are
192
+ carried as JSON inside `data-island-props`.
193
+
194
+ ```ejs
195
+ <div data-island="counter" data-island-props='{"start":5}'></div>
196
+ ```
197
+
198
+ **2. A `mount` in the module:** the island provides a named export called
199
+ `mount(element, props)`.
200
+
201
+ ```js
202
+ // client/islands/counter.js
203
+ /**
204
+ * @param {HTMLElement} element
205
+ * @param {{ start?: number }} props
206
+ */
207
+ export function mount(element, props) {
208
+ let value = props.start ?? 0;
209
+
210
+ const button = document.createElement("button");
211
+ button.type = "button";
212
+
213
+ const paint = () => {
214
+ button.textContent = `Clicks: ${value}`;
215
+ };
216
+
217
+ button.addEventListener("click", () => {
218
+ value += 1;
219
+ paint();
220
+ });
221
+
222
+ paint();
223
+ element.append(button);
224
+ }
225
+ ```
226
+
227
+ **3. Registration:** `client/entries/main.js` maps the island name to a dynamic
228
+ import and starts the runtime.
229
+
230
+ ```js
231
+ import { registerAll, start } from "jskelet/client";
232
+
233
+ registerAll({
234
+ counter: () => import("../islands/counter.js"),
235
+ });
236
+
237
+ start();
238
+ ```
239
+
240
+ It is critical that the values are dynamic imports: the module is downloaded
241
+ only if that island actually exists on the page **and** when the element becomes
242
+ visible. In other words, growing this map does not grow the initial payload.
243
+ Hydration strategies (`data-island-eager`, `data-island-idle`) and the complete
244
+ runtime API are in [05-islands.md](./05-islands.md).
245
+
246
+ ## CLI commands
247
+
248
+ `bin/jskelet.mjs` offers four subcommands. Each runs in a separate Node process;
249
+ the reason is that `dev` manages two long-lived processes and the server needs
250
+ ESM resolve hooks (`--import`) at process start.
251
+
252
+ | Command | What it does |
253
+ | --- | --- |
254
+ | `jskelet dev` | Build watch + server, in a single terminal. Live reload, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
255
+ | `jskelet build` | One-shot prod build: fonts → icon sprite → CSS → client JS → images → manifest → precompress. `production` if `NODE_ENV` is not given. |
256
+ | `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. |
258
+
259
+ An unknown command, or a call with no arguments, prints the usage text.
260
+
261
+ Every command runs with two Node flags:
262
+
263
+ - `--env-file=.env` — passed only if the file really exists; otherwise no flag
264
+ is added and no warning is printed.
265
+ - `--import <register.mjs>` — installs the ESM hooks that resolve the
266
+ `compilerOptions.paths` aliases (`@/lib/x`) from `jsconfig.json` /
267
+ `tsconfig.json` and extensionless relative imports (`./cache` →
268
+ `./cache.js`). (`jskelet dev` installs these hooks in its own child
269
+ processes, not in the outer process.)
270
+
271
+ ## Import paths
272
+
273
+ The `package.json` → `exports` map defines the stable surface. In examples, use
274
+ only these specifiers:
275
+
276
+ | Specifier | Contents |
277
+ | --- | --- |
278
+ | `jskelet` | Server API: `route`, `renderPage`, `renderView`, `renderNotFound`, `createApp`, `startServer`, `notFound`, `redirect`, `permanentRedirect`, `cache`, `withRequestCache`, `reportUpstreamFailure`, `asset`, `hasAsset`, `optimizedImage`, `getSpriteIds`, `headHints`, `renderHeadMeta`, HTML cache functions, `prewarm`, `createProxy`, `getConfig`, `loadConfig` and the html/tag helpers |
279
+ | `jskelet/server` | The same module as `jskelet` (an alias for readability) |
280
+ | `jskelet/client` | Browser runtime: `register`, `registerAll`, `hydrate`, `observeDocument`, `start`, `createStore`, DOM helpers, `startSafeImages` |
281
+ | `jskelet/html` | `esc`, `attrs`, `cx`, `cn`, `jsonScript` |
282
+ | `jskelet/tags` | `link`, `image`, `icon`, `preloadImage`, `toKebab` |
283
+ | `jskelet/log` | Console output helpers (`banner`, `event`, `task`, `size`, `ms`, …) |
284
+ | `jskelet/register` | Alias + extension hooks via `node --import jskelet/register` |
285
+ | `jskelet/layout` | The path to the framework's default `layout.ejs` file |
286
+
287
+ ## What's next
288
+
289
+ - Why it works this way: [02-architecture.md](./02-architecture.md)
290
+ - More routes and catch-all patterns: [03-routing.md](./03-routing.md)
291
+ - Taking over the layout, and metadata: [04-rendering.md](./04-rendering.md)
292
+ - Tuning the cache: [06-caching.md](./06-caching.md)