hono-svelte 0.4.1 → 0.5.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.
- package/CHANGELOG.md +68 -0
- package/README.md +77 -10
- package/dist/cli.js +53 -3
- package/dist/handlers.d.ts +2 -2
- package/dist/handlers.js +25 -12
- package/dist/index.d.ts +14 -9
- package/dist/index.js +56 -28
- package/dist/page-data.d.ts +7 -0
- package/dist/page-data.js +119 -0
- package/dist/scaffold.d.ts +10 -2
- package/dist/scaffold.js +344 -16
- package/dist/ssr-manifest.d.ts +0 -1
- package/dist/ssr-manifest.js +0 -4
- package/dist/vite.d.ts +2 -5
- package/dist/vite.js +77 -28
- package/package.json +40 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- **Per-page data typing (opt-in, `pages({ dts: true })`)**: declare
|
|
6
|
+
`export type Data` (or `interface Data`) in a page's `<script module>` and
|
|
7
|
+
the generated `.d.ts` adds a typed overload per page —
|
|
8
|
+
`c.render("dashboard", { data })` is checked by `tsc` (a missing/mistyped
|
|
9
|
+
field fails the typecheck instead of shipping `undefined`). The declaration
|
|
10
|
+
is inlined verbatim, so it must be self-contained; types that reference
|
|
11
|
+
imports are skipped with a one-time warning and keep
|
|
12
|
+
`Record<string, unknown>` data. `RenderProps` is now generic
|
|
13
|
+
(`RenderProps<TData = Record<string, unknown>>`) and `pages().dataTypes()`
|
|
14
|
+
exposes the extractions. The dts also regenerates when a page's `Data`
|
|
15
|
+
type changes (dev watcher + `buildStart`).
|
|
16
|
+
- **Neutral `<body>`**: the shell no longer hardcodes DaisyUI classes
|
|
17
|
+
(`bg-base-200 min-h-screen text-base-content`) — it emits a plain,
|
|
18
|
+
CSS-framework-agnostic document. Style `body`/`html` from your global
|
|
19
|
+
stylesheet instead (`body { background: var(--color-base-200); ... }`, or
|
|
20
|
+
`@apply` with Tailwind — the playground does exactly that in
|
|
21
|
+
`styles.css`). **Breaking** for apps relying on the hardcoded classes.
|
|
22
|
+
- **`shell({ ssrFallback })`**: when SSR of a page throws at request time,
|
|
23
|
+
log the error and fall back to client rendering (entry script) instead of
|
|
24
|
+
a 500. Default `false` keeps the previous behavior.
|
|
25
|
+
- `prefetch: "all"` / `"hover"` now emit `rel="modulepreload"` (prefetch on a
|
|
26
|
+
module URL could double-fetch in some browsers; modulepreload also warms
|
|
27
|
+
the entry's import graph).
|
|
28
|
+
- `dataLimit` warning fires once per **(page, limit)** instead of once per
|
|
29
|
+
process — a second oversized page is reported too, with the entry name in
|
|
30
|
+
the message.
|
|
31
|
+
- **Removed dead stubs**: `versionsMatch()` (always returned `true`), the
|
|
32
|
+
unused `hasClient` export from the ssr-manifest stub/virtual manifest, and
|
|
33
|
+
the no-op `checkPagesDir`. **Breaking: `versionsMatch` is no longer
|
|
34
|
+
exported.**
|
|
35
|
+
- Fully typed handlers/middleware: `notFoundHandler()` / `errorHandler()`
|
|
36
|
+
match Hono's `NotFoundHandler`/`ErrorHandler` signatures and `shell()` no
|
|
37
|
+
longer uses `any` internally.
|
|
38
|
+
- **CLI: `hono-svelte add page <name>`** — scaffolds
|
|
39
|
+
`src/pages/<name>.svelte` (static by default, skip-if-exists, traversal
|
|
40
|
+
refused). `isValidPageName()` / `newPageFile()` exported for tests.
|
|
41
|
+
- Metadata: `engines` now declares `bun >= 1.4` and `node >= 26`, the
|
|
42
|
+
`./package.json` export subpath was added, and TypeScript 7 builds the
|
|
43
|
+
package.
|
|
44
|
+
- **Security floors in `peerDependencies`**: `hono ^4.13.8`, `svelte ^5.57.0`,
|
|
45
|
+
`vite ^8.3.0` — every published advisory sits outside these floors
|
|
46
|
+
(hono < 4.12.12 and 4.12.0–4.12.33, patched in 4.12.34; svelte ≤ 5.55.6,
|
|
47
|
+
patched in 5.55.7; vite ≤ 8.0.15, patched in 8.0.16; transitive `devalue`
|
|
48
|
+
resolves to the patched 5.9.2, which also closes CVE-2026-81176). The
|
|
49
|
+
versions installed in the repo and in the example are the latest published
|
|
50
|
+
and none is affected by a known advisory — the ranges just avoid
|
|
51
|
+
advertising vulnerable versions to consumers and scanners.
|
|
52
|
+
- CI: ubuntu + windows runners (Windows-only regressions like the old
|
|
53
|
+
`isSafeAppPath` bug are caught now), coverage via `@vitest/coverage-v8`
|
|
54
|
+
with thresholds (`npm test` runs it), all on the officially supported
|
|
55
|
+
runtimes (Node 26, TS 7, Vite 8, Bun 1.4+).
|
|
56
|
+
- `hono-svelte init` now scaffolds TypeScript 7 + `@types/node` 26 in the
|
|
57
|
+
generated app.
|
|
58
|
+
|
|
59
|
+
## 0.4.2
|
|
60
|
+
|
|
61
|
+
- `hono-svelte init` now scaffolds a complete runnable app (works with
|
|
62
|
+
`npx`, `bunx` and `pnpm dlx` — writes text files only, no install/network).
|
|
63
|
+
Minimal flavor (default): `package.json`, `tsconfig.json`, `.gitignore`,
|
|
64
|
+
`vite.config.ts` (with `pages({ dts: true })`), two pages (`index` zero-JS,
|
|
65
|
+
`hello` with `data`), `src/routes/index.ts`, `src/styles.css`.
|
|
66
|
+
`init --full` adds auth (cookie session), dashboard with nested layout,
|
|
67
|
+
and typed RPC (`hc<AppType>`) — `/auth` → login → `/dashboard`.
|
|
68
|
+
Generated scripts (`dev`/`build`/`typecheck`/`start`) run under npm and bun.
|
|
69
|
+
`InitFlavor` type exported.
|
|
70
|
+
|
|
3
71
|
## 0.4.1
|
|
4
72
|
|
|
5
73
|
- Fix `hono-svelte init` refusing every file on Windows: `isSafeAppPath()`
|
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ Real sites and dashboards mix simple pages (landing, login, terms) with interact
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
Requires Node
|
|
18
|
+
Requires Bun 1.4+, Node 26+, Hono 4, Svelte 5, Vite 8, and TypeScript 7.
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
|
|
21
|
+
bun add hono-svelte
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## Quick start
|
|
@@ -126,7 +126,9 @@ Works for static (zero-JS) pages too: the shell passes `data` as SSR props **and
|
|
|
126
126
|
app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Declare `export type Data` in the page's `<script module>` to type it per page — see [per-page data typing](#per-page-data-typing).
|
|
130
|
+
|
|
131
|
+
Large payloads trigger a `console.warn` — once per page (default limit 100KB, `shell({ dataLimit })`).
|
|
130
132
|
Anything sensitive stays in the API (`hc<AppType>`, HttpOnly cookie). Never put secrets in `data`.
|
|
131
133
|
|
|
132
134
|
## Status, headers and per-page head
|
|
@@ -180,6 +182,8 @@ c.render("dashboard/page1", { data: { title: "Page 1", description: "..." } });
|
|
|
180
182
|
|
|
181
183
|
Layouts never become entries (`pages().layouts()` / `layoutChain(entry)` inspect them; `layouts: false` restores legacy ignore).
|
|
182
184
|
|
|
185
|
+
> **Hydration wrapper:** each layout level wraps the page's SSR output in one `<div>` (a Svelte 5 raw-snippet requirement — an empty snippet render breaks hydration). The wrapper is hydration-safe, but it is part of the DOM: account for it in CSS selectors (e.g. `:global(> div)`).
|
|
186
|
+
|
|
183
187
|
|
|
184
188
|
## Error pages
|
|
185
189
|
|
|
@@ -193,11 +197,30 @@ app.onError(errorHandler()); // renders `500.svelte` with status 500
|
|
|
193
197
|
## CLI
|
|
194
198
|
|
|
195
199
|
```sh
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
# Start a new app (no install needed):
|
|
201
|
+
bunx hono-svelte init # minimal: 2 pages + routes + configs
|
|
202
|
+
bunx hono-svelte init --full # + auth, dashboard w/ layout, typed RPC
|
|
203
|
+
|
|
204
|
+
cd my-app && bun install && bun run dev
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`init` scaffolds a complete runnable app: `package.json`, `tsconfig.json`,
|
|
208
|
+
`vite.config.ts` (with `pages({ dts: true })`), `src/pages/` and
|
|
209
|
+
`src/routes/`. The `--full` flavor adds cookie-session auth, a dashboard with
|
|
210
|
+
nested layout, and a typed API (`hc<AppType>`) — open `/auth`, sign in, explore
|
|
211
|
+
`/dashboard`. (npm/pnpm work too: `npx hono-svelte init`, `npm install`.)
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
bunx hono-svelte add page blog/post-1 # create src/pages/blog/post-1.svelte
|
|
215
|
+
bunx hono-svelte doctor # check dist, link and vite config
|
|
216
|
+
bunx hono-svelte doctor --app=./my-app # check another app directory
|
|
199
217
|
```
|
|
200
218
|
|
|
219
|
+
`add page <name>` writes a static page (no `<script>` → zero JS; add one to
|
|
220
|
+
hydrate on the client). Existing files are skipped, traversal names are
|
|
221
|
+
refused, and the default pagesDir is `src/pages` (a custom one set via
|
|
222
|
+
`pages({ pagesDir })` is not visible to the CLI — adjust the path manually).
|
|
223
|
+
|
|
201
224
|
## API
|
|
202
225
|
|
|
203
226
|
### `shell(options?)`
|
|
@@ -216,11 +239,12 @@ Hono middleware that provides `c.render(entry, { title?, data?, status?, headers
|
|
|
216
239
|
| `head` | `""` | Extra HTML in `<head>` (fonts, meta tags) |
|
|
217
240
|
| `nonce` | — | CSP nonce string or `(c) => string` (applied to shell `<script>`/`<link>`) |
|
|
218
241
|
| `preload` | `true` in prod | `<link rel="modulepreload">` for the entry (+ manifest imports) |
|
|
219
|
-
| `prefetch` | `false` | `"all"`
|
|
242
|
+
| `prefetch` | `false` | `"all"` emits `modulepreload` for other entries; `"hover"` injects a script that preloads them on link hover |
|
|
220
243
|
| `dataLimit` | `102400` | Warn once above this many `data` bytes (`false` disables) |
|
|
221
244
|
| `status` | `200` | Default response status |
|
|
222
245
|
| `headers` | `{}` | Default response headers (per-render `headers` merge over them) |
|
|
223
246
|
| `strict` | `true` | Unknown entries throw a dev-friendly error; `false` disables |
|
|
247
|
+
| `ssrFallback` | `false` | SSR failures log and fall back to client rendering instead of a 500 |
|
|
224
248
|
| `knownEntries` | auto from manifest | Override the page list used by strict mode and prefetch |
|
|
225
249
|
|
|
226
250
|
`shell()` also exposes `availableEntries()` for diagnostics.
|
|
@@ -237,7 +261,7 @@ Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entri
|
|
|
237
261
|
| `dts` | `false` | Typed `c.render` entries: `true` writes `src/hono-svelte-entries.d.ts`, a string sets a custom path |
|
|
238
262
|
| `alwaysClient` | `[]` | Pages that always get JS, even without `<script>` |
|
|
239
263
|
|
|
240
|
-
Handy methods: `input()` (client build entries), `entries()` (all pages), `staticEntries()` (static pages only), `hasClient(entry)`, `layouts()`, `layoutChain(entry)`, `types()` (entry union), `typeDeclarations()` (hono module snippet), `dtsPath()` / `writeDts()` (generated `.d.ts`), `validateConfig()` (fail fast on bad Vite setup).
|
|
264
|
+
Handy methods: `input()` (client build entries), `entries()` (all pages), `staticEntries()` (static pages only), `hasClient(entry)`, `layouts()`, `layoutChain(entry)`, `dataTypes()` (extracted `Data` declarations), `types()` (entry union), `typeDeclarations()` (hono module snippet), `dtsPath()` / `writeDts()` (generated `.d.ts`), `validateConfig()` (fail fast on bad Vite setup).
|
|
241
265
|
|
|
242
266
|
### Typing `c.render`
|
|
243
267
|
|
|
@@ -278,13 +302,56 @@ declare module "hono" {
|
|
|
278
302
|
}
|
|
279
303
|
```
|
|
280
304
|
|
|
281
|
-
`data` stays `Record<string, unknown>` —
|
|
305
|
+
`data` stays `Record<string, unknown>` by default — declare a `Data` type in the page for per-page typing (below). Programmatic access: `pages().types()` → `"admin" | "home"`, `pages().typeDeclarations()` for the snippet, `dtsPath()` / `writeDts()` for the file.
|
|
306
|
+
|
|
307
|
+
### Per-page data typing
|
|
308
|
+
|
|
309
|
+
Declare the page payload once — the `Data` type in a `<script module>` — and reuse it as the page props:
|
|
310
|
+
|
|
311
|
+
```svelte
|
|
312
|
+
<!-- src/pages/dashboard.svelte -->
|
|
313
|
+
<script module lang="ts">
|
|
314
|
+
export type Data = { plan: string; seats: number };
|
|
315
|
+
</script>
|
|
316
|
+
|
|
317
|
+
<script lang="ts">
|
|
318
|
+
let { plan, seats }: Data = $props();
|
|
319
|
+
</script>
|
|
320
|
+
|
|
321
|
+
<h1>{plan} — {seats} seats</h1>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
With `pages({ dts: true })`, the generated `.d.ts` adds a typed overload per
|
|
325
|
+
page, so `c.render("dashboard", { data: { plan: "pro" } })` is checked by
|
|
326
|
+
`tsc` — a missing `plan` fails the typecheck instead of shipping `undefined`
|
|
327
|
+
to the client:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// generated (excerpt)
|
|
331
|
+
type HonoSvelteData_dashboard = { plan: string; seats: number };
|
|
332
|
+
|
|
333
|
+
declare module "hono" {
|
|
334
|
+
interface ContextRenderer {
|
|
335
|
+
(entryName: "dashboard", props?: import("hono-svelte").RenderProps<HonoSvelteData_dashboard>): Response | Promise<Response>;
|
|
336
|
+
(entryName: HonoSvelteEntries, props?: import("hono-svelte").RenderProps): Response | Promise<Response>;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The `Data` declaration is inlined verbatim into the dts, so it must be
|
|
342
|
+
**self-contained** — inline shapes or built-ins only. References to imported
|
|
343
|
+
types can't be resolved from the dts location; such pages keep
|
|
344
|
+
`Record<string, unknown>` data with a one-time warning. Both
|
|
345
|
+
`export type Data = ...` and `export interface Data { ... }` work (including
|
|
346
|
+
legacy `<script context="module">`). `pages().dataTypes()` exposes the
|
|
347
|
+
extracted declarations.
|
|
282
348
|
|
|
283
349
|
## Production tips
|
|
284
350
|
|
|
285
351
|
- Pass the Vite manifest to `shell({ assets: manifest })` for hashed files; manifest CSS and shared chunks are emitted automatically (`modulepreload`).
|
|
286
352
|
- Serve hashed files with long cache and `immutable` (`immutableHeaders()` / `CACHE_IMMUTABLE`); the HTML shell itself suits `CACHE_NO_STORE`.
|
|
287
353
|
- CSP: pass `shell({ nonce })` (string or from `secureHeaders()` context) — shell tags carry the nonce. Without a nonce, static pages keep working with `script-src 'self'` (no executable inline script, except the opt-in `prefetch: "hover"` helper).
|
|
354
|
+
- The shell emits a neutral `<body>` (no framework classes) — style `body`/`html` from your global stylesheet (`stylesHref`), e.g. `body { @apply bg-base-200 min-h-screen text-base-content; }` with Tailwind/DaisyUI.
|
|
288
355
|
- Run both client and server builds before serving; the example in `examples/playground/` shows the full setup.
|
|
289
356
|
|
|
290
357
|
## Example
|
|
@@ -292,7 +359,7 @@ declare module "hono" {
|
|
|
292
359
|
`examples/playground/` is a real Hono app using the package: static landing, login, and a dashboard with typed RPC and cookie session. To run it:
|
|
293
360
|
|
|
294
361
|
```sh
|
|
295
|
-
cd examples/playground &&
|
|
362
|
+
cd examples/playground && bun install && bun run build
|
|
296
363
|
```
|
|
297
364
|
|
|
298
365
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { checkAppLink, doctorChecks, initFiles, isSafeAppPath, newPageFile, } from "./scaffold.js";
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
const command = args[0];
|
|
10
10
|
function flagValue(name) {
|
|
@@ -12,9 +12,14 @@ function flagValue(name) {
|
|
|
12
12
|
const hit = args.find((a) => a.startsWith(pref));
|
|
13
13
|
return hit ? hit.slice(pref.length) : undefined;
|
|
14
14
|
}
|
|
15
|
+
function hasFlag(name) {
|
|
16
|
+
return args.includes(`--${name}`);
|
|
17
|
+
}
|
|
15
18
|
const appRoot = flagValue("app") ?? process.cwd();
|
|
19
|
+
const flavor = hasFlag("full") ? "full" : "minimal";
|
|
16
20
|
function init() {
|
|
17
|
-
|
|
21
|
+
const files = initFiles(flavor);
|
|
22
|
+
for (const file of files) {
|
|
18
23
|
const dest = join(appRoot, file.path);
|
|
19
24
|
if (!isSafeAppPath(appRoot, dest)) {
|
|
20
25
|
console.error(`[hono-svelte] refusing to write outside ${appRoot}: ${file.path}`);
|
|
@@ -29,10 +34,50 @@ function init() {
|
|
|
29
34
|
writeFileSync(dest, file.content);
|
|
30
35
|
console.log(`[hono-svelte] wrote ${file.path}`);
|
|
31
36
|
}
|
|
37
|
+
console.log(`[hono-svelte] done (${flavor}). Next:`);
|
|
38
|
+
if (flavor === "minimal") {
|
|
39
|
+
console.log(` 1. bun install`);
|
|
40
|
+
console.log(` 2. bun run dev (or: bun --bun run dev)`);
|
|
41
|
+
console.log(` Tip: --full scaffolds auth + dashboard + layout + typed RPC.`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(` 1. bun install`);
|
|
45
|
+
console.log(` 2. bun run dev (or: bun --bun run dev) → /auth → /dashboard`);
|
|
46
|
+
}
|
|
32
47
|
}
|
|
33
48
|
function packageRootForCli() {
|
|
34
49
|
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
50
|
}
|
|
51
|
+
function addPage() {
|
|
52
|
+
const name = args[2];
|
|
53
|
+
if (!name) {
|
|
54
|
+
console.error("[hono-svelte] usage: hono-svelte add page <name> (e.g. blog/post-1)");
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const file = newPageFile(name);
|
|
59
|
+
if (!file) {
|
|
60
|
+
console.error(`[hono-svelte] invalid page name: ${name} — use a relative entry name like "blog/post-1" (no ../, no leading /, not "layout")`);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const dest = join(appRoot, file.path);
|
|
65
|
+
if (!isSafeAppPath(appRoot, dest)) {
|
|
66
|
+
console.error(`[hono-svelte] refusing to write outside ${appRoot}: ${file.path}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (existsSync(dest)) {
|
|
71
|
+
console.log(`[hono-svelte] exists, skipping: ${file.path}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
75
|
+
writeFileSync(dest, file.content);
|
|
76
|
+
const entryName = file.path.replace(/^src\/pages\//, "").replace(/\.svelte$/, "");
|
|
77
|
+
console.log(`[hono-svelte] wrote ${file.path}`);
|
|
78
|
+
console.log(`[hono-svelte] next: render it from a route — c.render("${entryName}")`);
|
|
79
|
+
console.log(`[hono-svelte] (default pagesDir is src/pages; set a custom one via pages({ pagesDir }))`);
|
|
80
|
+
}
|
|
36
81
|
function doctor() {
|
|
37
82
|
const packageRoot = packageRootForCli();
|
|
38
83
|
const issues = doctorChecks({ packageRoot, appRoot });
|
|
@@ -53,7 +98,12 @@ if (command === "init")
|
|
|
53
98
|
init();
|
|
54
99
|
else if (command === "doctor")
|
|
55
100
|
doctor();
|
|
101
|
+
else if (command === "add" && args[1] === "page")
|
|
102
|
+
addPage();
|
|
56
103
|
else {
|
|
57
|
-
console.log("Usage: hono-svelte <init|doctor
|
|
104
|
+
console.log("Usage: hono-svelte <init|doctor|add page <name>> [--app=<dir>] [--full]");
|
|
105
|
+
console.log(" init [--full] scaffold a runnable app (minimal by default)");
|
|
106
|
+
console.log(" add page <name> create src/pages/<name>.svelte (static, zero JS)");
|
|
107
|
+
console.log(" doctor [--app=<dir>] check dist, link and vite config");
|
|
58
108
|
process.exitCode = 1;
|
|
59
109
|
}
|
package/dist/handlers.d.ts
CHANGED
|
@@ -7,10 +7,10 @@ export type ErrorPageOptions = {
|
|
|
7
7
|
head?: string | HeadProps;
|
|
8
8
|
};
|
|
9
9
|
/** Hono `notFound` handler: `app.notFound(notFoundHandler())`. */
|
|
10
|
-
export declare function notFoundHandler(opts?: ErrorPageOptions): (c: any) =>
|
|
10
|
+
export declare function notFoundHandler(opts?: ErrorPageOptions): (c: any) => Response | Promise<Response>;
|
|
11
11
|
export type ServerErrorHandlerOptions = ErrorPageOptions & {
|
|
12
12
|
/** Log the error. `true` = `console.error`. @default true */
|
|
13
13
|
log?: boolean | ((err: unknown) => void);
|
|
14
14
|
};
|
|
15
15
|
/** Hono `onError` handler: `app.onError(errorHandler())`. */
|
|
16
|
-
export declare function errorHandler(opts?: ServerErrorHandlerOptions): (err:
|
|
16
|
+
export declare function errorHandler(opts?: ServerErrorHandlerOptions): (err: Error, c: any) => Response | Promise<Response>;
|
package/dist/handlers.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
// Conventional error-page handlers: `404.svelte` / `500.svelte` pages
|
|
2
2
|
// rendered with the matching HTTP status.
|
|
3
|
+
//
|
|
4
|
+
// The context parameter stays `any` ON PURPOSE — validated empirically:
|
|
5
|
+
// typing it with hono types works with a single hono copy (and even with
|
|
6
|
+
// duplicate copies of the same version), but breaks when the consumer's
|
|
7
|
+
// resolution picks a hono copy with a DIFFERENT version than the one this
|
|
8
|
+
// package's d.ts resolves to (file:/npm-link/monorepo setups — e.g.
|
|
9
|
+
// app hono 4.13.7 vs package types 4.13.8 fail on `[GET_MATCH_RESULT]`
|
|
10
|
+
// symbol members). hono's `Context<any, ...>` default is not a supertype of
|
|
11
|
+
// concrete contexts either. `any` keeps the handlers assignable in every
|
|
12
|
+
// copy/version layout; everything else (return type, the render call) is
|
|
13
|
+
// typed via the narrow cast below.
|
|
14
|
+
function entryRender(c) {
|
|
15
|
+
return c.render;
|
|
16
|
+
}
|
|
17
|
+
function pageProps(opts, status) {
|
|
18
|
+
return {
|
|
19
|
+
title: opts.title,
|
|
20
|
+
data: opts.data,
|
|
21
|
+
head: opts.head,
|
|
22
|
+
status,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
3
25
|
/** Hono `notFound` handler: `app.notFound(notFoundHandler())`. */
|
|
4
26
|
export function notFoundHandler(opts = {}) {
|
|
5
27
|
const entry = opts.entry ?? "404";
|
|
28
|
+
// See the file header for why the context param stays `any`.
|
|
6
29
|
return (c) => {
|
|
7
|
-
return c
|
|
8
|
-
title: opts.title,
|
|
9
|
-
data: opts.data,
|
|
10
|
-
head: opts.head,
|
|
11
|
-
status: 404,
|
|
12
|
-
});
|
|
30
|
+
return entryRender(c)(entry, pageProps(opts, 404));
|
|
13
31
|
};
|
|
14
32
|
}
|
|
15
33
|
/** Hono `onError` handler: `app.onError(errorHandler())`. */
|
|
@@ -21,11 +39,6 @@ export function errorHandler(opts = {}) {
|
|
|
21
39
|
console.error(err);
|
|
22
40
|
else if (typeof log === "function")
|
|
23
41
|
log(err);
|
|
24
|
-
return c
|
|
25
|
-
title: opts.title,
|
|
26
|
-
data: opts.data,
|
|
27
|
-
head: opts.head,
|
|
28
|
-
status: 500,
|
|
29
|
-
});
|
|
42
|
+
return entryRender(c)(entry, pageProps(opts, 500));
|
|
30
43
|
};
|
|
31
44
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import type { Next } from "hono";
|
|
2
2
|
import { type AssetsResolver, type ViteManifest } from "./assets.js";
|
|
3
3
|
import { type HeadProps } from "./head.js";
|
|
4
|
-
export { getIds } from "./ids.js";
|
|
5
|
-
export type { SharedIds } from "./ids.js";
|
|
6
|
-
export { renderHead } from "./head.js";
|
|
7
|
-
export type { HeadMetaItem, HeadProps } from "./head.js";
|
|
8
4
|
export { CACHE_IMMUTABLE, CACHE_NO_STORE, createManifestAssets, immutableHeaders, manifestCssFor, manifestImportsFor, manifestKeysFor, } from "./assets.js";
|
|
9
5
|
export type { AssetsResolver, ManifestAssetsOptions, ViteManifest, ViteManifestChunk, } from "./assets.js";
|
|
10
|
-
export {
|
|
6
|
+
export { errorHandler, notFoundHandler } from "./handlers.js";
|
|
11
7
|
export type { ErrorPageOptions, ServerErrorHandlerOptions } from "./handlers.js";
|
|
12
|
-
export {
|
|
13
|
-
export type {
|
|
8
|
+
export { renderHead } from "./head.js";
|
|
9
|
+
export type { HeadMetaItem, HeadProps } from "./head.js";
|
|
10
|
+
export { getIds } from "./ids.js";
|
|
11
|
+
export type { SharedIds } from "./ids.js";
|
|
12
|
+
export { checkAppLink, doctorChecks, initFiles, isSafeAppPath, isValidPageName, newPageFile } from "./scaffold.js";
|
|
13
|
+
export type { DoctorIssue, DoctorOptions, InitFile, InitFlavor } from "./scaffold.js";
|
|
14
14
|
export type SsrPageLoader = () => Promise<{
|
|
15
15
|
default: unknown;
|
|
16
16
|
}>;
|
|
17
|
-
export type RenderProps = {
|
|
17
|
+
export type RenderProps<TData = Record<string, unknown>> = {
|
|
18
18
|
title?: string;
|
|
19
|
-
data?:
|
|
19
|
+
data?: TData;
|
|
20
20
|
/** HTTP status of the shell response. @default 200 */
|
|
21
21
|
status?: number;
|
|
22
22
|
/** Extra response headers merged into the shell response. */
|
|
@@ -69,6 +69,11 @@ export type ShellOptions = {
|
|
|
69
69
|
knownEntries?: string[];
|
|
70
70
|
/** `false` disables unknown-entry validation. @default true */
|
|
71
71
|
strict?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* When SSR of a page throws at request time: log the error and fall back
|
|
74
|
+
* to client rendering (entry script) instead of a 500. @default false
|
|
75
|
+
*/
|
|
76
|
+
ssrFallback?: boolean;
|
|
72
77
|
/** Default status when `c.render(entry)` omits it. @default 200 */
|
|
73
78
|
status?: number;
|
|
74
79
|
/** Default headers merged into every shell response. */
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import { renderHead } from "./head.js";
|
|
|
3
3
|
import { getIds } from "./ids.js";
|
|
4
4
|
import { allEntries as autoAllEntries, ssrLayouts as autoSsrLayouts, ssrPages as autoSsrPages, } from "./ssr-manifest.js";
|
|
5
5
|
import { devEntryUrl } from "./virtual.js";
|
|
6
|
-
export { getIds } from "./ids.js";
|
|
7
|
-
export { renderHead } from "./head.js";
|
|
8
6
|
export { CACHE_IMMUTABLE, CACHE_NO_STORE, createManifestAssets, immutableHeaders, manifestCssFor, manifestImportsFor, manifestKeysFor, } from "./assets.js";
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
7
|
+
export { errorHandler, notFoundHandler } from "./handlers.js";
|
|
8
|
+
export { renderHead } from "./head.js";
|
|
9
|
+
export { getIds } from "./ids.js";
|
|
10
|
+
export { checkAppLink, doctorChecks, initFiles, isSafeAppPath, isValidPageName, newPageFile } from "./scaffold.js";
|
|
11
11
|
function escapeAttr(value) {
|
|
12
12
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
13
13
|
}
|
|
@@ -24,7 +24,7 @@ function isValidEntryName(entryName) {
|
|
|
24
24
|
function nonceAttr(nonce) {
|
|
25
25
|
return nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
|
|
26
26
|
}
|
|
27
|
-
function serializePageData(data, dataId, dataLimit) {
|
|
27
|
+
function serializePageData(entryName, data, dataId, dataLimit) {
|
|
28
28
|
if (!data)
|
|
29
29
|
return { json: "", html: "" };
|
|
30
30
|
let json;
|
|
@@ -36,7 +36,7 @@ function serializePageData(data, dataId, dataLimit) {
|
|
|
36
36
|
}
|
|
37
37
|
if (json === undefined)
|
|
38
38
|
return { json: "", html: "" };
|
|
39
|
-
checkDataLimit(json, dataLimit);
|
|
39
|
+
checkDataLimit(entryName, json, dataLimit);
|
|
40
40
|
const safe = json.replace(/</g, "\\u003c");
|
|
41
41
|
const lt = String.fromCharCode(60);
|
|
42
42
|
return {
|
|
@@ -45,19 +45,21 @@ function serializePageData(data, dataId, dataLimit) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
const warnedDataLimit = new Set();
|
|
48
|
-
function checkDataLimit(json, dataLimit) {
|
|
48
|
+
function checkDataLimit(entryName, json, dataLimit) {
|
|
49
49
|
if (dataLimit === false)
|
|
50
50
|
return;
|
|
51
51
|
const bytes = new TextEncoder().encode(json).length;
|
|
52
52
|
if (bytes <= dataLimit)
|
|
53
53
|
return;
|
|
54
|
-
|
|
54
|
+
// One warning per (page, limit): a second page over the limit must still
|
|
55
|
+
// be reported (the key used to be just the limit — one warn per process).
|
|
56
|
+
const key = entryName + ":" + String(dataLimit);
|
|
55
57
|
if (warnedDataLimit.has(key))
|
|
56
58
|
return;
|
|
57
59
|
warnedDataLimit.add(key);
|
|
58
60
|
const kb = (bytes / 1024).toFixed(1);
|
|
59
61
|
const limitKb = (dataLimit / 1024).toFixed(0);
|
|
60
|
-
console.warn(`[hono-svelte] page data is ${kb}KB (limit ${limitKb}KB). ` +
|
|
62
|
+
console.warn(`[hono-svelte] page "${entryName}" data is ${kb}KB (limit ${limitKb}KB). ` +
|
|
61
63
|
`data is inlined in the HTML — move large/sensitive payloads to a typed API (hc<AppType>).`);
|
|
62
64
|
}
|
|
63
65
|
/** Visible for tests. */
|
|
@@ -84,7 +86,8 @@ function detectProd() {
|
|
|
84
86
|
// plain node / vitest: no import.meta.env — bundled server is prod
|
|
85
87
|
return true;
|
|
86
88
|
}
|
|
87
|
-
return globalThis.process?.env?.NODE_ENV ===
|
|
89
|
+
return (globalThis.process?.env?.NODE_ENV ===
|
|
90
|
+
"production");
|
|
88
91
|
}
|
|
89
92
|
function isDev() {
|
|
90
93
|
try {
|
|
@@ -95,7 +98,8 @@ function isDev() {
|
|
|
95
98
|
// plain node / vitest
|
|
96
99
|
return false;
|
|
97
100
|
}
|
|
98
|
-
return globalThis.process?.env?.NODE_ENV !==
|
|
101
|
+
return (globalThis.process?.env?.NODE_ENV !==
|
|
102
|
+
"production");
|
|
99
103
|
}
|
|
100
104
|
function defaultKnownEntries(ssrPages) {
|
|
101
105
|
const keys = Object.keys(ssrPages);
|
|
@@ -137,7 +141,7 @@ function prefetchHoverScript(nonce, dataId, urls) {
|
|
|
137
141
|
const map = JSON.stringify(Object.fromEntries(urls.map((u) => [u.entry, u.url])));
|
|
138
142
|
const code = `(function(){var m=${map};` +
|
|
139
143
|
`function pre(u){if(document.querySelector('link[rel=\"prefetch\"][href=\"'+u+'\"]'))return;` +
|
|
140
|
-
`var l=document.createElement('link');l.rel='
|
|
144
|
+
`var l=document.createElement('link');l.rel='modulepreload';l.href=u;document.head.appendChild(l);}` +
|
|
141
145
|
`document.addEventListener('mouseover',function(e){var a=e.target&&e.target.closest?e.target.closest('a[href]'):null;` +
|
|
142
146
|
`if(!a)return;try{var p=new URL(a.href,location.origin).pathname.replace(/^\\//,'');` +
|
|
143
147
|
`var u=m[p];if(u)pre(u);}catch(_){}},{passive:true});})();`;
|
|
@@ -153,6 +157,7 @@ export function shell(options = {}) {
|
|
|
153
157
|
const defaultHeaders = options.headers ?? {};
|
|
154
158
|
const dataLimit = options.dataLimit ?? 102400;
|
|
155
159
|
const strict = options.strict ?? true;
|
|
160
|
+
const ssrFallback = options.ssrFallback ?? false;
|
|
156
161
|
const extraStyles = options.styles ?? [];
|
|
157
162
|
const resolveStyles = typeof options.stylesHref === "function"
|
|
158
163
|
? options.stylesHref
|
|
@@ -242,7 +247,7 @@ export function shell(options = {}) {
|
|
|
242
247
|
const childSnippet = createRawSnippet(() => ({ render: () => `<div>${childHtml}</div>` }));
|
|
243
248
|
const wrapped = render(layoutMod.default, {
|
|
244
249
|
...(pageData !== undefined ? { props: pageData } : {}),
|
|
245
|
-
props: { ...
|
|
250
|
+
props: { ...pageData, children: childSnippet },
|
|
246
251
|
...(nonce ? { csp: { nonce } } : {}),
|
|
247
252
|
});
|
|
248
253
|
html = wrapped.html ?? wrapped.body ?? "";
|
|
@@ -252,7 +257,22 @@ export function shell(options = {}) {
|
|
|
252
257
|
}
|
|
253
258
|
return { html, head: headValue };
|
|
254
259
|
}
|
|
255
|
-
|
|
260
|
+
// The context param stays `any` ON PURPOSE (see src/handlers.ts header for
|
|
261
|
+
// the full rationale: hono's unique symbols + `Context<any>` not being a
|
|
262
|
+
// supertype of concrete contexts). Everything the shell touches is typed
|
|
263
|
+
// through the casts below — no `any` leaks into the rendered response.
|
|
264
|
+
// The context param stays `any` ON PURPOSE — validated empirically:
|
|
265
|
+
// typing it (MiddlewareHandler / Context / generics) works with a single
|
|
266
|
+
// hono copy and even with duplicate copies of the SAME version, but breaks
|
|
267
|
+
// when the consumer's resolution picks a hono copy with a different version
|
|
268
|
+
// than the one this package's d.ts resolves to (file:/npm-link/monorepo
|
|
269
|
+
// topologies — e.g. app hono 4.13.7 vs package types 4.13.8 fail with
|
|
270
|
+
// `[GET_MATCH_RESULT]`-symbol mismatches). A d.ts free of hono type
|
|
271
|
+
// references is immune to any copy/version layout. The
|
|
272
|
+
// render/setRenderer boundary needs casts regardless (hono's default
|
|
273
|
+
// Renderer only accepts string content), so `any` here costs no real
|
|
274
|
+
// type safety — the casts carry the correctness burden.
|
|
275
|
+
const inner = async (c, next) => {
|
|
256
276
|
c.setRenderer(async (entryName, props) => {
|
|
257
277
|
if (!isValidEntryName(entryName)) {
|
|
258
278
|
throw new Error(`hono-svelte: invalid entryName: ${JSON.stringify(entryName)}`);
|
|
@@ -265,7 +285,7 @@ export function shell(options = {}) {
|
|
|
265
285
|
const chain = layoutChainFor(entryName)
|
|
266
286
|
.map((l) => ssrLayouts[l])
|
|
267
287
|
.filter((l) => l !== undefined);
|
|
268
|
-
|
|
288
|
+
let isSsr = loader !== undefined && !hasClientEntry(entryName);
|
|
269
289
|
if (strict && !isSsr) {
|
|
270
290
|
const known = availableEntries();
|
|
271
291
|
if (known && !known.includes(entryName)) {
|
|
@@ -278,14 +298,24 @@ export function shell(options = {}) {
|
|
|
278
298
|
const nonce = resolveNonce(c);
|
|
279
299
|
const nAttr = nonceAttr(nonce);
|
|
280
300
|
const stylesHref = resolveStyles(isProd) ?? (isProd ? "/static/styles.css" : "/src/styles.css");
|
|
281
|
-
const { html: dataHtml } = serializePageData(props?.data, ids.dataId, dataLimit);
|
|
301
|
+
const { html: dataHtml } = serializePageData(entryName, props?.data, ids.dataId, dataLimit);
|
|
282
302
|
const lt = String.fromCharCode(60);
|
|
283
303
|
let bodyHtml = "";
|
|
284
304
|
let ssrHead = "";
|
|
285
305
|
if (isSsr) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
try {
|
|
307
|
+
const rendered = await renderSsrBody(loader, props?.data, nonce, chain);
|
|
308
|
+
bodyHtml = rendered.html;
|
|
309
|
+
ssrHead = rendered.head;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
if (!ssrFallback)
|
|
313
|
+
throw err;
|
|
314
|
+
console.error(`[hono-svelte] SSR failed for "${entryName}" — falling back to client rendering:`, err);
|
|
315
|
+
bodyHtml = "";
|
|
316
|
+
ssrHead = "";
|
|
317
|
+
isSsr = false;
|
|
318
|
+
}
|
|
289
319
|
}
|
|
290
320
|
const resolveUrl = (entry) => isProd ? resolveAssetUrl(assetsOpt, assetsBase, entry) : devEntryUrl(entry);
|
|
291
321
|
let scriptHtml = "";
|
|
@@ -293,7 +323,8 @@ export function shell(options = {}) {
|
|
|
293
323
|
let prefetchHtml = "";
|
|
294
324
|
if (!isSsr) {
|
|
295
325
|
const src = resolveUrl(entryName);
|
|
296
|
-
scriptHtml =
|
|
326
|
+
scriptHtml =
|
|
327
|
+
lt + `script type="module" src="${escapeAttr(src)}"${nAttr}>` + lt + "/script>";
|
|
297
328
|
const shouldPreload = options.preload ?? isProd;
|
|
298
329
|
const manifest = isProd ? manifestFor(assetsOpt) : undefined;
|
|
299
330
|
if (shouldPreload) {
|
|
@@ -312,7 +343,7 @@ export function shell(options = {}) {
|
|
|
312
343
|
const known = availableEntries();
|
|
313
344
|
if (options.prefetch === "all" && known) {
|
|
314
345
|
for (const u of prefetchUrls("all", entryName, known, resolveUrl)) {
|
|
315
|
-
prefetchHtml += lt + `link rel="
|
|
346
|
+
prefetchHtml += lt + `link rel="modulepreload" href="${escapeAttr(u)}"${nAttr} />`;
|
|
316
347
|
}
|
|
317
348
|
}
|
|
318
349
|
else if (options.prefetch === "hover" && known) {
|
|
@@ -322,7 +353,7 @@ export function shell(options = {}) {
|
|
|
322
353
|
}
|
|
323
354
|
}
|
|
324
355
|
const status = props?.status ?? defaultStatus;
|
|
325
|
-
const headers = { ...defaultHeaders, ...
|
|
356
|
+
const headers = { ...defaultHeaders, ...props?.headers };
|
|
326
357
|
const pageHead = props?.head !== undefined ? renderHead(props.head) : "";
|
|
327
358
|
const manifestNow = isProd ? manifestFor(assetsOpt) : undefined;
|
|
328
359
|
const cssLinks = (manifestNow ? manifestCssFor(manifestNow, entryName) : [])
|
|
@@ -342,16 +373,13 @@ export function shell(options = {}) {
|
|
|
342
373
|
prefetchHtml +
|
|
343
374
|
scriptHtml +
|
|
344
375
|
`</head>` +
|
|
345
|
-
`<body
|
|
376
|
+
`<body><div id="${ids.rootId}">${bodyHtml}</div>` +
|
|
346
377
|
dataHtml +
|
|
347
378
|
`</body></html>`, status, headers);
|
|
348
379
|
});
|
|
349
380
|
await next();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return inner(c, next);
|
|
353
|
-
}
|
|
354
|
-
const mw = shellMiddleware;
|
|
381
|
+
};
|
|
382
|
+
const mw = inner;
|
|
355
383
|
mw.availableEntries = availableEntries;
|
|
356
384
|
return mw;
|
|
357
385
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type PageDataExtraction = {
|
|
2
|
+
/** Declaration text (`type Data = ...` / `interface Data { ... }`, `export` stripped). Null when the page declares no `Data`. */
|
|
3
|
+
declaration: string | null;
|
|
4
|
+
/** Imported identifiers referenced by the declaration (unusable for the dts). */
|
|
5
|
+
importedRefs: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function extractPageDataType(source: string): PageDataExtraction;
|