vinojs 0.0.1 → 0.1.1

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/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # Vino
2
+
3
+ Hono + Vite meta-framework designed for Cloudflare Workers.
4
+
5
+ Early WIP - core changes will happen
6
+
7
+ - **Pages** — filesystem routes in `src/pages` using Nitro path syntax
8
+ - **Layouts** — `src/layout` files wrap matching routes
9
+ - **Server** — your Hono app; Vino mounts pages onto it
10
+ - **SSR** — `hono/jsx`
11
+ - **Hydration** — `definePage` for a whole page, `src/components/` for widgets (`hono/jsx/dom`)
12
+ - **SSG** — prerender static routes (and dynamic routes with `ssgParams`) at build time; `partial` writes layout shells, `full` writes complete HTML
13
+ - **Dev / deploy** — `@cloudflare/vite-plugin`
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add vinojs hono
19
+ pnpm add -D vite @cloudflare/vite-plugin wrangler
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ `src/server.ts` — you own the Hono app; extra routes stay on it:
25
+
26
+ ```ts
27
+ import { Hono } from "hono";
28
+ import { createVino, mountVino } from "vinojs";
29
+ import { api } from "./api";
30
+
31
+ const app = new Hono().route("/api", api);
32
+ mountVino(app);
33
+ export default app;
34
+ export type AppType = typeof app;
35
+ ```
36
+
37
+ `vite.config.ts`:
38
+
39
+ ```ts
40
+ import { defineConfig } from "vite";
41
+ import { cloudflare } from "@cloudflare/vite-plugin";
42
+ import { vino } from "vinojs/vite";
43
+
44
+ export default defineConfig({
45
+ plugins: [vino(), cloudflare({ viteEnvironment: { name: "ssr" } })],
46
+ });
47
+ ```
48
+
49
+ `wrangler.jsonc`:
50
+
51
+ ```jsonc
52
+ {
53
+ "name": "my-app",
54
+ "main": "./src/server.ts",
55
+ "compatibility_date": "2026-08-18",
56
+ }
57
+ ```
58
+
59
+ ## Routing (`src/pages`)
60
+
61
+ | File | URL |
62
+ | ---------------------- | ------------- |
63
+ | `index.tsx` | `/` |
64
+ | `about.tsx` | `/about` |
65
+ | `(marketing)/jobs.tsx` | `/jobs` |
66
+ | `blog/[slug].tsx` | `/blog/:slug` |
67
+ | `docs/[...slug].tsx` | `/docs/*` |
68
+
69
+ The file **is** the page. Export a default component, `definePage(...)`, `defineServerPage(...)`, a Hono sub-app, or named `GET` / `POST` handlers.
70
+
71
+ ```tsx
72
+ import { definePage } from "vinojs";
73
+ import { useState } from "hono/jsx";
74
+
75
+ export default definePage(() => {
76
+ const [n, setN] = useState(0);
77
+ return <button onClick={() => setN(n + 1)}>{n}</button>;
78
+ });
79
+ ```
80
+
81
+ ```tsx
82
+ import { definePage } from "vinojs";
83
+
84
+ export default definePage({
85
+ render: ({ data }) => <h1>{data.title}</h1>,
86
+ data: async (c) => ({ title: "Hello" }),
87
+ config: { prerender: "full", clientRouting: true },
88
+ });
89
+ ```
90
+
91
+ `definePage` SSRs with `hono/jsx`, then hydrates `#vino-page` with `hono/jsx/dom`. `data` and `config` are colocated on the page.
92
+
93
+ Colocate loaders in `*.server.ts` and pass them through `defineServerFn`. The result is a `data` loader today; the same wrapper is how server functions will be called later (no `'use server'`).
94
+
95
+ ```ts
96
+ // [slug].server.ts
97
+ import { defineServerFn } from "vinojs";
98
+
99
+ export default defineServerFn((c) => {
100
+ const slug = c.req.param("slug") ?? "";
101
+ return { slug, title: "Hello" };
102
+ });
103
+ ```
104
+
105
+ ```tsx
106
+ // [slug].tsx
107
+ import { definePage } from "vinojs";
108
+ import data from "./[slug].server";
109
+
110
+ export default definePage({
111
+ data,
112
+ render: ({ data }) => <h1>{data.title}</h1>,
113
+ });
114
+ ```
115
+
116
+ `*.server.ts` files are not routes. Client bundles get stubs instead of the implementation.
117
+
118
+ Use `defineServerPage` for HTML that is never hydrated:
119
+
120
+ ```tsx
121
+ import { defineServerPage } from "vinojs";
122
+
123
+ export default defineServerPage(() => <h1>About</h1>);
124
+ ```
125
+
126
+ `prerender` may be `'full'` (complete HTML at build), `'partial'` (layout shell + empty `#vino-page`; client fetches content), or `false`.
127
+
128
+ ### Layouts (`src/layout`)
129
+
130
+ Layout files are compiled with the same path syntax as pages and wrap matching routes (root → leaf). `index.tsx` is the HTML document.
131
+
132
+ | File | Wraps |
133
+ | ----------- | --------------------- |
134
+ | `index.tsx` | every page |
135
+ | `blog.tsx` | `/blog` and `/blog/*` |
136
+ | `admin.tsx` | `/admin` |
137
+
138
+ ### Hydratable pages
139
+
140
+ `definePage(...)` SSRs with `hono/jsx`, then hydrates `#vino-page` with `hono/jsx/dom`. `defineServerPage(...)` SSRs only.
141
+
142
+ ### Hydratable components
143
+
144
+ Put widgets in `src/components/` (or name `$Widget.tsx` / `Widget.component.tsx`). They SSR as `<vino-component>` and hydrate on the client. Pass JSON-serializable props only.
145
+
146
+ ### Navigation
147
+
148
+ `clientRouting: true` in `definePage({ config })` or `vino({ clientRouting: true })` intercepts same-origin links, fetches page content only (`X-Vino-Nav`), swaps `#vino-page`, and rehydrates. Layout stays mounted. Default is on via the Vite plugin.
149
+
150
+ ## API
151
+
152
+ - `createVino()` — `vinojs`
153
+ - `mountVino(app)` — `vinojs`
154
+ - `definePage(fn | { render, data?, config? })` — hydrate the page (`vinojs`)
155
+ - `defineServerPage(...)` — SSR only, never hydrate (`vinojs`)
156
+ - `defineServerFn(fn)` — server-only function; pass as `definePage({ data })` (`vinojs`)
157
+ - `createClient()` — `vinojs/client` (injected automatically)
158
+ - `vino({ pagesDir, layoutsDir, clientRouting, prerender })` — `vinojs/vite`
159
+
160
+ APIs stay on your Hono app. Use `hono/client` `hc` against those routes; page HTML is never fetched through `hc`.
161
+
162
+ ## Example
163
+
164
+ ```bash
165
+ pnpm install
166
+ pnpm build
167
+ pnpm --filter @vinojs/example-basic dev
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,41 @@
1
+ import { u as VinoPayload } from "./types-CQITdkpF.mjs";
2
+ //#region src/client/create-client.d.ts
3
+ interface CreateClientOptions {
4
+ hydrate?: (vnode: unknown, el: Element) => void | Promise<void>;
5
+ createElement?: (type: unknown, props: unknown) => unknown;
6
+ }
7
+ type ComponentLoaders = Record<string, () => Promise<Record<string, unknown>>>;
8
+ type ClientPageLoaders = Record<string, () => Promise<{
9
+ default?: unknown;
10
+ }>>;
11
+ type VinoHmrPayload = {
12
+ kind: "component";
13
+ id: string;
14
+ mod?: Record<string, unknown>;
15
+ } | {
16
+ kind: "client-page";
17
+ id: string;
18
+ mod?: {
19
+ default?: unknown;
20
+ };
21
+ } | {
22
+ kind: "ssr";
23
+ } | {
24
+ kind: "layout";
25
+ } | {
26
+ kind: "registry";
27
+ components?: ComponentLoaders;
28
+ clientPages?: ClientPageLoaders;
29
+ };
30
+ type HmrFn = (payload: VinoHmrPayload) => void;
31
+ /**
32
+ * Fill partial shells, hydrate `#vino-page` / `<vino-component>`, and intercept
33
+ * same-origin links with content-only swaps when `clientRouting` is enabled.
34
+ */
35
+ declare function createClient(options?: CreateClientOptions): {
36
+ ready: Promise<VinoPayload | null>;
37
+ hmr: HmrFn;
38
+ };
39
+ //#endregion
40
+ export { type CreateClientOptions, type VinoHmrPayload, createClient };
41
+ //# sourceMappingURL=client.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../src/client/create-client.ts"],"mappings":";;UAIiB;EACf,WAAW,gBAAgB,IAAI,mBAAmB;EAClD,iBAAiB,eAAe;;KAG7B,mBAAmB,qBAAqB,QAAQ;KAChD,oBAAoB,qBAAqB;EAAU;;KAc5C;EACN;EAAmB;EAAY,MAAM;;EACrC;EAAqB;EAAY;IAAQ;;;EACzC;;EACA;;EACA;EAAkB,aAAa;EAAkB,cAAc;;KAEhE,SAAS,SAAS;;;;;iBAgVP,aAAa,UAAS"}
@@ -0,0 +1,287 @@
1
+ import { c as PAGE_SLOT_ID, l as PAYLOAD_SCRIPT_ID, o as NAV_HEADER, t as COMPONENT_TAG } from "./constants-BVT2bJEd.mjs";
2
+ import { s as resolvePageRender } from "./definePage--9Pz7rHs.mjs";
3
+ //#region src/client/create-client.ts
4
+ const islands = /* @__PURE__ */ new Map();
5
+ let pageRoot = null;
6
+ let componentLoaders;
7
+ let clientPageLoaders;
8
+ let hmrListener;
9
+ const hmrPending = [];
10
+ const hmrGlobal = globalThis;
11
+ function dispatchHmr(payload) {
12
+ if (hmrListener) hmrListener(payload);
13
+ else hmrPending.push(payload);
14
+ }
15
+ hmrGlobal.__VINO_HMR__ = dispatchHmr;
16
+ function bindHmr(fn) {
17
+ hmrListener = fn;
18
+ const queued = hmrPending.splice(0);
19
+ for (const payload of queued) fn(payload);
20
+ }
21
+ function pruneDisconnected() {
22
+ for (const [el, rec] of islands) {
23
+ if (el.isConnected) continue;
24
+ rec.root?.unmount();
25
+ islands.delete(el);
26
+ }
27
+ if (pageRoot && !pageRoot.el.isConnected) {
28
+ pageRoot.root?.unmount();
29
+ pageRoot = null;
30
+ }
31
+ }
32
+ function readPayload(root = document) {
33
+ const el = root.querySelector(`#${PAYLOAD_SCRIPT_ID}`);
34
+ if (!el?.textContent) return null;
35
+ try {
36
+ return JSON.parse(el.textContent);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ function writePayload(payload) {
42
+ let el = document.getElementById(PAYLOAD_SCRIPT_ID);
43
+ if (!el) {
44
+ el = document.createElement("script");
45
+ el.id = PAYLOAD_SCRIPT_ID;
46
+ el.type = "application/json";
47
+ document.body.appendChild(el);
48
+ }
49
+ el.textContent = JSON.stringify(payload).replaceAll("<", "\\u003c");
50
+ }
51
+ async function loadDom() {
52
+ const [{ hydrateRoot }, { createElement }] = await Promise.all([import("hono/jsx/dom/client"), import("hono/jsx/dom")]);
53
+ return {
54
+ hydrateRoot,
55
+ createElement
56
+ };
57
+ }
58
+ async function getComponentLoaders() {
59
+ componentLoaders ??= (await import("virtual:vino/components")).components;
60
+ return componentLoaders;
61
+ }
62
+ async function getClientPageLoaders() {
63
+ clientPageLoaders ??= (await import("virtual:vino/client-pages")).clientPages;
64
+ return clientPageLoaders;
65
+ }
66
+ function vnodeFor(options, createElement, type, props) {
67
+ return options.createElement ? options.createElement(type, props) : createElement(type, props);
68
+ }
69
+ async function mountVnode(options, el, vnode, existing) {
70
+ if (existing) {
71
+ existing.render(vnode);
72
+ return existing;
73
+ }
74
+ if (options.hydrate) {
75
+ await options.hydrate(vnode, el);
76
+ return null;
77
+ }
78
+ const { hydrateRoot } = await loadDom();
79
+ return hydrateRoot(el, vnode);
80
+ }
81
+ async function hydrateComponents(options, root = document) {
82
+ const components = await getComponentLoaders();
83
+ const { createElement } = await loadDom();
84
+ const nodes = root.querySelectorAll(COMPONENT_TAG);
85
+ for (const el of nodes) {
86
+ const id = el.getAttribute("data-component");
87
+ const exportName = el.getAttribute("data-export") ?? "default";
88
+ const propsJson = el.getAttribute("data-props") ?? "{}";
89
+ if (!id) continue;
90
+ const loader = components[id];
91
+ if (!loader) {
92
+ console.warn(`[vino] missing hydratable component ${id}`);
93
+ continue;
94
+ }
95
+ const Component = (await loader())[exportName];
96
+ if (typeof Component !== "function") continue;
97
+ const props = JSON.parse(propsJson);
98
+ const vnode = vnodeFor(options, createElement, Component, props);
99
+ const previous = islands.get(el);
100
+ if (previous?.root) previous.root.unmount();
101
+ const mounted = await mountVnode(options, el, vnode, null);
102
+ islands.set(el, {
103
+ id,
104
+ exportName,
105
+ props,
106
+ root: mounted
107
+ });
108
+ }
109
+ }
110
+ async function hydrateClientPage(payload, options) {
111
+ if (!payload.clientPage) return;
112
+ const root = document.getElementById(PAGE_SLOT_ID);
113
+ if (!root) return;
114
+ const load = (await getClientPageLoaders())[payload.clientPage];
115
+ if (!load) return;
116
+ const mod = await load();
117
+ await renderClientPage(payload.clientPage, mod, payload, options, root);
118
+ }
119
+ async function renderClientPage(path, mod, payload, options, root) {
120
+ const Page = resolvePageRender(mod.default);
121
+ if (!Page) return;
122
+ const { createElement } = await loadDom();
123
+ const vnode = vnodeFor(options, createElement, Page, {
124
+ data: payload.data,
125
+ params: payload.params
126
+ });
127
+ const reuse = pageRoot?.el === root ? pageRoot.root : null;
128
+ if (pageRoot && pageRoot.el !== root) pageRoot.root?.unmount();
129
+ pageRoot = {
130
+ el: root,
131
+ path,
132
+ root: await mountVnode(options, root, vnode, reuse)
133
+ };
134
+ }
135
+ async function patchComponent(id, mod, options) {
136
+ const { createElement } = await loadDom();
137
+ for (const [el, rec] of islands) {
138
+ if (rec.id !== id) continue;
139
+ if (!el.isConnected) {
140
+ rec.root?.unmount();
141
+ islands.delete(el);
142
+ continue;
143
+ }
144
+ const Component = mod[rec.exportName];
145
+ if (typeof Component !== "function") continue;
146
+ rec.root = await mountVnode(options, el, vnodeFor(options, createElement, Component, rec.props), rec.root);
147
+ }
148
+ }
149
+ async function patchClientPage(path, mod, options) {
150
+ const payload = readPayload();
151
+ if (!payload?.clientPage || payload.clientPage !== path) return;
152
+ const root = document.getElementById(PAGE_SLOT_ID);
153
+ if (!root) return;
154
+ await renderClientPage(path, mod, payload, options, root);
155
+ }
156
+ function syncNav(path) {
157
+ const links = document.querySelectorAll("nav a[href]");
158
+ for (const link of links) {
159
+ if (!(link instanceof HTMLAnchorElement)) continue;
160
+ const href = link.getAttribute("href");
161
+ if (!href || href.startsWith("http")) continue;
162
+ if (href === path || href !== "/" && (path === href || path.startsWith(`${href}/`))) link.setAttribute("aria-current", "page");
163
+ else link.removeAttribute("aria-current");
164
+ }
165
+ }
166
+ async function hydrateSlot(options, payload) {
167
+ pruneDisconnected();
168
+ if (payload) await hydrateClientPage(payload, options);
169
+ await hydrateComponents(options, document.getElementById("vino-page") ?? document);
170
+ if (payload?.path) syncNav(payload.path);
171
+ return payload;
172
+ }
173
+ function parseContentFragment(html) {
174
+ const doc = new DOMParser().parseFromString(`<!DOCTYPE html><html><body>${html}</body></html>`, "text/html");
175
+ const payload = readPayload(doc);
176
+ return {
177
+ slot: doc.getElementById(PAGE_SLOT_ID),
178
+ payload,
179
+ title: payload?.title
180
+ };
181
+ }
182
+ async function fetchContent(href) {
183
+ const url = new URL(href, location.href);
184
+ return parseContentFragment(await (await fetch(url.href, { headers: { [NAV_HEADER]: "1" } })).text());
185
+ }
186
+ async function applyContent(href, push, options, fragment) {
187
+ const { slot: nextSlot, payload: nextPayload, title } = fragment;
188
+ if (nextPayload?.clientRouting === false && push) {
189
+ location.href = href;
190
+ return nextPayload;
191
+ }
192
+ const current = document.getElementById(PAGE_SLOT_ID);
193
+ if (current && nextSlot) current.replaceWith(nextSlot);
194
+ else if (nextSlot) document.body.appendChild(nextSlot);
195
+ if (nextPayload) writePayload(nextPayload);
196
+ if (title) document.title = title;
197
+ else if (nextPayload?.title) document.title = nextPayload.title;
198
+ const url = new URL(href, location.href);
199
+ if (push) history.pushState({ vino: true }, "", url.href);
200
+ return hydrateSlot(options, nextPayload ?? readPayload());
201
+ }
202
+ async function navigate(href, push, options) {
203
+ return applyContent(href, push, options, await fetchContent(href));
204
+ }
205
+ async function fillPartialShell(options, payload) {
206
+ if (!payload?.partial) return payload;
207
+ const slot = document.getElementById(PAGE_SLOT_ID);
208
+ if (!slot) return payload;
209
+ if (slot.childNodes.length > 0) return hydrateSlot(options, {
210
+ ...payload,
211
+ partial: false
212
+ });
213
+ const fragment = await fetchContent(location.href);
214
+ return applyContent(location.href, false, options, fragment);
215
+ }
216
+ function bindRouting(options, payload) {
217
+ if (!payload?.clientRouting) return;
218
+ document.addEventListener("click", (event) => {
219
+ const target = event.target;
220
+ if (!(target instanceof Element)) return;
221
+ const anchor = target.closest("a");
222
+ if (!anchor || !shouldIntercept(anchor) || isModifiedClick(event)) return;
223
+ if (!document.getElementById("vino-page")) return;
224
+ event.preventDefault();
225
+ navigate(anchor.href, true, options);
226
+ });
227
+ window.addEventListener("popstate", () => {
228
+ navigate(location.href, false, options);
229
+ });
230
+ }
231
+ function isModifiedClick(event) {
232
+ return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0;
233
+ }
234
+ function shouldIntercept(anchor) {
235
+ if (anchor.hasAttribute("download") || anchor.target && anchor.target !== "_self") return false;
236
+ if (anchor.origin !== location.origin) return false;
237
+ if (anchor.getAttribute("rel")?.includes("external")) return false;
238
+ return true;
239
+ }
240
+ async function handleHmr(payload, options) {
241
+ if (payload.kind === "registry") {
242
+ if (payload.components) componentLoaders = payload.components;
243
+ if (payload.clientPages) clientPageLoaders = payload.clientPages;
244
+ return;
245
+ }
246
+ if (payload.kind === "layout") {
247
+ location.reload();
248
+ return;
249
+ }
250
+ if (payload.kind === "ssr") {
251
+ if (!document.getElementById("vino-page")) {
252
+ location.reload();
253
+ return;
254
+ }
255
+ await navigate(location.href, false, options);
256
+ return;
257
+ }
258
+ if (payload.kind === "component" && payload.mod) {
259
+ await patchComponent(payload.id, payload.mod, options);
260
+ return;
261
+ }
262
+ if (payload.kind === "client-page" && payload.mod) await patchClientPage(payload.id, payload.mod, options);
263
+ }
264
+ /**
265
+ * Fill partial shells, hydrate `#vino-page` / `<vino-component>`, and intercept
266
+ * same-origin links with content-only swaps when `clientRouting` is enabled.
267
+ */
268
+ function createClient(options = {}) {
269
+ const hmr = (payload) => {
270
+ handleHmr(payload, options);
271
+ };
272
+ bindHmr(hmr);
273
+ return {
274
+ ready: (async () => {
275
+ let payload = readPayload();
276
+ if (payload?.partial) payload = await fillPartialShell(options, payload) ?? payload;
277
+ else payload = await hydrateSlot(options, payload) ?? payload;
278
+ bindRouting(options, payload);
279
+ return payload;
280
+ })(),
281
+ hmr
282
+ };
283
+ }
284
+ //#endregion
285
+ export { createClient };
286
+
287
+ //# sourceMappingURL=client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client/create-client.ts"],"sourcesContent":["import { COMPONENT_TAG, NAV_HEADER, PAGE_SLOT_ID, PAYLOAD_SCRIPT_ID } from \"../vino/constants.ts\";\nimport { resolvePageRender } from \"../vino/definePage.ts\";\nimport type { VinoPayload } from \"../vino/types.ts\";\n\nexport interface CreateClientOptions {\n hydrate?: (vnode: unknown, el: Element) => void | Promise<void>;\n createElement?: (type: unknown, props: unknown) => unknown;\n}\n\ntype ComponentLoaders = Record<string, () => Promise<Record<string, unknown>>>;\ntype ClientPageLoaders = Record<string, () => Promise<{ default?: unknown }>>;\n\ninterface DomRoot {\n render: (vnode: unknown) => void;\n unmount: () => void;\n}\n\ninterface IslandRecord {\n id: string;\n exportName: string;\n props: Record<string, unknown>;\n root: DomRoot | null;\n}\n\nexport type VinoHmrPayload =\n | { kind: \"component\"; id: string; mod?: Record<string, unknown> }\n | { kind: \"client-page\"; id: string; mod?: { default?: unknown } }\n | { kind: \"ssr\" }\n | { kind: \"layout\" }\n | { kind: \"registry\"; components?: ComponentLoaders; clientPages?: ClientPageLoaders };\n\ntype HmrFn = (payload: VinoHmrPayload) => void;\n\nconst islands = new Map<Element, IslandRecord>();\nlet pageRoot: { el: Element; path: string; root: DomRoot | null } | null = null;\nlet componentLoaders: ComponentLoaders | undefined;\nlet clientPageLoaders: ClientPageLoaders | undefined;\nlet hmrListener: HmrFn | undefined;\nconst hmrPending: VinoHmrPayload[] = [];\n\nconst hmrGlobal = globalThis as typeof globalThis & { __VINO_HMR__?: HmrFn };\n\nfunction dispatchHmr(payload: VinoHmrPayload) {\n if (hmrListener) hmrListener(payload);\n else hmrPending.push(payload);\n}\n\nhmrGlobal.__VINO_HMR__ = dispatchHmr;\n\nfunction bindHmr(fn: HmrFn) {\n hmrListener = fn;\n const queued = hmrPending.splice(0);\n for (const payload of queued) fn(payload);\n}\n\nfunction pruneDisconnected() {\n for (const [el, rec] of islands) {\n if (el.isConnected) continue;\n rec.root?.unmount();\n islands.delete(el);\n }\n if (pageRoot && !pageRoot.el.isConnected) {\n pageRoot.root?.unmount();\n pageRoot = null;\n }\n}\n\nfunction readPayload(root: ParentNode = document): VinoPayload | null {\n const el = root.querySelector(`#${PAYLOAD_SCRIPT_ID}`);\n if (!el?.textContent) return null;\n try {\n return JSON.parse(el.textContent) as VinoPayload;\n } catch {\n return null;\n }\n}\n\nfunction writePayload(payload: VinoPayload) {\n let el = document.getElementById(PAYLOAD_SCRIPT_ID) as HTMLScriptElement | null;\n if (!el) {\n el = document.createElement(\"script\");\n el.id = PAYLOAD_SCRIPT_ID;\n el.type = \"application/json\";\n document.body.appendChild(el);\n }\n el.textContent = JSON.stringify(payload).replaceAll(\"<\", \"\\\\u003c\");\n}\n\nasync function loadDom() {\n const [{ hydrateRoot }, { createElement }] = await Promise.all([\n import(\"hono/jsx/dom/client\"),\n import(\"hono/jsx/dom\"),\n ]);\n return { hydrateRoot, createElement };\n}\n\nasync function getComponentLoaders(): Promise<ComponentLoaders> {\n componentLoaders ??= (await import(\"virtual:vino/components\")).components;\n return componentLoaders;\n}\n\nasync function getClientPageLoaders(): Promise<ClientPageLoaders> {\n clientPageLoaders ??= (await import(\"virtual:vino/client-pages\")).clientPages;\n return clientPageLoaders;\n}\n\nfunction vnodeFor(\n options: CreateClientOptions,\n createElement: (type: never, props: never) => unknown,\n type: unknown,\n props: unknown,\n) {\n return options.createElement\n ? options.createElement(type, props)\n : createElement(type as never, props as never);\n}\n\nasync function mountVnode(\n options: CreateClientOptions,\n el: Element,\n vnode: unknown,\n existing: DomRoot | null,\n): Promise<DomRoot | null> {\n if (existing) {\n existing.render(vnode);\n return existing;\n }\n if (options.hydrate) {\n await options.hydrate(vnode, el);\n return null;\n }\n const { hydrateRoot } = await loadDom();\n return hydrateRoot(el as HTMLElement, vnode as never) as DomRoot;\n}\n\nasync function hydrateComponents(options: CreateClientOptions, root: ParentNode = document) {\n const components = await getComponentLoaders();\n const { createElement } = await loadDom();\n const nodes = root.querySelectorAll(COMPONENT_TAG);\n for (const el of nodes) {\n const id = el.getAttribute(\"data-component\");\n const exportName = el.getAttribute(\"data-export\") ?? \"default\";\n const propsJson = el.getAttribute(\"data-props\") ?? \"{}\";\n if (!id) continue;\n const loader = components[id];\n if (!loader) {\n console.warn(`[vino] missing hydratable component ${id}`);\n continue;\n }\n const mod = await loader();\n const Component = mod[exportName];\n if (typeof Component !== \"function\") continue;\n const props = JSON.parse(propsJson) as Record<string, unknown>;\n const vnode = vnodeFor(options, createElement, Component, props);\n const previous = islands.get(el);\n if (previous?.root) previous.root.unmount();\n const mounted = await mountVnode(options, el, vnode, null);\n islands.set(el, { id, exportName, props, root: mounted });\n }\n}\n\nasync function hydrateClientPage(payload: VinoPayload, options: CreateClientOptions) {\n if (!payload.clientPage) return;\n const root = document.getElementById(PAGE_SLOT_ID);\n if (!root) return;\n const clientPages = await getClientPageLoaders();\n const load = clientPages[payload.clientPage];\n if (!load) return;\n const mod = await load();\n await renderClientPage(payload.clientPage, mod, payload, options, root);\n}\n\nasync function renderClientPage(\n path: string,\n mod: { default?: unknown },\n payload: VinoPayload,\n options: CreateClientOptions,\n root: Element,\n) {\n const Page = resolvePageRender(mod.default);\n if (!Page) return;\n const { createElement } = await loadDom();\n const props = { data: payload.data, params: payload.params };\n const vnode = vnodeFor(options, createElement, Page, props);\n const reuse = pageRoot?.el === root ? pageRoot.root : null;\n if (pageRoot && pageRoot.el !== root) pageRoot.root?.unmount();\n const mounted = await mountVnode(options, root, vnode, reuse);\n pageRoot = { el: root, path, root: mounted };\n}\n\nasync function patchComponent(id: string, mod: Record<string, unknown>, options: CreateClientOptions) {\n const { createElement } = await loadDom();\n for (const [el, rec] of islands) {\n if (rec.id !== id) continue;\n if (!el.isConnected) {\n rec.root?.unmount();\n islands.delete(el);\n continue;\n }\n const Component = mod[rec.exportName];\n if (typeof Component !== \"function\") continue;\n const vnode = vnodeFor(options, createElement, Component, rec.props);\n rec.root = await mountVnode(options, el, vnode, rec.root);\n }\n}\n\nasync function patchClientPage(\n path: string,\n mod: { default?: unknown },\n options: CreateClientOptions,\n) {\n const payload = readPayload();\n if (!payload?.clientPage || payload.clientPage !== path) return;\n const root = document.getElementById(PAGE_SLOT_ID);\n if (!root) return;\n await renderClientPage(path, mod, payload, options, root);\n}\n\nfunction syncNav(path: string) {\n const links = document.querySelectorAll(\"nav a[href]\");\n for (const link of links) {\n if (!(link instanceof HTMLAnchorElement)) continue;\n const href = link.getAttribute(\"href\");\n if (!href || href.startsWith(\"http\")) continue;\n const active =\n href === path || (href !== \"/\" && (path === href || path.startsWith(`${href}/`)));\n if (active) link.setAttribute(\"aria-current\", \"page\");\n else link.removeAttribute(\"aria-current\");\n }\n}\n\nasync function hydrateSlot(options: CreateClientOptions, payload: VinoPayload | null) {\n pruneDisconnected();\n if (payload) await hydrateClientPage(payload, options);\n const slot = document.getElementById(PAGE_SLOT_ID);\n await hydrateComponents(options, slot ?? document);\n if (payload?.path) syncNav(payload.path);\n return payload;\n}\n\nfunction parseContentFragment(html: string): {\n slot: Element | null;\n payload: VinoPayload | null;\n title?: string;\n} {\n const doc = new DOMParser().parseFromString(\n `<!DOCTYPE html><html><body>${html}</body></html>`,\n \"text/html\",\n );\n const payload = readPayload(doc);\n const slot = doc.getElementById(PAGE_SLOT_ID);\n return { slot, payload, title: payload?.title };\n}\n\nasync function fetchContent(\n href: string,\n): Promise<{ slot: Element | null; payload: VinoPayload | null; title?: string }> {\n const url = new URL(href, location.href);\n const res = await fetch(url.href, { headers: { [NAV_HEADER]: \"1\" } });\n const html = await res.text();\n return parseContentFragment(html);\n}\n\nasync function applyContent(\n href: string,\n push: boolean,\n options: CreateClientOptions,\n fragment: { slot: Element | null; payload: VinoPayload | null; title?: string },\n) {\n const { slot: nextSlot, payload: nextPayload, title } = fragment;\n if (nextPayload?.clientRouting === false && push) {\n location.href = href;\n return nextPayload;\n }\n\n const current = document.getElementById(PAGE_SLOT_ID);\n if (current && nextSlot) {\n current.replaceWith(nextSlot);\n } else if (nextSlot) {\n document.body.appendChild(nextSlot);\n }\n\n if (nextPayload) writePayload(nextPayload);\n if (title) document.title = title;\n else if (nextPayload?.title) document.title = nextPayload.title;\n\n const url = new URL(href, location.href);\n if (push) history.pushState({ vino: true }, \"\", url.href);\n\n return hydrateSlot(options, nextPayload ?? readPayload());\n}\n\nasync function navigate(href: string, push: boolean, options: CreateClientOptions) {\n const fragment = await fetchContent(href);\n return applyContent(href, push, options, fragment);\n}\n\nasync function fillPartialShell(options: CreateClientOptions, payload: VinoPayload | null) {\n if (!payload?.partial) return payload;\n const slot = document.getElementById(PAGE_SLOT_ID);\n if (!slot) return payload;\n if (slot.childNodes.length > 0) return hydrateSlot(options, { ...payload, partial: false });\n\n const fragment = await fetchContent(location.href);\n return applyContent(location.href, false, options, fragment);\n}\n\nfunction bindRouting(options: CreateClientOptions, payload: VinoPayload | null) {\n if (!payload?.clientRouting) return;\n document.addEventListener(\"click\", (event) => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const anchor = target.closest(\"a\");\n if (!anchor || !shouldIntercept(anchor) || isModifiedClick(event)) return;\n // Prefer slot-swap when the page slot exists.\n if (!document.getElementById(PAGE_SLOT_ID)) return;\n event.preventDefault();\n void navigate(anchor.href, true, options);\n });\n window.addEventListener(\"popstate\", () => {\n void navigate(location.href, false, options);\n });\n}\n\nfunction isModifiedClick(event: MouseEvent) {\n return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0;\n}\n\nfunction shouldIntercept(anchor: HTMLAnchorElement) {\n if (anchor.hasAttribute(\"download\") || (anchor.target && anchor.target !== \"_self\")) return false;\n if (anchor.origin !== location.origin) return false;\n const rel = anchor.getAttribute(\"rel\");\n if (rel?.includes(\"external\")) return false;\n return true;\n}\n\nasync function handleHmr(payload: VinoHmrPayload, options: CreateClientOptions) {\n if (payload.kind === \"registry\") {\n if (payload.components) componentLoaders = payload.components;\n if (payload.clientPages) clientPageLoaders = payload.clientPages;\n return;\n }\n if (payload.kind === \"layout\") {\n location.reload();\n return;\n }\n if (payload.kind === \"ssr\") {\n if (!document.getElementById(PAGE_SLOT_ID)) {\n location.reload();\n return;\n }\n await navigate(location.href, false, options);\n return;\n }\n if (payload.kind === \"component\" && payload.mod) {\n await patchComponent(payload.id, payload.mod, options);\n return;\n }\n if (payload.kind === \"client-page\" && payload.mod) {\n await patchClientPage(payload.id, payload.mod, options);\n }\n}\n\n/**\n * Fill partial shells, hydrate `#vino-page` / `<vino-component>`, and intercept\n * same-origin links with content-only swaps when `clientRouting` is enabled.\n */\nexport function createClient(options: CreateClientOptions = {}) {\n const hmr: HmrFn = (payload) => {\n void handleHmr(payload, options);\n };\n bindHmr(hmr);\n const ready = (async () => {\n let payload = readPayload();\n if (payload?.partial) {\n payload = (await fillPartialShell(options, payload)) ?? payload;\n } else {\n payload = (await hydrateSlot(options, payload)) ?? payload;\n }\n bindRouting(options, payload);\n return payload;\n })();\n return { ready, hmr };\n}\n"],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { o as readPageDefinition } from "./definePage--9Pz7rHs.mjs";
2
+ //#region src/vino/config.ts
3
+ function loadMergedConfig(page, pageMod) {
4
+ const merged = { ...page.config };
5
+ const inline = readPageDefinition(pageMod?.default)?.config;
6
+ if (inline) Object.assign(merged, inline);
7
+ return merged;
8
+ }
9
+ async function loadMergedData(_page, c, pageMod) {
10
+ const fn = readPageDefinition(pageMod?.default)?.data;
11
+ if (!fn) return void 0;
12
+ return await fn(c);
13
+ }
14
+ //#endregion
15
+ //#region src/vino/compile.ts
16
+ const GROUP_RE = /^\([^)]+\)$/;
17
+ const CATCH_ALL_RE = /^\[\.\.\.(.+)\]$/;
18
+ const PARAM_RE = /^\[([^\]]+)\]$/;
19
+ function toPosix(relativePath) {
20
+ return relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
21
+ }
22
+ function stripExt(posixPath) {
23
+ return posixPath.replace(/\.(tsx|ts|jsx|js|mjs)$/, "");
24
+ }
25
+ /**
26
+ * Compile a pages-dir-relative file path to a Hono route.
27
+ * `(group)` dirs are omitted, `[param]` becomes `:param`, `[...slug]` becomes `:slug{.+}`,
28
+ * trailing `index` is dropped.
29
+ */
30
+ function compileFilePath(relativePath) {
31
+ const segs = stripExt(toPosix(relativePath)).split("/").filter(Boolean).filter((seg) => !GROUP_RE.test(seg));
32
+ if (segs.at(-1) === "index") segs.pop();
33
+ const out = [];
34
+ const paramNames = [];
35
+ let isCatchAll = false;
36
+ for (const seg of segs) {
37
+ const catchAll = CATCH_ALL_RE.exec(seg);
38
+ if (catchAll) {
39
+ paramNames.push(catchAll[1]);
40
+ out.push(`:${catchAll[1]}{.+}`);
41
+ isCatchAll = true;
42
+ continue;
43
+ }
44
+ const param = PARAM_RE.exec(seg);
45
+ if (param && !param[1].startsWith("...")) {
46
+ paramNames.push(param[1]);
47
+ out.push(`:${param[1]}`);
48
+ continue;
49
+ }
50
+ out.push(seg);
51
+ }
52
+ const pattern = out.length === 0 ? "/" : `/${out.join("/")}`;
53
+ const path = pattern.replaceAll("{.+}", "");
54
+ const isStatic = paramNames.length === 0 && !isCatchAll;
55
+ return {
56
+ path,
57
+ pattern,
58
+ isCatchAll,
59
+ isStatic,
60
+ paramNames
61
+ };
62
+ }
63
+ function pathSegments(path) {
64
+ return path.split("/").filter(Boolean);
65
+ }
66
+ function pathDepth(path) {
67
+ return pathSegments(path).length;
68
+ }
69
+ /**
70
+ * True if `parent` is `/` or a prefix of `child`.
71
+ * A `:param` segment in `parent` matches any child segment; a static parent
72
+ * segment does not match a param child segment.
73
+ */
74
+ function pathCovers(parent, child) {
75
+ if (parent === "/" || parent === "") return true;
76
+ const a = pathSegments(parent);
77
+ const b = pathSegments(child);
78
+ if (a.length > b.length) return false;
79
+ return a.every((seg, i) => {
80
+ if (seg === b[i]) return true;
81
+ if (seg.startsWith(":")) return true;
82
+ return false;
83
+ });
84
+ }
85
+ function applyParams(path, params) {
86
+ let url = path;
87
+ for (const [key, value] of Object.entries(params)) url = url.replace(`:${key}`, value);
88
+ if (!url.startsWith("/")) url = `/${url}`;
89
+ return url;
90
+ }
91
+ function urlToHtmlFile(url) {
92
+ const clean = url.split("?")[0].split("#")[0];
93
+ if (clean === "/") return "index.html";
94
+ const trimmed = clean.replace(/^\//, "").replace(/\/$/, "");
95
+ if (clean.endsWith("/")) return `${trimmed}/index.html`;
96
+ return `${trimmed}.html`;
97
+ }
98
+ function routeRank(pattern) {
99
+ if (pattern.includes("{.+}")) return 2;
100
+ if (pattern.includes(":")) return 1;
101
+ return 0;
102
+ }
103
+ function compareRoutes(a, b) {
104
+ const d = routeRank(a.pattern) - routeRank(b.pattern);
105
+ if (d !== 0) return d;
106
+ return b.pattern.length - a.pattern.length;
107
+ }
108
+ //#endregion
109
+ export { pathDepth as a, loadMergedData as c, pathCovers as i, compareRoutes as n, urlToHtmlFile as o, compileFilePath as r, loadMergedConfig as s, applyParams as t };
110
+
111
+ //# sourceMappingURL=compile-glNMkyyx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-glNMkyyx.mjs","names":[],"sources":["../src/vino/config.ts","../src/vino/compile.ts"],"sourcesContent":["import { readPageDefinition } from \"./definePage.ts\";\nimport type { DataLoader, ManifestPage, PageConfig, PageModule } from \"./types.ts\";\n\nexport function loadMergedConfig(page: ManifestPage, pageMod?: PageModule): PageConfig {\n const merged: PageConfig = { ...page.config };\n const inline = readPageDefinition(pageMod?.default)?.config;\n if (inline) Object.assign(merged, inline);\n return merged;\n}\n\nexport async function loadMergedData(\n _page: ManifestPage,\n c: Parameters<DataLoader>[0],\n pageMod?: PageModule,\n): Promise<unknown> {\n const fn = readPageDefinition(pageMod?.default)?.data;\n if (!fn) return undefined;\n return await fn(c);\n}\n","export interface CompiledRoute {\n /** Filesystem-derived path using `:param` (no Hono regex). `/`, `/about`, `/blog/:slug`. */\n path: string;\n /** Hono route pattern (`:slug{.+}` for catch-all). */\n pattern: string;\n isCatchAll: boolean;\n isStatic: boolean;\n paramNames: string[];\n}\n\nconst GROUP_RE = /^\\([^)]+\\)$/;\nconst CATCH_ALL_RE = /^\\[\\.\\.\\.(.+)\\]$/;\nconst PARAM_RE = /^\\[([^\\]]+)\\]$/;\n\nfunction toPosix(relativePath: string): string {\n return relativePath.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//, \"\");\n}\n\nfunction stripExt(posixPath: string): string {\n return posixPath.replace(/\\.(tsx|ts|jsx|js|mjs)$/, \"\");\n}\n\n/**\n * Compile a pages-dir-relative file path to a Hono route.\n * `(group)` dirs are omitted, `[param]` becomes `:param`, `[...slug]` becomes `:slug{.+}`,\n * trailing `index` is dropped.\n */\nexport function compileFilePath(relativePath: string): CompiledRoute {\n const posix = toPosix(relativePath);\n const withoutExt = stripExt(posix);\n\n const rawSegs = withoutExt.split(\"/\").filter(Boolean);\n const segs = rawSegs.filter((seg) => !GROUP_RE.test(seg));\n if (segs.at(-1) === \"index\") segs.pop();\n\n const out: string[] = [];\n const paramNames: string[] = [];\n let isCatchAll = false;\n\n for (const seg of segs) {\n const catchAll = CATCH_ALL_RE.exec(seg);\n if (catchAll) {\n paramNames.push(catchAll[1]);\n out.push(`:${catchAll[1]}{.+}`);\n isCatchAll = true;\n continue;\n }\n const param = PARAM_RE.exec(seg);\n if (param && !param[1].startsWith(\"...\")) {\n paramNames.push(param[1]);\n out.push(`:${param[1]}`);\n continue;\n }\n out.push(seg);\n }\n\n const pattern = out.length === 0 ? \"/\" : `/${out.join(\"/\")}`;\n const path = pattern.replaceAll(\"{.+}\", \"\");\n const isStatic = paramNames.length === 0 && !isCatchAll;\n\n return { path, pattern, isCatchAll, isStatic, paramNames };\n}\n\nexport function pathSegments(path: string): string[] {\n return path.split(\"/\").filter(Boolean);\n}\n\nexport function pathDepth(path: string): number {\n return pathSegments(path).length;\n}\n\n/**\n * True if `parent` is `/` or a prefix of `child`.\n * A `:param` segment in `parent` matches any child segment; a static parent\n * segment does not match a param child segment.\n */\nexport function pathCovers(parent: string, child: string): boolean {\n if (parent === \"/\" || parent === \"\") return true;\n const a = pathSegments(parent);\n const b = pathSegments(child);\n if (a.length > b.length) return false;\n return a.every((seg, i) => {\n const other = b[i];\n if (seg === other) return true;\n if (seg.startsWith(\":\")) return true;\n return false;\n });\n}\n\nexport function applyParams(path: string, params: Record<string, string>): string {\n let url = path;\n for (const [key, value] of Object.entries(params)) {\n url = url.replace(`:${key}`, value);\n }\n if (!url.startsWith(\"/\")) url = `/${url}`;\n return url;\n}\n\nexport function urlToHtmlFile(url: string): string {\n const clean = url.split(\"?\")[0].split(\"#\")[0];\n if (clean === \"/\") return \"index.html\";\n const trimmed = clean.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n if (clean.endsWith(\"/\")) return `${trimmed}/index.html`;\n return `${trimmed}.html`;\n}\n\nexport function routeRank(pattern: string): number {\n if (pattern.includes(\"{.+}\")) return 2;\n if (pattern.includes(\":\")) return 1;\n return 0;\n}\n\nexport function compareRoutes(a: { pattern: string }, b: { pattern: string }): number {\n const d = routeRank(a.pattern) - routeRank(b.pattern);\n if (d !== 0) return d;\n return b.pattern.length - a.pattern.length;\n}\n"],"mappings":"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"}