hono-svelte 0.3.0 → 0.4.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 +67 -0
- package/README.md +287 -56
- package/dist/assets.d.ts +26 -0
- package/dist/assets.js +60 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -0
- package/dist/handlers.d.ts +16 -0
- package/dist/handlers.js +31 -0
- package/dist/head.d.ts +19 -0
- package/dist/head.js +42 -0
- package/dist/ids.js +3 -3
- package/dist/index.d.ts +68 -3
- package/dist/index.js +297 -24
- package/dist/scaffold.d.ts +22 -0
- package/dist/scaffold.js +107 -0
- package/dist/ssr-manifest.d.ts +8 -0
- package/dist/ssr-manifest.js +14 -5
- package/dist/virtual.js +5 -5
- package/dist/vite.d.ts +49 -1
- package/dist/vite.js +277 -23
- package/package.json +67 -53
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- **Typed `c.render` entries (opt-in)**: `pages({ dts })` writes a `.d.ts` file
|
|
6
|
+
with the entry union (`HonoSvelteEntries`) + `declare module "hono"`
|
|
7
|
+
augmentation. `true` writes `src/hono-svelte-entries.d.ts` (relative to the
|
|
8
|
+
Vite root); a string sets a custom path. Regenerated on `buildStart` and by
|
|
9
|
+
the dev watcher when entries change. `dtsPath()` / `writeDts()` exposed on
|
|
10
|
+
the plugin. The playground wires it (`pages({ dts: true })` +
|
|
11
|
+
`/// <reference path="./hono-svelte-entries.d.ts" />` in `env.d.ts`), so a
|
|
12
|
+
typo in `c.render("...")` fails `tsc` instead of 500ing at runtime.
|
|
13
|
+
`data` stays `Record<string, unknown>` (per-page payload types are a future
|
|
14
|
+
feature).
|
|
15
|
+
- **Layouts, single source of truth**: `<dir>/layout.svelte` wraps pages
|
|
16
|
+
automatically (SSR inside-out + client hydrate, `layouts()` /
|
|
17
|
+
`layoutChain()` to inspect, `layouts: false` = legacy ignore). Pages must
|
|
18
|
+
NOT import their layout manually — the plugin fails fast at build time
|
|
19
|
+
(`manually imports ... but the layout is applied automatically`) instead of
|
|
20
|
+
rendering the layout twice. Layout props (`title`, `description`) come from
|
|
21
|
+
the route via `c.render(entry, { data })`, with `title = "Dashboard"`
|
|
22
|
+
fallback in the layout. Rule shared by plugin and shell: static means
|
|
23
|
+
"no `<script>`", layouts don't matter; `__clientEntries` travels in the
|
|
24
|
+
virtual manifest so the shell picks the SSR branch for static+layouts pages.
|
|
25
|
+
- **Runtime fixes**: static `import.meta.env.PROD/DEV` access (Vite module
|
|
26
|
+
runner forbids dynamic access — dev 500 fixed); static
|
|
27
|
+
`createRawSnippet` import in layout entries (no duplicated Svelte runtime —
|
|
28
|
+
`Illegal invocation` fixed); layout children snippet renders `<div>` wrapper
|
|
29
|
+
(empty render broke hydration — `Cannot set properties of null` fixed);
|
|
30
|
+
bundled server defaults to prod so plain `node ./dist/index.js` works on
|
|
31
|
+
Windows with no `NODE_ENV` prefix.
|
|
32
|
+
- `c.render(entry, { status, headers, head })`: per-render status/headers
|
|
33
|
+
(merged over shell defaults) and CSP-safe per-page head tags
|
|
34
|
+
(`description`, `canonical`, `og`, `twitter`, `meta`, `extra`).
|
|
35
|
+
- SSR static pages now receive `data` as component props (plus the embedded JSON).
|
|
36
|
+
- Strict entries: unknown entryNames throw a dev-friendly error (`Available pages: ...`, generic in prod); `shell({ strict: false })` disables; `availableEntries()` exposes the list.
|
|
37
|
+
- `pages().types()` / `typeDeclarations()` generate the entry union + hono module snippet.
|
|
38
|
+
- Nested layouts: `<dir>/layout.svelte` wraps pages (SSR inside-out, `layouts()`/`layoutChain()`, `layouts: false` = legacy).
|
|
39
|
+
- `notFoundHandler()` / `errorHandler()` for conventional `404.svelte` / `500.svelte` pages.
|
|
40
|
+
- `hono-svelte init|doctor` CLI (`bin`) with `--app` flag.
|
|
41
|
+
- `pages().validateConfig()` fails fast on missing svelte plugin / pagesDir.
|
|
42
|
+
- `shell({ assets })` accepts a Vite manifest (hashed URLs, CSS, shared-chunk preload) with fallback; `preload`/`prefetch` controls; `styles` extra CSS.
|
|
43
|
+
- `shell({ nonce })` CSP support on shell tags; `svelte/server` CSP passthrough for SSR.
|
|
44
|
+
- Cache helpers: `immutableHeaders()`, `CACHE_IMMUTABLE`, `CACHE_NO_STORE`.
|
|
45
|
+
- `data` size one-time warning (`shell({ dataLimit })`, default 100KB).
|
|
46
|
+
|
|
47
|
+
## 0.3.1
|
|
48
|
+
|
|
49
|
+
- Package metadata: repository/homepage/author, publishConfig, LICENSE + CHANGELOG in files.
|
|
50
|
+
- No code changes since 0.3.0.
|
|
51
|
+
|
|
52
|
+
## 0.3.0
|
|
53
|
+
|
|
54
|
+
- Fully in-memory package: entries and manifest are virtual modules (no files written to disk).
|
|
55
|
+
- Automatic zero-JS: `.svelte` pages without `<script>` render on the server (`svelte/server`)
|
|
56
|
+
and download no JS on the client.
|
|
57
|
+
- Deterministic opaque IDs derived from the entryName (no salt, no env, no disk).
|
|
58
|
+
- Zero config: the shell resolves the SSR map on its own (relative import redirected by the plugin).
|
|
59
|
+
- Test suite (vitest) and CI (typecheck + tests + build + publint).
|
|
60
|
+
|
|
61
|
+
## 0.2.0
|
|
62
|
+
|
|
63
|
+
- Opaque IDs (`r-` + hash) replace configurable `rootId`/`dataId`.
|
|
64
|
+
|
|
65
|
+
## 0.1.0
|
|
66
|
+
|
|
67
|
+
- First version: `shell()` middleware + `pages()` plugin with disk-generated entries.
|
package/README.md
CHANGED
|
@@ -1,71 +1,302 @@
|
|
|
1
1
|
# hono-svelte
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
Render Svelte 5 pages in Hono apps with a single `c.render("page")` — no SPA, no build boilerplate.
|
|
4
|
+
|
|
5
|
+
Each `.svelte` file becomes an independent page. Static pages reach the browser as ready-made HTML with **zero JavaScript**. Interactive pages receive only their own JS. Works in dev with HMR and in production with per-page bundles.
|
|
6
|
+
|
|
7
|
+
## Why use it
|
|
8
|
+
|
|
9
|
+
Real sites and dashboards mix simple pages (landing, login, terms) with interactive screens (panels, forms). In a traditional SPA, a landing visitor downloads the entire dashboard's JS. With hono-svelte:
|
|
10
|
+
|
|
11
|
+
- **Static pages cost zero JS** — HTML arrives ready from the server;
|
|
12
|
+
- **Interactive pages cost only their own JS** — independent bundles, no loading the rest of the app;
|
|
13
|
+
- **Public initial data ships in the HTML** — the page mounts with content, no extra fetch;
|
|
14
|
+
- **Sensitive data stays in the API** — typed end to end with the Hono client.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Requires Node 22+, Hono 4, Svelte 5, and Vite 6/7/8.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install hono-svelte
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
**1. Configure Vite** — add the pages plugin in every mode and use the entry list in the client build:
|
|
18
27
|
|
|
19
28
|
```ts
|
|
20
|
-
|
|
29
|
+
import build from "@hono/vite-build/node";
|
|
30
|
+
import devServer from "@hono/vite-dev-server";
|
|
31
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
21
32
|
import { pages } from "hono-svelte/vite";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
import { resolve } from "node:path";
|
|
34
|
+
import { defineConfig } from "vite";
|
|
35
|
+
|
|
36
|
+
const appPages = pages();
|
|
37
|
+
|
|
38
|
+
export default defineConfig(({ command, mode }) => {
|
|
39
|
+
if (mode === "client") {
|
|
40
|
+
return {
|
|
41
|
+
plugins: [svelte(), appPages],
|
|
42
|
+
build: {
|
|
43
|
+
rollupOptions: {
|
|
44
|
+
input: { ...appPages.input(), styles: resolve("src/styles.css") },
|
|
45
|
+
output: {
|
|
46
|
+
entryFileNames: "static/[name].js",
|
|
47
|
+
chunkFileNames: "static/chunks/[name]-[hash].js",
|
|
48
|
+
assetFileNames: "static/[name][extname]",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (command === "serve") {
|
|
56
|
+
return {
|
|
57
|
+
plugins: [svelte(), appPages, devServer({ entry: "src/routes/index.ts" })],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
plugins: [svelte(), appPages, build({ entry: "src/routes/index.ts", staticRoot: "./dist" })],
|
|
63
|
+
};
|
|
64
|
+
});
|
|
27
65
|
```
|
|
28
66
|
|
|
67
|
+
**2. Register the shell on the server:**
|
|
68
|
+
|
|
29
69
|
```ts
|
|
30
|
-
|
|
70
|
+
import { Hono } from "hono";
|
|
31
71
|
import { shell } from "hono-svelte";
|
|
32
72
|
|
|
33
|
-
app.use("/*", shell({ title: "
|
|
73
|
+
const app = new Hono().use("/*", shell({ title: "My App", lang: "en" }));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**3. Create pages** in `src/pages` — one page per file:
|
|
77
|
+
|
|
78
|
+
```svelte
|
|
79
|
+
<!-- src/pages/home.svelte — no <script>: becomes plain HTML, zero JS -->
|
|
80
|
+
<main>
|
|
81
|
+
<h1>Welcome</h1>
|
|
82
|
+
<a href="/dashboard">Sign in</a>
|
|
83
|
+
</main>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```svelte
|
|
87
|
+
<!-- src/pages/dashboard.svelte — has <script>: gets its own JS -->
|
|
88
|
+
<script lang="ts">
|
|
89
|
+
import { hc } from "hono/client";
|
|
90
|
+
import type { AppType } from "../routes/api";
|
|
91
|
+
|
|
92
|
+
let { plan } = $props<{ plan: string }>();
|
|
93
|
+
const client = hc<AppType>("/");
|
|
94
|
+
|
|
95
|
+
let time = $state("--:--");
|
|
96
|
+
async function refresh() {
|
|
97
|
+
const res = await client.api.time.$get();
|
|
98
|
+
time = (await res.json()).time;
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<main>
|
|
103
|
+
<h1>Dashboard — {plan} plan</h1>
|
|
104
|
+
<p>Server time: {time}</p>
|
|
105
|
+
<button onclick={refresh}>Refresh</button>
|
|
106
|
+
</main>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**4. Render in routes:**
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
app.get("/", (c) => c.render("home"));
|
|
34
113
|
app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
|
|
35
114
|
```
|
|
36
115
|
|
|
116
|
+
The rule is simple: **if the `.svelte` file has no `<script>`, the page ships as plain HTML. If it has one, it hydrates on the client** with initial data available via `$props()`.
|
|
117
|
+
|
|
118
|
+
Files starting with `_` and `layout.svelte` are ignored (convention for partials and layouts).
|
|
119
|
+
|
|
120
|
+
## Passing data to the page
|
|
121
|
+
|
|
122
|
+
Small, public initial data (plan name, title, preferences) goes in `data` and arrives via `$props()` — no extra request.
|
|
123
|
+
Works for static (zero-JS) pages too: the shell passes `data` as SSR props **and** embeds the JSON for hydration:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Large payloads trigger a one-time `console.warn` (default limit 100KB, `shell({ dataLimit })`).
|
|
130
|
+
Anything sensitive stays in the API (`hc<AppType>`, HttpOnly cookie). Never put secrets in `data`.
|
|
131
|
+
|
|
132
|
+
## Status, headers and per-page head
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
app.get("/old", (c) => c.render("gone", { status: 410, headers: { "X-Gone": "1" } }));
|
|
136
|
+
|
|
137
|
+
app.get("/post", (c) =>
|
|
138
|
+
c.render("post", {
|
|
139
|
+
head: {
|
|
140
|
+
description: "A post about islands",
|
|
141
|
+
canonical: "https://example.com/post",
|
|
142
|
+
og: { title: "Islands", image: "https://example.com/og.png" },
|
|
143
|
+
twitter: { card: "summary_large_image" },
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`<svelte:head>` inside pages also works (shell forwards SSR `head`). Page `head` never emits executable inline scripts (CSP-safe; `extra` is for trusted raw HTML like fonts or JSON-LD).
|
|
150
|
+
|
|
151
|
+
## Nested layouts
|
|
152
|
+
|
|
153
|
+
`<dir>/layout.svelte` wraps every page under `<dir>/` (SSR inside-out, outermost first). A root `layout.svelte` wraps everything. Layouts receive `children` plus all page `data` as props — e.g. `title`/`description` passed via `c.render(entry, { data })`:
|
|
154
|
+
|
|
155
|
+
```svelte
|
|
156
|
+
<!-- src/pages/dashboard/layout.svelte -->
|
|
157
|
+
<script lang="ts">
|
|
158
|
+
import type { Snippet } from "svelte";
|
|
159
|
+
type Props = { title?: string; description?: string; children: Snippet };
|
|
160
|
+
let { title = "Dashboard", description = "", children }: Props = $props();
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<div class="layout">
|
|
164
|
+
<h1>{title}</h1>
|
|
165
|
+
{@render children()}
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// routes/dashboard/page1.ts — layout data comes from the route, not the page
|
|
171
|
+
c.render("dashboard/page1", { data: { title: "Page 1", description: "..." } });
|
|
172
|
+
```
|
|
173
|
+
|
|
37
174
|
```svelte
|
|
38
|
-
<!--
|
|
39
|
-
<
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
import type { RenderProps } from "hono-svelte";
|
|
65
|
-
|
|
66
|
-
declare module "hono" {
|
|
67
|
-
interface ContextRenderer {
|
|
68
|
-
(entryName: string, props?: RenderProps): Response | Promise<Response>;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
175
|
+
<!-- src/pages/dashboard/page1.svelte — content only, NO layout import -->
|
|
176
|
+
<section class="card">Page 1 content</section>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> Pages must NOT import their layout manually — the shell applies it automatically. The plugin fails fast at build time (`manually imports ... but the layout is applied automatically`) instead of rendering `<main><div><main>` twice.
|
|
180
|
+
|
|
181
|
+
Layouts never become entries (`pages().layouts()` / `layoutChain(entry)` inspect them; `layouts: false` restores legacy ignore).
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## Error pages
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { errorHandler, notFoundHandler } from "hono-svelte";
|
|
188
|
+
|
|
189
|
+
app.notFound(notFoundHandler()); // renders `404.svelte` with status 404
|
|
190
|
+
app.onError(errorHandler()); // renders `500.svelte` with status 500
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## CLI
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
npx hono-svelte init # scaffold src/pages, vite.config.ts, env.d.ts
|
|
197
|
+
npx hono-svelte doctor # check dist, link and vite config
|
|
198
|
+
npx hono-svelte doctor --app=./my-app # check another app directory
|
|
71
199
|
```
|
|
200
|
+
|
|
201
|
+
## API
|
|
202
|
+
|
|
203
|
+
### `shell(options?)`
|
|
204
|
+
|
|
205
|
+
Hono middleware that provides `c.render(entry, { title?, data?, status?, headers?, head? })` on every route.
|
|
206
|
+
|
|
207
|
+
| Option | Default | Description |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `title` | `"App"` | Title used when the route doesn't provide one |
|
|
210
|
+
| `lang` | `"en"` | `<html>` `lang` attribute |
|
|
211
|
+
| `assetsBase` | `"/static"` | Prefix for JS files in production (legacy; prefer `assets`) |
|
|
212
|
+
| `assets` | `"/static"` | String prefix, Vite manifest object, or resolver `(entry) => url` (hashed files) |
|
|
213
|
+
| `isProd` | auto | Force prod/dev asset behavior |
|
|
214
|
+
| `stylesHref` | `"/static/styles.css"` in prod, `"/src/styles.css"` in dev | Global stylesheet (or an `(isProd) => string` function) |
|
|
215
|
+
| `styles` | `[]` | Extra CSS hrefs emitted per response |
|
|
216
|
+
| `head` | `""` | Extra HTML in `<head>` (fonts, meta tags) |
|
|
217
|
+
| `nonce` | — | CSP nonce string or `(c) => string` (applied to shell `<script>`/`<link>`) |
|
|
218
|
+
| `preload` | `true` in prod | `<link rel="modulepreload">` for the entry (+ manifest imports) |
|
|
219
|
+
| `prefetch` | `false` | `"all"` prefetches other entries; `"hover"` injects a hover-prefetch script |
|
|
220
|
+
| `dataLimit` | `102400` | Warn once above this many `data` bytes (`false` disables) |
|
|
221
|
+
| `status` | `200` | Default response status |
|
|
222
|
+
| `headers` | `{}` | Default response headers (per-render `headers` merge over them) |
|
|
223
|
+
| `strict` | `true` | Unknown entries throw a dev-friendly error; `false` disables |
|
|
224
|
+
| `knownEntries` | auto from manifest | Override the page list used by strict mode and prefetch |
|
|
225
|
+
|
|
226
|
+
`shell()` also exposes `availableEntries()` for diagnostics.
|
|
227
|
+
|
|
228
|
+
### `pages(options?)`
|
|
229
|
+
|
|
230
|
+
Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entries.
|
|
231
|
+
|
|
232
|
+
| Option | Default | Description |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `pagesDir` | `"src/pages"` | Pages folder |
|
|
235
|
+
| `ignore` | `["**/_*.svelte"]` | Ignored patterns (`layout.svelte` is a layout, not a page) |
|
|
236
|
+
| `layouts` | `true` | Nested `<dir>/layout.svelte` support (`false` = legacy ignore) |
|
|
237
|
+
| `dts` | `false` | Typed `c.render` entries: `true` writes `src/hono-svelte-entries.d.ts`, a string sets a custom path |
|
|
238
|
+
| `alwaysClient` | `[]` | Pages that always get JS, even without `<script>` |
|
|
239
|
+
|
|
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).
|
|
241
|
+
|
|
242
|
+
### Typing `c.render`
|
|
243
|
+
|
|
244
|
+
Opt-in typed entry names — a typo fails `tsc` instead of 500ing at runtime:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// vite.config.ts
|
|
248
|
+
const appPages = pages({ dts: true }); // writes src/hono-svelte-entries.d.ts
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// src/env.d.ts — the generated file carries the `declare module "hono"`
|
|
253
|
+
// augmentation, so this file is only a reference. Remove any manual
|
|
254
|
+
// `(entryName: string)` declaration or it will swallow the union.
|
|
255
|
+
/// <reference path="./hono-svelte-entries.d.ts" />
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// generated src/hono-svelte-entries.d.ts (commit it — CI needs no Vite run)
|
|
260
|
+
export type HonoSvelteEntries = "auth" | "dashboard/index" | "index";
|
|
261
|
+
|
|
262
|
+
declare module "hono" {
|
|
263
|
+
interface ContextRenderer {
|
|
264
|
+
(entryName: HonoSvelteEntries, props?: import("hono-svelte").RenderProps): Response | Promise<Response>;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`dts: "path/to/entries.d.ts"` sets a custom path (relative to the Vite root). The file regenerates on `buildStart` and via the dev watcher when entries change. Without `dts` (default `false`), declare the renderer manually with a plain `string`:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import type { RenderProps } from "hono-svelte";
|
|
273
|
+
|
|
274
|
+
declare module "hono" {
|
|
275
|
+
interface ContextRenderer {
|
|
276
|
+
(entryName: string, props?: RenderProps): Response | Promise<Response>;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`data` stays `Record<string, unknown>` — per-page payload types are a future feature. Programmatic access: `pages().types()` → `"admin" | "home"`, `pages().typeDeclarations()` for the snippet, `dtsPath()` / `writeDts()` for the file.
|
|
282
|
+
|
|
283
|
+
## Production tips
|
|
284
|
+
|
|
285
|
+
- Pass the Vite manifest to `shell({ assets: manifest })` for hashed files; manifest CSS and shared chunks are emitted automatically (`modulepreload`).
|
|
286
|
+
- Serve hashed files with long cache and `immutable` (`immutableHeaders()` / `CACHE_IMMUTABLE`); the HTML shell itself suits `CACHE_NO_STORE`.
|
|
287
|
+
- 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).
|
|
288
|
+
- Run both client and server builds before serving; the example in `examples/playground/` shows the full setup.
|
|
289
|
+
|
|
290
|
+
## Example
|
|
291
|
+
|
|
292
|
+
`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
|
+
|
|
294
|
+
```sh
|
|
295
|
+
cd examples/playground && npm install && npm run build
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT — see [LICENSE](./LICENSE). Changelog in [CHANGELOG.md](./CHANGELOG.md).
|
|
301
|
+
|
|
302
|
+
Anything sensitive, large, or frequently changing stays in the API, fetched after mount with the typed Hono client (`hc<AppType>`) — with the session in an `HttpOnly` cookie as usual. Never put secrets in `data`: it is visible in the HTML.
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ViteManifestChunk = {
|
|
2
|
+
file: string;
|
|
3
|
+
css?: string[];
|
|
4
|
+
imports?: string[];
|
|
5
|
+
isEntry?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type ViteManifest = Record<string, ViteManifestChunk>;
|
|
8
|
+
export type AssetsResolver = (entryName: string) => string;
|
|
9
|
+
export type ManifestAssetsOptions = {
|
|
10
|
+
/** Prefix for manifest file paths. @default "/" */
|
|
11
|
+
prefix?: string;
|
|
12
|
+
/** Fallback base when the entry is missing from the manifest. @default "/static" */
|
|
13
|
+
fallbackBase?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Manifest keys tried for an entry (virtual ids first, bare entry last). */
|
|
16
|
+
export declare function manifestKeysFor(entryName: string): string[];
|
|
17
|
+
export declare function createManifestAssets(manifest: ViteManifest, opts?: ManifestAssetsOptions): AssetsResolver;
|
|
18
|
+
/** Hashed CSS files for an entry, via the Vite manifest. */
|
|
19
|
+
export declare function manifestCssFor(manifest: ViteManifest, entryName: string, prefix?: string): string[];
|
|
20
|
+
/** Shared-chunk URLs (for `modulepreload`), via the Vite manifest. */
|
|
21
|
+
export declare function manifestImportsFor(manifest: ViteManifest, entryName: string, prefix?: string): string[];
|
|
22
|
+
/** Long-term caching for hashed `/static` files. */
|
|
23
|
+
export declare const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
|
|
24
|
+
/** Safe default for HTML shell responses. */
|
|
25
|
+
export declare const CACHE_NO_STORE = "no-store";
|
|
26
|
+
export declare function immutableHeaders(extra?: Record<string, string>): Record<string, string>;
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Asset URL resolution (Vite manifest with hashed files) + cache helpers.
|
|
2
|
+
//
|
|
3
|
+
// Default behavior stays zero-config: `/static/<entry>.js` (unhashed names,
|
|
4
|
+
// as in the README example). Pass a Vite manifest object to `shell({ assets })`
|
|
5
|
+
// when the client build uses hashed file names.
|
|
6
|
+
const NULL = String.fromCharCode(0);
|
|
7
|
+
function joinPrefix(prefix, file) {
|
|
8
|
+
return prefix.replace(/\/$/, "") + "/" + file.replace(/^\//, "");
|
|
9
|
+
}
|
|
10
|
+
/** Manifest keys tried for an entry (virtual ids first, bare entry last). */
|
|
11
|
+
export function manifestKeysFor(entryName) {
|
|
12
|
+
return [
|
|
13
|
+
NULL + "virtual:hono-svelte/entry/" + entryName,
|
|
14
|
+
"virtual:hono-svelte/entry/" + entryName,
|
|
15
|
+
entryName,
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
function findChunk(manifest, entryName) {
|
|
19
|
+
for (const key of manifestKeysFor(entryName)) {
|
|
20
|
+
const chunk = manifest[key];
|
|
21
|
+
if (chunk)
|
|
22
|
+
return chunk;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
export function createManifestAssets(manifest, opts = {}) {
|
|
27
|
+
const prefix = opts.prefix ?? "/";
|
|
28
|
+
const fallbackBase = (opts.fallbackBase ?? "/static").replace(/\/$/, "");
|
|
29
|
+
return (entryName) => {
|
|
30
|
+
const chunk = findChunk(manifest, entryName);
|
|
31
|
+
if (chunk?.file)
|
|
32
|
+
return joinPrefix(prefix, chunk.file);
|
|
33
|
+
return `${fallbackBase}/${entryName}.js`;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Hashed CSS files for an entry, via the Vite manifest. */
|
|
37
|
+
export function manifestCssFor(manifest, entryName, prefix = "/") {
|
|
38
|
+
const chunk = findChunk(manifest, entryName);
|
|
39
|
+
return (chunk?.css ?? []).map((f) => joinPrefix(prefix, f));
|
|
40
|
+
}
|
|
41
|
+
/** Shared-chunk URLs (for `modulepreload`), via the Vite manifest. */
|
|
42
|
+
export function manifestImportsFor(manifest, entryName, prefix = "/") {
|
|
43
|
+
const chunk = findChunk(manifest, entryName);
|
|
44
|
+
if (!chunk)
|
|
45
|
+
return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const imp of chunk.imports ?? []) {
|
|
48
|
+
const dep = manifest[imp];
|
|
49
|
+
if (dep?.file)
|
|
50
|
+
out.push(joinPrefix(prefix, dep.file));
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** Long-term caching for hashed `/static` files. */
|
|
55
|
+
export const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
|
|
56
|
+
/** Safe default for HTML shell responses. */
|
|
57
|
+
export const CACHE_NO_STORE = "no-store";
|
|
58
|
+
export function immutableHeaders(extra = {}) {
|
|
59
|
+
return { "Cache-Control": CACHE_IMMUTABLE, ...extra };
|
|
60
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin CLI wrapper: `hono-svelte init` / `hono-svelte doctor`.
|
|
3
|
+
// Compiled to `dist/cli.js` and invoked via the `hono-svelte` bin.
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { doctorChecks, checkAppLink, initFiles, isSafeAppPath } from "./scaffold.js";
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0];
|
|
10
|
+
function flagValue(name) {
|
|
11
|
+
const pref = `--${name}=`;
|
|
12
|
+
const hit = args.find((a) => a.startsWith(pref));
|
|
13
|
+
return hit ? hit.slice(pref.length) : undefined;
|
|
14
|
+
}
|
|
15
|
+
const appRoot = flagValue("app") ?? process.cwd();
|
|
16
|
+
function init() {
|
|
17
|
+
for (const file of initFiles()) {
|
|
18
|
+
const dest = join(appRoot, file.path);
|
|
19
|
+
if (!isSafeAppPath(appRoot, dest)) {
|
|
20
|
+
console.error(`[hono-svelte] refusing to write outside ${appRoot}: ${file.path}`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (file.skipIfExists && existsSync(dest)) {
|
|
25
|
+
console.log(`[hono-svelte] exists, skipping: ${file.path}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
29
|
+
writeFileSync(dest, file.content);
|
|
30
|
+
console.log(`[hono-svelte] wrote ${file.path}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function packageRootForCli() {
|
|
34
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
|
+
}
|
|
36
|
+
function doctor() {
|
|
37
|
+
const packageRoot = packageRootForCli();
|
|
38
|
+
const issues = doctorChecks({ packageRoot, appRoot });
|
|
39
|
+
const linkIssue = checkAppLink(appRoot);
|
|
40
|
+
if (linkIssue)
|
|
41
|
+
issues.push(linkIssue);
|
|
42
|
+
if (issues.length === 0) {
|
|
43
|
+
console.log("[hono-svelte] doctor: all good!");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const issue of issues) {
|
|
47
|
+
console.log(`[hono-svelte] ${issue.code}: ${issue.message}`);
|
|
48
|
+
console.log(` fix: ${issue.fix}`);
|
|
49
|
+
}
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
if (command === "init")
|
|
53
|
+
init();
|
|
54
|
+
else if (command === "doctor")
|
|
55
|
+
doctor();
|
|
56
|
+
else {
|
|
57
|
+
console.log("Usage: hono-svelte <init|doctor> [--app=<dir>]");
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HeadProps } from "./head.js";
|
|
2
|
+
export type ErrorPageOptions = {
|
|
3
|
+
/** Entry name. @default "404" (not-found) / "500" (error) */
|
|
4
|
+
entry?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
data?: Record<string, unknown>;
|
|
7
|
+
head?: string | HeadProps;
|
|
8
|
+
};
|
|
9
|
+
/** Hono `notFound` handler: `app.notFound(notFoundHandler())`. */
|
|
10
|
+
export declare function notFoundHandler(opts?: ErrorPageOptions): (c: any) => any;
|
|
11
|
+
export type ServerErrorHandlerOptions = ErrorPageOptions & {
|
|
12
|
+
/** Log the error. `true` = `console.error`. @default true */
|
|
13
|
+
log?: boolean | ((err: unknown) => void);
|
|
14
|
+
};
|
|
15
|
+
/** Hono `onError` handler: `app.onError(errorHandler())`. */
|
|
16
|
+
export declare function errorHandler(opts?: ServerErrorHandlerOptions): (err: unknown, c: any) => any;
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Conventional error-page handlers: `404.svelte` / `500.svelte` pages
|
|
2
|
+
// rendered with the matching HTTP status.
|
|
3
|
+
/** Hono `notFound` handler: `app.notFound(notFoundHandler())`. */
|
|
4
|
+
export function notFoundHandler(opts = {}) {
|
|
5
|
+
const entry = opts.entry ?? "404";
|
|
6
|
+
return (c) => {
|
|
7
|
+
return c.render(entry, {
|
|
8
|
+
title: opts.title,
|
|
9
|
+
data: opts.data,
|
|
10
|
+
head: opts.head,
|
|
11
|
+
status: 404,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Hono `onError` handler: `app.onError(errorHandler())`. */
|
|
16
|
+
export function errorHandler(opts = {}) {
|
|
17
|
+
const entry = opts.entry ?? "500";
|
|
18
|
+
const log = opts.log ?? true;
|
|
19
|
+
return (err, c) => {
|
|
20
|
+
if (log === true)
|
|
21
|
+
console.error(err);
|
|
22
|
+
else if (typeof log === "function")
|
|
23
|
+
log(err);
|
|
24
|
+
return c.render(entry, {
|
|
25
|
+
title: opts.title,
|
|
26
|
+
data: opts.data,
|
|
27
|
+
head: opts.head,
|
|
28
|
+
status: 500,
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/head.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type HeadMetaItem = {
|
|
2
|
+
name?: string;
|
|
3
|
+
property?: string;
|
|
4
|
+
httpEquiv?: string;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
export type HeadProps = {
|
|
8
|
+
description?: string;
|
|
9
|
+
canonical?: string;
|
|
10
|
+
themeColor?: string;
|
|
11
|
+
meta?: HeadMetaItem[];
|
|
12
|
+
/** Open Graph tags: `{ title, description, image }` -> `property="og:*"`. */
|
|
13
|
+
og?: Record<string, string>;
|
|
14
|
+
/** Twitter tags: `{ card, title }` -> `name="twitter:*"`. */
|
|
15
|
+
twitter?: Record<string, string>;
|
|
16
|
+
/** Raw HTML appended as-is (fonts, ld+json, verification tags). */
|
|
17
|
+
extra?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function renderHead(head: string | HeadProps | undefined): string;
|