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/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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
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/ids.js CHANGED
@@ -1,6 +1,6 @@
1
- // IDs opacos derivados do entryName. Puro, sem dependencias de runtime.
2
- // (funciona em Node, Workers, Deno, Bun). Nao e segredo de estado:
3
- // apenas evita expor dados do servidor no HTML.
1
+ // Opaque IDs derived from the entryName. Pure, no runtime dependencies.
2
+ // (works on Node, Workers, Deno, Bun). Not a state secret:
3
+ // it just avoids exposing server internals in the HTML.
4
4
  function hashString(input) {
5
5
  let h1 = 0xdeadbeef;
6
6
  let h2 = 0x41c6ce57;
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
- /** Override manual do mapa entryName -> loader (opcional; sem isso o shell
18
- * resolve o mapa automaticamente via ssr-manifest). */
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
- export declare function shell(options?: ShellOptions): (c: any, next: Next) => Promise<void>;
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
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,13 @@
1
+ import { createManifestAssets, manifestCssFor, manifestImportsFor, } from "./assets.js";
2
+ import { renderHead } from "./head.js";
1
3
  import { getIds } from "./ids.js";
2
- import { ssrPages as autoSsrPages } from "./ssr-manifest.js";
4
+ import { allEntries as autoAllEntries, ssrLayouts as autoSsrLayouts, ssrPages as autoSsrPages, } from "./ssr-manifest.js";
3
5
  import { devEntryUrl } from "./virtual.js";
4
6
  export { getIds } from "./ids.js";
7
+ export { renderHead } from "./head.js";
8
+ export { CACHE_IMMUTABLE, CACHE_NO_STORE, createManifestAssets, immutableHeaders, manifestCssFor, manifestImportsFor, manifestKeysFor, } from "./assets.js";
9
+ export { notFoundHandler, errorHandler } from "./handlers.js";
10
+ export { initFiles, doctorChecks, checkAppLink, versionsMatch, isSafeAppPath } from "./scaffold.js";
5
11
  function escapeAttr(value) {
6
12
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
7
13
  }
@@ -15,70 +21,337 @@ function isValidEntryName(entryName) {
15
21
  const parts = entryName.split("/");
16
22
  return parts.every((p) => p.length > 0 && p !== "." && p !== ".." && !p.includes(String.fromCharCode(0)));
17
23
  }
18
- function serializePageData(data, dataId) {
24
+ function nonceAttr(nonce) {
25
+ return nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
26
+ }
27
+ function serializePageData(data, dataId, dataLimit) {
19
28
  if (!data)
20
- return "";
29
+ return { json: "", html: "" };
21
30
  let json;
22
31
  try {
23
32
  json = JSON.stringify(data);
24
33
  }
25
34
  catch {
26
- throw new Error("hono-svelte: props.data precisa ser serializavel em JSON");
35
+ throw new Error("hono-svelte: props.data must be JSON-serializable");
27
36
  }
28
37
  if (json === undefined)
38
+ return { json: "", html: "" };
39
+ checkDataLimit(json, dataLimit);
40
+ const safe = json.replace(/</g, "\\u003c");
41
+ const lt = String.fromCharCode(60);
42
+ return {
43
+ json,
44
+ html: lt + 'script type="application/json" id="' + dataId + '">' + safe + lt + "/script>",
45
+ };
46
+ }
47
+ const warnedDataLimit = new Set();
48
+ function checkDataLimit(json, dataLimit) {
49
+ if (dataLimit === false)
50
+ return;
51
+ const bytes = new TextEncoder().encode(json).length;
52
+ if (bytes <= dataLimit)
53
+ return;
54
+ const key = String(dataLimit);
55
+ if (warnedDataLimit.has(key))
56
+ return;
57
+ warnedDataLimit.add(key);
58
+ const kb = (bytes / 1024).toFixed(1);
59
+ const limitKb = (dataLimit / 1024).toFixed(0);
60
+ console.warn(`[hono-svelte] page data is ${kb}KB (limit ${limitKb}KB). ` +
61
+ `data is inlined in the HTML — move large/sensitive payloads to a typed API (hc<AppType>).`);
62
+ }
63
+ /** Visible for tests. */
64
+ export function __resetDataLimitWarned() {
65
+ warnedDataLimit.clear();
66
+ }
67
+ /**
68
+ * Build-time flags. `import.meta.env.PROD/DEV` MUST be accessed statically
69
+ * (no `as unknown`, no intermediate variable) so Vite/esbuild can replace
70
+ * them with literals per bundle. In plain node (no Vite define) the access
71
+ * throws at module-eval time, hence the try/catch.
72
+ *
73
+ * NOTE: the server bundle is ALWAYS production (built with
74
+ * `vite build --mode production`), so when Vite replaced the flag the answer
75
+ * is baked in. The NODE_ENV fallback only matters for unbundled runtimes
76
+ * (tests, `vite dev` SSR). `node ./dist/index.js` needs no env prefix.
77
+ */
78
+ function detectProd() {
79
+ try {
80
+ if (import.meta.env.PROD !== undefined)
81
+ return import.meta.env.PROD;
82
+ }
83
+ catch {
84
+ // plain node / vitest: no import.meta.env — bundled server is prod
85
+ return true;
86
+ }
87
+ return globalThis.process?.env?.NODE_ENV === "production";
88
+ }
89
+ function isDev() {
90
+ try {
91
+ if (import.meta.env.DEV !== undefined)
92
+ return import.meta.env.DEV;
93
+ }
94
+ catch {
95
+ // plain node / vitest
96
+ return false;
97
+ }
98
+ return globalThis.process?.env?.NODE_ENV !== "production";
99
+ }
100
+ function defaultKnownEntries(ssrPages) {
101
+ const keys = Object.keys(ssrPages);
102
+ if (ssrPages.__allEntries !== undefined) {
103
+ return ssrPages.__allEntries;
104
+ }
105
+ return keys.length > 0 ? keys : undefined;
106
+ }
107
+ function renderNotFoundHint(available, _prod) {
108
+ if (!available || available.length === 0) {
109
+ return "hono-svelte: unknown entry (check pagesDir and the entryName passed to c.render)";
110
+ }
111
+ const list = [...available].sort().slice(0, 20).join('", "');
112
+ const more = available.length > 20 ? ` (+${available.length - 20} more)` : "";
113
+ return `hono-svelte: unknown entry. Available pages: "${list}"${more}`;
114
+ }
115
+ function resolveAssetUrl(assets, assetsBase, entryName) {
116
+ const resolver = typeof assets === "function"
117
+ ? assets
118
+ : typeof assets === "object" && assets !== null
119
+ ? createManifestAssets(assets)
120
+ : (entry) => `${assetsBase}/${entry}.js`;
121
+ return resolver(entryName);
122
+ }
123
+ function manifestFor(assets) {
124
+ return typeof assets === "object" && assets !== null ? assets : undefined;
125
+ }
126
+ function prefetchUrls(mode, entryName, allEntries, resolveUrl) {
127
+ if (!mode || !allEntries)
128
+ return [];
129
+ if (mode === "all")
130
+ return allEntries.filter((e) => e !== entryName).map(resolveUrl);
131
+ return [];
132
+ }
133
+ function prefetchHoverScript(nonce, dataId, urls) {
134
+ if (urls.length === 0)
29
135
  return "";
30
- json = json.replace(/</g, "\\u003c");
31
136
  const lt = String.fromCharCode(60);
32
- return lt + 'script type="application/json" id="' + dataId + '">' + json + lt + "/script>";
137
+ const map = JSON.stringify(Object.fromEntries(urls.map((u) => [u.entry, u.url])));
138
+ const code = `(function(){var m=${map};` +
139
+ `function pre(u){if(document.querySelector('link[rel=\"prefetch\"][href=\"'+u+'\"]'))return;` +
140
+ `var l=document.createElement('link');l.rel='prefetch';l.href=u;document.head.appendChild(l);}` +
141
+ `document.addEventListener('mouseover',function(e){var a=e.target&&e.target.closest?e.target.closest('a[href]'):null;` +
142
+ `if(!a)return;try{var p=new URL(a.href,location.origin).pathname.replace(/^\\//,'');` +
143
+ `var u=m[p];if(u)pre(u);}catch(_){}},{passive:true});})();`;
144
+ return lt + `script${nonceAttr(nonce)}>${code}` + lt + "/script>";
33
145
  }
34
146
  export function shell(options = {}) {
35
147
  const titleDefault = options.title ?? "App";
36
148
  const lang = options.lang ?? "en";
37
149
  const assetsBase = (options.assetsBase ?? "/static").replace(/\/$/, "");
150
+ const assetsOpt = options.assets ?? assetsBase;
38
151
  const head = options.head ?? "";
152
+ const defaultStatus = options.status ?? 200;
153
+ const defaultHeaders = options.headers ?? {};
154
+ const dataLimit = options.dataLimit ?? 102400;
155
+ const strict = options.strict ?? true;
156
+ const extraStyles = options.styles ?? [];
39
157
  const resolveStyles = typeof options.stylesHref === "function"
40
158
  ? options.stylesHref
41
159
  : () => options.stylesHref;
42
- // Zero config: com pages() ativo, este modulo e redirecionado para o
43
- // manifesto in-memory pelo resolveId do plugin (enforce: "pre").
160
+ // Zero config: with pages() active, this module is redirected to the
161
+ // in-memory manifest by the plugin's resolveId (enforce: "pre").
44
162
  const ssrPages = options.ssrPages ?? autoSsrPages;
45
- return async function shellMiddleware(c, next) {
163
+ // NOTE: `p` in the bundle below is the layouts map. It must come from the
164
+ // VIRTUAL manifest (ssrPages.__layouts), never from the stub — the stub's
165
+ // ssrLayouts is always {}. When the caller passes a manual ssrPages map,
166
+ // __layouts travels on the same object (see vite.ts manifestSource).
167
+ const ssrLayouts = ssrPages.__layouts ??
168
+ autoSsrPages.__layouts ??
169
+ autoSsrLayouts;
170
+ const staticKnown = options.knownEntries ??
171
+ (autoAllEntries.length > 0 ? [...autoAllEntries] : undefined) ??
172
+ defaultKnownEntries(ssrPages);
173
+ function availableEntries() {
174
+ if (staticKnown)
175
+ return staticKnown;
176
+ return defaultKnownEntries(ssrPages);
177
+ }
178
+ /**
179
+ * Client entries have a JS bundle (input() in the plugin). The virtual
180
+ * manifest carries them via `clientEntries`; the static stub has none.
181
+ * Layout-wrapped STATIC pages are SSR (no script branch) — their chain
182
+ * is rendered server-side.
183
+ */
184
+ function hasClientEntry(entryName) {
185
+ const fromManifest = ssrPages.__clientEntries;
186
+ if (fromManifest)
187
+ return fromManifest.includes(entryName);
188
+ const auto = autoSsrPages.__clientEntries;
189
+ if (auto)
190
+ return auto.includes(entryName);
191
+ return false;
192
+ }
193
+ function layoutChainFor(entryName) {
194
+ if (entryName === "layout" || entryName.endsWith("/layout"))
195
+ return [];
196
+ const layouts = Object.keys(ssrLayouts);
197
+ if (layouts.length === 0)
198
+ return [];
199
+ const chain = [];
200
+ const parts = entryName.split("/");
201
+ for (let i = 1; i < parts.length; i++) {
202
+ const layout = parts.slice(0, i).join("/") + "/layout";
203
+ if (layouts.includes(layout))
204
+ chain.push(layout);
205
+ }
206
+ if (layouts.includes("layout"))
207
+ chain.unshift("layout");
208
+ return chain;
209
+ }
210
+ function resolveNonce(c) {
211
+ if (typeof options.nonce === "function") {
212
+ try {
213
+ return options.nonce(c) ?? undefined;
214
+ }
215
+ catch {
216
+ return undefined;
217
+ }
218
+ }
219
+ return options.nonce;
220
+ }
221
+ async function renderSsrBody(loader, pageData, nonce, layoutChain) {
222
+ const mod = await loader();
223
+ const { render } = await import("svelte/server");
224
+ const renderOpts = {};
225
+ if (pageData !== undefined)
226
+ renderOpts.props = pageData;
227
+ if (nonce)
228
+ renderOpts.csp = { nonce };
229
+ const rendered = render(mod.default, renderOpts);
230
+ let html = rendered.html ?? rendered.body ?? "";
231
+ let headValue = typeof rendered.head === "function" ? rendered.head() : (rendered.head ?? "");
232
+ if (layoutChain.length > 0) {
233
+ const { createRawSnippet } = await import("svelte");
234
+ for (const layoutLoader of layoutChain) {
235
+ const layoutMod = await layoutLoader();
236
+ const childHtml = html;
237
+ // The snippet MUST render a single wrapper element: the server
238
+ // renderer pushes render() output verbatim, and the client
239
+ // createRawSnippet takes get_first_child(fragment). An empty render
240
+ // breaks hydration ("Cannot set properties of null"). The extra div
241
+ // is hydration-safe (hydrate reuses SSR DOM).
242
+ const childSnippet = createRawSnippet(() => ({ render: () => `<div>${childHtml}</div>` }));
243
+ const wrapped = render(layoutMod.default, {
244
+ ...(pageData !== undefined ? { props: pageData } : {}),
245
+ props: { ...(pageData ?? {}), children: childSnippet },
246
+ ...(nonce ? { csp: { nonce } } : {}),
247
+ });
248
+ html = wrapped.html ?? wrapped.body ?? "";
249
+ const layoutHead = typeof wrapped.head === "function" ? wrapped.head() : (wrapped.head ?? "");
250
+ headValue = layoutHead + headValue;
251
+ }
252
+ }
253
+ return { html, head: headValue };
254
+ }
255
+ async function inner(c, next) {
46
256
  c.setRenderer(async (entryName, props) => {
47
257
  if (!isValidEntryName(entryName)) {
48
- throw new Error(`hono-svelte: entryName invalido: ${JSON.stringify(entryName)}`);
258
+ throw new Error(`hono-svelte: invalid entryName: ${JSON.stringify(entryName)}`);
259
+ }
260
+ const loader = ssrPages[entryName];
261
+ // A page is SSR when it is static (no <script>) — LAYOUTS DON'T MATTER
262
+ // here. A static page wrapped in layouts SSRs the page and wraps it in
263
+ // the layout chain (same rule as pages().staticEntries/clientEntries).
264
+ // Client entries (has <script>) always go the script branch.
265
+ const chain = layoutChainFor(entryName)
266
+ .map((l) => ssrLayouts[l])
267
+ .filter((l) => l !== undefined);
268
+ const isSsr = loader !== undefined && !hasClientEntry(entryName);
269
+ if (strict && !isSsr) {
270
+ const known = availableEntries();
271
+ if (known && !known.includes(entryName)) {
272
+ throw new Error(`${renderNotFoundHint(known)} (got ${JSON.stringify(entryName)})`);
273
+ }
49
274
  }
50
275
  const ids = getIds(entryName);
51
276
  const title = props?.title ?? titleDefault;
52
- const isProd = import.meta.env?.PROD ?? false;
277
+ const isProd = options.isProd ?? detectProd();
278
+ const nonce = resolveNonce(c);
279
+ const nAttr = nonceAttr(nonce);
53
280
  const stylesHref = resolveStyles(isProd) ?? (isProd ? "/static/styles.css" : "/src/styles.css");
54
- const dataHtml = props?.data ? serializePageData(props.data, ids.dataId) : "";
281
+ const { html: dataHtml } = serializePageData(props?.data, ids.dataId, dataLimit);
55
282
  const lt = String.fromCharCode(60);
56
- const loader = ssrPages[entryName];
57
283
  let bodyHtml = "";
58
284
  let ssrHead = "";
59
- if (loader !== undefined) {
60
- const mod = await loader();
61
- const { render } = await import("svelte/server");
62
- const rendered = render(mod.default);
285
+ if (isSsr) {
286
+ const rendered = await renderSsrBody(loader, props?.data, nonce, chain);
63
287
  bodyHtml = rendered.html;
64
- ssrHead = rendered.head ?? "";
288
+ ssrHead = rendered.head;
65
289
  }
290
+ const resolveUrl = (entry) => isProd ? resolveAssetUrl(assetsOpt, assetsBase, entry) : devEntryUrl(entry);
66
291
  let scriptHtml = "";
67
- if (loader === undefined) {
68
- const src = isProd ? `${assetsBase}/${entryName}.js` : devEntryUrl(entryName);
69
- scriptHtml = lt + `script type="module" src="${escapeAttr(src)}">` + lt + "/script>";
292
+ let preloadHtml = "";
293
+ let prefetchHtml = "";
294
+ if (!isSsr) {
295
+ const src = resolveUrl(entryName);
296
+ scriptHtml = lt + `script type="module" src="${escapeAttr(src)}"${nAttr}>` + lt + "/script>";
297
+ const shouldPreload = options.preload ?? isProd;
298
+ const manifest = isProd ? manifestFor(assetsOpt) : undefined;
299
+ if (shouldPreload) {
300
+ const urls = [src];
301
+ if (manifest) {
302
+ for (const imp of manifestImportsFor(manifest, entryName)) {
303
+ if (!urls.includes(imp))
304
+ urls.push(imp);
305
+ }
306
+ }
307
+ preloadHtml = urls
308
+ .map((u) => lt + `link rel="modulepreload" href="${escapeAttr(u)}"${nAttr} />`)
309
+ .join("");
310
+ }
311
+ if (isProd && options.prefetch) {
312
+ const known = availableEntries();
313
+ if (options.prefetch === "all" && known) {
314
+ for (const u of prefetchUrls("all", entryName, known, resolveUrl)) {
315
+ prefetchHtml += lt + `link rel="prefetch" href="${escapeAttr(u)}"${nAttr} />`;
316
+ }
317
+ }
318
+ else if (options.prefetch === "hover" && known) {
319
+ const others = known.filter((e) => e !== entryName && ssrPages[e] === undefined);
320
+ prefetchHtml = prefetchHoverScript(nonce, ids.dataId, others.map((e) => ({ entry: e, url: resolveUrl(e) })));
321
+ }
322
+ }
70
323
  }
324
+ const status = props?.status ?? defaultStatus;
325
+ const headers = { ...defaultHeaders, ...(props?.headers ?? {}) };
326
+ const pageHead = props?.head !== undefined ? renderHead(props.head) : "";
327
+ const manifestNow = isProd ? manifestFor(assetsOpt) : undefined;
328
+ const cssLinks = (manifestNow ? manifestCssFor(manifestNow, entryName) : [])
329
+ .map((href) => lt + `link rel="stylesheet" href="${escapeAttr(href)}"${nAttr} />`)
330
+ .join("") +
331
+ extraStyles
332
+ .map((href) => lt + `link rel="stylesheet" href="${escapeAttr(href)}"${nAttr} />`)
333
+ .join("");
71
334
  return c.html(`<!doctype html><html lang="${escapeAttr(lang)}"><head><title>${escapeAttr(title)}</title>` +
72
335
  `<meta charset="utf-8" /><meta content="width=device-width, initial-scale=1" name="viewport" />` +
73
336
  (head ? head : "") +
74
- `<link rel="stylesheet" href="${escapeAttr(stylesHref)}" />` +
337
+ `<link rel="stylesheet" href="${escapeAttr(stylesHref)}"${nAttr} />` +
338
+ cssLinks +
75
339
  ssrHead +
340
+ pageHead +
341
+ preloadHtml +
342
+ prefetchHtml +
76
343
  scriptHtml +
77
344
  `</head>` +
78
345
  `<body class="bg-base-200 min-h-screen text-base-content"><div id="${ids.rootId}">${bodyHtml}</div>` +
79
346
  dataHtml +
80
- `</body></html>`);
347
+ `</body></html>`, status, headers);
81
348
  });
82
349
  await next();
83
- };
350
+ }
351
+ async function shellMiddleware(c, next) {
352
+ return inner(c, next);
353
+ }
354
+ const mw = shellMiddleware;
355
+ mw.availableEntries = availableEntries;
356
+ return mw;
84
357
  }
@@ -0,0 +1,22 @@
1
+ export type InitFile = {
2
+ path: string;
3
+ content: string;
4
+ skipIfExists: boolean;
5
+ };
6
+ export declare function initFiles(): InitFile[];
7
+ export type DoctorIssue = {
8
+ /** Machine-readable code, e.g. "missing-dist". */
9
+ code: string;
10
+ message: string;
11
+ fix: string;
12
+ };
13
+ export type DoctorOptions = {
14
+ /** Root of the hono-svelte package (where `dist/` lives). @default cwd */
15
+ packageRoot?: string;
16
+ /** Root of the app being checked. @default cwd */
17
+ appRoot?: string;
18
+ };
19
+ export declare function versionsMatch(_linkedVersion: string, _appWants: string): boolean;
20
+ export declare function doctorChecks(opts?: DoctorOptions): DoctorIssue[];
21
+ export declare function checkAppLink(appRoot: string): DoctorIssue | null;
22
+ export declare function isSafeAppPath(appRoot: string, target: string): boolean;
@@ -0,0 +1,107 @@
1
+ // `hono-svelte init` + `hono-svelte doctor` helpers.
2
+ //
3
+ // The package exposes pure helpers (`initFiles`, `doctorChecks`) so they can
4
+ // be unit-tested; the thin CLI wrapper lives in `src/cli.ts` (compiled to
5
+ // `dist/cli.js`, invoked via the `hono-svelte-doctor` bin).
6
+ import { existsSync } from "node:fs";
7
+ import { isAbsolute, join, resolve } from "node:path";
8
+ const ENV_DTS = `import type { RenderProps } from "hono-svelte";
9
+
10
+ declare module "hono" {
11
+ interface ContextRenderer {
12
+ (entryName: string, props?: RenderProps): Response | Promise<Response>;
13
+ }
14
+ }
15
+ `;
16
+ const VITE_CONFIG = `import build from "@hono/vite-build/node";
17
+ import devServer from "@hono/vite-dev-server";
18
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
19
+ import { pages } from "hono-svelte/vite";
20
+ import { resolve } from "node:path";
21
+ import { defineConfig } from "vite";
22
+
23
+ const appPages = pages();
24
+
25
+ export default defineConfig(({ command, mode }) => {
26
+ if (mode === "client") {
27
+ return {
28
+ plugins: [svelte(), appPages],
29
+ build: {
30
+ rollupOptions: {
31
+ input: { ...appPages.input(), styles: resolve("src/styles.css") },
32
+ output: {
33
+ entryFileNames: "static/[name].js",
34
+ chunkFileNames: "static/chunks/[name]-[hash].js",
35
+ assetFileNames: "static/[name][extname]",
36
+ },
37
+ },
38
+ },
39
+ };
40
+ }
41
+
42
+ if (command === "serve") {
43
+ return {
44
+ plugins: [svelte(), appPages, devServer({ entry: "src/routes/index.ts" })],
45
+ };
46
+ }
47
+
48
+ return {
49
+ plugins: [svelte(), appPages, build({ entry: "src/routes/index.ts", staticRoot: "./dist" })],
50
+ };
51
+ });
52
+ `;
53
+ const INDEX_SVELTE = `<main>
54
+ <h1>Welcome</h1>
55
+ <a href="/dashboard">Sign in</a>
56
+ </main>
57
+ `;
58
+ export function initFiles() {
59
+ return [
60
+ { path: "src/env.d.ts", content: ENV_DTS, skipIfExists: true },
61
+ { path: "vite.config.ts", content: VITE_CONFIG, skipIfExists: true },
62
+ { path: "src/pages/index.svelte", content: INDEX_SVELTE, skipIfExists: true },
63
+ ];
64
+ }
65
+ export function versionsMatch(_linkedVersion, _appWants) {
66
+ return true;
67
+ }
68
+ export function doctorChecks(opts = {}) {
69
+ const packageRoot = opts.packageRoot ? resolve(opts.packageRoot) : process.cwd();
70
+ const appRoot = opts.appRoot ? resolve(opts.appRoot) : process.cwd();
71
+ const issues = [];
72
+ for (const file of ["dist/index.js", "dist/vite.js", "dist/index.d.ts", "dist/vite.d.ts"]) {
73
+ if (!existsSync(join(packageRoot, file))) {
74
+ issues.push({
75
+ code: "missing-dist",
76
+ message: `hono-svelte dist is missing (${file} not found). The package was not built.`,
77
+ fix: "Run `npm run build` in the hono-svelte package directory.",
78
+ });
79
+ break;
80
+ }
81
+ }
82
+ const viteConfig = join(appRoot, "vite.config.ts");
83
+ if (!existsSync(viteConfig) && !existsSync(join(appRoot, "vite.config.js"))) {
84
+ issues.push({
85
+ code: "missing-vite-config",
86
+ message: "No vite config found in the app.",
87
+ fix: "Run `hono-svelte init` to scaffold one, or copy the README quick-start.",
88
+ });
89
+ }
90
+ return issues;
91
+ }
92
+ export function checkAppLink(appRoot) {
93
+ const appLink = join(resolve(appRoot), "node_modules", "hono-svelte");
94
+ if (!existsSync(join(appLink, "package.json"))) {
95
+ return {
96
+ code: "missing-link",
97
+ message: `node_modules/hono-svelte is missing or a stale junction in ${appRoot}.`,
98
+ fix: "Run `npm install` inside the app directory to recreate the file: link.",
99
+ };
100
+ }
101
+ return null;
102
+ }
103
+ export function isSafeAppPath(appRoot, target) {
104
+ const root = resolve(appRoot);
105
+ const abs = isAbsolute(target) ? target : resolve(root, target);
106
+ return abs === root || abs.startsWith(root + "/");
107
+ }
@@ -1,3 +1,11 @@
1
1
  export declare const ssrPages: Record<string, () => Promise<{
2
2
  default: unknown;
3
3
  }>>;
4
+ export declare const ssrLayouts: Record<string, () => Promise<{
5
+ default: unknown;
6
+ }>>;
7
+ export declare const allEntries: string[];
8
+ export declare const layoutEntries: string[];
9
+ export declare const clientEntries: string[];
10
+ export declare const staticEntries: string[];
11
+ export declare function hasClient(_entryName: string): boolean;