@pylonsync/functions 0.22.47 → 0.22.58
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/index.d.ts +1 -1
- package/dist/offline-sw.d.ts +46 -0
- package/dist/runtime.d.ts +5 -1
- package/dist/ssr-barrel-imports.d.ts +16 -0
- package/dist/ssr-client-bundler.d.ts +50 -0
- package/dist/ssr-client-nav.d.ts +30 -0
- package/dist/ssr-offline.d.ts +93 -0
- package/dist/ssr-precompress.d.ts +16 -0
- package/dist/ssr-route-assets.d.ts +35 -0
- package/dist/ssr-runtime.d.ts +43 -6
- package/dist/types.d.ts +53 -0
- package/package.json +2 -2
- package/src/index.ts +3 -0
- package/src/offline-sw.test.ts +393 -0
- package/src/offline-sw.ts +343 -0
- package/src/runtime-files.test.ts +55 -0
- package/src/runtime.ts +17 -4
- package/src/ssr-barrel-imports.test.ts +70 -0
- package/src/ssr-barrel-imports.ts +104 -0
- package/src/ssr-bucket-serverdata.test.ts +167 -0
- package/src/ssr-client-bundler.test.ts +223 -0
- package/src/ssr-client-bundler.ts +424 -63
- package/src/ssr-client-nav-runtime.fixture.ts +273 -0
- package/src/ssr-client-nav-runtime.test.ts +79 -0
- package/src/ssr-client-nav.test.ts +78 -0
- package/src/ssr-client-nav.ts +70 -0
- package/src/ssr-offline-shell.test.ts +253 -0
- package/src/ssr-offline.ts +311 -0
- package/src/ssr-precompress.test.ts +49 -0
- package/src/ssr-precompress.ts +67 -0
- package/src/ssr-route-assets.ts +68 -0
- package/src/ssr-runtime.ts +159 -28
- package/src/ssr-serverdata-fn.test.ts +2 -2
- package/src/types.ts +50 -1
package/dist/index.d.ts
CHANGED
|
@@ -27,4 +27,4 @@ export type { WorkflowDefinition, WorkflowRun, WorkflowRunRequest, WorkflowRunne
|
|
|
27
27
|
export { resetDb, installTestIsolation } from "./testing";
|
|
28
28
|
export { slugifyName, availableSlug } from "./slugify";
|
|
29
29
|
export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
|
|
30
|
-
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, Agents, AgentRunOptions, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, Files, ActionFiles, Crdt, StoreFileOptions, StoredFile, } from "./types";
|
|
30
|
+
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, Agents, AgentRunOptions, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmMediaSource, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, Files, ActionFiles, Crdt, StoreFileOptions, StoredFile, FileInfo, ReadFile, } from "./types";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The build-specific values inlined into `/pylon-sw.js`. */
|
|
2
|
+
export interface ServiceWorkerConfig {
|
|
3
|
+
/** Hash of everything below. Cache names carry it. */
|
|
4
|
+
version: string;
|
|
5
|
+
/** The client build's public prefix (`/_pylon/build/`, or a CDN URL). */
|
|
6
|
+
buildPrefix: string;
|
|
7
|
+
/** URLs to keep at install: the offline routes' build files, the web app
|
|
8
|
+
* manifest, the icons. */
|
|
9
|
+
precache: string[];
|
|
10
|
+
/** Offline routes without dynamic segments: their HTML is kept at install. */
|
|
11
|
+
shells: string[];
|
|
12
|
+
/** Every offline route pattern (`/leads/[id]` included). */
|
|
13
|
+
routes: string[];
|
|
14
|
+
/** Path of the start URL; its shell answers any navigation with no
|
|
15
|
+
* better match. */
|
|
16
|
+
startPath: string;
|
|
17
|
+
/** How long a navigation to an offline route waits for the network before
|
|
18
|
+
* the worker answers with the shell. */
|
|
19
|
+
navigationTimeoutMs: number;
|
|
20
|
+
}
|
|
21
|
+
/** What the worker does with one request. */
|
|
22
|
+
export type RequestKind =
|
|
23
|
+
/** A page load: network first, shell when offline. */
|
|
24
|
+
"navigate"
|
|
25
|
+
/** A file of the client build: cache first. */
|
|
26
|
+
| "build"
|
|
27
|
+
/** The web app manifest or an icon: cache first. */
|
|
28
|
+
| "precached"
|
|
29
|
+
/** Not intercepted, never cached. */
|
|
30
|
+
| "network";
|
|
31
|
+
/**
|
|
32
|
+
* Classify one request. Pure; `self.location.origin` is passed in.
|
|
33
|
+
*/
|
|
34
|
+
export declare function classifyRequest(request: {
|
|
35
|
+
url: string;
|
|
36
|
+
method: string;
|
|
37
|
+
mode: string;
|
|
38
|
+
}, config: ServiceWorkerConfig, origin: string): RequestKind;
|
|
39
|
+
/**
|
|
40
|
+
* The worker. `sw` is the ServiceWorkerGlobalScope (`self`); passing it in
|
|
41
|
+
* keeps the body free of globals so tests can supply their own. `classify`
|
|
42
|
+
* is `classifyRequest`, passed in for the same reason.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pylonServiceWorker(sw: any, config: ServiceWorkerConfig, classify: typeof classifyRequest): void;
|
|
45
|
+
/** The `/pylon-sw.js` source for one build. */
|
|
46
|
+
export declare function renderServiceWorker(config: ServiceWorkerConfig): string;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
* each ctx.db / ctx.scheduler / ctx.runMutation call), so the map never
|
|
15
15
|
* needs to queue multiple RPCs per call_id.
|
|
16
16
|
*/
|
|
17
|
-
import type { DbReader, DbWriter, Llm, Rooms, Workflows, Audit, ApiKeys } from "./types";
|
|
17
|
+
import type { DbReader, DbWriter, Files, ActionFiles, Llm, Rooms, Workflows, Audit, ApiKeys } from "./types";
|
|
18
18
|
export declare function buildDbReader(callId: string, ssrRead?: boolean): DbReader;
|
|
19
|
+
/** `ctx.files` — signed download URLs, minted host-side, and what the host knows of a file. */
|
|
20
|
+
export declare function buildFiles(callId: string): Files;
|
|
21
|
+
/** `ctx.files` in an action: {@link buildFiles} plus store, read and delete. */
|
|
22
|
+
export declare function buildActionFiles(callId: string): ActionFiles;
|
|
19
23
|
export declare function buildSsrFnCaller(callId: string): (name: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
20
24
|
export declare function buildDbWriter(callId: string): DbWriter;
|
|
21
25
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The namespaces a barrel entry re-exports (exported name → module
|
|
3
|
+
* specifier), or null when `source` is not a pure namespace barrel: any
|
|
4
|
+
* statement other than `import * as X from "..."` and `export { ... }` of
|
|
5
|
+
* those names makes it null.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseNamespaceBarrel(source: string): Map<string, string> | null;
|
|
8
|
+
/**
|
|
9
|
+
* Rewrite `import { A, B as C } from "<barrel>"` in `source` to one
|
|
10
|
+
* namespace import per name, from `resolveTarget(spec)` (the specifier the
|
|
11
|
+
* importing file should use for the re-exported module). Type-only imports
|
|
12
|
+
* and names the barrel does not re-export stay on the barrel import.
|
|
13
|
+
* `barrels` maps a bare specifier to its parsed barrel. Returns the source
|
|
14
|
+
* unchanged when nothing applies.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rewriteBarrelImports(source: string, barrels: (specifier: string) => Map<string, string> | null, resolveTarget: (barrel: string, spec: string) => string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ManifestFonts } from "./ssr-fonts";
|
|
2
2
|
import { type ManifestBuildConfig } from "./build-compat";
|
|
3
|
+
import { type BundleOffline } from "./ssr-offline";
|
|
3
4
|
type Send = (msg: Record<string, unknown>) => void;
|
|
4
5
|
interface BundleClientMessage {
|
|
5
6
|
type: "bundle_client";
|
|
@@ -57,6 +58,43 @@ export declare function discoverLoadingModules(fs: any, path: any, cwd: string,
|
|
|
57
58
|
* server.
|
|
58
59
|
*/
|
|
59
60
|
export declare function generateLoadingRegistry(components: string[]): string;
|
|
61
|
+
/**
|
|
62
|
+
* The shared hydration dispatcher + router. ONE module, imported
|
|
63
|
+
* by every per-route entry. Bun's splitter sees N entries reach
|
|
64
|
+
* for it and pulls it (and React via the transitive imports) into
|
|
65
|
+
* a shared chunk.
|
|
66
|
+
*
|
|
67
|
+
* Two responsibilities:
|
|
68
|
+
* 1. `hydrate(component, Page, Layouts)` — called by each route
|
|
69
|
+
* entry. The first call (matching the SSR'd component) calls
|
|
70
|
+
* hydrateRoot; subsequent calls (after a client-side
|
|
71
|
+
* navigation) re-render the cached root with the new tree.
|
|
72
|
+
* Either way, the entry also registers itself in the route
|
|
73
|
+
* cache so future navigations don't need to refetch the chunk.
|
|
74
|
+
* 2. `navigate(href)` — fetches the new SSR HTML, parses out the
|
|
75
|
+
* `__PYLON_DATA__` payload, dynamically loads the new route's
|
|
76
|
+
* entry from the manifest, and re-renders into the existing
|
|
77
|
+
* React root. Layouts that match the previous render survive
|
|
78
|
+
* reconciliation (React keeps their state).
|
|
79
|
+
*
|
|
80
|
+
* Global click handler intercepts `a[data-pylon-link]` clicks for
|
|
81
|
+
* client-side nav. popstate handles back/forward. The runtime
|
|
82
|
+
* exposes `window.__pylon` for the `<Link>` component to call
|
|
83
|
+
* directly (prefetch, navigate).
|
|
84
|
+
*/
|
|
85
|
+
export declare const CLIENT_RUNTIME_SOURCE = "// Generated by Pylon SSR (Phase 2 client runtime).\n// DO NOT EDIT \u2014 overwritten on every pylon dev / build.\n\nimport { createElement, Fragment, Suspense } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { configureClient, registerOfflineWorker } from \"@pylonsync/react\";\nimport { createPylonBoundary, nearestBoundaryComponent } from \"./client-boundary\";\nimport { LOADING_MODULES } from \"./loading-registry\";\nimport { createNavPayloadCache } from \"./nav-cache\";\nimport { matchRoute, prefetchTargets } from \"./route-match\";\nimport { clientRouteFor, isOfflinePath } from \"./client-nav\";\nimport { withMetaSlot } from \"./page-tree\";\n\nconst routeCache = Object.create(null);\nlet activeRoot = null;\n// Component path of the route currently mounted (the manifest key). Its cached\n// layout chain is what a pending-navigation skeleton renders inside.\nlet currentComponent = null;\n// Destination of an in-flight client navigation. Read by hydrateRoot's\n// onUncaughtError so a re-render that throws mid-nav degrades to a full page\n// load instead of a white screen. Null when no nav is in flight.\nlet pendingNav = null;\nlet manifestPromise = null;\nconst prefetchedChunks = new Set();\n\nfunction buildTree(Page, Layouts, props, loadingFor) {\n let tree = createElement(Page, props);\n // The server wraps the page of a route with a loading.tsx in ONE Suspense\n // boundary, fallback = that loading module (ssr-runtime.ts, #278). The\n // tree that hydrates the server HTML must have the same boundary, or React\n // finds <Suspense> where it expects the page's first element, throws a\n // hydration mismatch (#418) and re-renders the whole document; clicks in\n // that window are lost. `loadingFor` is the page's component path.\n const Loading = loadingFor ? nearestLoading(loadingFor) : null;\n if (Loading) {\n tree = createElement(Suspense, { fallback: createElement(Loading, props) }, tree);\n }\n // The server renders the page's metadata in this slot; the browser leaves\n // it empty. Same shape on both sides, so useId values match (page-tree.ts).\n tree = withMetaSlot(createElement, Fragment, null, tree);\n for (let i = Layouts.length - 1; i >= 0; i--) {\n const Layout = Layouts[i];\n if (!Layout) continue;\n tree = createElement(Layout, props, tree);\n }\n return tree;\n}\n\n// The loading module the server wraps `component` in: the nearest\n// loading.tsx walking up from the page's directory.\nfunction nearestLoading(component) {\n const key = nearestBoundaryComponent(component, \"loading\", Object.keys(LOADING_MODULES));\n return key ? LOADING_MODULES[key] : null;\n}\n\n// ---------------------------------------------------------------------------\n// Client error / not-found boundary. notFound() (from @pylonsync/react) and\n// any other error thrown DURING a client render \u2014 the normal case when an\n// async lookup 404s after hydration \u2014 would otherwise propagate uncaught and\n// blank the whole document. React error boundaries must be class components;\n// this one catches the throw, resolves the nearest not-found.tsx / error.tsx\n// for the active route from the build manifest (the same nearest-ancestor\n// model the server's findBoundary uses), and renders it in its own layout\n// chain. It sits at the root and is transparent until something throws, so\n// hydration still matches the server HTML exactly. It resets on navigation\n// via a changing navEpoch prop (NOT a key \u2014 a key change would remount every\n// layout on each nav and lose their state).\n// ---------------------------------------------------------------------------\n\nlet navEpoch = 0;\n// The props the active page was rendered with (auth, serverData, params, \u2026).\n// The boundary reuses them so a boundary module AND its layout chain \u2014 e.g. an\n// auth-guarding dashboard layout that reads props.auth \u2014 render with the SAME\n// context the page had. The server renders boundaries with the page props too;\n// a minimal hand-built props object would make such a layout see no auth and\n// (for instance) redirect to /login instead of showing the not-found page.\nlet currentPageProps = {};\n\n// Seed data for the in-flight navigation \u2014 the object handed to <Link seed>,\n// exposed to the destination page via useRouteSeed() so it can paint its\n// Suspense fallback with real content before the SSR fetch lands. Set at the\n// start of every navigation (to the seed, or null), so useRouteSeed() reads a\n// value that's consistent for the whole nav. Null on hard load / seedless nav.\nlet currentSeed = null;\n\n// The not-found / error boundary lives in a real module (./client-boundary,\n// emitted next to this file by the bundler) so its render behavior is\n// unit-testable instead of buried in this string. Wire the runtime's\n// internals in as deps; the returned withBoundary(tree, component, navEpoch)\n// wraps every page tree as the transparent root error boundary.\nconst { withBoundary } = createPylonBoundary({\n loadManifest,\n loadRouteEntry,\n navigate,\n buildTree,\n getPageProps: () => currentPageProps,\n getResetHref: () => location.pathname + location.search,\n});\n\n// Deterministic stringify \u2014 MUST match stableStringify in ssr-runtime.ts so\n// a serverData call's cache key is identical on server and client.\nfunction stableStringify(v) {\n if (v === null || v === undefined || typeof v !== \"object\") {\n return JSON.stringify(v === undefined ? null : v);\n }\n if (Array.isArray(v)) return \"[\" + v.map(stableStringify).join(\",\") + \"]\";\n const keys = Object.keys(v).sort();\n return (\n \"{\" +\n keys.map((k) => JSON.stringify(k) + \":\" + stableStringify(v[k])).join(\",\") +\n \"}\"\n );\n}\n\nconst SERVER_DATA_METHODS = [\n \"get\",\n \"list\",\n \"lookup\",\n \"query\",\n \"queryGraph\",\n \"paginate\",\n \"search\",\n];\n\n// A React-recognized fulfilled thenable: use() reads .value synchronously\n// (status === \"fulfilled\") instead of suspending. Critical for hydration \u2014\n// the server streamed the post-Suspense content, so the client must render\n// that content on the FIRST pass without re-suspending (no fallback flash,\n// no mismatch).\nfunction fulfilledThenable(value) {\n return {\n status: \"fulfilled\",\n value,\n then(onFulfilled) {\n return onFulfilled ? onFulfilled(value) : value;\n },\n };\n}\n\n// Client stand-in for the server's serverData handle. Each method returns a\n// pre-fulfilled thenable (cached per key) sourced from the SSR'd ssrData map,\n// keyed identically to the server. Misses yield undefined \u2014 the page\n// rendered with whatever the server fetched, so a hit is expected.\nfunction makeClientServerData(ssrData) {\n const cache = ssrData || {};\n const pc = new Map();\n const wrap = (prefix) => {\n const out = {};\n for (const m of SERVER_DATA_METHODS) {\n out[m] = (...args) => {\n const key = prefix + m + \":\" + stableStringify(args);\n if (!pc.has(key)) pc.set(key, fulfilledThenable(cache[key]));\n return pc.get(key);\n };\n }\n return out;\n };\n const sd = wrap(\"\");\n sd.unsafe = wrap(\"u:\");\n // serverData.fn(name, args) \u2014 key MUST match the server's makeServerData\n // (\"fn:\" + name + \":\" + stableStringify(args ?? {})) so hydration replays\n // the query function's SSR'd result without a mismatch.\n sd.fn = (name, args) => {\n const key = \"fn:\" + name + \":\" + stableStringify(args ?? {});\n if (!pc.has(key)) pc.set(key, fulfilledThenable(cache[key]));\n return pc.get(key);\n };\n return sd;\n}\n\n// serverData stand-in used during the OPTIMISTIC first paint of a navigation:\n// every method returns a never-resolving thenable, so a page that use()s it\n// suspends and shows its Suspense fallback (which useRouteSeed() renders from\n// the clicked-link seed). The thenable is cached per key so React never sees an\n// \"uncached promise\" if it re-renders the optimistic tree. It never resolves on\n// its own \u2014 navigate() replaces the whole tree with real, fulfilled serverData\n// the moment the SSR fetch lands, which is what actually swaps fallback\u2192content.\nfunction makePendingServerData() {\n const pc = new Map();\n const pending = () => ({ then() {} });\n const wrap = (prefix) => {\n const out = {};\n for (const m of SERVER_DATA_METHODS) {\n out[m] = (...args) => {\n const key = prefix + m + \":\" + stableStringify(args);\n if (!pc.has(key)) pc.set(key, pending());\n return pc.get(key);\n };\n }\n return out;\n };\n const sd = wrap(\"\");\n sd.unsafe = wrap(\"u:\");\n sd.fn = (name, args) => {\n const key = \"fn:\" + name + \":\" + stableStringify(args ?? {});\n if (!pc.has(key)) pc.set(key, pending());\n return pc.get(key);\n };\n return sd;\n}\n\n// Server-only response controller has no meaning on the client (the status/\n// redirect/cookies already shipped). Give pages a no-op so a body that\n// touches props.response during hydration doesn't crash.\nfunction makeNoopResponse() {\n const noop = () => {};\n return {\n setStatus: noop,\n setHeader: noop,\n setCookie: noop,\n redirect: noop,\n notFound: noop,\n };\n}\n\n// Rehydrate the live, server-only props (serverData + response) that were\n// stripped before serialization, so the client tree matches the server's.\n// For a hydrated error boundary (#279), synthesize the reset() the server\n// rendered as a no-op: re-fetch + re-render the current URL (a transient\n// error clears to the page; a deterministic one re-shows the boundary).\n// The current route's dynamic params (e.g. { projectId: \"p_1\" }). Lives here,\n// not in the DOM __PYLON_DATA__ (which navigate() never rewrites), so useParams()\n// in a deep client child has a reactive source. A fresh object per nav \u2192 stable\n// reference between navs (useSyncExternalStore needs that).\nlet currentParams = {};\nfunction setNavParamsRaw(params) {\n currentParams = params || {};\n}\nfunction setNavParams(data) {\n setNavParamsRaw(data && data.props && data.props.params);\n}\n\nfunction withClientProps(data) {\n const props = { ...(data.props || {}) };\n props.serverData = makeClientServerData(data.ssrData);\n props.response = makeNoopResponse();\n if (data.kind === \"error\") {\n props.reset = function () {\n navigate(location.pathname + location.search, { replace: true });\n };\n }\n return props;\n}\n\nfunction readPylonData() {\n const dataEl = document.getElementById(\"__PYLON_DATA__\");\n if (!dataEl) return null;\n try {\n return JSON.parse(dataEl.textContent || \"{}\");\n } catch {\n return null;\n }\n}\n\nasync function loadManifest() {\n if (!manifestPromise) {\n manifestPromise = fetch(\"/_pylon/build/manifest.json\", {\n credentials: \"same-origin\",\n })\n .then((r) => (r.ok ? r.json() : null))\n .catch(() => null);\n }\n return manifestPromise;\n}\n\nfunction preloadChunks(routeInfo) {\n if (!routeInfo) return;\n const prefix = routeInfo.public_prefix || \"/_pylon/build/\";\n const all = [routeInfo.file, ...(routeInfo.imports || [])];\n for (const c of all) {\n // prefetch() passes file: \"\" (HTML-only prefetch has no route entry);\n // an empty name would modulepreload the bare prefix (\"/_pylon/build/\")\n // and the host answers that with a 400 on every page load.\n if (!c) continue;\n if (prefetchedChunks.has(c)) continue;\n prefetchedChunks.add(c);\n const link = document.createElement(\"link\");\n link.rel = \"modulepreload\";\n link.href = prefix + c;\n document.head.appendChild(link);\n }\n}\n\n// Run fn once the page itself has finished loading. modulepreload fetches at\n// high priority, and a sidebar of links warms on first paint (Link's\n// IntersectionObserver), so warming a dozen route chunks inline would compete\n// with the current page's own scripts and data \u2014 buying a faster second\n// navigation with a slower first render.\nfunction whenLoaded(fn) {\n if (document.readyState === \"complete\") {\n fn();\n return;\n }\n window.addEventListener(\"load\", fn, { once: true });\n}\n\n// Run fn when the browser is idle after the page loaded, so warming the next\n// route never competes with this one.\nfunction whenIdle(fn) {\n whenLoaded(() => {\n if (typeof window.requestIdleCallback === \"function\") {\n window.requestIdleCallback(fn, { timeout: 2000 });\n } else {\n setTimeout(fn, 200);\n }\n });\n}\n\n// Import a client-navigable route's entry ahead of the click, so navigate()\n// finds it in routeCache and renders in the same frame. Only after the first\n// hydration: an entry that loads before it would be taken for the page.\nfunction warmRouteEntry(component) {\n if (!activeRoot || routeCache[component]) return;\n loadRouteEntry(component).catch(() => {});\n}\n\n// Marks a request as a client-side NAVIGATION rather than a document load.\n// The server answers with the head metadata + __PYLON_DATA__ and skips the\n// page markup, which this runtime re-renders from that data anyway. The host\n// keys its render cache on the same header, so the two shapes never share an\n// entry. Prefetch and navigate MUST send identical headers, or the prefetched\n// response is the wrong shape for the click that consumes it.\nconst NAV_REQUEST_HEADERS = { Accept: \"text/html\", \"X-Pylon-Nav\": \"1\" };\n\n// Payloads warmed by a hover, consumed by the click that follows. See\n// ./nav-cache for why this is in memory rather than an HTTP-level prefetch.\nconst navPayloads = createNavPayloadCache({\n now: () => Date.now(),\n fetchPage: (target) =>\n fetch(target, {\n credentials: \"same-origin\",\n headers: NAV_REQUEST_HEADERS,\n }).then((res) => {\n // A redirect means the URL navigate() would commit isn't the one that\n // answered; let the real navigation resolve that itself.\n if (!res.ok || res.redirected) return null;\n return res.text();\n }),\n});\n\nasync function prefetch(href, opts) {\n const url = new URL(href, location.href);\n if (url.origin !== location.origin) return;\n const manifest = await loadManifest();\n // A route that renders from the client never fetches its page, so there is\n // no payload to warm. Its entry is warmed instead, below.\n const clientRoute = clientRouteFor(manifest, url.pathname, matchRoute);\n // The page payload, but ONLY on a real intent signal (hover / touch). It\n // costs a full SSR render, so warming a screenful of links on sight would\n // spend a dozen renders per page load to save one.\n if (opts && opts.document && !clientRoute) {\n navPayloads.prefetch(url.pathname + url.search);\n }\n if (!manifest) return;\n // Chunk prefetch. The page payload above is the half that was already\n // cheap: nothing can render until the destination's own entry chunk lands,\n // so warming only the HTML leaves a full round-trip on the click.\n const targets = prefetchTargets(manifest, url.pathname);\n const warm = () => {\n preloadChunks({\n public_prefix: manifest.public_prefix,\n file: targets.file,\n imports: targets.imports,\n });\n if (clientRoute) warmRouteEntry(clientRoute.component);\n };\n // Intent (hover, touch, pointerdown, focus): the tap is coming, fetch now.\n // Otherwise (a link on screen) wait until the page has painted and the\n // browser is idle, so warming routes never counts against this page's\n // first paint, and skip it on a connection the user asked to save.\n if (opts && opts.document) warm();\n else if (!saveData()) whenIdle(warm);\n}\n\n// The user asked for less data (Save-Data), or the connection is 2G.\n// Prefetch on sight costs bytes the user may never use.\nfunction saveData() {\n const c = typeof navigator !== \"undefined\" ? navigator.connection : undefined;\n if (!c) return false;\n return c.saveData === true || /(^|-)2g$/.test(c.effectiveType || \"\");\n}\n\nasync function loadRouteEntry(component) {\n if (routeCache[component]) return routeCache[component];\n const manifest = await loadManifest();\n if (!manifest) throw new Error(\"manifest unavailable\");\n const routeInfo = manifest.routes[component];\n if (!routeInfo) throw new Error(\"unknown component \" + component);\n const prefix = manifest.public_prefix || \"/_pylon/build/\";\n preloadChunks({ ...routeInfo, public_prefix: prefix });\n // Dynamic import the entry. It will call hydrate(...) which\n // populates routeCache[component], then this returns.\n await import(/* @vite-ignore */ prefix + routeInfo.file);\n if (!routeCache[component]) {\n throw new Error(\"route entry did not register: \" + component);\n }\n return routeCache[component];\n}\n\nexport function hydrate(component, Page, Layouts, metadata) {\n // Always cache the route for nav. `metadata` is the page's static\n // `metadata` export: a navigation with no page fetch takes its title.\n routeCache[component] = { Page, Layouts, metadata };\n // Start the manifest fetch NOW rather than on first use. Nothing can resolve\n // a route without it \u2014 the first <Link> reaching the viewport warms its\n // chunks only after it lands \u2014 so leaving it lazy put a round trip in front\n // of all prefetching. In flight from here, it resolves during hydration.\n void loadManifest();\n const data = readPylonData();\n // Namespace client storage by the app's manifest name (injected into\n // __PYLON_DATA__ as `app`) BEFORE any db hook / auth helper reads a token.\n // Without this every app on a shared origin (all localhost:4321 in dev)\n // shares the default `pylon_token` and clobbers each other's sessions.\n // configureClient runs once here; it also migrates a legacy default-keyspace\n // token so an existing session survives the switch.\n if (data && data.app) configureClient({ appName: data.app });\n // First hydrate: the entry's component MATCHES the SSR'd page.\n // Establish the root + install the click + popstate handlers\n // exactly once.\n if (!activeRoot) {\n if (!data || data.component !== component) {\n console.warn(\n \"[pylon ssr] entry/__PYLON_DATA__ mismatch \u2014 initial hydration skipped\",\n );\n return;\n }\n setNavParams(data);\n currentPageProps = withClientProps(data);\n currentComponent = data.component;\n const tree = withBoundary(\n buildTree(Page, Layouts, currentPageProps, data.component),\n data.component,\n navEpoch,\n );\n activeRoot = hydrateRoot(document, tree, {\n // Safety net for client navigation. If a nav re-render throws an uncaught\n // error in React's commit phase, the URL has already changed but the page\n // can't swap \u2014 a white/stale screen. The classic trigger is a page that\n // renders hoisted <title>/<meta>/<link> in its own tree (use the\n // `metadata` export instead); React 19 owns those head nodes on the client\n // and reconciling them across routes can throw. Rather than strand the\n // user, fall back to a full page load of the pending destination, which\n // re-renders it cleanly from SSR. Non-navigation errors keep React's\n // default reporting.\n onUncaughtError(error) {\n if (pendingNav) {\n const dest = pendingNav;\n pendingNav = null;\n console.error(\n \"[pylon ssr] client navigation failed to render; falling back to a full page load:\",\n error,\n );\n window.location.href = dest;\n return;\n }\n console.error(error);\n },\n });\n installNavHandlers();\n // Registers the offline service worker for an app that opted in, and\n // removes one left on this origin by an earlier build otherwise.\n // `build` is the build this page runs, for useAppUpdate. An option\n // rather than its own import, so an app on an older @pylonsync/react\n // still builds.\n registerOfflineWorker({\n optedIn: !!data.offline,\n production: !(data.offline && data.offline.dev),\n signedIn: !!(data.props && data.props.session && data.props.session.exists),\n build:\n data.offline && data.offline.version\n ? { version: data.offline.version, builtAt: data.offline.builtAt || null }\n : undefined,\n });\n if (data.offline) {\n // The service worker answered this load with the page it kept for\n // another URL: a dynamic offline route, or no network. The page on\n // screen is that URL's; render the address bar's URL from the replica.\n const shellPath = data.props && data.props.pathname;\n if (typeof shellPath === \"string\" && shellPath !== location.pathname) {\n navigate(location.pathname + location.search + location.hash, {\n replace: true,\n shellPath,\n });\n }\n }\n return;\n }\n // Subsequent hydrate calls fire from dynamic-loaded entries\n // during navigation. The render is driven by navigate(), so we\n // only need to populate the cache (already done above).\n}\n\n// Swap the page's SEO/social <head> tags on a client-side navigation.\n// The SSR runtime marks every page-metadata <meta>/<link> with\n// data-pylon-meta; we drop the current set and import the incoming page's\n// set, so description / canonical / og:* / twitter:* / icons track the new\n// route. The layout's charset/viewport and Pylon's injected stylesheet\n// links carry no marker, so they survive untouched (no FOUC). The page\n// component never renders these tags on the client, so React doesn't own\n// them \u2014 this manual swap can't fight hydration.\nfunction syncHeadMeta(doc) {\n const head = document.head;\n if (!head) return;\n const current = head.querySelectorAll(\"[data-pylon-meta]\");\n for (let i = 0; i < current.length; i++) current[i].remove();\n const incoming = doc.head\n ? doc.head.querySelectorAll(\"[data-pylon-meta]\")\n : [];\n for (let i = 0; i < incoming.length; i++) {\n head.appendChild(document.importNode(incoming[i], true));\n }\n}\n\n// How long a navigation may run before the destination's loading.tsx takes\n// over the page area. Below this, the transition finishes on its own and\n// swapping to a skeleton would read as a flicker rather than as feedback.\nconst PENDING_PLACEHOLDER_MS = 100;\n\n// Paint the destination's nearest loading.tsx in place of the current page,\n// so a click that can't complete immediately is still acknowledged. Returns\n// whether anything was rendered \u2014 apps without a loading.tsx keep the old\n// behavior (previous page stays put until the new one is ready).\n//\n// Rendered inside the CURRENTLY MOUNTED layout chain, not the destination's:\n// the destination's layouts live in the chunk this skeleton exists to cover,\n// so waiting for them would defeat the point. Sibling routes (the common case\n// for a nav that's slow enough to reach here) share that chain anyway, and\n// React keeps those layouts mounted across the swap \u2014 only the page area\n// changes.\n//\n// Props are the current page's, verbatim. Layouts re-render with the data they\n// already had, so one that read serverData or params can't suspend on a\n// never-resolving handle or crash on a cache miss halfway through a\n// transition. A skeleton has no data of its own to show.\nfunction renderPendingPlaceholder(pathname, manifest, myEpoch) {\n if (!activeRoot || !currentComponent) return false;\n const matched = matchRoute(manifest, pathname);\n if (!matched) return false;\n const key = nearestBoundaryComponent(\n matched.component,\n \"loading\",\n Object.keys(LOADING_MODULES),\n );\n const Loading = key ? LOADING_MODULES[key] : null;\n if (!Loading) return false;\n const current = routeCache[currentComponent];\n const tree = withBoundary(\n buildTree(Loading, (current && current.Layouts) || [], currentPageProps),\n currentComponent,\n myEpoch,\n );\n activeRoot.render(tree);\n return true;\n}\n\n// The path and query the page last rendered. A popstate that keeps them and\n// only changes the hash is a same-page jump (an anchor link, a back press\n// between anchors), not a navigation: re-rendering for it would reset the\n// scroll to the top.\nlet renderedPath = \"\";\n\nasync function navigate(href, opts) {\n const push = !opts || opts.push !== false;\n const url = new URL(href, location.href);\n if (url.origin !== location.origin) {\n window.location.href = href;\n return;\n }\n const target = url.pathname + url.search;\n\n // Claim an epoch for this navigation. A later navigate() bumps navEpoch, so\n // every await below can bail once it's been superseded by a newer nav \u2014\n // otherwise a slow fetch could render a stale destination over a newer one.\n const myEpoch = ++navEpoch;\n currentSeed = opts && opts.seed != null ? opts.seed : null;\n\n // Flipped the moment this nav commits or gives up, so a pending-placeholder\n // timer that fires late can never paint a skeleton over real content.\n const navState = { settled: false };\n\n // ---- Instant navigation --------------------------------------------------\n // A route whose page exports `navigation = \"client\"`, or whose URL is an\n // offline route, renders from the client with no page fetch: its data comes\n // from the local replica. The manifest is in hand after hydration, and a\n // Link warms the entry ahead of the click, so this paints in the same frame.\n if (activeRoot) {\n const manifest = await loadManifest();\n if (myEpoch !== navEpoch) return;\n const matched = clientRouteFor(manifest, url.pathname, matchRoute);\n if (matched) {\n let route = null;\n try {\n route = await loadRouteEntry(matched.component);\n } catch (e) {\n // Fall through to the fetch below, which reloads the page when the\n // entry cannot load.\n console.warn(\"[pylon ssr] client navigation fell back to a page fetch:\", e);\n }\n if (myEpoch !== navEpoch) return;\n if (route) {\n renderFromClient(url, matched, route, myEpoch, opts, push);\n return;\n }\n }\n }\n const fullLoad = () => {\n navState.settled = true;\n window.location.href = href;\n };\n\n // ---- Optimistic first paint --------------------------------------------\n // With a seed AND a client-resolvable route, render the destination NOW \u2014\n // before the SSR fetch \u2014 with a pending serverData, so the page shows its\n // Suspense fallback painted from the seed (via useRouteSeed). The real render\n // below swaps in full data in place. Any failure (no route match, chunk load\n // error) falls through to the normal blocking fetch-then-render path, so a\n // seed can never break navigation. Requires the page to keep its serverData\n // reads inside a <Suspense> (the store detail page does) \u2014 a bare top-level\n // use() would suspend the whole page with no seeded fallback to show.\n let didOptimistic = false;\n if (currentSeed != null && activeRoot) {\n try {\n const manifest = await loadManifest();\n if (myEpoch !== navEpoch) return;\n const matched = matchRoute(manifest, url.pathname);\n if (matched) {\n const route = await loadRouteEntry(matched.component);\n if (myEpoch !== navEpoch) return;\n setNavParamsRaw(matched.params);\n currentPageProps = {\n params: matched.params,\n serverData: makePendingServerData(),\n response: makeNoopResponse(),\n };\n const tree = withBoundary(\n buildTree(route.Page, route.Layouts, currentPageProps),\n matched.component,\n myEpoch,\n );\n pendingNav = target;\n currentComponent = matched.component;\n activeRoot.render(tree);\n if (opts && opts.replace) {\n history.replaceState({ component: matched.component }, \"\", target);\n } else if (push) {\n history.pushState({ component: matched.component }, \"\", target);\n }\n window.dispatchEvent(new Event(\"pylon:navigation\"));\n renderedPath = location.pathname + location.search;\n window.scrollTo(0, 0);\n didOptimistic = true;\n }\n } catch (e) {\n // Fall through to the normal fetch-then-render path below.\n }\n }\n\n // ---- Pending-state placeholder -----------------------------------------\n // Nothing below paints until the SSR payload AND the destination's chunk are\n // both in hand, which on a cold route is long enough for the click to look\n // ignored. Hand the page area to the destination's loading.tsx once the nav\n // outlives the flicker threshold. Skipped when a seed already painted the\n // real destination \u2014 that's strictly better than a skeleton.\n let placeholderTimer = null;\n if (!didOptimistic && activeRoot) {\n placeholderTimer = setTimeout(() => {\n placeholderTimer = null;\n // loadManifest resolves instantly once warm (Link prefetch primes it on\n // first paint); the settled check covers the cold first navigation,\n // where the fetch can win the race with it.\n loadManifest().then((manifest) => {\n if (navState.settled || myEpoch !== navEpoch) return;\n if (renderPendingPlaceholder(url.pathname, manifest, myEpoch)) {\n renderedPath = location.pathname + location.search;\n window.scrollTo(0, 0);\n }\n });\n }, PENDING_PLACEHOLDER_MS);\n }\n\n // ---- Real fetch + render -----------------------------------------------\n let html;\n try {\n // A hover-prefetch usually has this in hand, or in flight \u2014 either way the\n // click joins it instead of opening a second request for the same page.\n const prefetched = navPayloads.take(target);\n html = prefetched ? await prefetched : null;\n if (html == null) {\n const res = await fetch(target, {\n credentials: \"same-origin\",\n headers: NAV_REQUEST_HEADERS,\n });\n if (!res.ok) {\n fullLoad();\n return;\n }\n html = await res.text();\n }\n } catch {\n // No network. An offline route renders from the replica; anything else\n // needs the server.\n if (myEpoch !== navEpoch) return;\n if (await renderOffline(url, myEpoch, opts, push)) {\n navState.settled = true;\n if (placeholderTimer) clearTimeout(placeholderTimer);\n return;\n }\n if (opts && opts.shellPath) {\n // This load already came from a kept page and the URL cannot open\n // offline. Reloading would get the same kept page again; show its URL.\n navState.settled = true;\n if (placeholderTimer) clearTimeout(placeholderTimer);\n history.replaceState({ component: currentComponent }, \"\", opts.shellPath);\n renderedPath = location.pathname + location.search;\n window.dispatchEvent(new Event(\"pylon:navigation\"));\n return;\n }\n fullLoad();\n return;\n }\n if (myEpoch !== navEpoch) return;\n const doc = new DOMParser().parseFromString(html, \"text/html\");\n const dataEl = doc.getElementById(\"__PYLON_DATA__\");\n if (!dataEl) {\n fullLoad();\n return;\n }\n let data;\n try {\n data = JSON.parse(dataEl.textContent || \"{}\");\n } catch {\n fullLoad();\n return;\n }\n let route;\n try {\n route = await loadRouteEntry(data.component);\n } catch (e) {\n console.warn(\"[pylon ssr] nav fallback (entry load failed):\", e);\n fullLoad();\n return;\n }\n if (myEpoch !== navEpoch) return;\n navState.settled = true;\n if (placeholderTimer) clearTimeout(placeholderTimer);\n document.title = doc.title || document.title;\n syncHeadMeta(doc);\n setNavParams(data);\n // Keep currentSeed set for the rest of this nav: useRouteData renders the seed\n // as CONTENT (not a Suspense fallback) and upgrades it to real data in place,\n // so the seed must stay readable across the optimistic\u2192real render. It's reset\n // at the START of the next navigation. (Clearing it here would degrade the\n // page to its skeleton for a frame during the swap \u2014 a visible flash.)\n currentPageProps = withClientProps(data);\n // Reuse myEpoch (do NOT bump) so the optimistic tree and the real tree share\n // a boundary identity: React resolves the Suspense fallback into the real\n // content in place instead of remounting the whole subtree (no flash).\n const tree = withBoundary(\n buildTree(route.Page, route.Layouts, currentPageProps),\n data.component,\n myEpoch,\n );\n // Track the in-flight destination so hydrateRoot's onUncaughtError can fall\n // back to a full page load if this re-render throws in React's commit phase\n // (instead of leaving the URL changed but the page unswapped). Cleared on the\n // next macrotask once the commit has settled with no error.\n pendingNav = target;\n currentComponent = data.component;\n // Drop every other prefetched payload once a navigation commits: they were\n // rendered against the previous page's state, and whatever the user does\n // here can invalidate them. Hovering on the new page re-warms in one request.\n navPayloads.clear();\n activeRoot.render(tree);\n setTimeout(() => {\n if (pendingNav === target) pendingNav = null;\n }, 0);\n if (didOptimistic) {\n // URL + scroll already handled during the optimistic paint; only keep the\n // history entry's component in sync with the authoritative SSR component.\n history.replaceState({ component: data.component }, \"\", target);\n } else {\n if (opts && opts.replace) {\n history.replaceState({ component: data.component }, \"\", target);\n } else if (push) {\n history.pushState({ component: data.component }, \"\", target);\n }\n // Notify the router hooks (useSearchParams / usePathname) so deep children\n // re-read location after a Link click or a router.push(). popstate already\n // covers back/forward, but pushState/replaceState fire no event.\n window.dispatchEvent(new Event(\"pylon:navigation\"));\n renderedPath = location.pathname + location.search;\n // After a successful nav, scroll to top (Next.js default).\n window.scrollTo(0, 0);\n }\n}\n\n// Render an offline route with no server, after the page fetch failed: the\n// page reads its data from the local replica (db.useQuery), so it needs only\n// its code, its params and the identity-free props the last page had. Returns\n// false when the URL is not an offline route, or its code is not on the\n// device.\nasync function renderOffline(url, myEpoch, opts, push) {\n const manifest = await loadManifest();\n if (!manifest || !activeRoot) return false;\n if (!isOfflinePath(manifest, url.pathname, matchRoute)) return false;\n const matched = matchRoute(manifest, url.pathname);\n if (!matched) return false;\n let route;\n try {\n route = await loadRouteEntry(matched.component);\n } catch (e) {\n return false;\n }\n if (myEpoch !== navEpoch) return true;\n renderFromClient(url, matched, route, myEpoch, opts, push);\n return true;\n}\n\n// Commit a navigation rendered from the client (instant navigation, or an\n// offline route with no network). History, the navigation event and the\n// scroll reset are the same as a fetched navigation's.\nfunction renderFromClient(url, matched, route, myEpoch, opts, push) {\n const prev = currentPageProps || {};\n const searchParams = {};\n url.searchParams.forEach((v, k) => {\n searchParams[k] = v;\n });\n const data = {\n component: matched.component,\n props: {\n pathname: url.pathname,\n url: url.pathname,\n host: prev.host || \"\",\n params: matched.params,\n searchParams,\n auth: prev.auth,\n session: prev.session,\n headers: {},\n cookies: {},\n },\n ssrData: {},\n };\n setNavParams(data);\n currentPageProps = withClientProps(data);\n const tree = withBoundary(\n buildTree(route.Page, route.Layouts, currentPageProps),\n data.component,\n myEpoch,\n );\n const target = url.pathname + url.search + url.hash;\n // See the fetched path: a commit that throws falls back to a page load.\n pendingNav = target;\n currentComponent = data.component;\n navPayloads.clear();\n const title = route.metadata && route.metadata.title;\n if (typeof title === \"string\" && title) document.title = title;\n activeRoot.render(tree);\n setTimeout(() => {\n if (pendingNav === target) pendingNav = null;\n }, 0);\n if (opts && opts.replace) {\n history.replaceState({ component: data.component }, \"\", target);\n } else if (push) {\n history.pushState({ component: data.component }, \"\", target);\n }\n window.dispatchEvent(new Event(\"pylon:navigation\"));\n renderedPath = location.pathname + location.search;\n window.scrollTo(0, 0);\n}\n\nfunction installNavHandlers() {\n document.addEventListener(\"click\", (e) => {\n if (e.defaultPrevented) return;\n if (e.button !== 0) return;\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n const target = e.target;\n if (!target || !target.closest) return;\n const link = target.closest(\"a[data-pylon-link]\");\n if (!link) return;\n const href = link.getAttribute(\"href\");\n if (!href) return;\n if (link.target && link.target !== \"\" && link.target !== \"_self\") return;\n if (href.startsWith(\"http://\") || href.startsWith(\"https://\") || href.startsWith(\"//\")) {\n // External \u2014 let the browser handle.\n return;\n }\n e.preventDefault();\n // A <Link seed> registers its seed in window.__pylonLinkSeeds keyed by the\n // anchor element; pass it through so navigate() can paint optimistically.\n const seeds =\n typeof window !== \"undefined\" ? window.__pylonLinkSeeds : undefined;\n const seed = seeds ? seeds.get(link) : undefined;\n navigate(href, seed != null ? { seed } : undefined);\n });\n renderedPath = location.pathname + location.search;\n window.addEventListener(\"popstate\", () => {\n const next = location.pathname + location.search;\n if (next === renderedPath) return;\n navigate(next, { push: false });\n });\n // Progressive-enhancement form submit (#276): intercept <form data-pylon-form>\n // so the page doesn't full-reload. fetch the same route.ts endpoint, follow\n // the handler's redirect, and swap the destination in via navigate(). Without\n // JS this listener never runs and the browser submits natively (POST \u2192\n // handler \u2192 303 \u2192 GET) \u2014 identical result, just a full navigation.\n document.addEventListener(\"submit\", (e) => {\n if (e.defaultPrevented) return;\n const form = e.target;\n if (!form || form.tagName !== \"FORM\") return;\n if (!form.hasAttribute(\"data-pylon-form\")) return;\n const action = form.getAttribute(\"action\");\n if (!action) return;\n // Off-origin actions / new-tab targets \u2192 let the browser submit.\n if (action.startsWith(\"http://\") || action.startsWith(\"https://\") || action.startsWith(\"//\")) return;\n const tgt = form.getAttribute(\"target\");\n if (tgt && tgt !== \"\" && tgt !== \"_self\") return;\n const method = (form.getAttribute(\"method\") || \"post\").toUpperCase();\n e.preventDefault();\n // urlencoded body (matches the server's parser; files use the native path).\n const body = new URLSearchParams();\n const fd = new FormData(form);\n fd.forEach((v, k) => {\n if (typeof v === \"string\") body.append(k, v);\n });\n fetch(action, {\n method,\n body,\n credentials: \"same-origin\",\n headers: { Accept: \"text/html\" },\n })\n .then((res) => {\n // fetch followed the handler's 303 \u2192 res.url is the destination page.\n // Drive a client navigation to it (re-renders without a full reload).\n const dest = new URL(res.url || action, location.href);\n if (dest.origin === location.origin) {\n navigate(dest.pathname + dest.search);\n } else {\n window.location.href = dest.href;\n }\n })\n .catch(() => {\n // Network/abort \u2014 fall back to a native submit so the user isn't stuck.\n form.removeAttribute(\"data-pylon-form\");\n form.submit();\n });\n });\n}\n\n// Expose for <Link> component prefetch.\nconst pylonGlobal = {\n prefetch,\n navigate,\n // Read by useParams(); a getter so it always reflects the latest nav.\n get params() {\n return currentParams;\n },\n // Read by useRouteSeed(); the seed the active navigation was started with\n // (from <Link seed>), or null. A getter so it always reflects the latest nav.\n get seed() {\n return currentSeed;\n },\n};\nif (typeof window !== \"undefined\") {\n window.__pylon = pylonGlobal;\n}\n";
|
|
86
|
+
/**
|
|
87
|
+
* Write the client runtime (`client-runtime.ts`) and the sibling modules it
|
|
88
|
+
* imports into `stageDir`. Each sibling's source of truth is this package's
|
|
89
|
+
* real, unit-tested module (ssr-client-boundary.ts and so on), not an inline
|
|
90
|
+
* string. The app-specific `loading-registry.ts` and route entries are
|
|
91
|
+
* written by the caller.
|
|
92
|
+
*/
|
|
93
|
+
export declare function writeClientRuntimeModules(fs: any, path: any, stageDir: string): Promise<void>;
|
|
94
|
+
/** Whether a page exports a static `metadata` object. The route entry passes
|
|
95
|
+
* it to the runtime, which takes the title from it on a navigation with no
|
|
96
|
+
* page fetch. A static scan, so a page without one imports nothing extra. */
|
|
97
|
+
export declare function exportsStaticMetadata(source: string): boolean;
|
|
60
98
|
/**
|
|
61
99
|
* Manifest schema. One entry per route, indexed by the same
|
|
62
100
|
* project-relative component path the SSR side passes through.
|
|
@@ -90,12 +128,20 @@ export interface PylonBundleManifest {
|
|
|
90
128
|
/** URL pattern (e.g. `/p/[slug]`) — page routes only. Lets the client
|
|
91
129
|
* matcher resolve an href to this route for optimistic navigation. */
|
|
92
130
|
path?: string;
|
|
131
|
+
/** `"client"` when the page exports \`navigation = "client"\`: the
|
|
132
|
+
* runtime renders it with no page fetch (ssr-client-nav.ts). */
|
|
133
|
+
navigation?: "client";
|
|
93
134
|
}>;
|
|
94
135
|
/** Self-hosted fonts (next/font parity): structured `@font-face`s + the
|
|
95
136
|
* `:root` CSS variables + the woff2 files to preload. Global (route-
|
|
96
137
|
* independent); rendered into every SSR `<head>` against `public_prefix`.
|
|
97
138
|
* Absent when the app declares no `font({...})`. */
|
|
98
139
|
fonts?: ManifestFonts;
|
|
140
|
+
/** `buildManifest({ offline })`: the install tags for every page's head,
|
|
141
|
+
* the routes the client may render with no network, and the service
|
|
142
|
+
* worker version. The worker is `pylon-sw.js` in the outdir. Absent when
|
|
143
|
+
* the app did not opt in. */
|
|
144
|
+
offline?: BundleOffline;
|
|
99
145
|
/** Set when the Tailwind compile failed for this build: the pages are
|
|
100
146
|
* serving WITHOUT styles. Dev surfaces it as an on-page banner (see
|
|
101
147
|
* pylonDevHud) — a silent unstyled page reads as a CSS bug, not a
|
|
@@ -133,6 +179,10 @@ export interface ClientBuildOptions {
|
|
|
133
179
|
/** The app manifest (fonts, build settings). Default
|
|
134
180
|
* `<cwd>/pylon.manifest.json`. */
|
|
135
181
|
manifestPath?: string;
|
|
182
|
+
/** Write brotli and gzip copies of the build's text files for the
|
|
183
|
+
* runtime to send. Default: on, except in dev (`NODE_ENV=development`),
|
|
184
|
+
* where every edit rebuilds and the browser is local. */
|
|
185
|
+
precompress?: boolean;
|
|
136
186
|
}
|
|
137
187
|
/** The `build` block of `<cwd>/pylon.manifest.json`, if any. */
|
|
138
188
|
export declare function readManifestBuildConfig(fs: any, path: any, cwd: string): ManifestBuildConfig | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A route match, as `matchRoute` in ssr-route-match.ts returns it. */
|
|
2
|
+
export interface ClientRouteMatch {
|
|
3
|
+
component: string;
|
|
4
|
+
params: Record<string, string | string[]>;
|
|
5
|
+
}
|
|
6
|
+
/** The bundle manifest fields this module reads. */
|
|
7
|
+
export interface ClientNavManifest {
|
|
8
|
+
routes: Record<string, {
|
|
9
|
+
path?: string;
|
|
10
|
+
navigation?: string;
|
|
11
|
+
}>;
|
|
12
|
+
offline?: {
|
|
13
|
+
routes?: string[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
type MatchRoute = (manifest: any, pathname: string) => ClientRouteMatch | null;
|
|
17
|
+
/**
|
|
18
|
+
* Whether page source declares `export const navigation = "client"` (single,
|
|
19
|
+
* double or backtick quotes, with an optional type annotation or `as const`).
|
|
20
|
+
* A static scan: the bundler reads the page file without running it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function declaresClientNavigation(source: string): boolean;
|
|
23
|
+
/** Whether `pathname` is one of the app's offline routes. */
|
|
24
|
+
export declare function isOfflinePath(manifest: ClientNavManifest | null, pathname: string, matchRoute: MatchRoute): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The page to render from the client for `pathname`, or null when the
|
|
27
|
+
* navigation must fetch the page (no match, or the page did not opt in).
|
|
28
|
+
*/
|
|
29
|
+
export declare function clientRouteFor(manifest: ClientNavManifest | null, pathname: string, matchRoute: MatchRoute): ClientRouteMatch | null;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ServiceWorkerConfig } from "./offline-sw";
|
|
2
|
+
import { type RouteAssetManifest } from "./ssr-route-assets";
|
|
3
|
+
/** `buildManifest({ offline })` as the SDK writes it to pylon.manifest.json. */
|
|
4
|
+
export interface ManifestOfflineInput {
|
|
5
|
+
name: string;
|
|
6
|
+
shortName?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
icons?: Array<{
|
|
9
|
+
src: string;
|
|
10
|
+
sizes: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
purpose?: string;
|
|
13
|
+
}>;
|
|
14
|
+
themeColor?: string;
|
|
15
|
+
backgroundColor?: string;
|
|
16
|
+
startUrl: string;
|
|
17
|
+
display: string;
|
|
18
|
+
routes: string[];
|
|
19
|
+
}
|
|
20
|
+
/** What the bundle manifest keeps for the render and the client runtime. */
|
|
21
|
+
export interface BundleOffline {
|
|
22
|
+
name: string;
|
|
23
|
+
shortName?: string;
|
|
24
|
+
themeColor?: string;
|
|
25
|
+
icons?: Array<{
|
|
26
|
+
src: string;
|
|
27
|
+
sizes: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
purpose?: string;
|
|
30
|
+
}>;
|
|
31
|
+
startUrl: string;
|
|
32
|
+
routes: string[];
|
|
33
|
+
/** The service worker's version (its cache names carry it). */
|
|
34
|
+
version: string;
|
|
35
|
+
/** When this build was made (ISO 8601). Not part of `version`: two
|
|
36
|
+
* machines that build the same app get the same version. */
|
|
37
|
+
builtAt: string;
|
|
38
|
+
}
|
|
39
|
+
/** `pylon-version.json` in the client build, served at `/_pylon/version`.
|
|
40
|
+
* `useAppUpdate` in @pylonsync/react polls it. */
|
|
41
|
+
export interface BuildVersionFile {
|
|
42
|
+
version: string;
|
|
43
|
+
builtAt: string;
|
|
44
|
+
/** Hash of what a home-screen app reads only when it is added (see
|
|
45
|
+
* `installIdentity`). */
|
|
46
|
+
install: string;
|
|
47
|
+
}
|
|
48
|
+
/** The file the runtime serves at `/_pylon/version`. */
|
|
49
|
+
export declare const VERSION_FILE = "pylon-version.json";
|
|
50
|
+
/** How long a navigation to an offline route waits for the network before
|
|
51
|
+
* the worker shows the kept page. Long enough for a slow but working
|
|
52
|
+
* connection to win; short enough that a dead one does not leave a blank
|
|
53
|
+
* screen. */
|
|
54
|
+
export declare const NAVIGATION_TIMEOUT_MS = 3000;
|
|
55
|
+
/** The `offline` block of `<cwd>/pylon.manifest.json`, or null. */
|
|
56
|
+
export declare function readManifestOffline(fs: any, path: any, cwd: string, manifestPath?: string): ManifestOfflineInput | null;
|
|
57
|
+
/** Every route pattern in `routes` that names a page in this build, with
|
|
58
|
+
* the page's component. A pattern with no page is reported, not dropped
|
|
59
|
+
* silently. */
|
|
60
|
+
export declare function resolveOfflineRoutes(manifest: RouteAssetManifest, routes: string[]): {
|
|
61
|
+
matched: Array<{
|
|
62
|
+
pattern: string;
|
|
63
|
+
component: string;
|
|
64
|
+
}>;
|
|
65
|
+
unknown: string[];
|
|
66
|
+
};
|
|
67
|
+
/** True for a pattern with a dynamic segment: one shell cannot serve it. */
|
|
68
|
+
export declare function isDynamicPattern(pattern: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Hash of what iOS reads from the web app manifest and the head tags only
|
|
71
|
+
* when the app is added to the home screen: the name, the short name, the
|
|
72
|
+
* display mode, the start URL and the icons (with the bytes of each icon
|
|
73
|
+
* file in `public/`). When it changes, an installed app keeps the old
|
|
74
|
+
* values until it is removed and added again.
|
|
75
|
+
*
|
|
76
|
+
* The manifest is the one the runtime serves: `public/manifest.webmanifest`,
|
|
77
|
+
* then `<appDir>/manifest.webmanifest`, else the `offline` config (see
|
|
78
|
+
* `web_manifest_source` in crates/runtime/src/frontend/offline.rs).
|
|
79
|
+
*/
|
|
80
|
+
export declare function installIdentity(fs: any, path: any, cwd: string, appDirRel: string, offline: ManifestOfflineInput): Promise<string>;
|
|
81
|
+
/** The service worker config for this build. `install` is
|
|
82
|
+
* `installIdentity`; it is part of the version, so a manifest change
|
|
83
|
+
* installs a new worker (which precaches the new manifest). */
|
|
84
|
+
export declare function offlineWorkerConfig(manifest: RouteAssetManifest, offline: ManifestOfflineInput, install?: string): Promise<{
|
|
85
|
+
config: ServiceWorkerConfig;
|
|
86
|
+
unknownRoutes: string[];
|
|
87
|
+
}>;
|
|
88
|
+
/** Write `pylon-sw.js` and `pylon-version.json` into `outdir` and return
|
|
89
|
+
* the bundle manifest's `offline` block. `cwd` and `appDirRel` locate the
|
|
90
|
+
* app's own manifest file and its icons. */
|
|
91
|
+
export declare function buildOfflineWorker(fs: any, path: any, outdir: string, manifest: RouteAssetManifest, offline: ManifestOfflineInput, cwd: string, appDirRel: string): Promise<BundleOffline>;
|
|
92
|
+
/** `<head>` tags that make the app installable. */
|
|
93
|
+
export declare function offlineHeadTags(offline: BundleOffline): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Smaller files gain less than the bytes of the encoding headers. */
|
|
2
|
+
export declare const MIN_COMPRESS_BYTES = 1024;
|
|
3
|
+
export interface PrecompressResult {
|
|
4
|
+
/** Files that got at least one compressed copy. */
|
|
5
|
+
files: number;
|
|
6
|
+
/** Total bytes of those files, and of their brotli copies. */
|
|
7
|
+
bytes: number;
|
|
8
|
+
brotliBytes: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Write `<file>.br` (brotli, quality 11) and `<file>.gz` (gzip, level 9)
|
|
12
|
+
* next to every compressible file in `outdir` of at least
|
|
13
|
+
* `MIN_COMPRESS_BYTES`. A copy that is not smaller than the file is not
|
|
14
|
+
* written. Copies left by an earlier build are replaced.
|
|
15
|
+
*/
|
|
16
|
+
export declare function precompressBuild(fs: any, path: any, zlib: any, outdir: string): PrecompressResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The bundle-manifest fields this module reads. */
|
|
2
|
+
export interface RouteAssetManifest {
|
|
3
|
+
public_prefix?: string;
|
|
4
|
+
polyfills?: string;
|
|
5
|
+
routes: Record<string, {
|
|
6
|
+
file: string;
|
|
7
|
+
imports?: string[];
|
|
8
|
+
css?: string[];
|
|
9
|
+
path?: string;
|
|
10
|
+
} | undefined>;
|
|
11
|
+
fonts?: {
|
|
12
|
+
preload?: string[];
|
|
13
|
+
} | null;
|
|
14
|
+
}
|
|
15
|
+
/** Outdir-relative files for one route, in the order the page loads them. */
|
|
16
|
+
export interface RouteAssetFiles {
|
|
17
|
+
/** `<link rel="stylesheet">` (or inlined `<style>`) in the head. */
|
|
18
|
+
styles: string[];
|
|
19
|
+
/** `<link rel="modulepreload">` in the head. */
|
|
20
|
+
preloads: string[];
|
|
21
|
+
/** `<link rel="preload" as="font">` in the head. */
|
|
22
|
+
fonts: string[];
|
|
23
|
+
/** `<script type="module">` in the body tail: the polyfills, then the
|
|
24
|
+
* route entry. */
|
|
25
|
+
scripts: string[];
|
|
26
|
+
}
|
|
27
|
+
/** The scripts the hydration tail loads for a route entry. */
|
|
28
|
+
export declare function routeScriptFiles(route: {
|
|
29
|
+
file: string;
|
|
30
|
+
}, polyfills: string | undefined): string[];
|
|
31
|
+
/** Every client-build file the page for `component` loads, or null when the
|
|
32
|
+
* build has no entry for it. */
|
|
33
|
+
export declare function routeAssetFiles(manifest: RouteAssetManifest, component: string): RouteAssetFiles | null;
|
|
34
|
+
/** All of a route's files as URLs under the build's public prefix. */
|
|
35
|
+
export declare function routeAssetUrls(manifest: RouteAssetManifest, component: string): string[];
|
package/dist/ssr-runtime.d.ts
CHANGED
|
@@ -490,6 +490,13 @@ export declare function buildHydrationTail(args: {
|
|
|
490
490
|
bucketAuth?: {
|
|
491
491
|
signedIn: boolean;
|
|
492
492
|
};
|
|
493
|
+
/** The app opted into `buildManifest({ offline })`: the client registers
|
|
494
|
+
* the service worker (production only), and `useAppUpdate` compares this
|
|
495
|
+
* build's version with `/_pylon/version`. */
|
|
496
|
+
offline?: {
|
|
497
|
+
version: string;
|
|
498
|
+
builtAt?: string;
|
|
499
|
+
};
|
|
493
500
|
/** Client-side NAVIGATION payload: emit the `__PYLON_DATA__` blob and
|
|
494
501
|
* nothing else. The client already has the runtime and resolves the route
|
|
495
502
|
* entry from the build manifest, so the entry `<script>`, the preload tags
|
|
@@ -529,7 +536,7 @@ export declare function errorDigest(err: any): string;
|
|
|
529
536
|
* can be serialized into `__PYLON_DATA__.ssrData` and replayed on the client
|
|
530
537
|
* — keeping hydration free of mismatches.
|
|
531
538
|
*/
|
|
532
|
-
export declare function makeServerData(reader: any, valueCache: Record<string, any>, callFn?: (name: string, args?: Record<string, unknown>) => Promise<unknown>,
|
|
539
|
+
export declare function makeServerData(reader: any, valueCache: Record<string, any>, callFn?: (name: string, args?: Record<string, unknown>) => Promise<unknown>, onIdentityRead?: () => void, onRead?: () => void): any;
|
|
533
540
|
/**
|
|
534
541
|
* #278: does this route STREAM (vs buffer the whole document)? Streaming is
|
|
535
542
|
* opt-in: a `loading.tsx` (route-level Suspense) or `export const streaming =
|
|
@@ -576,11 +583,11 @@ export declare function computeCacheVerdict(args: {
|
|
|
576
583
|
* `authTouched` proves a bucketable body depends on nothing but the binary
|
|
577
584
|
* session bit.
|
|
578
585
|
* - `dynamicTouched` — reading request headers/cookies makes it request-specific.
|
|
579
|
-
* - `strictPolicies` —
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
586
|
+
* - `strictPolicies` — kept as a veto. Safe serverData reads are filtered by
|
|
587
|
+
* the caller's read policies in EVERY mode (not only strict), so an
|
|
588
|
+
* identity-carrying render that reads serverData sets `authTouched` itself
|
|
589
|
+
* (makeServerData's onIdentityRead); that, not this flag, is what keeps a
|
|
590
|
+
* signed-in render with policy-filtered rows out of the bucket.
|
|
584
591
|
* - cookieCount / forceDynamic / wantsStream / non-200 — same reasons as the
|
|
585
592
|
* anon verdict (a Set-Cookie, a streaming head, or an error/redirect can't be
|
|
586
593
|
* safely shared).
|
|
@@ -600,6 +607,36 @@ export declare function computeBucketVerdict(args: {
|
|
|
600
607
|
wantsStream: boolean;
|
|
601
608
|
status: number;
|
|
602
609
|
}): boolean;
|
|
610
|
+
/** Whether the host forwarded the service worker's offline-shell request
|
|
611
|
+
* header (`X-Pylon-Offline-Shell: 1`). */
|
|
612
|
+
export declare function isOfflineShellRequest(headers: Record<string, unknown> | undefined): boolean;
|
|
613
|
+
/**
|
|
614
|
+
* May the offline service worker keep this render as the route's shell? The
|
|
615
|
+
* shell is stored on the device and shown, with no network, to whoever is
|
|
616
|
+
* signed in at the time, so its HTML must not depend on who rendered it:
|
|
617
|
+
*
|
|
618
|
+
* - `authTouched`: reading `props.auth` (or a `serverData.fn` call with an
|
|
619
|
+
* identity) makes the HTML one user's. Read `props.session.exists` for a
|
|
620
|
+
* signed-in check; the shell keeps that bit and nothing else.
|
|
621
|
+
* - `dynamicTouched`: request headers and cookies are one request's.
|
|
622
|
+
* - `serverDataTouched`: offline there is no server; an offline route
|
|
623
|
+
* renders its data from the replica with `db.useQuery`.
|
|
624
|
+
* - a Set-Cookie, or any status but 200 (a redirect to sign-in, an error).
|
|
625
|
+
*
|
|
626
|
+
* Pure and exported for the leak-class tests.
|
|
627
|
+
*/
|
|
628
|
+
export declare function computeOfflineShellVerdict(args: {
|
|
629
|
+
authTouched: boolean;
|
|
630
|
+
dynamicTouched: boolean;
|
|
631
|
+
serverDataTouched: boolean;
|
|
632
|
+
cookieCount: number;
|
|
633
|
+
status: number;
|
|
634
|
+
}): {
|
|
635
|
+
ok: true;
|
|
636
|
+
} | {
|
|
637
|
+
ok: false;
|
|
638
|
+
reason: string;
|
|
639
|
+
};
|
|
603
640
|
/**
|
|
604
641
|
* Dev HUD: explain the cache verdict for a finished render in human terms — the
|
|
605
642
|
* verdict, its TTL, and (when dynamic) the SINGLE reason it isn't cached. Pure so
|
package/dist/types.d.ts
CHANGED
|
@@ -752,7 +752,27 @@ export type LlmContentBlock = {
|
|
|
752
752
|
tool_use_id: string;
|
|
753
753
|
content: string;
|
|
754
754
|
is_error?: boolean;
|
|
755
|
+
}
|
|
756
|
+
/** An image the model reads: png, jpeg, gif or webp, as base64. */
|
|
757
|
+
| {
|
|
758
|
+
type: "image";
|
|
759
|
+
source: LlmMediaSource;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* A document the model reads: a PDF or plain text, as base64. Anthropic
|
|
763
|
+
* only: the OpenAI provider refuses it (LLM_UNSUPPORTED_CONTENT).
|
|
764
|
+
*/
|
|
765
|
+
| {
|
|
766
|
+
type: "document";
|
|
767
|
+
source: LlmMediaSource;
|
|
768
|
+
title?: string;
|
|
755
769
|
};
|
|
770
|
+
/** The bytes of an image or document block. */
|
|
771
|
+
export interface LlmMediaSource {
|
|
772
|
+
type: "base64";
|
|
773
|
+
media_type: string;
|
|
774
|
+
data: string;
|
|
775
|
+
}
|
|
756
776
|
export interface LlmTool {
|
|
757
777
|
name: string;
|
|
758
778
|
description?: string;
|
|
@@ -981,6 +1001,33 @@ export interface Files {
|
|
|
981
1001
|
signedUrl(fileId: string, opts?: {
|
|
982
1002
|
ttlSecs?: number;
|
|
983
1003
|
}): Promise<string>;
|
|
1004
|
+
/**
|
|
1005
|
+
* What the host knows of a stored file, in any function. On a backend
|
|
1006
|
+
* that tracks owners (local disk, a private S3 bucket) `ownerId` is who
|
|
1007
|
+
* uploaded or stored it, and a file that does not exist is null. A
|
|
1008
|
+
* backend that does not (`ownerTracked: false`, the Stack0 CDN) knows no
|
|
1009
|
+
* owner: there a file id is a capability, so check who may use it
|
|
1010
|
+
* yourself, for example by recording uploads in your own rows.
|
|
1011
|
+
*/
|
|
1012
|
+
info(fileId: string): Promise<FileInfo | null>;
|
|
1013
|
+
}
|
|
1014
|
+
/** {@link Files.info}. */
|
|
1015
|
+
export interface FileInfo {
|
|
1016
|
+
id: string;
|
|
1017
|
+
/** Recorded at upload, when the backend keeps it. */
|
|
1018
|
+
contentType: string | null;
|
|
1019
|
+
/** The uploader, or null when the backend does not track it or the system stored it. */
|
|
1020
|
+
ownerId: string | null;
|
|
1021
|
+
public: boolean;
|
|
1022
|
+
/** The backend records owners, so `ownerId` and a null answer can be trusted. */
|
|
1023
|
+
ownerTracked: boolean;
|
|
1024
|
+
}
|
|
1025
|
+
/** {@link ActionFiles.read}. */
|
|
1026
|
+
export interface ReadFile {
|
|
1027
|
+
id: string;
|
|
1028
|
+
contentType: string | null;
|
|
1029
|
+
size: number;
|
|
1030
|
+
data: Uint8Array;
|
|
984
1031
|
}
|
|
985
1032
|
/** Options for {@link ActionFiles.store}. */
|
|
986
1033
|
export interface StoreFileOptions {
|
|
@@ -1019,6 +1066,12 @@ export interface ActionFiles extends Files {
|
|
|
1019
1066
|
delete(fileId: string): Promise<{
|
|
1020
1067
|
deleted: boolean;
|
|
1021
1068
|
}>;
|
|
1069
|
+
/**
|
|
1070
|
+
* A stored file's bytes, with server trust: the function decides who may
|
|
1071
|
+
* read it. At most the host's upload limit (25 MiB, or
|
|
1072
|
+
* `PYLON_MAX_UPLOAD_BYTES`).
|
|
1073
|
+
*/
|
|
1074
|
+
read(fileId: string): Promise<ReadFile>;
|
|
1022
1075
|
}
|
|
1023
1076
|
/**
|
|
1024
1077
|
* Shard tickets: short-lived, signed permission to join one realtime shard,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/functions",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.58",
|
|
4
4
|
"description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@types/react-dom": "^19.0.0",
|
|
59
59
|
"browserslist": "^4.29.1",
|
|
60
60
|
"core-js": "^3.50.0",
|
|
61
|
-
"happy-dom": "^
|
|
61
|
+
"happy-dom": "^20.8.9",
|
|
62
62
|
"lightningcss": "~1.32.0",
|
|
63
63
|
"react": "^19.0.0",
|
|
64
64
|
"react-dom": "^19.0.0"
|
package/src/index.ts
CHANGED
|
@@ -99,6 +99,7 @@ export type {
|
|
|
99
99
|
Llm,
|
|
100
100
|
LlmMessage,
|
|
101
101
|
LlmContentBlock,
|
|
102
|
+
LlmMediaSource,
|
|
102
103
|
LlmTool,
|
|
103
104
|
LlmCompleteRequest,
|
|
104
105
|
LlmCompleteResponse,
|
|
@@ -120,4 +121,6 @@ export type {
|
|
|
120
121
|
Crdt,
|
|
121
122
|
StoreFileOptions,
|
|
122
123
|
StoredFile,
|
|
124
|
+
FileInfo,
|
|
125
|
+
ReadFile,
|
|
123
126
|
} from "./types";
|