@voltro/web 0.1.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/ssr.d.ts ADDED
@@ -0,0 +1,322 @@
1
+ import { ComponentType } from 'react';
2
+ import { PipeableStream } from 'react-dom/server';
3
+ import { ReactNode } from 'react';
4
+
5
+ declare interface ErrorBoundaryProps {
6
+ readonly error: unknown;
7
+ readonly reset: () => void;
8
+ }
9
+
10
+ /**
11
+ * How the client-side JS hydrates a server-rendered page.
12
+ *
13
+ * - 'full' — Hydrate the entire page tree as one React root.
14
+ * Default for `'spa'` pages and the safe default for
15
+ * `'static'` pages that haven't been thought through.
16
+ * Subscriptions + interactivity work everywhere.
17
+ * - 'islands' — Skip the full-tree hydration. The client runtime
18
+ * only hydrates `<div data-voltro-island>` markers
19
+ * emitted by `island()`. Other parts of the page stay
20
+ * pure static HTML with no React lifecycle running.
21
+ * Best perf for content-heavy pages with isolated
22
+ * interactive zones.
23
+ *
24
+ * Defaults to `'full'` when not specified.
25
+ */
26
+ declare type InteractiveMode = 'full' | 'islands' | 'none';
27
+
28
+ declare interface LoaderContext {
29
+ readonly params: Readonly<Record<string, string>>;
30
+ readonly pathname: string;
31
+ /** Aborted when the user navigates away before the loader resolves. */
32
+ readonly signal: AbortSignal;
33
+ /** Request headers when the loader runs server-side via `voltro
34
+ * start`. Lowercased keys. Empty `{}` for client-side loader
35
+ * invocations (browser-fetch goes through the rpc layer, not the
36
+ * loader). Use this for tenant resolution, auth headers, etc.
37
+ * until the auth slice ships a typed Subject. */
38
+ readonly headers?: Readonly<Record<string, string>>;
39
+ /** Server-side one-shot rpc fetch. Invokes any backend rpc (queries
40
+ * included — the FIRST snapshot of a streaming query is resolved and
41
+ * returned) over the api's HTTP rpc surface (`POST /rpc`), forwarding
42
+ * the request's `cookie` header so the api resolves the same Subject +
43
+ * tenant the WS path would. Bound to the app's single / first api.
44
+ *
45
+ * Present ONLY when the loader runs server-side (`voltro start` /
46
+ * `voltro dev` SSR). `undefined` for client-side loader invocations —
47
+ * in the browser, use `useSubscription` in the component for live data
48
+ * instead; the loader's `query` is for SSR first-paint + `meta`. */
49
+ readonly query?: <T = unknown>(tag: string, input?: Record<string, unknown>) => Promise<T>;
50
+ }
51
+
52
+ declare type LoaderFn<T = unknown> = (ctx: LoaderContext) => Promise<T> | T;
53
+
54
+ declare interface PageDescriptor<TLoaderData = unknown> {
55
+ /** URL pattern, e.g. `/`, `/about`, `/users/[id]`, `/docs/[...slug]`. */
56
+ readonly pattern: string;
57
+ /** The page component. Present on eager routes; on a LAZY route (see
58
+ * `load`) it's filled by the Router once the page chunk has loaded. */
59
+ readonly Component?: ComponentType | undefined;
60
+ /** LAZY route: a thunk that dynamically imports the page module. When set,
61
+ * the page chunk is fetched only when this route first matches, so the
62
+ * initial bundle never imports every page. The Router resolves
63
+ * `Component` / `loader` / `meta` / … from the loaded module; the layout
64
+ * `chain` stays eager. */
65
+ readonly load?: (() => Promise<Record<string, unknown>>) | undefined;
66
+ /** Static meta, or a function of `{ params, loaderData, locale }` —
67
+ * so a document title can read what the page loader fetched (the
68
+ * dynamic metadata case) AND can localise per-locale at SSG time
69
+ * for URL-prefix i18n routing. `loaderData` is the page loader's
70
+ * result; `locale` is the active i18n locale during pre-render
71
+ * (driven by `params.locale` for `[locale]/...` routes, falling
72
+ * back to the app's defaultLocale). On the client, `locale` is
73
+ * the URL-derived locale when the route is locale-prefixed,
74
+ * otherwise the framework's resolved locale (cookie / Accept-
75
+ * Language / default). */
76
+ readonly meta?: PageMeta | ((ctx: {
77
+ readonly params: Readonly<Record<string, string>>;
78
+ readonly loaderData: unknown;
79
+ readonly locale: string;
80
+ }) => PageMeta) | undefined;
81
+ /** Async data preload; result available via useLoaderData<T>(). */
82
+ readonly loader?: LoaderFn<TLoaderData> | undefined;
83
+ /** Catches render + loader errors for THIS leaf. Tighter scope than `chain[*].Error`. */
84
+ readonly ErrorBoundary?: ComponentType<ErrorBoundaryProps> | undefined;
85
+ /** Shown while the loader is in flight (first navigation to the route). */
86
+ readonly Pending?: ComponentType | undefined;
87
+ /**
88
+ * Outer-to-inner layer chain. Each segment maps to one directory level
89
+ * (or route group) on the way from the pages root down to this leaf.
90
+ * Codegen produces this from the discovery walk; user code rarely
91
+ * writes it by hand.
92
+ */
93
+ readonly chain?: ReadonlyArray<RouteSegment> | undefined;
94
+ /** How the page's HTML is produced. Defaults to 'static' so pages
95
+ * pre-render by default — SEO + first-paint speed are wins users
96
+ * pay nothing for. Opt out with `'spa'` for pages that need a
97
+ * per-request render. */
98
+ readonly renderMode?: RenderMode | undefined;
99
+ /** How the page's client-side JS hydrates. Defaults to `'full'`
100
+ * (whole tree). Pages designed around the islands pattern should
101
+ * set this to `'islands'` so only marked interactive zones
102
+ * hydrate; the rest stays pure static HTML. */
103
+ readonly interactive?: InteractiveMode | undefined;
104
+ }
105
+
106
+ declare interface PageMeta {
107
+ readonly title?: string;
108
+ readonly description?: string;
109
+ /** Canonical URL for this page. Emitted as `<link rel="canonical">`.
110
+ * Relative paths are accepted — search engines resolve them
111
+ * against the document's base URL. Use this on duplicated routes
112
+ * (locale variants, alternate URL forms) to declare the preferred
113
+ * one indexable URL. */
114
+ readonly canonical?: string;
115
+ /** Arbitrary additional meta tags. `name` OR `property` is required. */
116
+ readonly tags?: ReadonlyArray<{
117
+ readonly name?: string;
118
+ readonly property?: string;
119
+ readonly content: string;
120
+ }>;
121
+ /** Arbitrary additional `<link>` tags. Use for `alternate` hreflang,
122
+ * prev/next pagination, RSS feeds, manifest, etc. (For the common
123
+ * canonical case, prefer the dedicated `canonical` field above.) */
124
+ readonly links?: ReadonlyArray<{
125
+ readonly rel: string;
126
+ readonly href: string;
127
+ readonly hreflang?: string;
128
+ readonly type?: string;
129
+ readonly title?: string;
130
+ }>;
131
+ /** JSON-LD structured-data payloads. Each entry is emitted as a
132
+ * separate `<script type="application/ld+json">` so search engines
133
+ * and LLM crawlers can pick up rich metadata (Article, Product,
134
+ * SoftwareApplication, BreadcrumbList, FAQPage, etc.). Anything
135
+ * JSON-serialisable is accepted; `@context` + `@type` are
136
+ * authored, not synthesised. */
137
+ readonly jsonLd?: ReadonlyArray<Record<string, unknown>>;
138
+ }
139
+
140
+ export declare const parseCookieHeader: (raw: string | undefined) => Record<string, string>;
141
+
142
+ /**
143
+ * Helper for the build pipeline: stringify the resolved meta into a
144
+ * `<head>`-ready HTML fragment. Returns an empty string when no meta
145
+ * is set so the caller can blindly inject the result.
146
+ */
147
+ export declare const renderMetaToHtml: (meta: PageMeta | null) => string;
148
+
149
+ /**
150
+ * How a page's HTML is produced.
151
+ *
152
+ * - 'static' — Pre-rendered to flat HTML at build time. Default.
153
+ * Shipped as `dist/<route>/index.html`. JS bundle
154
+ * still loads to hydrate islands + handle SPA-style
155
+ * navigation, but the first paint is instant + SEO
156
+ * friendly. Loaders MUST be side-effect-free + must
157
+ * not require per-request context (subject, headers).
158
+ * - 'spa' — Client-only rendering. No HTML pre-render; the
159
+ * shipped HTML is a shell that mounts on the
160
+ * client. Useful for pages that genuinely need a
161
+ * fresh client render every load (most reactive
162
+ * dashboards).
163
+ *
164
+ * - 'ssr' — Rendered on every request, server-side, via `voltro
165
+ * start`. Loaders run per request with access to
166
+ * `headers` + `query` (the api HTTP rpc); the resolved
167
+ * HTML is sent fresh each time. Use for personalised /
168
+ * tenant-scoped first paint.
169
+ * - 'isr' — Like 'ssr' on the first request, then cached and
170
+ * revalidated in the background on a TTL.
171
+ */
172
+ declare type RenderMode = 'static' | 'spa' | 'ssr' | 'isr';
173
+
174
+ export declare interface RenderPageOptions {
175
+ /** Descriptor produced by codegen (`pageRoute(...)`). */
176
+ readonly descriptor: PageDescriptor;
177
+ /** URL pattern's resolved params. `{}` for static, no-param routes. */
178
+ readonly params: Readonly<Record<string, string>>;
179
+ /** Pathname this render resolves under. Used by `useLocation()` on
180
+ * the server side. */
181
+ readonly pathname: string;
182
+ /** Page (leaf) loader data — pre-resolved by the caller. The build /
183
+ * SSR pipeline runs the descriptor's `loader` (if any) and passes the
184
+ * result here; reachable via `useLoaderData()` inside the page. */
185
+ readonly loaderData: unknown;
186
+ /** Per-layout loader data, keyed by chain-segment index. The pipeline
187
+ * runs each `chain[i].loader` and passes the results here; each layout
188
+ * reads ITS own via `useLoaderData()`. Omit when no layout loaders. */
189
+ readonly segmentLoaderData?: Readonly<Record<number, unknown>>;
190
+ /** Request snapshot (cookies + headers) exposed to the rendered tree
191
+ * via `useServerRequest()`. Optional: omit on builds (static
192
+ * prerender) where no per-request data exists; provide on
193
+ * per-request SSR so consumers can resolve cookie-driven preferences
194
+ * like locale or theme. */
195
+ readonly requestContext?: ServerRequestContextValue;
196
+ /** Outer wrapper to apply around the rendered tree. The build and
197
+ * the SSR runtime pass an `<I18nProvider>` wrapper here when the
198
+ * consumer app has `locales` configured — without it, any page
199
+ * calling `useT()` would crash at render time ("Could not find
200
+ * required `intl` object"). The wrapper is generic on purpose:
201
+ * any cross-cutting provider (i18n, theme, feature-flag context)
202
+ * rides this slot so `@voltro/web` stays free of optional deps. */
203
+ readonly outerWrap?: (children: ReactNode) => ReactNode;
204
+ /** Active locale for this render. Drives meta resolution so a page's
205
+ * `meta` function can localise title / description / canonical /
206
+ * hreflang per locale. The build passes `params.locale ?? defaultLocale`
207
+ * so locale-prefixed routes (`[locale]/...`) get their per-locale
208
+ * catalog at meta-resolve time. Optional: when absent, page
209
+ * `meta(ctx)` callbacks see `ctx.locale = 'en'` as a safe default. */
210
+ readonly locale?: string;
211
+ }
212
+
213
+ export declare interface RenderPageResult {
214
+ /** HTML for the page body — goes inside `<div id="root">…</div>`. */
215
+ readonly html: string;
216
+ /** Resolved page meta (after running the descriptor's `meta`
217
+ * function with `params` if present). Caller renders this into
218
+ * the document head. */
219
+ readonly meta: PageMeta | null;
220
+ }
221
+
222
+ export declare interface RenderPageStreamOptions extends RenderPageOptions {
223
+ /** Called once React commits the shell (all Suspense boundaries that
224
+ * weren't deferred have resolved). Flush the `<head>` (with `meta`) +
225
+ * opening `<body>` here, then pipe the returned stream into the response.
226
+ * The build pipeline uses this to send bytes before the whole tree is
227
+ * ready — the streaming-SSR win over `renderToString`. */
228
+ readonly onShellReady?: () => void;
229
+ /** Called if the shell itself fails to render (before any bytes flushed) —
230
+ * the pipeline should fall back to an error page / a 500. */
231
+ readonly onShellError?: (error: unknown) => void;
232
+ /** Called when the ENTIRE tree (incl. deferred Suspense content) has
233
+ * streamed. Close the response's trailing `</body></html>` here. */
234
+ readonly onAllReady?: () => void;
235
+ /** Bootstrap `<script>` module URLs injected by React at the end of the
236
+ * shell — the client bundle that hydrates the streamed HTML. */
237
+ readonly bootstrapModules?: ReadonlyArray<string>;
238
+ }
239
+
240
+ export declare interface RenderPageStreamResult {
241
+ /** The React pipeable stream. `pipe(res)` into a Node writable to flush the
242
+ * page body; `abort()` to cancel a slow render. */
243
+ readonly stream: PipeableStream;
244
+ /** Resolved page meta — available synchronously (meta runs before render),
245
+ * so the caller can write `<head>` in `onShellReady` before piping. */
246
+ readonly meta: PageMeta | null;
247
+ }
248
+
249
+ /**
250
+ * Render a page to an HTML string using its layout chain + loader data. The
251
+ * returned `html` is the body fragment — the caller composes the surrounding
252
+ * document. Renders synchronously via `renderToString`; for the streaming
253
+ * path (flush the shell before the whole tree is ready) use
254
+ * {@link renderPageToStream}.
255
+ */
256
+ export declare const renderPageToHtml: (options: RenderPageOptions) => RenderPageResult;
257
+
258
+ /**
259
+ * Streaming SSR — the richer path alongside {@link renderPageToHtml}. Renders
260
+ * the page via React's `renderToPipeableStream` so the server can flush the
261
+ * shell (and each Suspense boundary as it resolves) instead of buffering the
262
+ * whole document into one string. Same tree, same providers, same meta as the
263
+ * sync path; the difference is time-to-first-byte and native Suspense
264
+ * streaming.
265
+ *
266
+ * The caller drives the response: write the doctype + `<head>` (using the
267
+ * synchronously-returned `meta`) + opening `<body><div id="root">` in
268
+ * `onShellReady`, `stream.pipe(res)`, then close the document in `onAllReady`.
269
+ */
270
+ export declare const renderPageToStream: (options: RenderPageStreamOptions) => RenderPageStreamResult;
271
+
272
+ /**
273
+ * One layer in a page's nesting chain. Each represents a directory level
274
+ * in the file-convention layout (or a route group).
275
+ *
276
+ * Layouts wrap their children — outer layers wrap inner ones. Errors are
277
+ * caught by the deepest layer with an `Error` component; if none, the
278
+ * error bubbles up the chain. NotFound applies when an URL is under this
279
+ * layer's scope but no concrete page matches.
280
+ *
281
+ * The chain persists across navigation within its scope — React preserves
282
+ * the layout component instances because their position in the tree is
283
+ * stable for any leaf within that subtree.
284
+ */
285
+ declare interface RouteSegment {
286
+ /** Outer-vs-inner ordering label (debug only). */
287
+ readonly id?: string | undefined;
288
+ readonly Layout?: ComponentType<{
289
+ children: ReactNode;
290
+ }> | undefined;
291
+ readonly Error?: ComponentType<ErrorBoundaryProps> | undefined;
292
+ readonly Pending?: ComponentType | undefined;
293
+ readonly NotFound?: ComponentType | undefined;
294
+ /** Async data preload for THIS layout level. Runs in parallel with the
295
+ * page loader (and the other layouts') before mount; the result is
296
+ * reachable via `useLoaderData<T>()` from INSIDE this layout (each
297
+ * level sees its own loader's data). A `layout.tsx` exports it like a
298
+ * page does — codegen wires it onto the segment. Server-side it
299
+ * receives request `headers` (session/tenant resolution). */
300
+ readonly loader?: LoaderFn | undefined;
301
+ }
302
+
303
+ /**
304
+ * Serialise loader data into a `<script>` payload safe to embed in
305
+ * HTML. The result is wrapped in `<script type="application/json"
306
+ * id="__voltro_state__">` by the caller; here we just produce the
307
+ * JSON body with `</` sequences escaped (the well-known SSR XSS
308
+ * vector).
309
+ */
310
+ export declare const serialiseStateForInlining: (state: unknown) => string;
311
+
312
+ declare interface ServerRequestContextValue {
313
+ readonly cookies: Readonly<Record<string, string>>;
314
+ readonly headers: Readonly<Record<string, string>>;
315
+ /** Raw request URL as it came off the wire (path + query). Useful
316
+ * for SSR pages that need to read `?q=…` style search params
317
+ * without touching anything client-only. Empty string for build-
318
+ * time SSG renders where there is no incoming request. */
319
+ readonly url: string;
320
+ }
321
+
322
+ export { }
package/dist/ssr.js ADDED
@@ -0,0 +1,76 @@
1
+ import { c as e, d as t, r as n, t as r, x as i } from "./serverContext-D5-Dmh8d.js";
2
+ import { createElement as a } from "react";
3
+ import { renderToPipeableStream as o, renderToString as s } from "react-dom/server";
4
+ //#region src/ssr.tsx
5
+ var c = (t, n, r, i) => {
6
+ let o = a(e.Provider, { value: r }, a(t));
7
+ if (!n) return o;
8
+ for (let t = n.length - 1; t >= 0; t--) {
9
+ let r = n[t];
10
+ if (r.Layout) {
11
+ let n = r.Layout;
12
+ o = a(e.Provider, { value: i(t) }, a(n, { children: o }));
13
+ }
14
+ }
15
+ return o;
16
+ }, l = (e) => {
17
+ let { descriptor: n, params: o, pathname: s, loaderData: l, segmentLoaderData: u, requestContext: d, outerWrap: f, locale: p } = e, m = i(n.meta, o, l, p), h = {
18
+ pathname: s,
19
+ search: "",
20
+ params: o,
21
+ loaderData: l,
22
+ navigate: () => {
23
+ throw Error("navigate() is not supported during server-side rendering. Trigger navigations on the client only.");
24
+ },
25
+ prefetch: () => {},
26
+ registerBlocker: () => () => {},
27
+ blocked: null
28
+ }, g = n.Component;
29
+ if (!g) throw Error(`SSR received a descriptor with no Component for "${s}". Lazy page routes are a client-only optimization and must not be used on the server.`);
30
+ let _ = a(t.Provider, { value: h }, c(g, n.chain, l, (e) => u?.[e]));
31
+ return d && (_ = a(r.Provider, { value: d }, _)), f && (_ = f(_)), {
32
+ tree: _,
33
+ meta: m
34
+ };
35
+ }, u = (e) => {
36
+ let { tree: t, meta: n } = l(e);
37
+ return {
38
+ html: s(t),
39
+ meta: n
40
+ };
41
+ }, d = (e) => {
42
+ let { tree: t, meta: n } = l(e);
43
+ return {
44
+ stream: o(t, {
45
+ ...e.bootstrapModules ? { bootstrapModules: [...e.bootstrapModules] } : {},
46
+ ...e.onShellReady ? { onShellReady: e.onShellReady } : {},
47
+ ...e.onShellError ? { onShellError: e.onShellError } : {},
48
+ ...e.onAllReady ? { onAllReady: e.onAllReady } : {}
49
+ }),
50
+ meta: n
51
+ };
52
+ }, f = (e) => {
53
+ if (!e) return "";
54
+ let t = [];
55
+ if (typeof e.title == "string" && t.push(`<title>${m(e.title)}</title>`), e.description && t.push(`<meta name="description" content="${h(e.description)}" />`), e.canonical && t.push(`<link rel="canonical" href="${h(e.canonical)}" />`), e.tags) for (let n of e.tags) {
56
+ let e = n.name ? `name="${h(n.name)}"` : n.property ? `property="${h(n.property)}"` : "";
57
+ e && t.push(`<meta ${e} content="${h(n.content)}" />`);
58
+ }
59
+ if (e.links) for (let n of e.links) {
60
+ let e = [
61
+ `rel="${h(n.rel)}"`,
62
+ `href="${h(n.href)}"`,
63
+ n.hreflang ? `hreflang="${h(n.hreflang)}"` : "",
64
+ n.type ? `type="${h(n.type)}"` : "",
65
+ n.title ? `title="${h(n.title)}"` : ""
66
+ ].filter(Boolean).join(" ");
67
+ t.push(`<link ${e} />`);
68
+ }
69
+ if (e.jsonLd) for (let n of e.jsonLd) {
70
+ let e = JSON.stringify(n).replace(/<\/(script)/gi, "<\\/$1").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
71
+ t.push(`<script type="application/ld+json" data-voltro-page-jsonld>${e}<\/script>`);
72
+ }
73
+ return t.join("\n ");
74
+ }, p = (e) => JSON.stringify(e ?? null).replace(/</g, "\\u003c"), m = (e) => e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"), h = (e) => e.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
75
+ //#endregion
76
+ export { n as parseCookieHeader, f as renderMetaToHtml, u as renderPageToHtml, d as renderPageToStream, p as serialiseStateForInlining };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@voltro/web",
3
+ "version": "0.1.0",
4
+ "description": "The Voltro web framework — file-based routing, render modes (SSR / SSG / islands), the page-export contract, data hooks, and the browser mount.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./mount": {
27
+ "types": "./dist/mount.d.ts",
28
+ "import": "./dist/mount.js",
29
+ "default": "./dist/mount.js"
30
+ },
31
+ "./hooks": {
32
+ "types": "./dist/hooks.d.ts",
33
+ "import": "./dist/hooks.js",
34
+ "default": "./dist/hooks.js"
35
+ },
36
+ "./runtime": {
37
+ "types": "./dist/frameworkBoot.d.ts",
38
+ "import": "./dist/frameworkBoot.js",
39
+ "default": "./dist/frameworkBoot.js"
40
+ },
41
+ "./ssr": {
42
+ "types": "./dist/ssr.d.ts",
43
+ "import": "./dist/ssr.js",
44
+ "default": "./dist/ssr.js"
45
+ }
46
+ },
47
+ "main": "./dist/index.js",
48
+ "module": "./dist/index.js",
49
+ "types": "./dist/index.d.ts",
50
+ "sideEffects": false,
51
+ "engines": {
52
+ "node": ">=24.0.0"
53
+ },
54
+ "dependencies": {
55
+ "@voltro/client": "0.1.0",
56
+ "@voltro/ui": "0.1.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@effect/platform": "^0.96.2",
60
+ "@effect/rpc": "^0.75.1",
61
+ "effect": "^3.21.4",
62
+ "react": "^19.0.0",
63
+ "react-dom": "^19.0.0"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ }
68
+ }