jskelet 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
|
@@ -1,329 +1,329 @@
|
|
|
1
|
-
# 02 — Architecture and the reasoning behind the decisions
|
|
2
|
-
|
|
3
|
-
This document does not explain how JSkelet works, but **why it works this way**.
|
|
4
|
-
The path a request takes from the server to the browser, why the island model is
|
|
5
|
-
tied to visibility, why the HTML is produced in full, why the cache lives in
|
|
6
|
-
process memory and why the middleware order must not be shuffled — that is all
|
|
7
|
-
here. Most of the reasoning comes from the measurement notes in the headers of
|
|
8
|
-
the source files; for the APIs themselves see documents
|
|
9
|
-
[03](./03-routing.md), [04](./04-rendering.md), [05](./05-islands.md) and
|
|
10
|
-
[06](./06-caching.md).
|
|
11
|
-
|
|
12
|
-
## The basic premise
|
|
13
|
-
|
|
14
|
-
On a news or content site, almost everything the visitor sees is already ready
|
|
15
|
-
on the server. Interaction, on the other hand, is scattered point by point: a
|
|
16
|
-
search box, a drawer, a chart, a comment form. With that profile, rebuilding the
|
|
17
|
-
whole page on the client (hydration) is the biggest cost you pay, and in return
|
|
18
|
-
the visitor gains nothing.
|
|
19
|
-
|
|
20
|
-
JSkelet puts this observation at the centre of the architecture:
|
|
21
|
-
|
|
22
|
-
1. **The server HTML is complete.** Even if JS never runs, the page can be read,
|
|
23
|
-
navigated and indexed.
|
|
24
|
-
2. **JS only adds behaviour.** Every interactive piece is attached as an
|
|
25
|
-
independent "island", with its own module, at its own time.
|
|
26
|
-
3. **Page production is cached.** There is no point in producing the same HTML
|
|
27
|
-
again on every request; a memory cache with a TTL takes the place of ISR.
|
|
28
|
-
4. **Templates are compiled at build time (`.jsk`).** No request-time parsing;
|
|
29
|
-
EJS remains as a legacy path. Features may co-locate under
|
|
30
|
-
`features/<name>/{server,views,client}` — route URLs stay explicit.
|
|
31
|
-
|
|
32
|
-
## The path of a request
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
Request
|
|
36
|
-
├─ rewrites(beforeFiles) config → proxy or a change to req.url
|
|
37
|
-
├─ compression brotli/gzip negotiation (quality 5)
|
|
38
|
-
├─ headers static cache + config headers()
|
|
39
|
-
├─ devGate if the gate is on, 404 without a token
|
|
40
|
-
├─ redirects config redirects(), first match wins
|
|
41
|
-
├─ trailingSlash 308 when config trailingSlash is true
|
|
42
|
-
├─ robots.txt appends framework Disallow rules to the user's body
|
|
43
|
-
├─ staticPrecompressed .br/.gz copies produced at build (quality 11)
|
|
44
|
-
├─ express.static files under public/
|
|
45
|
-
├─ (dev) devtools only when NODE_ENV=development
|
|
46
|
-
├─ admin (if enabled) /_jskelet/admin
|
|
47
|
-
├─ image optimizer (remote) /_jskelet/image — when allowHosts is set
|
|
48
|
-
├─ body parsers urlencoded 64kb + json 256kb
|
|
49
|
-
├─ rewrites(afterFiles) after static has been tried
|
|
50
|
-
├─ routes
|
|
51
|
-
│ └─ route(controller)
|
|
52
|
-
│ └─ withHtmlCache TTL + stale-while-revalidate
|
|
53
|
-
│ └─ withUpstreamTracking
|
|
54
|
-
│ └─ withRequestCache
|
|
55
|
-
│ └─ controller → renderPage → .jsk (or legacy EJS)
|
|
56
|
-
├─ 404 → hooks.notFound()
|
|
57
|
-
└─ error handling redirect/notFound + 500 fallback
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Why the middleware order is this order
|
|
61
|
-
|
|
62
|
-
The real value of the `src/server/create-app.js` file is the order; every
|
|
63
|
-
position has a reason, and moving things around leads to silent breakage.
|
|
64
|
-
|
|
65
|
-
- **`rewrites(beforeFiles)` comes even before static files.** Otherwise a rule
|
|
66
|
-
that moves the `/assets/x.js` path somewhere else would never take effect,
|
|
67
|
-
because `express.static` answers the request first.
|
|
68
|
-
- **`compression` before static.** If it came after, static files would never be
|
|
69
|
-
compressed.
|
|
70
|
-
- **`headers` → `devGate` → `redirects` → `trailingSlash`.** The gate's 404 must
|
|
71
|
-
come before the redirects: an environment that has not gone live should not
|
|
72
|
-
leak even its redirect rules to the outside. `trailingSlash` sits after config
|
|
73
|
-
redirects so explicit rules see the requested path first; the canonical slash
|
|
74
|
-
form is enforced as a second step.
|
|
75
|
-
- **`robots.txt` before static, and inside compression.** The body the
|
|
76
|
-
application wrote is left intact; the framework appends `Disallow` rules
|
|
77
|
-
for its own endpoints. A response that already has `Content-Encoding` is
|
|
78
|
-
not rewritten — the block is added to plain text, and compression stays
|
|
79
|
-
outside.
|
|
80
|
-
- **`staticPrecompressed` before `express.static`.** If there are `.br`/`.gz`
|
|
81
|
-
copies produced at build time, those are served (brotli quality 11);
|
|
82
|
-
otherwise the request falls through to the `static` below it and the
|
|
83
|
-
middleware compresses on the fly (quality 5). Recompressing a hashed,
|
|
84
|
-
`immutable` file on every request is wasted CPU. In production the `stat`
|
|
85
|
-
result (present or missing) stays in process memory; in development it does not.
|
|
86
|
-
- **Admin panel** (when `admin().enabled` / `JSKELET_ADMIN`): after static,
|
|
87
|
-
before body parsers and routes. Carries its own body parsers so the app
|
|
88
|
-
cannot shadow the path. When off, the module is never loaded.
|
|
89
|
-
- **Body parsers after static.** Image requests should not pay the cost of body
|
|
90
|
-
parsing.
|
|
91
|
-
- **`rewrites(afterFiles)` after static has been tried and before pages.** The
|
|
92
|
-
equivalent of Next.js's two-phase rewrite semantics.
|
|
93
|
-
- **404 and error handling last.** The error handler also catches the
|
|
94
|
-
`notFound`/`redirect` control flow, because those can be thrown outside a
|
|
95
|
-
controller as well (e.g. inside a middleware).
|
|
96
|
-
|
|
97
|
-
The framework turns off `x-powered-by` and writes a brandable header in its
|
|
98
|
-
place, sets `etag` to `strong` and enables `trust proxy`. `trust proxy` is
|
|
99
|
-
required for the correct protocol and client IP behind a reverse proxy
|
|
100
|
-
([10-deployment.md](./10-deployment.md)).
|
|
101
|
-
|
|
102
|
-
## The island model: why visibility-based hydration
|
|
103
|
-
|
|
104
|
-
`src/client/registry.js` hands every `[data-island]` element to an
|
|
105
|
-
`IntersectionObserver` (`rootMargin: "200px 0px"`). Elements on screen are
|
|
106
|
-
triggered on the very first observation; those off screen are **never
|
|
107
|
-
downloaded** until they are scrolled to. Heavy modules like the chart library on
|
|
108
|
-
the home page thus drop out of the initial load entirely.
|
|
109
|
-
|
|
110
|
-
There are three behaviours, all controlled from the HTML:
|
|
111
|
-
|
|
112
|
-
- **Default:** tied to visibility.
|
|
113
|
-
- **`data-island-eager`:** independent of visibility, attaches immediately. For
|
|
114
|
-
global behaviours like the header or the cookie banner.
|
|
115
|
-
- **`data-island-idle`:** even if it is visible, it waits until `load` has
|
|
116
|
-
completed and the main thread is free. So that heavy modules that are visible
|
|
117
|
-
in the first viewport but not critical (e.g. a mini chart that pulls in a
|
|
118
|
-
chart library) do not compete with LCP.
|
|
119
|
-
|
|
120
|
-
Two further details came out of measurement:
|
|
121
|
-
|
|
122
|
-
- **The attaching work is deferred to idle time** (`requestIdleCallback`,
|
|
123
|
-
`timeout: 500`). If many islands that become visible at once turn into a
|
|
124
|
-
single long task, TBT and INP suffer.
|
|
125
|
-
- **Elements with no layout box are attached directly.** A `hidden`
|
|
126
|
-
drawer/dialog has no layout box and `IntersectionObserver` will never report
|
|
127
|
-
it; that is why `hydrate()` reads the measurements in one pass
|
|
128
|
-
(`getClientRects().length`) and, instead of handing boxless elements to the
|
|
129
|
-
observer, attaches them immediately.
|
|
130
|
-
|
|
131
|
-
One consequence of this: **image error handling is not an island.** An
|
|
132
|
-
image-heavy page can have 80+ `<img>` elements, and attaching a separate island
|
|
133
|
-
to each one (observer + dynamic import + mount) is a serious hydration cost just
|
|
134
|
-
for the possibility of an error. `startSafeImages()` instead installs a single
|
|
135
|
-
capture-phase listener on the document ([05-islands.md](./05-islands.md)).
|
|
136
|
-
|
|
137
|
-
## Why the server HTML is complete
|
|
138
|
-
|
|
139
|
-
The layout and the page template produce the entirety of the content the visitor
|
|
140
|
-
will see. There is no "show a skeleton, then fill it in" pattern on the client
|
|
141
|
-
side. This buys three things:
|
|
142
|
-
|
|
143
|
-
1. **SEO:** the crawler does not have to wait for JS.
|
|
144
|
-
2. **LCP:** the largest contentful element arrives in the first HTML response;
|
|
145
|
-
downloading, parsing and executing JS is not on the LCP path.
|
|
146
|
-
3. **CLS:** because content is not injected later, the layout does not shift.
|
|
147
|
-
|
|
148
|
-
The same principle is applied on the `<head>` side too. The layout prints
|
|
149
|
-
resource hints (`preconnect`, LCP `preload`) at the **very beginning** of the
|
|
150
|
-
`<head>`; delaying those writes straight to LCP.
|
|
151
|
-
|
|
152
|
-
### Why a single, render-blocking stylesheet
|
|
153
|
-
|
|
154
|
-
No separate "critical CSS" is produced. In measurement, because the inline
|
|
155
|
-
critical CSS did not fully cover the first viewport, the page reflowed once the
|
|
156
|
-
sheet arrived (CLS 0.307 on a list page) and the same ~27 KB was repeated in
|
|
157
|
-
every HTML response. Leaving the compressed global `app.css` render-blocking is
|
|
158
|
-
both faster and free of CLS; on the second visit it already comes from the
|
|
159
|
-
`immutable` cache.
|
|
160
|
-
|
|
161
|
-
Page-specific rules can use `styles/pages/*.css` plus controller
|
|
162
|
-
`styles: [...]` ([08-build.md](./08-build.md)); those are also render-blocking
|
|
163
|
-
but only on the pages that ask for them. Keep Tailwind utilities in the global
|
|
164
|
-
sheet — a full `@import "tailwindcss"` in a page sheet duplicates utility
|
|
165
|
-
output.
|
|
166
|
-
|
|
167
|
-
The same logic applies to icons: instead of a separate request per icon, an SVG
|
|
168
|
-
sprite is produced at build time from only the symbols actually used in the
|
|
169
|
-
source. Shipping the whole Phosphor set is 1500+ icons, that is several
|
|
170
|
-
megabytes; the scan typically keeps the sprite at 10-30 symbols
|
|
171
|
-
([08-build.md](./08-build.md)).
|
|
172
|
-
|
|
173
|
-
## Cache strategy: in-memory TTL instead of ISR
|
|
174
|
-
|
|
175
|
-
`src/server/html-cache.js` keeps an LRU HTML cache with a TTL, keyed by route +
|
|
176
|
-
query (at most 500 entries). When the TTL expires the entry is not thrown away
|
|
177
|
-
immediately: within the `stale` window the old HTML returns instantly and the
|
|
178
|
-
refresh runs in the background (stale-while-revalidate, `STALE_FACTOR = 1`, i.e.
|
|
179
|
-
the stale window is as long as the TTL).
|
|
180
|
-
|
|
181
|
-
The gain: after the first warm-up no request ever waits for a render. The price:
|
|
182
|
-
the data in the HTML can be at most `revalidate + one refresh round` behind.
|
|
183
|
-
That price is acceptable, because live fields such as prices are updated on the
|
|
184
|
-
client from a WebSocket and the lag is not visible on screen.
|
|
185
|
-
|
|
186
|
-
The decision not to write to disk is deliberate. The equivalent of Next's
|
|
187
|
-
build-time prerender is prewarm, but the output is not written to disk: because
|
|
188
|
-
the cache lives in process memory, the warm-up is done when the process comes
|
|
189
|
-
up. The gain is the same — the first visitor does not wait for a cold render —
|
|
190
|
-
but the data is not frozen; every entry ages with the route's `revalidate`
|
|
191
|
-
duration ([06-caching.md](./06-caching.md)).
|
|
192
|
-
|
|
193
|
-
### Keeping the compressed body in the cache
|
|
194
|
-
|
|
195
|
-
Every cached entry stores the brotli/gzip output alongside the HTML (the
|
|
196
|
-
`encoded` map shares its lifetime with the HTML). The same page is not
|
|
197
|
-
re-brotli'd on every request. Because `Content-Encoding` is set inside `route()`
|
|
198
|
-
on this path, the compression middleware does not kick in.
|
|
199
|
-
|
|
200
|
-
### Why transient and permanent upstream errors are handled differently
|
|
201
|
-
|
|
202
|
-
If an upstream went down during the render, the output contains incomplete data,
|
|
203
|
-
and such HTML is **not** written to the cache: the next request tries again.
|
|
204
|
-
|
|
205
|
-
But this only applies to *transient* errors (network errors, 408, 425, 429 and
|
|
206
|
-
all 5xx). Deterministic answers like 400/403/404 do not get better by retrying;
|
|
207
|
-
turning off the cache because of them would mean rendering the page from scratch
|
|
208
|
-
on every visit — the content comes back in the same incomplete state anyway, the
|
|
209
|
-
visitor merely pays the render time. That is why permanent errors are only
|
|
210
|
-
logged and do not block the cache.
|
|
211
|
-
|
|
212
|
-
The direction in which this information reaches the framework is also
|
|
213
|
-
deliberately inverted: the framework does not know the data layer, the data
|
|
214
|
-
layer notifies the framework (`reportUpstreamFailure()`). If nobody ever calls
|
|
215
|
-
it, the cost is an empty array.
|
|
216
|
-
|
|
217
|
-
### The nesting order of the three scopes
|
|
218
|
-
|
|
219
|
-
`route()` sets up this order:
|
|
220
|
-
|
|
221
|
-
```
|
|
222
|
-
withHtmlCache( withUpstreamTracking( withRequestCache( controller ) ) )
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
The order matters: **the per-request cache must be innermost** so that two calls
|
|
226
|
-
within the same render collapse into a single upstream request; **upstream
|
|
227
|
-
tracking must be inside the HTML cache** so that output produced with incomplete
|
|
228
|
-
data is not written to the cache.
|
|
229
|
-
|
|
230
|
-
## Fault tolerance: no single gap takes the site down
|
|
231
|
-
|
|
232
|
-
There is a principle repeated throughout the framework: missing configuration or
|
|
233
|
-
missing build output produces a degraded but working page instead of an error.
|
|
234
|
-
|
|
235
|
-
- **If the config file is missing or unreadable**, a warning is printed and the
|
|
236
|
-
server comes up with the defaults. A broken edit must not make the site
|
|
237
|
-
impossible to open. In the same way, if one of the
|
|
238
|
-
`headers()`/`redirects()`/`rewrites()`/`cache()` sections throws, only that
|
|
239
|
-
section is ignored.
|
|
240
|
-
- **If hooks throw**, the framework falls back to its own default and warns.
|
|
241
|
-
- **If the build did not run**, `asset()` returns `/assets/<name>`, `hasAsset()`
|
|
242
|
-
is false and the layout does not print the stylesheet/script tags at all. When
|
|
243
|
-
`jskelet build` is forgotten you see an unstyled but working page instead of
|
|
244
|
-
an error.
|
|
245
|
-
- **A broken route module in dev** prints a warning and is skipped; **in
|
|
246
|
-
production it throws.** Going live with a half-built route table means pages
|
|
247
|
-
that silently return 404.
|
|
248
|
-
- **If the 404 render blows up too**, a minimal, template-free HTML is returned;
|
|
249
|
-
the visitor should not see an empty response.
|
|
250
|
-
- **A single request error does not take the process down:**
|
|
251
|
-
`unhandledRejection` and `uncaughtException` are logged and the process stays
|
|
252
|
-
up. On a news site, an error on a single page must not take the whole site
|
|
253
|
-
down.
|
|
254
|
-
|
|
255
|
-
## Why there is no file-system-based routing
|
|
256
|
-
|
|
257
|
-
Order matters. If a single-segment catch-all such as `/:slug` is registered
|
|
258
|
-
before the `/about` route, "about" is mistaken for a slug. Making the order
|
|
259
|
-
visible instead of hiding it in file names makes diagnosis easier: either you
|
|
260
|
-
give an explicit list via `jskelet.config.mjs` → `routes`, or you have the
|
|
261
|
-
`routes/` directory scanned alphabetically and put a numeric prefix on the file
|
|
262
|
-
names (`10-pages.js`, `50-blog.js`, `99-catch-all.js`). Details:
|
|
263
|
-
[03-routing.md](./03-routing.md).
|
|
264
|
-
|
|
265
|
-
## Why a single source of truth for config
|
|
266
|
-
|
|
267
|
-
`src/config/index.js` normalizes the project root, the directory paths, the
|
|
268
|
-
branding, the hooks and the rules. Other modules do not compute paths, they call
|
|
269
|
-
`getConfig()`. The reason is concrete: once the framework lives inside
|
|
270
|
-
`node_modules/`, every file that tries to find the root by counting `../..`
|
|
271
|
-
breaks. For the same reason there is a single mutation point on the build side
|
|
272
|
-
too (`initBuildPaths()`).
|
|
273
|
-
|
|
274
|
-
If `getConfig()` is used without `loadConfig()` having been called, it throws
|
|
275
|
-
instead of assuming an empty project root: a silently wrong path turns into
|
|
276
|
-
hard-to-diagnose problems like "why is there no stylesheet".
|
|
277
|
-
|
|
278
|
-
## Why this dependency list
|
|
279
|
-
|
|
280
|
-
There are three runtime dependencies: `express`, `esbuild`, `tailwind-merge`.
|
|
281
|
-
`ejs` is an optional peer only for legacy `.ejs` templates. Everything else
|
|
282
|
-
(Tailwind, PostCSS, lightningcss, sharp, the Phosphor icons) is an **optional
|
|
283
|
-
peer dependency**, and if it is absent the corresponding build step is skipped.
|
|
284
|
-
|
|
285
|
-
Two decisions deserve a separate explanation:
|
|
286
|
-
|
|
287
|
-
- **`node:zlib` instead of the `compression` package.** The package does not
|
|
288
|
-
support brotli and brings a seven-deep dependency tree; doing the brotli +
|
|
289
|
-
gzip negotiation by hand is enough. Brotli is preferred: on the home page HTML
|
|
290
|
-
it is ~35% smaller than gzip.
|
|
291
|
-
- **`tailwind-merge` stays at runtime.** Class computation is done only on the
|
|
292
|
-
server, it never enters the client bundle, so it has no effect on page weight.
|
|
293
|
-
A hand-written group table, on the other hand, produced visual regressions
|
|
294
|
-
because it mixed up width/colour pairs like `border-2` +
|
|
295
|
-
`border-transparent` and dropped classes.
|
|
296
|
-
|
|
297
|
-
Optional packages are resolved from the **application's** `node_modules`, not
|
|
298
|
-
from the framework's own. If the framework is installed via a `file:` or
|
|
299
|
-
workspace link, a plain `import "postcss"` looks in the framework's tree — not
|
|
300
|
-
in the application's.
|
|
301
|
-
|
|
302
|
-
## Why alias and extension hooks
|
|
303
|
-
|
|
304
|
-
`node --import jskelet/register` does two things:
|
|
305
|
-
|
|
306
|
-
1. It resolves the `compilerOptions.paths` aliases in `jsconfig.json` /
|
|
307
|
-
`tsconfig.json` (`@/lib/x` → `<root>/lib/x`). Because the editor and the
|
|
308
|
-
runtime are fed from the same file, the two do not drift apart.
|
|
309
|
-
2. It adds extensions to extensionless relative imports (`./cache` →
|
|
310
|
-
`./cache.js`). Node ESM does not do this, and it is the most common breaking
|
|
311
|
-
point in code migrated from a bundler.
|
|
312
|
-
|
|
313
|
-
The `@/` resolution on the esbuild side mimics the same behaviour, so that
|
|
314
|
-
modules under `lib/` can use the same import style both on the server and in the
|
|
315
|
-
browser.
|
|
316
|
-
|
|
317
|
-
`--import` expects a module **specifier**, not a file path. On Windows an
|
|
318
|
-
absolute path like `H:\...` is mistaken for a URL with the `h:` scheme and
|
|
319
|
-
rejected; that is why the framework uses `pathToFileURL(...).href` everywhere.
|
|
320
|
-
For the same reason the config, the route modules and the components are
|
|
321
|
-
imported with a `file://` URL too.
|
|
322
|
-
|
|
323
|
-
## What's next
|
|
324
|
-
|
|
325
|
-
- The route and controller contract: [03-routing.md](./03-routing.md)
|
|
326
|
-
- The template layer and metadata: [04-rendering.md](./04-rendering.md)
|
|
327
|
-
- The island runtime API: [05-islands.md](./05-islands.md)
|
|
328
|
-
- Cache settings and prewarm: [06-caching.md](./06-caching.md)
|
|
329
|
-
- The inner workings of the dev flow: [09-dev-tools.md](./09-dev-tools.md)
|
|
1
|
+
# 02 — Architecture and the reasoning behind the decisions
|
|
2
|
+
|
|
3
|
+
This document does not explain how JSkelet works, but **why it works this way**.
|
|
4
|
+
The path a request takes from the server to the browser, why the island model is
|
|
5
|
+
tied to visibility, why the HTML is produced in full, why the cache lives in
|
|
6
|
+
process memory and why the middleware order must not be shuffled — that is all
|
|
7
|
+
here. Most of the reasoning comes from the measurement notes in the headers of
|
|
8
|
+
the source files; for the APIs themselves see documents
|
|
9
|
+
[03](./03-routing.md), [04](./04-rendering.md), [05](./05-islands.md) and
|
|
10
|
+
[06](./06-caching.md).
|
|
11
|
+
|
|
12
|
+
## The basic premise
|
|
13
|
+
|
|
14
|
+
On a news or content site, almost everything the visitor sees is already ready
|
|
15
|
+
on the server. Interaction, on the other hand, is scattered point by point: a
|
|
16
|
+
search box, a drawer, a chart, a comment form. With that profile, rebuilding the
|
|
17
|
+
whole page on the client (hydration) is the biggest cost you pay, and in return
|
|
18
|
+
the visitor gains nothing.
|
|
19
|
+
|
|
20
|
+
JSkelet puts this observation at the centre of the architecture:
|
|
21
|
+
|
|
22
|
+
1. **The server HTML is complete.** Even if JS never runs, the page can be read,
|
|
23
|
+
navigated and indexed.
|
|
24
|
+
2. **JS only adds behaviour.** Every interactive piece is attached as an
|
|
25
|
+
independent "island", with its own module, at its own time.
|
|
26
|
+
3. **Page production is cached.** There is no point in producing the same HTML
|
|
27
|
+
again on every request; a memory cache with a TTL takes the place of ISR.
|
|
28
|
+
4. **Templates are compiled at build time (`.jsk`).** No request-time parsing;
|
|
29
|
+
EJS remains as a legacy path. Features may co-locate under
|
|
30
|
+
`features/<name>/{server,views,client}` — route URLs stay explicit.
|
|
31
|
+
|
|
32
|
+
## The path of a request
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Request
|
|
36
|
+
├─ rewrites(beforeFiles) config → proxy or a change to req.url
|
|
37
|
+
├─ compression brotli/gzip negotiation (quality 5)
|
|
38
|
+
├─ headers static cache + config headers()
|
|
39
|
+
├─ devGate if the gate is on, 404 without a token
|
|
40
|
+
├─ redirects config redirects(), first match wins
|
|
41
|
+
├─ trailingSlash 308 when config trailingSlash is true
|
|
42
|
+
├─ robots.txt appends framework Disallow rules to the user's body
|
|
43
|
+
├─ staticPrecompressed .br/.gz copies produced at build (quality 11)
|
|
44
|
+
├─ express.static files under public/
|
|
45
|
+
├─ (dev) devtools only when NODE_ENV=development
|
|
46
|
+
├─ admin (if enabled) /_jskelet/admin
|
|
47
|
+
├─ image optimizer (remote) /_jskelet/image — when allowHosts is set
|
|
48
|
+
├─ body parsers urlencoded 64kb + json 256kb
|
|
49
|
+
├─ rewrites(afterFiles) after static has been tried
|
|
50
|
+
├─ routes
|
|
51
|
+
│ └─ route(controller)
|
|
52
|
+
│ └─ withHtmlCache TTL + stale-while-revalidate
|
|
53
|
+
│ └─ withUpstreamTracking
|
|
54
|
+
│ └─ withRequestCache
|
|
55
|
+
│ └─ controller → renderPage → .jsk (or legacy EJS)
|
|
56
|
+
├─ 404 → hooks.notFound()
|
|
57
|
+
└─ error handling redirect/notFound + 500 fallback
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Why the middleware order is this order
|
|
61
|
+
|
|
62
|
+
The real value of the `src/server/create-app.js` file is the order; every
|
|
63
|
+
position has a reason, and moving things around leads to silent breakage.
|
|
64
|
+
|
|
65
|
+
- **`rewrites(beforeFiles)` comes even before static files.** Otherwise a rule
|
|
66
|
+
that moves the `/assets/x.js` path somewhere else would never take effect,
|
|
67
|
+
because `express.static` answers the request first.
|
|
68
|
+
- **`compression` before static.** If it came after, static files would never be
|
|
69
|
+
compressed.
|
|
70
|
+
- **`headers` → `devGate` → `redirects` → `trailingSlash`.** The gate's 404 must
|
|
71
|
+
come before the redirects: an environment that has not gone live should not
|
|
72
|
+
leak even its redirect rules to the outside. `trailingSlash` sits after config
|
|
73
|
+
redirects so explicit rules see the requested path first; the canonical slash
|
|
74
|
+
form is enforced as a second step.
|
|
75
|
+
- **`robots.txt` before static, and inside compression.** The body the
|
|
76
|
+
application wrote is left intact; the framework appends `Disallow` rules
|
|
77
|
+
for its own endpoints. A response that already has `Content-Encoding` is
|
|
78
|
+
not rewritten — the block is added to plain text, and compression stays
|
|
79
|
+
outside.
|
|
80
|
+
- **`staticPrecompressed` before `express.static`.** If there are `.br`/`.gz`
|
|
81
|
+
copies produced at build time, those are served (brotli quality 11);
|
|
82
|
+
otherwise the request falls through to the `static` below it and the
|
|
83
|
+
middleware compresses on the fly (quality 5). Recompressing a hashed,
|
|
84
|
+
`immutable` file on every request is wasted CPU. In production the `stat`
|
|
85
|
+
result (present or missing) stays in process memory; in development it does not.
|
|
86
|
+
- **Admin panel** (when `admin().enabled` / `JSKELET_ADMIN`): after static,
|
|
87
|
+
before body parsers and routes. Carries its own body parsers so the app
|
|
88
|
+
cannot shadow the path. When off, the module is never loaded.
|
|
89
|
+
- **Body parsers after static.** Image requests should not pay the cost of body
|
|
90
|
+
parsing.
|
|
91
|
+
- **`rewrites(afterFiles)` after static has been tried and before pages.** The
|
|
92
|
+
equivalent of Next.js's two-phase rewrite semantics.
|
|
93
|
+
- **404 and error handling last.** The error handler also catches the
|
|
94
|
+
`notFound`/`redirect` control flow, because those can be thrown outside a
|
|
95
|
+
controller as well (e.g. inside a middleware).
|
|
96
|
+
|
|
97
|
+
The framework turns off `x-powered-by` and writes a brandable header in its
|
|
98
|
+
place, sets `etag` to `strong` and enables `trust proxy`. `trust proxy` is
|
|
99
|
+
required for the correct protocol and client IP behind a reverse proxy
|
|
100
|
+
([10-deployment.md](./10-deployment.md)).
|
|
101
|
+
|
|
102
|
+
## The island model: why visibility-based hydration
|
|
103
|
+
|
|
104
|
+
`src/client/registry.js` hands every `[data-island]` element to an
|
|
105
|
+
`IntersectionObserver` (`rootMargin: "200px 0px"`). Elements on screen are
|
|
106
|
+
triggered on the very first observation; those off screen are **never
|
|
107
|
+
downloaded** until they are scrolled to. Heavy modules like the chart library on
|
|
108
|
+
the home page thus drop out of the initial load entirely.
|
|
109
|
+
|
|
110
|
+
There are three behaviours, all controlled from the HTML:
|
|
111
|
+
|
|
112
|
+
- **Default:** tied to visibility.
|
|
113
|
+
- **`data-island-eager`:** independent of visibility, attaches immediately. For
|
|
114
|
+
global behaviours like the header or the cookie banner.
|
|
115
|
+
- **`data-island-idle`:** even if it is visible, it waits until `load` has
|
|
116
|
+
completed and the main thread is free. So that heavy modules that are visible
|
|
117
|
+
in the first viewport but not critical (e.g. a mini chart that pulls in a
|
|
118
|
+
chart library) do not compete with LCP.
|
|
119
|
+
|
|
120
|
+
Two further details came out of measurement:
|
|
121
|
+
|
|
122
|
+
- **The attaching work is deferred to idle time** (`requestIdleCallback`,
|
|
123
|
+
`timeout: 500`). If many islands that become visible at once turn into a
|
|
124
|
+
single long task, TBT and INP suffer.
|
|
125
|
+
- **Elements with no layout box are attached directly.** A `hidden`
|
|
126
|
+
drawer/dialog has no layout box and `IntersectionObserver` will never report
|
|
127
|
+
it; that is why `hydrate()` reads the measurements in one pass
|
|
128
|
+
(`getClientRects().length`) and, instead of handing boxless elements to the
|
|
129
|
+
observer, attaches them immediately.
|
|
130
|
+
|
|
131
|
+
One consequence of this: **image error handling is not an island.** An
|
|
132
|
+
image-heavy page can have 80+ `<img>` elements, and attaching a separate island
|
|
133
|
+
to each one (observer + dynamic import + mount) is a serious hydration cost just
|
|
134
|
+
for the possibility of an error. `startSafeImages()` instead installs a single
|
|
135
|
+
capture-phase listener on the document ([05-islands.md](./05-islands.md)).
|
|
136
|
+
|
|
137
|
+
## Why the server HTML is complete
|
|
138
|
+
|
|
139
|
+
The layout and the page template produce the entirety of the content the visitor
|
|
140
|
+
will see. There is no "show a skeleton, then fill it in" pattern on the client
|
|
141
|
+
side. This buys three things:
|
|
142
|
+
|
|
143
|
+
1. **SEO:** the crawler does not have to wait for JS.
|
|
144
|
+
2. **LCP:** the largest contentful element arrives in the first HTML response;
|
|
145
|
+
downloading, parsing and executing JS is not on the LCP path.
|
|
146
|
+
3. **CLS:** because content is not injected later, the layout does not shift.
|
|
147
|
+
|
|
148
|
+
The same principle is applied on the `<head>` side too. The layout prints
|
|
149
|
+
resource hints (`preconnect`, LCP `preload`) at the **very beginning** of the
|
|
150
|
+
`<head>`; delaying those writes straight to LCP.
|
|
151
|
+
|
|
152
|
+
### Why a single, render-blocking stylesheet
|
|
153
|
+
|
|
154
|
+
No separate "critical CSS" is produced. In measurement, because the inline
|
|
155
|
+
critical CSS did not fully cover the first viewport, the page reflowed once the
|
|
156
|
+
sheet arrived (CLS 0.307 on a list page) and the same ~27 KB was repeated in
|
|
157
|
+
every HTML response. Leaving the compressed global `app.css` render-blocking is
|
|
158
|
+
both faster and free of CLS; on the second visit it already comes from the
|
|
159
|
+
`immutable` cache.
|
|
160
|
+
|
|
161
|
+
Page-specific rules can use `styles/pages/*.css` plus controller
|
|
162
|
+
`styles: [...]` ([08-build.md](./08-build.md)); those are also render-blocking
|
|
163
|
+
but only on the pages that ask for them. Keep Tailwind utilities in the global
|
|
164
|
+
sheet — a full `@import "tailwindcss"` in a page sheet duplicates utility
|
|
165
|
+
output.
|
|
166
|
+
|
|
167
|
+
The same logic applies to icons: instead of a separate request per icon, an SVG
|
|
168
|
+
sprite is produced at build time from only the symbols actually used in the
|
|
169
|
+
source. Shipping the whole Phosphor set is 1500+ icons, that is several
|
|
170
|
+
megabytes; the scan typically keeps the sprite at 10-30 symbols
|
|
171
|
+
([08-build.md](./08-build.md)).
|
|
172
|
+
|
|
173
|
+
## Cache strategy: in-memory TTL instead of ISR
|
|
174
|
+
|
|
175
|
+
`src/server/html-cache.js` keeps an LRU HTML cache with a TTL, keyed by route +
|
|
176
|
+
query (at most 500 entries). When the TTL expires the entry is not thrown away
|
|
177
|
+
immediately: within the `stale` window the old HTML returns instantly and the
|
|
178
|
+
refresh runs in the background (stale-while-revalidate, `STALE_FACTOR = 1`, i.e.
|
|
179
|
+
the stale window is as long as the TTL).
|
|
180
|
+
|
|
181
|
+
The gain: after the first warm-up no request ever waits for a render. The price:
|
|
182
|
+
the data in the HTML can be at most `revalidate + one refresh round` behind.
|
|
183
|
+
That price is acceptable, because live fields such as prices are updated on the
|
|
184
|
+
client from a WebSocket and the lag is not visible on screen.
|
|
185
|
+
|
|
186
|
+
The decision not to write to disk is deliberate. The equivalent of Next's
|
|
187
|
+
build-time prerender is prewarm, but the output is not written to disk: because
|
|
188
|
+
the cache lives in process memory, the warm-up is done when the process comes
|
|
189
|
+
up. The gain is the same — the first visitor does not wait for a cold render —
|
|
190
|
+
but the data is not frozen; every entry ages with the route's `revalidate`
|
|
191
|
+
duration ([06-caching.md](./06-caching.md)).
|
|
192
|
+
|
|
193
|
+
### Keeping the compressed body in the cache
|
|
194
|
+
|
|
195
|
+
Every cached entry stores the brotli/gzip output alongside the HTML (the
|
|
196
|
+
`encoded` map shares its lifetime with the HTML). The same page is not
|
|
197
|
+
re-brotli'd on every request. Because `Content-Encoding` is set inside `route()`
|
|
198
|
+
on this path, the compression middleware does not kick in.
|
|
199
|
+
|
|
200
|
+
### Why transient and permanent upstream errors are handled differently
|
|
201
|
+
|
|
202
|
+
If an upstream went down during the render, the output contains incomplete data,
|
|
203
|
+
and such HTML is **not** written to the cache: the next request tries again.
|
|
204
|
+
|
|
205
|
+
But this only applies to *transient* errors (network errors, 408, 425, 429 and
|
|
206
|
+
all 5xx). Deterministic answers like 400/403/404 do not get better by retrying;
|
|
207
|
+
turning off the cache because of them would mean rendering the page from scratch
|
|
208
|
+
on every visit — the content comes back in the same incomplete state anyway, the
|
|
209
|
+
visitor merely pays the render time. That is why permanent errors are only
|
|
210
|
+
logged and do not block the cache.
|
|
211
|
+
|
|
212
|
+
The direction in which this information reaches the framework is also
|
|
213
|
+
deliberately inverted: the framework does not know the data layer, the data
|
|
214
|
+
layer notifies the framework (`reportUpstreamFailure()`). If nobody ever calls
|
|
215
|
+
it, the cost is an empty array.
|
|
216
|
+
|
|
217
|
+
### The nesting order of the three scopes
|
|
218
|
+
|
|
219
|
+
`route()` sets up this order:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
withHtmlCache( withUpstreamTracking( withRequestCache( controller ) ) )
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The order matters: **the per-request cache must be innermost** so that two calls
|
|
226
|
+
within the same render collapse into a single upstream request; **upstream
|
|
227
|
+
tracking must be inside the HTML cache** so that output produced with incomplete
|
|
228
|
+
data is not written to the cache.
|
|
229
|
+
|
|
230
|
+
## Fault tolerance: no single gap takes the site down
|
|
231
|
+
|
|
232
|
+
There is a principle repeated throughout the framework: missing configuration or
|
|
233
|
+
missing build output produces a degraded but working page instead of an error.
|
|
234
|
+
|
|
235
|
+
- **If the config file is missing or unreadable**, a warning is printed and the
|
|
236
|
+
server comes up with the defaults. A broken edit must not make the site
|
|
237
|
+
impossible to open. In the same way, if one of the
|
|
238
|
+
`headers()`/`redirects()`/`rewrites()`/`cache()` sections throws, only that
|
|
239
|
+
section is ignored.
|
|
240
|
+
- **If hooks throw**, the framework falls back to its own default and warns.
|
|
241
|
+
- **If the build did not run**, `asset()` returns `/assets/<name>`, `hasAsset()`
|
|
242
|
+
is false and the layout does not print the stylesheet/script tags at all. When
|
|
243
|
+
`jskelet build` is forgotten you see an unstyled but working page instead of
|
|
244
|
+
an error.
|
|
245
|
+
- **A broken route module in dev** prints a warning and is skipped; **in
|
|
246
|
+
production it throws.** Going live with a half-built route table means pages
|
|
247
|
+
that silently return 404.
|
|
248
|
+
- **If the 404 render blows up too**, a minimal, template-free HTML is returned;
|
|
249
|
+
the visitor should not see an empty response.
|
|
250
|
+
- **A single request error does not take the process down:**
|
|
251
|
+
`unhandledRejection` and `uncaughtException` are logged and the process stays
|
|
252
|
+
up. On a news site, an error on a single page must not take the whole site
|
|
253
|
+
down.
|
|
254
|
+
|
|
255
|
+
## Why there is no file-system-based routing
|
|
256
|
+
|
|
257
|
+
Order matters. If a single-segment catch-all such as `/:slug` is registered
|
|
258
|
+
before the `/about` route, "about" is mistaken for a slug. Making the order
|
|
259
|
+
visible instead of hiding it in file names makes diagnosis easier: either you
|
|
260
|
+
give an explicit list via `jskelet.config.mjs` → `routes`, or you have the
|
|
261
|
+
`routes/` directory scanned alphabetically and put a numeric prefix on the file
|
|
262
|
+
names (`10-pages.js`, `50-blog.js`, `99-catch-all.js`). Details:
|
|
263
|
+
[03-routing.md](./03-routing.md).
|
|
264
|
+
|
|
265
|
+
## Why a single source of truth for config
|
|
266
|
+
|
|
267
|
+
`src/config/index.js` normalizes the project root, the directory paths, the
|
|
268
|
+
branding, the hooks and the rules. Other modules do not compute paths, they call
|
|
269
|
+
`getConfig()`. The reason is concrete: once the framework lives inside
|
|
270
|
+
`node_modules/`, every file that tries to find the root by counting `../..`
|
|
271
|
+
breaks. For the same reason there is a single mutation point on the build side
|
|
272
|
+
too (`initBuildPaths()`).
|
|
273
|
+
|
|
274
|
+
If `getConfig()` is used without `loadConfig()` having been called, it throws
|
|
275
|
+
instead of assuming an empty project root: a silently wrong path turns into
|
|
276
|
+
hard-to-diagnose problems like "why is there no stylesheet".
|
|
277
|
+
|
|
278
|
+
## Why this dependency list
|
|
279
|
+
|
|
280
|
+
There are three runtime dependencies: `express`, `esbuild`, `tailwind-merge`.
|
|
281
|
+
`ejs` is an optional peer only for legacy `.ejs` templates. Everything else
|
|
282
|
+
(Tailwind, PostCSS, lightningcss, sharp, the Phosphor icons) is an **optional
|
|
283
|
+
peer dependency**, and if it is absent the corresponding build step is skipped.
|
|
284
|
+
|
|
285
|
+
Two decisions deserve a separate explanation:
|
|
286
|
+
|
|
287
|
+
- **`node:zlib` instead of the `compression` package.** The package does not
|
|
288
|
+
support brotli and brings a seven-deep dependency tree; doing the brotli +
|
|
289
|
+
gzip negotiation by hand is enough. Brotli is preferred: on the home page HTML
|
|
290
|
+
it is ~35% smaller than gzip.
|
|
291
|
+
- **`tailwind-merge` stays at runtime.** Class computation is done only on the
|
|
292
|
+
server, it never enters the client bundle, so it has no effect on page weight.
|
|
293
|
+
A hand-written group table, on the other hand, produced visual regressions
|
|
294
|
+
because it mixed up width/colour pairs like `border-2` +
|
|
295
|
+
`border-transparent` and dropped classes.
|
|
296
|
+
|
|
297
|
+
Optional packages are resolved from the **application's** `node_modules`, not
|
|
298
|
+
from the framework's own. If the framework is installed via a `file:` or
|
|
299
|
+
workspace link, a plain `import "postcss"` looks in the framework's tree — not
|
|
300
|
+
in the application's.
|
|
301
|
+
|
|
302
|
+
## Why alias and extension hooks
|
|
303
|
+
|
|
304
|
+
`node --import jskelet/register` does two things:
|
|
305
|
+
|
|
306
|
+
1. It resolves the `compilerOptions.paths` aliases in `jsconfig.json` /
|
|
307
|
+
`tsconfig.json` (`@/lib/x` → `<root>/lib/x`). Because the editor and the
|
|
308
|
+
runtime are fed from the same file, the two do not drift apart.
|
|
309
|
+
2. It adds extensions to extensionless relative imports (`./cache` →
|
|
310
|
+
`./cache.js`). Node ESM does not do this, and it is the most common breaking
|
|
311
|
+
point in code migrated from a bundler.
|
|
312
|
+
|
|
313
|
+
The `@/` resolution on the esbuild side mimics the same behaviour, so that
|
|
314
|
+
modules under `lib/` can use the same import style both on the server and in the
|
|
315
|
+
browser.
|
|
316
|
+
|
|
317
|
+
`--import` expects a module **specifier**, not a file path. On Windows an
|
|
318
|
+
absolute path like `H:\...` is mistaken for a URL with the `h:` scheme and
|
|
319
|
+
rejected; that is why the framework uses `pathToFileURL(...).href` everywhere.
|
|
320
|
+
For the same reason the config, the route modules and the components are
|
|
321
|
+
imported with a `file://` URL too.
|
|
322
|
+
|
|
323
|
+
## What's next
|
|
324
|
+
|
|
325
|
+
- The route and controller contract: [03-routing.md](./03-routing.md)
|
|
326
|
+
- The template layer and metadata: [04-rendering.md](./04-rendering.md)
|
|
327
|
+
- The island runtime API: [05-islands.md](./05-islands.md)
|
|
328
|
+
- Cache settings and prewarm: [06-caching.md](./06-caching.md)
|
|
329
|
+
- The inner workings of the dev flow: [09-dev-tools.md](./09-dev-tools.md)
|