@pyreon/runtime-server 0.18.0 → 0.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/runtime-server",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "SSR/SSG renderer for Pyreon — streaming HTML + static generation",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/runtime-server#readme",
6
6
  "bugs": {
@@ -43,7 +43,10 @@
43
43
  "prepublishOnly": "bun run build"
44
44
  },
45
45
  "dependencies": {
46
- "@pyreon/core": "^0.18.0",
47
- "@pyreon/reactivity": "^0.18.0"
46
+ "@pyreon/core": "^0.20.0",
47
+ "@pyreon/reactivity": "^0.20.0"
48
+ },
49
+ "devDependencies": {
50
+ "@pyreon/manifest": "0.13.1"
48
51
  }
49
52
  }
@@ -0,0 +1,122 @@
1
+ import { defineManifest } from '@pyreon/manifest'
2
+
3
+ export default defineManifest({
4
+ name: '@pyreon/runtime-server',
5
+ title: 'SSR Renderer',
6
+ tagline:
7
+ 'SSR/SSG VNode→HTML renderer — renderToString, renderToStream (out-of-order Suspense), per-request ALS context/store isolation',
8
+ description:
9
+ "Pyreon's server-side renderer: walks a VNode tree and produces HTML. Signal accessors are called synchronously to SNAPSHOT their current value — no effects, no reactivity on the server (reactivity resumes post-hydration on the client). Async component functions are awaited. `renderToStream` flushes progressively and resolves Suspense boundaries out-of-order (fallback first, then a `<template>` + inline swap script). Concurrency-safe: every `renderToString` / `renderToStream` / `runWithRequestContext` call runs in its own `AsyncLocalStorage` store so concurrent requests never share context frames; `configureStoreIsolation()` extends the same isolation to the `@pyreon/store` registry. Most apps consume this transitively through `@pyreon/server` (`createHandler` / `prerender`) rather than calling it directly.",
10
+ category: 'server',
11
+ features: [
12
+ 'renderToString(vnode) → Promise<string> — one-shot HTML, awaits async components',
13
+ 'renderToStream(vnode) → ReadableStream<string> — progressive, out-of-order Suspense (30s timeout → fallback stays)',
14
+ 'Per-request ALS context isolation — concurrent requests never share provide() frames',
15
+ 'runWithRequestContext(fn) — isolated context+store for Pyreon APIs called outside renderToString',
16
+ 'configureStoreIsolation(setStoreRegistryProvider) — opt-in per-request @pyreon/store isolation',
17
+ 'Compiler-emitted reactive props resolved via makeReactiveProps (parity with CSR mount.ts)',
18
+ 'For-list key markers URL-encoded so user keys can never break out of the HTML comment',
19
+ 'decodeKeyFromMarker — symmetric inverse of the For-key marker encoder (devtools/future hydration)',
20
+ ],
21
+ api: [
22
+ {
23
+ name: 'renderToString',
24
+ kind: 'function',
25
+ signature: 'renderToString(root: VNode | null): Promise<string>',
26
+ summary:
27
+ 'Render a VNode tree to a single HTML string. Each call runs in a fresh isolated ALS context stack (and store registry if `configureStoreIsolation` was called) so concurrent requests never bleed `provide()` frames into each other. Signal accessors are invoked synchronously to snapshot their CURRENT value — there is no reactivity on the server, so a `<div>{count()}</div>` renders the value at render time and only becomes live after client hydration. Async component functions (`async function C()`) are awaited before the walk continues. Returns the empty string for a `null` root.',
28
+ example: `import { renderToString } from "@pyreon/runtime-server"
29
+
30
+ const html = await renderToString(<App />)`,
31
+ mistakes: [
32
+ 'Expecting signal writes after `renderToString` to change the output — SSR is one-shot; the string is already produced. Reactivity is a post-hydration (client) concern',
33
+ "Calling Pyreon context APIs (`useHead`, loaders) OUTSIDE `renderToString` and expecting per-request isolation — use `runWithRequestContext` for that; bare calls share the fallback stack across concurrent requests",
34
+ 'Reaching for `renderToString` directly when you have an HTTP handler — the `createHandler` in `@pyreon/server` wraps it with template precompilation, middleware, and loader-data injection; prefer that for request handling',
35
+ ],
36
+ seeAlso: ['renderToStream', 'runWithRequestContext'],
37
+ },
38
+ {
39
+ name: 'renderToStream',
40
+ kind: 'function',
41
+ signature: 'renderToStream(root: VNode | null): ReadableStream<string>',
42
+ summary:
43
+ 'Render to a Web-standard `ReadableStream<string>` with true progressive flushing — synchronous subtrees enqueue immediately, async component boundaries are awaited in order. Suspense boundaries stream OUT OF ORDER: the fallback is emitted inline at once, and the resolved children arrive later as a `<template>` + a tiny inline swap `<script>` that replaces the placeholder client-side — without blocking the rest of the page. Each call gets its own isolated ALS context stack. A Suspense boundary that does not resolve within 30s leaves its fallback in place (a dev-mode warning fires); a boundary that throws also leaves the fallback (no swap script emitted).',
44
+ example: `import { renderToStream } from "@pyreon/runtime-server"
45
+
46
+ return new Response(renderToStream(<App />), {
47
+ headers: { "content-type": "text/html" },
48
+ })`,
49
+ mistakes: [
50
+ 'Assuming Suspense children arrive in source order — they are swapped in as each boundary resolves; the fallback ships first, resolved content can arrive in any order',
51
+ 'Expecting `@pyreon/head` tags registered inside a Suspense child to reach the document `<head>` — the head is flushed in the shell BEFORE any boundary resolves, so async-loaded data does not contribute to it',
52
+ 'Treating a 30s-timed-out boundary as an error — by design the fallback simply stays; only a dev-mode `console.warn` signals it. Budget your async children well under 30s',
53
+ 'Buffering the whole stream before responding — that throws away the progressive-flush benefit; pass the stream straight into the `Response`',
54
+ ],
55
+ seeAlso: ['renderToString'],
56
+ },
57
+ {
58
+ name: 'runWithRequestContext',
59
+ kind: 'function',
60
+ signature: 'runWithRequestContext<T>(fn: () => Promise<T>): Promise<T>',
61
+ summary:
62
+ 'Run an async function inside a fresh, isolated ALS context stack (and store registry, if `configureStoreIsolation` was called). Use this when you need to call Pyreon context-aware APIs — `useHead`, `prefetchLoaderData`, router resolution — OUTSIDE a `renderToString` / `renderToStream` call but still want per-request isolation. Without it those calls land on a process-global fallback stack shared by every concurrent request.',
63
+ example: `import { runWithRequestContext } from "@pyreon/runtime-server"
64
+
65
+ const data = await runWithRequestContext(async () => {
66
+ await prefetchLoaderData(router, url.pathname, request)
67
+ return renderToString(<App />)
68
+ })`,
69
+ mistakes: [
70
+ 'Calling `prefetchLoaderData` / `useHead` before `renderToString` WITHOUT wrapping the whole sequence in one `runWithRequestContext` — the prefetch lands in a different (or the shared fallback) context than the render, so the render sees no loader data',
71
+ 'Wrapping a synchronous function — the signature is `() => Promise<T>`; return the promise (or make the fn `async`) so the ALS scope spans the awaited work',
72
+ ],
73
+ seeAlso: ['renderToString', 'configureStoreIsolation'],
74
+ },
75
+ {
76
+ name: 'configureStoreIsolation',
77
+ kind: 'function',
78
+ signature:
79
+ 'configureStoreIsolation(setStoreRegistryProvider: (fn: () => Map<string, unknown>) => void): void',
80
+ summary:
81
+ "Opt in to per-request `@pyreon/store` isolation. Call ONCE at server startup, passing `@pyreon/store`'s `setStoreRegistryProvider`. After this, every `renderToString` / `renderToStream` / `runWithRequestContext` call gets its own fresh store registry via ALS. WITHOUT calling it, store isolation is a no-op and all concurrent requests share ONE process-global store registry — request A's `defineStore` state is visible to request B (SSR state bleed across users).",
82
+ example: `import { configureStoreIsolation } from "@pyreon/runtime-server"
83
+ import { setStoreRegistryProvider } from "@pyreon/store"
84
+
85
+ // once, at server startup:
86
+ configureStoreIsolation(setStoreRegistryProvider)`,
87
+ mistakes: [
88
+ 'Not calling it at all in an SSR app that uses `@pyreon/store` — concurrent requests share one global registry, so one request’s store state leaks into another request’s render',
89
+ 'Calling it per request instead of once at startup — it only needs to wire the provider once; the per-request fresh `Map` is handled internally by the ALS run',
90
+ 'Passing something other than the `setStoreRegistryProvider` exported by `@pyreon/store` — the contract is specifically that provider-setter shape',
91
+ ],
92
+ seeAlso: ['runWithRequestContext', 'renderToString'],
93
+ },
94
+ {
95
+ name: 'decodeKeyFromMarker',
96
+ kind: 'function',
97
+ signature: 'decodeKeyFromMarker(encoded: string): string',
98
+ summary:
99
+ "Inverse of the internal For-list key encoder. `<For>` SSR emits per-item `<!--k:KEY-->` markers; the encoder URL-encodes the key and replaces every `-` with `%2D` so a user-supplied key can never form `-->` and break out of the HTML comment (an injection vector). `decodeKeyFromMarker` reverses that. Not used by the runtime today (hydration does not read per-item markers) — shipped alongside the encoder so future hydration or devtools consumers decode symmetrically without re-deriving the scheme.",
100
+ example: `import { decodeKeyFromMarker } from "@pyreon/runtime-server"
101
+
102
+ decodeKeyFromMarker("a%2Db") // "a-b"`,
103
+ mistakes: [
104
+ 'Assuming the runtime consumes this — it does not yet; it exists for forward-compat / devtools symmetry with the marker encoder',
105
+ ],
106
+ },
107
+ ],
108
+ gotchas: [
109
+ {
110
+ label: 'No reactivity on the server',
111
+ note: 'Signal accessors are called synchronously to snapshot their value — no effects are created. A `{count()}` expression renders the value at render time; it only becomes live after client hydration. SSR is one-shot.',
112
+ },
113
+ {
114
+ label: 'Usually consumed via @pyreon/server',
115
+ note: '`createHandler` (SSR HTTP) and `prerender` (SSG) in `@pyreon/server` wrap this renderer with template precompilation, middleware, and loader-data injection. Call `renderToString` / `renderToStream` directly only for custom integrations; for request handling prefer `@pyreon/server`.',
116
+ },
117
+ {
118
+ label: 'Server dev-gate convention',
119
+ note: 'As a server package, `@pyreon/runtime-server` correctly uses `typeof process !== "undefined" && process.env.NODE_ENV !== "production"` for its dev-mode perf-counter sink — NOT the browser `import.meta.env.DEV` form. It always runs in Node/Bun where `process` is real.',
120
+ },
121
+ ],
122
+ })
@@ -0,0 +1,39 @@
1
+ import {
2
+ renderApiReferenceEntries,
3
+ renderLlmsFullSection,
4
+ renderLlmsTxtLine,
5
+ } from '@pyreon/manifest'
6
+ import manifest from '../manifest'
7
+
8
+ describe('gen-docs — runtime-server snapshot', () => {
9
+ it('renders a llms.txt bullet starting with the package prefix', () => {
10
+ const line = renderLlmsTxtLine(manifest)
11
+ expect(line.startsWith('- @pyreon/runtime-server —')).toBe(true)
12
+ })
13
+
14
+ it('renders a llms-full.txt section with the right header', () => {
15
+ const section = renderLlmsFullSection(manifest)
16
+ expect(section.startsWith('## @pyreon/runtime-server —')).toBe(true)
17
+ expect(section).toContain('```typescript')
18
+ })
19
+
20
+ it('renders MCP api-reference entries for every api[] item', () => {
21
+ const record = renderApiReferenceEntries(manifest)
22
+ expect(Object.keys(record).sort()).toEqual([
23
+ 'runtime-server/configureStoreIsolation',
24
+ 'runtime-server/decodeKeyFromMarker',
25
+ 'runtime-server/renderToStream',
26
+ 'runtime-server/renderToString',
27
+ 'runtime-server/runWithRequestContext',
28
+ ])
29
+ })
30
+
31
+ it('carries the SSR foot-gun catalog into MCP mistakes for the flagship APIs', () => {
32
+ const r = renderApiReferenceEntries(manifest)
33
+ expect(r['runtime-server/renderToString']?.mistakes).toContain('one-shot')
34
+ expect(r['runtime-server/renderToStream']?.mistakes).toContain('Suspense')
35
+ expect(r['runtime-server/configureStoreIsolation']?.mistakes).toContain(
36
+ 'global registry',
37
+ )
38
+ })
39
+ })