hono-svelte 0.3.1 → 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 +66 -22
- package/README.md +123 -8
- 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/index.d.ts +68 -3
- package/dist/index.js +293 -20
- 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 +9 -0
- package/dist/vite.d.ts +48 -0
- package/dist/vite.js +264 -10
- package/package.json +67 -64
package/CHANGELOG.md
CHANGED
|
@@ -1,23 +1,67 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
|
|
23
67
|
- First version: `shell()` middleware + `pages()` plugin with disk-generated entries.
|
package/README.md
CHANGED
|
@@ -119,26 +119,111 @@ Files starting with `_` and `layout.svelte` are ignored (convention for partials
|
|
|
119
119
|
|
|
120
120
|
## Passing data to the page
|
|
121
121
|
|
|
122
|
-
Small, public initial data (plan name, title, preferences) goes in `data` and arrives via `$props()` — no extra request
|
|
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:
|
|
123
124
|
|
|
124
125
|
```ts
|
|
125
126
|
app.get("/dashboard", (c) => c.render("dashboard", { data: { plan: "pro" } }));
|
|
126
127
|
```
|
|
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
|
+
|
|
174
|
+
```svelte
|
|
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
|
|
199
|
+
```
|
|
128
200
|
|
|
129
201
|
## API
|
|
130
202
|
|
|
131
203
|
### `shell(options?)`
|
|
132
204
|
|
|
133
|
-
Hono middleware that provides `c.render(entry, { title?, data? })` on every route.
|
|
205
|
+
Hono middleware that provides `c.render(entry, { title?, data?, status?, headers?, head? })` on every route.
|
|
134
206
|
|
|
135
207
|
| Option | Default | Description |
|
|
136
208
|
|---|---|---|
|
|
137
209
|
| `title` | `"App"` | Title used when the route doesn't provide one |
|
|
138
210
|
| `lang` | `"en"` | `<html>` `lang` attribute |
|
|
139
|
-
| `assetsBase` | `"/static"` | Prefix for JS files in production |
|
|
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 |
|
|
140
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 |
|
|
141
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.
|
|
142
227
|
|
|
143
228
|
### `pages(options?)`
|
|
144
229
|
|
|
@@ -147,14 +232,41 @@ Vite plugin (`hono-svelte/vite`) that discovers pages and generates client entri
|
|
|
147
232
|
| Option | Default | Description |
|
|
148
233
|
|---|---|---|
|
|
149
234
|
| `pagesDir` | `"src/pages"` | Pages folder |
|
|
150
|
-
| `ignore` | `["**/
|
|
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 |
|
|
151
238
|
| `alwaysClient` | `[]` | Pages that always get JS, even without `<script>` |
|
|
152
239
|
|
|
153
|
-
Handy methods: `input()` (client build entries), `entries()` (all pages), `staticEntries()` (static pages only), `hasClient(entry)`.
|
|
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).
|
|
154
241
|
|
|
155
242
|
### Typing `c.render`
|
|
156
243
|
|
|
157
|
-
|
|
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`:
|
|
158
270
|
|
|
159
271
|
```ts
|
|
160
272
|
import type { RenderProps } from "hono-svelte";
|
|
@@ -166,10 +278,13 @@ declare module "hono" {
|
|
|
166
278
|
}
|
|
167
279
|
```
|
|
168
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
|
+
|
|
169
283
|
## Production tips
|
|
170
284
|
|
|
171
|
-
-
|
|
172
|
-
-
|
|
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).
|
|
173
288
|
- Run both client and server builds before serving; the example in `examples/playground/` shows the full setup.
|
|
174
289
|
|
|
175
290
|
## Example
|
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;
|
package/dist/head.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Per-page <head> tags (CSP-safe: meta/link tags plus an opt-in raw slot).
|
|
2
|
+
// No executable inline scripts are ever emitted here.
|
|
3
|
+
function escapeAttr(value) {
|
|
4
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
5
|
+
}
|
|
6
|
+
export function renderHead(head) {
|
|
7
|
+
if (!head)
|
|
8
|
+
return "";
|
|
9
|
+
if (typeof head === "string")
|
|
10
|
+
return head;
|
|
11
|
+
const out = [];
|
|
12
|
+
if (head.description !== undefined) {
|
|
13
|
+
out.push(`<meta name="description" content="${escapeAttr(head.description)}" />`);
|
|
14
|
+
}
|
|
15
|
+
if (head.canonical !== undefined) {
|
|
16
|
+
out.push(`<link rel="canonical" href="${escapeAttr(head.canonical)}" />`);
|
|
17
|
+
}
|
|
18
|
+
if (head.themeColor !== undefined) {
|
|
19
|
+
out.push(`<meta name="theme-color" content="${escapeAttr(head.themeColor)}" />`);
|
|
20
|
+
}
|
|
21
|
+
for (const m of head.meta ?? []) {
|
|
22
|
+
const attr = m.name
|
|
23
|
+
? `name="${escapeAttr(m.name)}"`
|
|
24
|
+
: m.property
|
|
25
|
+
? `property="${escapeAttr(m.property)}"`
|
|
26
|
+
: m.httpEquiv
|
|
27
|
+
? `http-equiv="${escapeAttr(m.httpEquiv)}"`
|
|
28
|
+
: null;
|
|
29
|
+
if (!attr)
|
|
30
|
+
continue;
|
|
31
|
+
out.push(`<meta ${attr} content="${escapeAttr(m.content)}" />`);
|
|
32
|
+
}
|
|
33
|
+
for (const [k, v] of Object.entries(head.og ?? {})) {
|
|
34
|
+
out.push(`<meta property="og:${escapeAttr(k)}" content="${escapeAttr(v)}" />`);
|
|
35
|
+
}
|
|
36
|
+
for (const [k, v] of Object.entries(head.twitter ?? {})) {
|
|
37
|
+
out.push(`<meta name="twitter:${escapeAttr(k)}" content="${escapeAttr(v)}" />`);
|
|
38
|
+
}
|
|
39
|
+
if (head.extra)
|
|
40
|
+
out.push(head.extra);
|
|
41
|
+
return out.join("");
|
|
42
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,89 @@
|
|
|
1
1
|
import type { Next } from "hono";
|
|
2
|
+
import { type AssetsResolver, type ViteManifest } from "./assets.js";
|
|
3
|
+
import { type HeadProps } from "./head.js";
|
|
2
4
|
export { getIds } from "./ids.js";
|
|
3
5
|
export type { SharedIds } from "./ids.js";
|
|
6
|
+
export { renderHead } from "./head.js";
|
|
7
|
+
export type { HeadMetaItem, HeadProps } from "./head.js";
|
|
8
|
+
export { CACHE_IMMUTABLE, CACHE_NO_STORE, createManifestAssets, immutableHeaders, manifestCssFor, manifestImportsFor, manifestKeysFor, } from "./assets.js";
|
|
9
|
+
export type { AssetsResolver, ManifestAssetsOptions, ViteManifest, ViteManifestChunk, } from "./assets.js";
|
|
10
|
+
export { notFoundHandler, errorHandler } from "./handlers.js";
|
|
11
|
+
export type { ErrorPageOptions, ServerErrorHandlerOptions } from "./handlers.js";
|
|
12
|
+
export { initFiles, doctorChecks, checkAppLink, versionsMatch, isSafeAppPath } from "./scaffold.js";
|
|
13
|
+
export type { DoctorIssue, DoctorOptions, InitFile } from "./scaffold.js";
|
|
4
14
|
export type SsrPageLoader = () => Promise<{
|
|
5
15
|
default: unknown;
|
|
6
16
|
}>;
|
|
7
17
|
export type RenderProps = {
|
|
8
18
|
title?: string;
|
|
9
19
|
data?: Record<string, unknown>;
|
|
20
|
+
/** HTTP status of the shell response. @default 200 */
|
|
21
|
+
status?: number;
|
|
22
|
+
/** Extra response headers merged into the shell response. */
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
/** Per-page head tags, merged after the shell `head` option. */
|
|
25
|
+
head?: string | HeadProps;
|
|
10
26
|
};
|
|
27
|
+
export type PrefetchMode = false | "hover" | "all";
|
|
11
28
|
export type ShellOptions = {
|
|
12
29
|
title?: string;
|
|
13
30
|
lang?: string;
|
|
14
31
|
assetsBase?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the client JS URL for an entry.
|
|
34
|
+
* - String: base prefix (`/static` -> `/static/<entry>.js`).
|
|
35
|
+
* - Vite manifest object or resolver function: hashed files.
|
|
36
|
+
* @default "/static"
|
|
37
|
+
*/
|
|
38
|
+
assets?: string | ViteManifest | AssetsResolver;
|
|
39
|
+
/** Extra CSS links per HTML response. Strings are hrefs verbatim. */
|
|
40
|
+
styles?: string[];
|
|
15
41
|
stylesHref?: string | ((isProd: boolean) => string);
|
|
42
|
+
/** Global head HTML (fonts, meta) — page `head` prop is appended after it. */
|
|
16
43
|
head?: string;
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
44
|
+
/**
|
|
45
|
+
* CSP nonce applied to `<script>`, `<link>` and `<style data-hs>` tags
|
|
46
|
+
* emitted by the shell. Pass a per-request nonce string or a function
|
|
47
|
+
* reading it from the Hono context (e.g. set by `secureHeaders()`).
|
|
48
|
+
*/
|
|
49
|
+
nonce?: string | ((c: unknown) => string | undefined);
|
|
50
|
+
/**
|
|
51
|
+
* `<link rel="modulepreload">` for the current entry (+ its manifest
|
|
52
|
+
* imports). @default true in prod, false in dev.
|
|
53
|
+
*/
|
|
54
|
+
preload?: boolean;
|
|
55
|
+
/** Prefetch other client entries. @default false */
|
|
56
|
+
prefetch?: PrefetchMode;
|
|
57
|
+
/**
|
|
58
|
+
* Warn once when serialized `data` exceeds this many bytes.
|
|
59
|
+
* @default 102400 (100KB). Set to `false` to disable.
|
|
60
|
+
*/
|
|
61
|
+
dataLimit?: number | false;
|
|
62
|
+
/** Manual override of the entryName -> loader map (otherwise resolved via ssr-manifest). */
|
|
19
63
|
ssrPages?: Record<string, SsrPageLoader>;
|
|
64
|
+
/**
|
|
65
|
+
* Known entry names. When set (or auto-detected from the manifest),
|
|
66
|
+
* unknown entries throw a dev-friendly error listing available pages.
|
|
67
|
+
* In production the error message is generic.
|
|
68
|
+
*/
|
|
69
|
+
knownEntries?: string[];
|
|
70
|
+
/** `false` disables unknown-entry validation. @default true */
|
|
71
|
+
strict?: boolean;
|
|
72
|
+
/** Default status when `c.render(entry)` omits it. @default 200 */
|
|
73
|
+
status?: number;
|
|
74
|
+
/** Default headers merged into every shell response. */
|
|
75
|
+
headers?: Record<string, string>;
|
|
76
|
+
/**
|
|
77
|
+
* Force prod/dev asset behavior. Defaults to auto-detect
|
|
78
|
+
* (`import.meta.env.PROD`, else `NODE_ENV === "production"`).
|
|
79
|
+
*/
|
|
80
|
+
isProd?: boolean;
|
|
20
81
|
};
|
|
21
82
|
export type ShellContext = {
|
|
22
83
|
render: (entryName: string, props?: RenderProps) => Response | Promise<Response>;
|
|
23
84
|
};
|
|
24
|
-
|
|
85
|
+
/** Visible for tests. */
|
|
86
|
+
export declare function __resetDataLimitWarned(): void;
|
|
87
|
+
export declare function shell(options?: ShellOptions): ((c: any, next: Next) => Promise<void>) & {
|
|
88
|
+
availableEntries: () => string[] | undefined;
|
|
89
|
+
};
|