@voltro/web 0.7.0 → 0.8.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 +21 -0
- package/dist/index.d.ts +81 -1
- package/dist/index.js +68 -45
- package/dist/mount-D3k8Ozdm.js +76 -0
- package/dist/mount.js +1 -1
- package/dist/routerState-ga64vk2B.js +99 -0
- package/dist/{serverContext-DSValgbm.js → serverContext-BW0GF8fv.js} +291 -249
- package/dist/ssr.d.ts +168 -14
- package/dist/ssr.js +40 -39
- package/package.json +3 -3
- package/dist/mount-DhGdfjSE.js +0 -74
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,27 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.8.0] — 2026-07-20
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/web, @voltro/cli** — **`defer()` + `<Await>`: stream one slow part of a page instead of blocking the whole response.** A loader blocks the entire response, so one slow field costs every byte of the page. A loader may now return `defer(eager, deferred)` — two explicit buckets. The eager half is awaited and renders into the shell; the deferred half is handed to the renderer as promises, read through the new `<Await>` component, and **flushed into the same response as each promise settles**. ```tsx export const renderMode = 'ssr' as const export const loader = async ({ query }) => defer( { user: await query('users.me') }, // in the shell { report: query('reports.quarterly') }, // streamed after it ) const { user, report } = useLoaderData<Awaited<ReturnType<typeof loader>>>() <Await value={report} fallback={<ReportSkeleton />}>{(r) => <ReportTable rows={r.rows} />}</Await> ``` Two buckets rather than "any promise-valued field is deferred": deferral is then something the author wrote down, not something inferred from a value's runtime shape, and `useLoaderData()` can type it — eager fields come back as values, deferred fields as `Promise<T>`, so the compiler says which ones need an `<Await>`. **Streaming is now wired for `renderMode: 'ssr'` on BOTH boot paths.** `voltro dev` and `voltro start` share one shell splitter + stream driver (`cli/src/ssrShell.ts`) rather than hand-mirroring the sequence — the same reason `buildSegmentChain` is shared. `renderPageToStream` was previously present but deliberately unwired, on the (correct, measured) grounds that streaming a Suspense-FREE tree changes event-loop blocking by zero. `defer()` is what makes the tree no longer Suspense-free, so that reasoning no longer applies and the doc comment saying so has been rewritten instead of left contradicting the code. Measured against a real `voltro start` (`scripts/measure-deferred-stream.mjs`, one 400ms deferred field): first body byte at 7ms, deferred chunk at 408ms, and a probe firing every 10ms for the duration of the streamed request was served 30 times at a 3ms median / 7ms max. The event loop stays free while the deferred value is pending — that, not TTFB alone, is the point. **`defer()` is a hard error, naming the page, on every mode where it provably cannot work** — each verified against React 19.2.7 rather than assumed: `renderMode: 'static'` (`renderToString` does not support Suspense; it emits an errored boundary plus a "switched to client rendering" template with no warning, so the artefact would ship a permanent fallback), `renderMode: 'isr'` (caches a completed HTML string), `interactive: 'none'` (no JS to run React's reveal scripts), `interactive: 'islands'` (the root never hydrates, so nothing consumes the streamed value). Awaiting-and-inlining on the artefact modes was considered and rejected: it makes `defer()` a silent no-op that still reads like it streams, and it needs a second wire format and a second `<Await>` path for no user-visible gain. **Why BREAKING.** Two things can turn code that compiled into code that does not, or change deployed behaviour: - `useLoaderData<T>()` now returns `LoaderData<T>`. For every concrete `T` that IS `T`, so ordinary call sites are unaffected — but a helper that passes an unresolved generic through (`<D,>(): D => useLoaderData<D>()`) stops compiling and must propagate `LoaderData<D>` instead. A transform cannot tell those apart without typechecking, hence a `manual` codemod. - `ssr` responses are chunked and carry no `content-length`, and `voltro start` needs an SSR bundle built by this version — so a deploy must re-run `voltro build`, and a buffering reverse proxy in front of the app will absorb the win until buffering is disabled for those routes. `<Await>` owns the `<Suspense>` boundary, the `use()`, and the settle `<script>` that publishes the resolved value to the client, so the server render and the hydration render are identical by construction rather than by a `typeof document` guard or `suppressHydrationWarning`. A rejected deferred value travels as a resolved error envelope and renders `errorFallback` in place on both sides — Fizz errors the whole boundary on a rejected `use()`, which would otherwise blow past `errorFallback` entirely. Coverage: `deferredStream.test.tsx` asserts chunk ordering + the concurrent-probe event-loop claim, `deferredHydration.test.tsx` hydrates a real streamed document in jsdom with zero console errors, `deferred.test.ts` pins all four hard errors, `ssrShell.test.ts` covers the shell split and the three stream-driver ordering hazards, and `webDevSsrLayoutLoader.test.ts` runs the SAME streaming assertions against a real `voltro dev` and a real `voltro start`. Non-deferring pages emit a byte-identical `__voltro_state__` payload, asserted against the exact strings a real build and a real serve produce.
|
|
47
|
+
- **@voltro/plugin-auth** — `UserRecord.passwordHash` is now optional (`string | null | undefined`), and the shipped `users` table's `passwordHash` column is nullable to match. It was required, which asserted that every identity has a password. SSO-only, magic-link-only, passkey-only and provider-PAT apps have none, so the only way they could call `subjectFromUser(...)` was to invent a fake hash — strictly worse than storing nothing, because a fake hash is a real value sitting in the column a verifier compares against. The alternative several apps took was to hand-build the `Subject` and lose the helper. The built `Subject` never needed the hash in the first place. **Why this is BREAKING even though nothing was removed:** widening is additive for code that WRITES a `UserRecord` and breaking for code that READS one. `const h: string = user.passwordHash`, `user.passwordHash.length` and `verifyPassword(pw, user.passwordHash)` all compiled before and do not now. The codemod is `manual` because a transform cannot typecheck and therefore cannot tell those sites apart — and both mechanical edits available to it are dangerous. `!` re-asserts the invariant that just stopped holding; `?? ''` manufactures a hash-shaped value and feeds it to a comparison, which is precisely the "a user with no password signs in with any password" failure this change exists to make impossible. An absent hash must be a refusal decision, never a default value. **The security half, which is where the work went.** `handleSignIn` treats an absent hash exactly as it treats an unknown email: it still burns a decoy scrypt, then returns the identical 401 `{ error: 'invalid credentials' }` with no `Set-Cookie`. So a password-less account can never be signed into with the password strategy, and the response reveals neither that the account exists nor that it lacks a password — a divergence there would be an oracle for which accounts are worth attacking through a different strategy. `null`, `undefined` and `''` are all treated as "no password". `passwordlessSignIn.test.ts` pins this across every shape of absence against every shape of submitted password (arbitrary, empty, `undefined`), plus the two indistinguishability properties. It was the only verification site in the package; nothing else reads the field to make a decision. Password RESET is unaffected and still promotes a password-less user: `updatePassword` assigns a hash to a user that had none, after which sign-in works normally and an arbitrary password still fails. `createdAt` stays required — every store can supply it (`insert` takes `Omit<UserRecord, 'createdAt'>`, so callers never pass one), so there is nothing to relax. The nullable column needs no migration work from you — it rides the declarative differ on `voltro db apply` / next boot.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **@voltro/testing, @voltro/runtime** — `makeTestContext` now makes its seeded store available to `runAsSystem`, so a row filter over a SHARED resource can be tested. `RowFilter.load` is `Effect<Ctx, unknown>` with `R = never`, so it cannot `yield*` an EffectStore service. A filter over rows the user OWNS needs no store (the subject carries the id), but a filter over rows SHARED with the user — "you see a list you are a member of" — must read a membership table, and `runAsSystem` is its only route there. Under `makeTestContext` that threw `runAsSystem: no data store available`, so the one rule most worth a test ("user B cannot see the row shared with user A") could not be asserted in-repo at all. The reported consequence was an app declining to ship a process-global filter over shared data it had no way to verify. `makeTestContext` registers its RAW `dataStore` + `schemaRegistry` via `setSystemStoreHandle` — never `ctx.store`, since `runAsSystem` applies `wrapStoreWithMixinBehaviour` for the system subject itself and a wrapped store would be wrapped twice (the system subject also bypasses row filters in production, so raw is the semantically correct layer). It is the outer store, not a transactional view: a `runAsSystem` block inside a mutation reads outside that mutation's transaction in production, and does here too. **Isolation, since the handle is a process global.** Registration is last-wins and never auto-cleared, so a bare `await runAsSystem(...)` written directly in a test resolves. Last-wins alone is not enough: two `makeTestContext` calls in one test allocate two separate `InMemoryDataStore`s even from one seed object, so a filter `load` on the first context would otherwise read the second's rows. The harness therefore re-points the handle at its own context for the span of that context's `load` and restores the previous value, making filter resolution independent of build order. Cross-file leakage does not arise — vitest isolates each test file's module registry — and within a file a test that wants the refusal back calls `clearSystemStoreHandle()`. Production is untouched: only the harness registers, so an app that registered no handle still gets the throw. This scopes an exception to the harness rather than relaxing the refusal. `@voltro/runtime` gains `getSystemStoreHandle()` (read the current handle so a caller can save/restore around a bounded span) and exports the `SystemStoreHandle` type. Both are additive. One behavioural caveat worth knowing: a test that built a `makeTestContext` and then asserted `runAsSystem` refuses in the same file will now see it succeed. Call `clearSystemStoreHandle()` first.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **@voltro/database, @voltro/cli** — `timestampMs` / `timestampMsOrNull` are now importable from a descriptor. 0.6.0 shipped them — and `rowSchema(table)` — documented for a `*.query.ts` `output`, but the browser-safety guard rejects `@voltro/database` and every subpath under it, so the documented usage aborted `voltro dev` with an import-chain error. A downstream app hit exactly that. The field schemas now ship from a new browser-safe entry, **`@voltro/database/wire`**, which contains plain `effect/Schema` values and imports `effect` and nothing else; the guard permits that one subpath. It is not a blanket allowlist entry — the guard resolves a workspace package to its `./src/*.ts` source and keeps walking, so a server import added to that module is still caught and still aborts boot. Both directions are covered by tests (`browserSafetyGuard.test.ts`): the subpath passes, bare `@voltro/database` still fails even with the subpath present, and a deliberately regressed wire module is caught. The package root keeps exporting the same values, so server-side code that already imports the root needs no second import. `rowSchema(table)` / `columnSchema(def)` are corrected rather than changed: they take the table as a VALUE, and reaching a table means importing an app's `database/schema.ts`, which imports `@voltro/database` — so a descriptor can never use them, no matter where they are packaged. That is a consequence of the browser/server boundary, not a packaging accident, and it is now stated where it was previously mis-stated. They remain the row CODEC for server-only code (`*.server.ts`, `*.seed.ts`, jobs, scripts): encoding rows for a file export or queue payload, decoding seed / import data against the real table shape. The doc comments, the docs site (en + de), the seeded agent guide, and the `voltro doctor` `hand-serialized-date` rule — which recommended `rowSchema(table)` in a descriptor's `output` and was therefore actively steering users into the boot crash — all now point at the field schemas instead. No API was removed or narrowed, and no user code that compiled stops compiling: the previously-recommended usage never got as far as a boot, so there is nothing to migrate. Apps that worked around this with hand-written `Date → epoch` converters can delete them and declare `timestampMs` on the field.
|
|
56
|
+
- **@voltro/cli** — `voltro dev` now runs LAYOUT loaders during SSR, like `voltro start` always did. A `layout.tsx` exporting a `loader` had it skipped entirely on the dev server's SSR pass: `useLoaderData()` inside that layout was `undefined` on the server-rendered first paint and only populated after hydration, while the identical code rendered correctly in production. The dev log gave the one visible tell — the `loadChain` phase reported `0ms`, because the phase only imported layout modules and never awaited a loader. This is dev/production parity drift, so the fix is structural rather than a second copy of the loop. Chain assembly plus the layout-loader run now live in ONE `buildSegmentChain`, called by both SSR paths; only module loading differs between them (`voltro start` resolves through its SSR module provider, `voltro dev` through Vite's `ssrLoadModule`) and that is injected. Everything a user can observe is single-sourced: the loader argument shape (`params`, `pathname`, `signal`, `headers`, `query` — identical to what a page loader receives, including the server-side rpc `query()` bound to the request's session cookie), the parallel run across chain segments, and the keying of each result by CHAIN index so a layout can never be handed a sibling's data. Dev also now shares ONE `AbortController` across the page loader and every layout loader on a request, wired to the response `close` event — navigating away cancels all in-flight loader fetches, not just the page's. A throwing layout loader is surfaced exactly as a throwing page loader already was on each path: `voltro start` maps `NotFoundError` / `RedirectError` onto a 404 / 3xx and lets anything else propagate, `voltro dev` logs the failure and falls back to the SPA shell. It is never swallowed into a silently empty layout. No documented behavior changes — layout-level loaders were already specified to run server-side, and dev was the outlier. Apps that worked around this by refetching in the layout after hydration can drop the workaround; nothing needs to change to pick up the fix.
|
|
57
|
+
- **@voltro/web** — The router no longer discards loader data it already holds while a route's loaders are still in flight. Its `pending` branch rendered the page under `<LoaderDataContext.Provider value={undefined}>` unconditionally, so `useLoaderData()` returned `undefined` for that window even when the router was holding that exact route's data. Where it actually bit: a `renderMode: 'static'` page. The prerender runs the PAGE loader and inlines its result into `__voltro_state__`, but it runs no LAYOUT loaders — so after hydration the client re-runs them and the router sits in `pending` until they settle, with the page's own data committed the whole time. During that window the page re-rendered with `undefined`, and an unguarded page (`data.title`, exactly what the docs tell you to write) threw into its `RouteErrorBoundary`. A fast layout loader hides this — it settles in the same microtask checkpoint and React batches the bad render away — so it only surfaces for real when a layout loader is slow, which is the case a real network call produces. The reuse is GATED, because the inverse is a worse bug: on a client-side navigation the committed data belongs to the route being left, and handing it to the incoming page would typecheck (both sides are `unknown` at that seam) and often look plausible on screen. The committed chain data now carries the pathname it was produced for, and the page-level provider reuses it only when that pathname is the one being rendered. The two diverge in exactly one situation — a route that opts into a `Pending` skeleton is displayed before its loaders settle — and that is the situation the gate exists for. Per-layout data got the same treatment one level down, with a finer key: during that same skeleton window a layout's committed value is reused only if the SAME `Layout` component still occupies that chain position. A shared shell therefore stays populated behind the incoming route's skeleton (the point of opting into one), while a DIFFERENT layout at that chain index now gets nothing instead of inheriting its predecessor's value by index. No public API changed (the api-extractor golden is unchanged) and no code that compiled stops compiling; a page that previously flashed `undefined` mid-pending now simply keeps its data. Covered end-to-end in `hydrateLoaderData.test.tsx` — a real SSR render → real state script → real `mount()` → real `hydrateRoot`, with a deliberately slow layout loader, plus both navigation directions and empty-console assertions.
|
|
58
|
+
- **@voltro/cli** — React Fast Refresh now works for pages and layouts in `voltro dev`. Until now **nothing** in a Voltro web app hot-updated: every edit — a page component, a layout component, a stylesheet-adjacent TSX tweak — forced a FULL PAGE RELOAD, discarding form input, scroll position, open dialogs and all client state. Measured in a real browser across both the streamed and the buffered SSR paths, so it was boot-path-agnostic and long-standing. Editing a page component now applies as a hot update with `useState` intact; editing a `loader` still reloads, deliberately. Two causes, both fixed. **(1)** react-refresh only accepts a module whose exports are all components, and a page exports `loader` / `renderMode` / `meta` beside its component, so every route module rejected itself and invalidated upward. **(2)** The rejection then reached the generated `.framework/app.tsx`, which was ALSO ineligible because it exported `preloadCurrentRoute` beside `App`, so it bubbled on to `main.tsx` — which accepts nothing, i.e. a full reload. The obvious fix — strip server-only exports from the client bundle, the Remix approach — is not available here: Voltro's loaders are **isomorphic** (`router.tsx` runs a route's `loader` in the browser on client-side navigation), so the loader must stay in the client graph. Instead, `voltro dev` registers each route module's non-component export names with `@vitejs/plugin-react`'s ignored-exports hook, so react-refresh judges only the components, and the framework decides the reload itself by comparing those exports' VALUES across the update — a function by its source text, everything else by its JSON form. A JSX-only edit recreates the `loader` function object but not its text, so it correctly reads as unchanged and hot-updates; an actual loader edit reads as changed and forces a reload. That reload is the correct outcome, not a limitation: a `loader` also runs server-side, the visible page was rendered from the old one, and the router caches loader results per route + params, so a silent hot swap would leave stale data on screen. The transform is **client-only and append-only** — it never removes an export, and it bails on the SSR environment entirely, because the dev renderer imports each page/layout through `ssrLoadModule` and reads `loader` / `renderMode` straight off the namespace. The generated dev entry is now split in three so the boundaries are clean: `app.tsx` exports `App` and nothing else (a valid refresh boundary), the route table + the mutable module registry HMR patches move to a new `.framework/routeTable.ts` — a plain `.ts` module that is deliberately NOT a boundary, so refreshing `app.tsx` cannot drop already-loaded page modules — and `main.tsx` stays side-effect-only. These are generated files, rewritten on every `voltro dev` boot; nothing user-authored changes. Covered by `routeFastRefresh.test.ts` (client transform injects, SSR transform is a no-op, and the reload decision — including the "re-evaluated but unedited loader is not a change" case that the whole thing turns on) and `webDevEntrySplit.test.ts` (app.tsx has exactly one export). The behaviour itself is browser-only and is driven by `scripts/browser-deferred-stream.mjs`, which now bumps a live `useState` counter before each edit and asserts it survives a component edit and resets on a loader edit — the only assertion that distinguishes a hot update from a reload.
|
|
59
|
+
- **@voltro/web, @voltro/cli** — Server-rendered loader data now reaches the client's FIRST render. Every `renderMode: 'ssr'` page with a loader — and every `layout.tsx` with one — previously hydrated with `useLoaderData()` returning `undefined`, because the inlined `__voltro_state__` payload was written by the SSR pipeline and read by nobody: `mount()` used the script tag as a boolean to pick `hydrateRoot` over `createRoot` and never parsed its contents, and `<Router>` started with no committed loader data, re-running every loader in an effect. Layout (chain-segment) data was not inlined in any shape at all. The consequences were not cosmetic. An ordinary SSR page that dereferences its own loader data (`data.value`) threw `TypeError: Cannot read properties of undefined` on the hydration render and fell into `RouteErrorBoundary`; a layout rendering its loader's value produced a genuine React hydration mismatch (server `ROOT_LAYOUT_LOADER_RAN`, client `ROOT_LAYOUT_LOADER_MISSING`), after which the tree was regenerated from scratch. Both are now measured in a real browser against `e2e-fixtures/web-layout-loader`, with zero console errors and zero page errors, and reproduced in the unit suite through a real `hydrateRoot`. The payload is one coherent object — `{ page, segments: { <chainIndex>: … }, ran }` — defined once in `@voltro/web`'s `routerState` module and emitted through a single `renderRouterStateScript()` helper that `voltro dev`, `voltro start` and `voltro build` all call. That is deliberate: the dev and serve SSR paths are independent assemblies, and hand-mirroring the shape into each is exactly the drift that let layout loaders go missing in dev in the first place. `ran` exists because JSON cannot express `undefined` — without it a loader that resolved to `undefined` is indistinguishable from one that never ran, and the client would re-run it. Values are keyed by the same chain index the renderer wraps layouts with, so a layout can never be handed its neighbour's data; escaping is unchanged (`<` is escaped, so a `</script>` inside loader data cannot break out of the tag). **Two behaviour changes worth knowing about, neither of which stops any code compiling.** First, `useLoaderData()` now returns the server's value on the initial render of a server-rendered page instead of `undefined`; components that branched on `undefined` to show a skeleton will simply stop showing it on first paint. Second, loaders no longer re-run on initial hydration — that re-run was the source of the post-hydration flash. A loader whose client-side re-execution an app was relying on (to refresh data or to trigger a side effect after mount) will no longer fire on first load; move that work into an effect. Client-side navigation is unaffected and runs loaders exactly as before, as does a fresh client mount with no server markup. Static prerender (`voltro build`) inlines the page loader's result the same way. It does not run layout loaders — it never did — so a static page's layouts continue to resolve their data on the client after mount.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
42
63
|
## [0.7.0] — 2026-07-20
|
|
43
64
|
|
|
44
65
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,24 @@ export declare type AppClients = ReadonlyMap<string, unknown>;
|
|
|
23
23
|
|
|
24
24
|
export declare const AppClientsContext: Context<AppClients | null>;
|
|
25
25
|
|
|
26
|
+
/** Render a deferred loader value behind a streamed `<Suspense>` boundary. */
|
|
27
|
+
export declare const Await: <T>({ value, fallback, children, errorFallback }: AwaitProps<T>) => ReactNode;
|
|
28
|
+
|
|
29
|
+
export declare interface AwaitProps<T> {
|
|
30
|
+
/** A deferred field off `useLoaderData()`. */
|
|
31
|
+
readonly value: Promise<T>;
|
|
32
|
+
/** Rendered until the value arrives. This is what the STREAMED SHELL
|
|
33
|
+
* contains, so keep it cheap and layout-stable. */
|
|
34
|
+
readonly fallback: ReactNode;
|
|
35
|
+
/** Rendered with the resolved value. */
|
|
36
|
+
readonly children: (value: T) => ReactNode;
|
|
37
|
+
/** Rendered if the deferred promise REJECTS. Without it a rejection
|
|
38
|
+
* propagates to the route's `error.tsx` and takes the whole page with it —
|
|
39
|
+
* usually the wrong trade for a subtree that was deferred precisely
|
|
40
|
+
* because it is optional. */
|
|
41
|
+
readonly errorFallback?: ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/** A navigation the blocker is holding back. `retry()` proceeds with the
|
|
27
45
|
* original navigation; `reset()` cancels it and clears the block. */
|
|
28
46
|
export declare interface BlockedNavigation {
|
|
@@ -143,6 +161,41 @@ export declare const compileRoute: (route: PageDescriptor) => CompiledRoute;
|
|
|
143
161
|
* unspecified keys fall through to these. */
|
|
144
162
|
export declare const defaultFallbackStrings: FallbackStrings;
|
|
145
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Split a loader's result into data that blocks the shell and data that
|
|
166
|
+
* streams in after it.
|
|
167
|
+
*
|
|
168
|
+
* Only legal on a page with `renderMode: 'ssr'` and `interactive: 'full'` —
|
|
169
|
+
* every other combination is a hard boot/build error naming the page, because
|
|
170
|
+
* there is no honest way to stream into a stored artefact or into a document
|
|
171
|
+
* with no React runtime. See {@link assertDeferralSupported}.
|
|
172
|
+
*
|
|
173
|
+
* Deliberately NOT a `const` type parameter: a loader's `{ title: 'Ok' }` must
|
|
174
|
+
* widen to `string`, the way any other loader's return type does. Pinning the
|
|
175
|
+
* literal would make every page's data type change whenever a default string
|
|
176
|
+
* in the loader changed.
|
|
177
|
+
*
|
|
178
|
+
* @param eager values already resolved (or awaited by the loader itself)
|
|
179
|
+
* @param deferred promises — NOT awaited by the loader
|
|
180
|
+
*/
|
|
181
|
+
export declare const defer: <TEager extends object, TDeferred extends Record<string, Promise<unknown>>>(eager: TEager, deferred: TDeferred) => DeferredLoaderResult<TEager, TDeferred>;
|
|
182
|
+
|
|
183
|
+
/** Brand key on the object `defer()` returns. A single explicit container
|
|
184
|
+
* check — NOT a scan of arbitrary loader results for markers by shape. */
|
|
185
|
+
declare const DEFERRED_RESULT_TAG: "__voltroDeferredLoaderResult";
|
|
186
|
+
|
|
187
|
+
/** What a loader returns when it defers part of its data. Opaque to user
|
|
188
|
+
* code: build it with {@link defer}, read it with `useLoaderData()`. */
|
|
189
|
+
export declare interface DeferredLoaderResult<TEager extends object = object, TDeferred extends Record<string, Promise<unknown>> = Record<string, Promise<unknown>>> {
|
|
190
|
+
readonly [DEFERRED_RESULT_TAG]: true;
|
|
191
|
+
/** Awaited before the shell renders — present in the SSR markup AND in the
|
|
192
|
+
* inlined `__voltro_state__` payload. */
|
|
193
|
+
readonly eager: TEager;
|
|
194
|
+
/** Streamed after the shell — each key becomes a promise on
|
|
195
|
+
* `useLoaderData()`, to be rendered through `<Await>`. */
|
|
196
|
+
readonly deferred: TDeferred;
|
|
197
|
+
}
|
|
198
|
+
|
|
146
199
|
export declare interface DevStatus {
|
|
147
200
|
/** Stable id; re-pushing the same id replaces the entry (lets a
|
|
148
201
|
* caller update its label without dropping + re-creating). */
|
|
@@ -340,6 +393,9 @@ export declare const installServerLogRelay: (options: ServerLogRelayOptions) =>
|
|
|
340
393
|
*/
|
|
341
394
|
export declare type InteractiveMode = 'full' | 'islands' | 'none';
|
|
342
395
|
|
|
396
|
+
/** True for exactly the object {@link defer} returns. */
|
|
397
|
+
export declare const isDeferredLoaderResult: (value: unknown) => value is DeferredLoaderResult;
|
|
398
|
+
|
|
343
399
|
/**
|
|
344
400
|
* Wrap a Component as an island. The returned Component renders the
|
|
345
401
|
* inner content wrapped in a marker `<div data-voltro-island>` that
|
|
@@ -402,6 +458,16 @@ export declare class LoaderCache {
|
|
|
402
458
|
abort(key: string): void;
|
|
403
459
|
/** Drop entries matching `pattern` (any params). */
|
|
404
460
|
invalidate(pattern: string): void;
|
|
461
|
+
/**
|
|
462
|
+
* Install an already-settled result for `key` without running anything.
|
|
463
|
+
* Used to adopt server-rendered loader data at hydration: the entry is
|
|
464
|
+
* `success` from the start, so the router's loader effect collects it
|
|
465
|
+
* immediately and the loader never re-runs on the initial render.
|
|
466
|
+
*
|
|
467
|
+
* Never overwrites an existing entry — a loader that a navigation already
|
|
468
|
+
* started (or settled) outranks a stale seed.
|
|
469
|
+
*/
|
|
470
|
+
seed(key: string, data: unknown): void;
|
|
405
471
|
/** Drop every entry. Test-only escape hatch. */
|
|
406
472
|
clear(): void;
|
|
407
473
|
}
|
|
@@ -432,6 +498,16 @@ export declare interface LoaderContext {
|
|
|
432
498
|
readonly query?: <T = unknown>(tag: string, input?: Record<string, unknown>) => Promise<T>;
|
|
433
499
|
}
|
|
434
500
|
|
|
501
|
+
/**
|
|
502
|
+
* What `useLoaderData<T>()` hands the component for a loader returning `T`.
|
|
503
|
+
* A plain loader is unchanged (`LoaderData<X>` is `X`); a deferring loader
|
|
504
|
+
* flattens to its eager fields PLUS a promise per deferred field, which is
|
|
505
|
+
* exactly what `<Await value={...}>` consumes.
|
|
506
|
+
*/
|
|
507
|
+
export declare type LoaderData<TLoaderResult> = TLoaderResult extends DeferredLoaderResult<infer TEager, infer TDeferred> ? {
|
|
508
|
+
readonly [K in keyof (TEager & TDeferred)]: (TEager & TDeferred)[K];
|
|
509
|
+
} : TLoaderResult;
|
|
510
|
+
|
|
435
511
|
/* Excluded from this release type: LoaderDataContext */
|
|
436
512
|
|
|
437
513
|
declare type LoaderEntry<T = unknown> = {
|
|
@@ -1001,8 +1077,12 @@ export declare const useFallbackStrings: (override?: PartialFallbackStrings) =>
|
|
|
1001
1077
|
* Returns the loader data for the nearest level in the render tree: inside
|
|
1002
1078
|
* a `layout.tsx` it's that layout's loader result; inside the page it's
|
|
1003
1079
|
* the page loader's. Narrowed to the caller's type.
|
|
1080
|
+
*
|
|
1081
|
+
* When the loader used `defer()`, `LoaderData<T>` flattens the two buckets
|
|
1082
|
+
* into one object: eager fields as values, deferred fields as PROMISES to be
|
|
1083
|
+
* rendered through `<Await>`. For every other loader `LoaderData<T>` is `T`.
|
|
1004
1084
|
*/
|
|
1005
|
-
export declare const useLoaderData: <T>() => T
|
|
1085
|
+
export declare const useLoaderData: <T>() => LoaderData<T>;
|
|
1006
1086
|
|
|
1007
1087
|
export declare const useLocation: () => string;
|
|
1008
1088
|
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,36 @@ import { t as e } from "./globalContext-d4A-ugDg.js";
|
|
|
2
2
|
import { AppClientsContext as t, useAppClient as n } from "./hooks.js";
|
|
3
3
|
import { a as r, c as i, i as a, l as o, o as s, r as c, s as l, u } from "./frameworkBoot-C_d7E8Fj.js";
|
|
4
4
|
import { a as d, c as f, i as p, l as m, o as h, s as g } from "./defaultFallbacks-CGs2z5qv.js";
|
|
5
|
-
import { i as _, n as ee, r as te, t as ne } from "./mount-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { i as _, n as ee, r as te, t as ne } from "./mount-D3k8Ozdm.js";
|
|
6
|
+
import { c as re, d as ie, h as ae, l as oe, p as se, u as ce } from "./routerState-ga64vk2B.js";
|
|
7
|
+
import { A as le, B as ue, C as de, D as fe, E as pe, F as me, I as v, L as y, M as b, N as x, O as S, P as C, R as w, S as T, T as E, V as D, _ as O, a as k, b as A, c as j, d as M, f as N, g as P, h as F, i as I, j as L, k as R, l as z, m as he, n as ge, o as _e, p as ve, r as ye, s as be, t as xe, u as Se, v as Ce, w as we, x as Te, y as Ee, z as De } from "./serverContext-BW0GF8fv.js";
|
|
8
|
+
import { Component as Oe, Suspense as ke, createElement as B, use as Ae, useCallback as je, useContext as Me, useSyncExternalStore as Ne } from "react";
|
|
9
|
+
import { Fragment as V, jsx as H, jsxs as U } from "react/jsx-runtime";
|
|
8
10
|
export * from "@voltro/client";
|
|
9
11
|
export * from "@voltro/ui";
|
|
10
|
-
//#region src/
|
|
11
|
-
var
|
|
12
|
+
//#region src/await.tsx
|
|
13
|
+
var Pe = ({ value: e, fallback: t, children: n, errorFallback: r }) => /* @__PURE__ */ H(ke, {
|
|
14
|
+
fallback: t,
|
|
15
|
+
children: /* @__PURE__ */ H(Ie, {
|
|
16
|
+
errorFallback: r,
|
|
17
|
+
children: /* @__PURE__ */ H(Fe, {
|
|
18
|
+
value: e,
|
|
19
|
+
errorFallback: r ?? null,
|
|
20
|
+
children: n
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
}), W = ({ body: e }) => /* @__PURE__ */ H("script", { dangerouslySetInnerHTML: { __html: e } }), Fe = ({ value: e, errorFallback: t, children: n }) => {
|
|
24
|
+
let r = Ae(e), i = ce(e), a = ae(r);
|
|
25
|
+
return a === void 0 ? /* @__PURE__ */ U(V, { children: [i === void 0 ? null : /* @__PURE__ */ H(W, { body: ie(i, r) }), n(r)] }) : /* @__PURE__ */ U(V, { children: [i === void 0 ? null : /* @__PURE__ */ H(W, { body: oe(i, a) }), t] });
|
|
26
|
+
}, Ie = class extends Oe {
|
|
27
|
+
state = { error: void 0 };
|
|
28
|
+
static getDerivedStateFromError(e) {
|
|
29
|
+
return { error: e };
|
|
30
|
+
}
|
|
31
|
+
render() {
|
|
32
|
+
return this.state.error === void 0 ? this.props.children : this.props.errorFallback ?? null;
|
|
33
|
+
}
|
|
34
|
+
}, G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), Le = ({ loader: e, children: t }) => B(K.Provider, { value: { loader: e } }, t), q = [
|
|
12
35
|
640,
|
|
13
36
|
750,
|
|
14
37
|
828,
|
|
@@ -17,7 +40,7 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
17
40
|
1920,
|
|
18
41
|
2048,
|
|
19
42
|
3840
|
|
20
|
-
],
|
|
43
|
+
], Re = (e, t, n, r) => {
|
|
21
44
|
let i = n !== void 0 && !r ? Array.from(new Set(q.filter((e) => e <= n * 2).concat(n))).sort((e, t) => e - t) : [...q];
|
|
22
45
|
return {
|
|
23
46
|
srcSet: i.map((n) => `${e({
|
|
@@ -29,10 +52,10 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
29
52
|
width: i[i.length - 1]
|
|
30
53
|
})
|
|
31
54
|
};
|
|
32
|
-
},
|
|
33
|
-
let f =
|
|
55
|
+
}, ze = ({ src: e, alt: t, width: n, height: r, fill: i = !1, sizes: a, priority: o = !1, loader: s, placeholder: c = "empty", blurDataURL: l, style: u, ...d }) => {
|
|
56
|
+
let f = Me(K).loader, p = s ?? f;
|
|
34
57
|
!i && (n === void 0 || r === void 0) && typeof console < "u" && console.warn(`<Image src="${e}"> needs both width and height (or fill) to reserve layout space and avoid CLS.`);
|
|
35
|
-
let { srcSet: m, fallback: h } =
|
|
58
|
+
let { srcSet: m, fallback: h } = Re(p, e, n, i), g = c === "blur" && l ? {
|
|
36
59
|
backgroundImage: `url(${l})`,
|
|
37
60
|
backgroundSize: "cover",
|
|
38
61
|
backgroundPosition: "center"
|
|
@@ -43,7 +66,7 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
43
66
|
width: "100%",
|
|
44
67
|
objectFit: "cover"
|
|
45
68
|
} : {};
|
|
46
|
-
return
|
|
69
|
+
return B("img", {
|
|
47
70
|
src: h,
|
|
48
71
|
srcSet: m,
|
|
49
72
|
sizes: a ?? (i ? "100vw" : void 0),
|
|
@@ -62,10 +85,10 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
62
85
|
},
|
|
63
86
|
...d
|
|
64
87
|
});
|
|
65
|
-
},
|
|
88
|
+
}, Be = ({ to: e, children: t, className: n }) => B("a", {
|
|
66
89
|
href: e,
|
|
67
90
|
className: n
|
|
68
|
-
}, t),
|
|
91
|
+
}, t), Ve = {
|
|
69
92
|
position: "sticky",
|
|
70
93
|
top: 0,
|
|
71
94
|
zIndex: 10,
|
|
@@ -76,13 +99,13 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
76
99
|
padding: "1rem 1.5rem",
|
|
77
100
|
borderBottom: "1px solid var(--voltro-blog-border, rgba(127,127,127,0.2))",
|
|
78
101
|
backdropFilter: "blur(8px)"
|
|
79
|
-
},
|
|
102
|
+
}, He = {
|
|
80
103
|
maxWidth: "48rem",
|
|
81
104
|
margin: "0 auto",
|
|
82
105
|
padding: "2rem 1.5rem",
|
|
83
106
|
width: "100%"
|
|
84
|
-
},
|
|
85
|
-
let { brand: t, topNav: n, children: r, pathname: i, footer: a, breadcrumbs: o, headerRight: s } = e, c = e.Link ??
|
|
107
|
+
}, Ue = (e) => {
|
|
108
|
+
let { brand: t, topNav: n, children: r, pathname: i, footer: a, breadcrumbs: o, headerRight: s } = e, c = e.Link ?? Be, l = B("nav", {
|
|
86
109
|
className: "voltro-blog-nav",
|
|
87
110
|
style: {
|
|
88
111
|
display: "flex",
|
|
@@ -91,20 +114,20 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
91
114
|
}
|
|
92
115
|
}, ...n.map((e) => {
|
|
93
116
|
let t = i === e.href;
|
|
94
|
-
return
|
|
117
|
+
return B(c, {
|
|
95
118
|
key: e.href,
|
|
96
119
|
to: e.href,
|
|
97
120
|
className: t ? "voltro-blog-nav-link is-active" : "voltro-blog-nav-link",
|
|
98
121
|
children: e.label
|
|
99
122
|
});
|
|
100
|
-
})), u =
|
|
123
|
+
})), u = B("header", {
|
|
101
124
|
className: "voltro-blog-header",
|
|
102
|
-
style:
|
|
103
|
-
},
|
|
125
|
+
style: Ve
|
|
126
|
+
}, B("div", { className: "voltro-blog-brand" }, t), s ? B("div", { style: {
|
|
104
127
|
display: "flex",
|
|
105
128
|
gap: "1rem",
|
|
106
129
|
alignItems: "center"
|
|
107
|
-
} }, l, s) : l), d = o && o.length > 0 ?
|
|
130
|
+
} }, l, s) : l), d = o && o.length > 0 ? B("nav", {
|
|
108
131
|
className: "voltro-blog-breadcrumbs",
|
|
109
132
|
"aria-label": "Breadcrumb",
|
|
110
133
|
style: {
|
|
@@ -115,20 +138,20 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
115
138
|
gap: "0.5rem"
|
|
116
139
|
}
|
|
117
140
|
}, ...o.flatMap((e, t) => {
|
|
118
|
-
let n = t === o.length - 1, r = e.href && !n ?
|
|
141
|
+
let n = t === o.length - 1, r = e.href && !n ? B(c, {
|
|
119
142
|
key: `c${t}`,
|
|
120
143
|
to: e.href,
|
|
121
144
|
children: e.label
|
|
122
|
-
}) :
|
|
145
|
+
}) : B("span", {
|
|
123
146
|
key: `c${t}`,
|
|
124
147
|
"aria-current": n ? "page" : void 0
|
|
125
148
|
}, e.label);
|
|
126
|
-
return n ? [r] : [r,
|
|
149
|
+
return n ? [r] : [r, B("span", { key: `s${t}` }, "/")];
|
|
127
150
|
})) : null;
|
|
128
|
-
return
|
|
151
|
+
return B("div", { className: "voltro-blog-layout" }, u, B("main", {
|
|
129
152
|
className: "voltro-blog-main",
|
|
130
|
-
style:
|
|
131
|
-
}, d, r), a ?
|
|
153
|
+
style: He
|
|
154
|
+
}, d, r), a ? B("footer", {
|
|
132
155
|
className: "voltro-blog-footer",
|
|
133
156
|
style: {
|
|
134
157
|
maxWidth: "48rem",
|
|
@@ -137,7 +160,7 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
137
160
|
borderTop: "1px solid var(--voltro-blog-border, rgba(127,127,127,0.2))"
|
|
138
161
|
}
|
|
139
162
|
}, a) : null);
|
|
140
|
-
}, J = "voltro:theme",
|
|
163
|
+
}, J = "voltro:theme", We = 31536e3, Ge = () => {
|
|
141
164
|
if (typeof document > "u") return "system";
|
|
142
165
|
try {
|
|
143
166
|
let e = document.cookie.match(/(?:^|;\s*)voltro:theme=([^;]*)/), t = e?.[1] === void 0 ? "" : decodeURIComponent(e[1]);
|
|
@@ -145,43 +168,43 @@ var G = ({ src: e }) => e, K = e("imageConfig", { loader: G }), xe = ({ loader:
|
|
|
145
168
|
} catch {
|
|
146
169
|
return "system";
|
|
147
170
|
}
|
|
148
|
-
},
|
|
171
|
+
}, Y = () => {
|
|
149
172
|
if (typeof window > "u" || !window.matchMedia) return !1;
|
|
150
173
|
try {
|
|
151
174
|
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
152
175
|
} catch {
|
|
153
176
|
return !1;
|
|
154
177
|
}
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
for (let e of
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
let t, n = () =>
|
|
178
|
+
}, X = (e) => e === "system" ? Y() ? "dark" : "light" : e, Z = 0, Q = /* @__PURE__ */ new Set(), $ = () => {
|
|
179
|
+
Z++;
|
|
180
|
+
for (let e of Q) e();
|
|
181
|
+
}, Ke = (e) => {
|
|
182
|
+
Q.add(e);
|
|
183
|
+
let t, n = () => $();
|
|
161
184
|
if (typeof window < "u" && window.matchMedia) try {
|
|
162
185
|
t = window.matchMedia("(prefers-color-scheme: dark)"), t.addEventListener("change", n);
|
|
163
186
|
} catch {}
|
|
164
187
|
return () => {
|
|
165
|
-
|
|
188
|
+
Q.delete(e);
|
|
166
189
|
try {
|
|
167
190
|
t?.removeEventListener("change", n);
|
|
168
191
|
} catch {}
|
|
169
192
|
};
|
|
170
|
-
},
|
|
171
|
-
let e =
|
|
172
|
-
return `${
|
|
173
|
-
},
|
|
174
|
-
let [, e, t] =
|
|
193
|
+
}, qe = () => {
|
|
194
|
+
let e = Ge();
|
|
195
|
+
return `${Z}:${e}:${X(e)}`;
|
|
196
|
+
}, Je = () => "0:system:light", Ye = () => {
|
|
197
|
+
let [, e, t] = Ne(Ke, qe, Je).split(":");
|
|
175
198
|
return {
|
|
176
199
|
theme: e,
|
|
177
200
|
resolvedTheme: t,
|
|
178
|
-
setTheme:
|
|
201
|
+
setTheme: je((e) => {
|
|
179
202
|
if (typeof document < "u") try {
|
|
180
|
-
document.cookie = e === "system" ? `${J}=; path=/; max-age=0; SameSite=Lax` : `${J}=${e}; path=/; max-age=${
|
|
203
|
+
document.cookie = e === "system" ? `${J}=; path=/; max-age=0; SameSite=Lax` : `${J}=${e}; path=/; max-age=${We}; SameSite=Lax`, document.documentElement.classList.toggle("dark", X(e) === "dark");
|
|
181
204
|
} catch {}
|
|
182
|
-
|
|
205
|
+
$();
|
|
183
206
|
}, [])
|
|
184
207
|
};
|
|
185
|
-
},
|
|
208
|
+
}, Xe = "framework-web";
|
|
186
209
|
//#endregion
|
|
187
|
-
export { t as AppClientsContext,
|
|
210
|
+
export { t as AppClientsContext, Pe as Await, Ue as BlogLayout, p as FallbackStringsProvider, ze as Image, Le as ImageConfigProvider, _e as Link, be as LoaderCache, j as LoaderDataContext, i as NavigationIndicator, x as NotFoundError, z as PlainLink, o as ReconnectContext, C as RedirectError, Se as Router, M as RouterContext, xe as ServerRequestContext, ge as ServerRequestProvider, J as THEME_COOKIE, Xe as WEB_NAME, N as compileRoute, d as defaultFallbackStrings, re as defer, ve as externalUrl, he as findNotFound, c as formatPrintf, ee as getIslandComponent, De as getRouteSnapshot, g as getStatuses, te as hydrateIslandsOnPage, a as installBrowserConsoleBridge, r as installServerLogRelay, se as isDeferredLoaderResult, me as isNotFound, v as isRedirect, _ as island, F as lazyPageRoute, P as loaderCacheKey, O as matchRoute, ne as mount, y as notFound, Ce as pageRoute, ye as parseCookieHeader, G as passthroughImageLoader, Ee as preloadRouteModule, f as pushStatus, w as redirect, s as registerClientTrace, A as resolveAnchorNavigation, Te as resolveMeta, T as segmentLoaderKey, ue as setRouteSnapshot, de as sortRoutesByPriority, D as subscribeRouteSnapshot, m as subscribeStatuses, l as subscribeTraceErrors, n as useAppClient, we as useBlocker, h as useFallbackStrings, E as useLoaderData, pe as useLocation, fe as useNavigate, S as useParams, R as usePrefetch, u as useReconnect, I as useSearchParams, k as useServerRequest, le as useSetSearchParams, Ye as useTheme, L as withHash, b as withQuery };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { t as e } from "./frameworkBoot-C_d7E8Fj.js";
|
|
2
|
+
import { o as t, r as n, t as r } from "./routerState-ga64vk2B.js";
|
|
3
|
+
import { StrictMode as i, createElement as a } from "react";
|
|
4
|
+
import { createRoot as o, hydrateRoot as s } from "react-dom/client";
|
|
5
|
+
import { jsx as c } from "react/jsx-runtime";
|
|
6
|
+
//#region src/islands.tsx
|
|
7
|
+
var l = /* @__PURE__ */ new Map(), u = (e, t) => {
|
|
8
|
+
l.set(t.name, e);
|
|
9
|
+
let n = t.hydrate ?? "visible", r = (r) => a("div", {
|
|
10
|
+
"data-voltro-island": "",
|
|
11
|
+
"data-island-name": t.name,
|
|
12
|
+
"data-island-hydrate": n,
|
|
13
|
+
"data-island-props": f(r)
|
|
14
|
+
}, a(e, r));
|
|
15
|
+
return r.displayName = `Island(${t.name})`, r;
|
|
16
|
+
}, d = (e) => l.get(e), f = (e) => JSON.stringify(e ?? {}).replace(/</g, "\\u003c"), p = (e) => {
|
|
17
|
+
if (!e) return {};
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(e);
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}, m = () => {
|
|
24
|
+
let e = document.querySelectorAll("[data-voltro-island]:not([data-voltro-hydrated])");
|
|
25
|
+
for (let t of e) {
|
|
26
|
+
let e = t.dataset.islandName ?? "", n = l.get(e);
|
|
27
|
+
if (!n) {
|
|
28
|
+
console.warn(`[voltro] island "${e}" referenced in DOM but not registered. Did the island file get imported in this bundle?`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let r = p(t.dataset.islandProps ?? null), i = t.dataset.islandHydrate ?? "visible";
|
|
32
|
+
t.setAttribute("data-voltro-hydrated", "pending");
|
|
33
|
+
let o = () => {
|
|
34
|
+
t.getAttribute("data-voltro-hydrated") !== "done" && (t.setAttribute("data-voltro-hydrated", "done"), s(t, a(n, r)));
|
|
35
|
+
};
|
|
36
|
+
if (i === "load") o();
|
|
37
|
+
else if (i === "idle") {
|
|
38
|
+
let e = globalThis.requestIdleCallback;
|
|
39
|
+
e ? e(o) : setTimeout(o, 0);
|
|
40
|
+
} else if (i === "visible") {
|
|
41
|
+
let e = new IntersectionObserver((t) => {
|
|
42
|
+
t.some((e) => e.isIntersecting) && (e.disconnect(), o());
|
|
43
|
+
}, { rootMargin: "64px" });
|
|
44
|
+
e.observe(t);
|
|
45
|
+
} else if (i === "interaction") {
|
|
46
|
+
let e = () => {
|
|
47
|
+
t.removeEventListener("pointerdown", e), t.removeEventListener("keydown", e), o();
|
|
48
|
+
};
|
|
49
|
+
t.addEventListener("pointerdown", e, { passive: !0 }), t.addEventListener("keydown", e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, h = (e) => `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws/${e}`, g = (a, l) => {
|
|
53
|
+
let u = document.getElementById(l.rootId ?? "root");
|
|
54
|
+
if (!u) throw Error(`@voltro/web: no #${l.rootId ?? "root"} element in the document`);
|
|
55
|
+
let d = Object.entries(l.apis).map(([e, t]) => ({
|
|
56
|
+
name: e,
|
|
57
|
+
group: t.group,
|
|
58
|
+
descriptors: t.descriptors ?? {},
|
|
59
|
+
wsUrl: t.wsUrl ?? h(e),
|
|
60
|
+
headers: t.headers
|
|
61
|
+
})), f = document.getElementById(r), p = f !== null && u.children.length > 0, g = document.querySelector("meta[name=\"voltro-interactive\"]")?.getAttribute("content") ?? "full";
|
|
62
|
+
if (g === "none") return;
|
|
63
|
+
if (g === "islands") {
|
|
64
|
+
m();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
p && t(n(f.textContent, window.location.pathname));
|
|
68
|
+
let _ = /* @__PURE__ */ c(i, { children: /* @__PURE__ */ c(e, {
|
|
69
|
+
App: a,
|
|
70
|
+
apis: d,
|
|
71
|
+
...l.Devtools ? { Devtools: l.Devtools } : {}
|
|
72
|
+
}) });
|
|
73
|
+
p ? s(u, _) : o(u).render(_);
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { u as i, d as n, m as r, g as t };
|
package/dist/mount.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as e } from "./mount-
|
|
1
|
+
import { t as e } from "./mount-D3k8Ozdm.js";
|
|
2
2
|
export { e as mount };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//#region src/serialise.ts
|
|
2
|
+
var e = /* @__PURE__ */ RegExp("\\u2028", "g"), t = /* @__PURE__ */ RegExp("\\u2029", "g"), n = (n) => JSON.stringify(n ?? null).replace(/</g, "\\u003c").replace(e, "\\u2028").replace(t, "\\u2029"), r = "__voltroDeferredLoaderResult", i = (e, t) => ({
|
|
3
|
+
[r]: !0,
|
|
4
|
+
eager: e,
|
|
5
|
+
deferred: t
|
|
6
|
+
}), a = (e) => typeof e == "object" && !!e && e[r] === !0, o = (e) => a(e) ? {
|
|
7
|
+
...e.eager,
|
|
8
|
+
...e.deferred
|
|
9
|
+
} : e, s = Symbol.for("voltro.deferId"), c = (e, t) => (Object.defineProperty(e, s, {
|
|
10
|
+
value: t,
|
|
11
|
+
enumerable: !1,
|
|
12
|
+
configurable: !0
|
|
13
|
+
}), e), l = (e) => {
|
|
14
|
+
if (typeof e != "object" || !e) return;
|
|
15
|
+
let t = e[s];
|
|
16
|
+
return typeof t == "string" ? t : void 0;
|
|
17
|
+
}, u = (e, t) => `${e === "page" ? "p" : `s${e}`}:${t}`, d = "__voltro_defer__", f = `window.${d}=window.${d}||(function(){var m={};function g(i){var p=m[i];if(!p){var r,j;p=new Promise(function(a,b){r=a;j=b});p.catch(function(){});p.__vr=r;p.__vj=j;m[i]=p}return p}return{get:g,settle:function(i,v){var p=g(i);if(!p.__vs){p.__vs=1;p.status="fulfilled";p.value=v;p.__vr(v)}}}})()`, p = () => `<script>${f}<\/script>`, m = (e, t) => `window.${d}.settle(${n(e)},${n(t)})`, h = (e, t) => `window.${d}.settle(${n(e)},${n(v(t))})`, g = () => {
|
|
18
|
+
let e = globalThis, t = e[d];
|
|
19
|
+
if (t) return t;
|
|
20
|
+
let n = /* @__PURE__ */ new Map(), r = (e) => {
|
|
21
|
+
let t = n.get(e);
|
|
22
|
+
if (t) return t;
|
|
23
|
+
let r, i, a = new Promise((e, t) => {
|
|
24
|
+
r = e, i = t;
|
|
25
|
+
});
|
|
26
|
+
return a.catch(() => {}), a.__vr = r, a.__vj = i, n.set(e, a), a;
|
|
27
|
+
}, i = {
|
|
28
|
+
get: r,
|
|
29
|
+
settle: (e, t) => {
|
|
30
|
+
let n = r(e);
|
|
31
|
+
n.__vs || (n.__vs = !0, n.status = "fulfilled", n.value = t, n.__vr?.(t));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return e[d] = i, i;
|
|
35
|
+
}, _ = "__voltroDeferError", v = (e) => ({ [_]: e }), y = (e) => {
|
|
36
|
+
if (typeof e != "object" || !e) return;
|
|
37
|
+
let t = e[_];
|
|
38
|
+
return typeof t == "string" ? t : void 0;
|
|
39
|
+
}, b = (e, t) => {
|
|
40
|
+
if (!a(e)) return {
|
|
41
|
+
renderData: e,
|
|
42
|
+
stateData: e,
|
|
43
|
+
deferredIds: void 0
|
|
44
|
+
};
|
|
45
|
+
let n = { ...e.eager }, r = {};
|
|
46
|
+
for (let [i, a] of Object.entries(e.deferred)) {
|
|
47
|
+
let e = u(t, i);
|
|
48
|
+
r[i] = e, n[i] = c(Promise.resolve(a).catch((e) => v(e instanceof Error ? e.message : String(e))), e);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
renderData: n,
|
|
52
|
+
stateData: { ...e.eager },
|
|
53
|
+
deferredIds: r
|
|
54
|
+
};
|
|
55
|
+
}, x = (e) => {
|
|
56
|
+
let { page: t, renderMode: n, interactive: r } = e;
|
|
57
|
+
if (n !== "ssr") throw Error(`[voltro] "${t}" uses defer() in its loader but has renderMode: '${n}'. defer() streams values in AFTER the shell, which only a per-request response can do — '${n}' produces a stored HTML artefact with no "after". Set \`export const renderMode = 'ssr'\` on this page, or drop defer() and return the values directly.`);
|
|
58
|
+
if (r !== "full") throw Error(`[voltro] "${t}" uses defer() in its loader but has interactive: '${r}'. ` + (r === "none" ? "Revealing a streamed <Suspense> boundary requires React's inline reveal scripts, and interactive: 'none' ships no JS at all — the <Await> fallback would be permanent. " : "interactive: 'islands' never hydrates the page's React root, so nothing consumes the streamed values and the <Await> fallback would be permanent. ") + "Remove the `interactive` export (defaults to 'full'), or drop defer() and return the values directly.");
|
|
59
|
+
}, S = "__voltro_state__", C = (e) => {
|
|
60
|
+
let t = Object.keys(e.segmentLoaderData ?? {}).map((e) => Number(e)).filter((e) => Number.isInteger(e)).sort((e, t) => e - t), n = e.deferredPageIds !== void 0, r = e.deferredSegmentIds !== void 0 && Object.keys(e.deferredSegmentIds).length > 0;
|
|
61
|
+
return {
|
|
62
|
+
page: e.loaderData,
|
|
63
|
+
...e.segmentLoaderData ? { segments: e.segmentLoaderData } : {},
|
|
64
|
+
ran: {
|
|
65
|
+
...e.pageLoaderRan ? { page: !0 } : {},
|
|
66
|
+
...t.length > 0 ? { segments: t } : {}
|
|
67
|
+
},
|
|
68
|
+
...n || r ? { deferred: {
|
|
69
|
+
...n ? { page: e.deferredPageIds } : {},
|
|
70
|
+
...r ? { segments: e.deferredSegmentIds } : {}
|
|
71
|
+
} } : {}
|
|
72
|
+
};
|
|
73
|
+
}, w = (e) => `<script type="application/json" id="${S}">${n(C(e))}<\/script>`, T = /* @__PURE__ */ new Map(), E = (e, t) => {
|
|
74
|
+
if (!e) return null;
|
|
75
|
+
let n;
|
|
76
|
+
try {
|
|
77
|
+
n = JSON.parse(e);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (typeof n != "object" || !n || Array.isArray(n)) return null;
|
|
82
|
+
let r = n, i = new Set(r.ran?.segments ?? []), a = /* @__PURE__ */ new Map();
|
|
83
|
+
for (let e of i) a.set(e, D(r.segments?.[String(e)], r.deferred?.segments?.[String(e)]));
|
|
84
|
+
return {
|
|
85
|
+
page: D(r.page, r.deferred?.page),
|
|
86
|
+
pageRan: r.ran?.page === !0,
|
|
87
|
+
segments: a.size > 0 ? a : T,
|
|
88
|
+
pathname: t
|
|
89
|
+
};
|
|
90
|
+
}, D = (e, t) => {
|
|
91
|
+
if (!t) return e;
|
|
92
|
+
let n = g(), r = { ...e ?? {} };
|
|
93
|
+
for (let [e, i] of Object.entries(t)) r[e] = c(n.get(i), i);
|
|
94
|
+
return r;
|
|
95
|
+
}, O = null, k = (e) => {
|
|
96
|
+
O = e;
|
|
97
|
+
}, A = () => O;
|
|
98
|
+
//#endregion
|
|
99
|
+
export { n as _, w as a, i as c, m as d, o as f, p as g, y as h, A as i, h as l, b as m, C as n, k as o, a as p, E as r, x as s, S as t, l as u };
|