@constela/server 13.0.0 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +98 -0
  2. package/dist/index.js +10 -0
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -201,6 +201,104 @@ Pass style presets via `RenderOptions.styles` for evaluation.
201
201
  .dark .shiki span { color: var(--shiki-dark); }
202
202
  ```
203
203
 
204
+ ## Streaming SSR
205
+
206
+ Render to a ReadableStream for progressive HTML delivery:
207
+
208
+ ```typescript
209
+ import { renderToStream, createHtmlTransformStream } from '@constela/server';
210
+
211
+ // Render program to stream
212
+ const contentStream = renderToStream(compiledProgram, {
213
+ flushStrategy: 'batched',
214
+ }, {
215
+ route: { params: { id: '123' }, query: {}, path: '/posts/123' },
216
+ imports: { config: siteConfig },
217
+ });
218
+
219
+ // Wrap with HTML document structure
220
+ const htmlStream = contentStream.pipeThrough(
221
+ createHtmlTransformStream({
222
+ title: 'My Page',
223
+ lang: 'en',
224
+ stylesheets: ['/styles.css'],
225
+ scripts: ['/client.js'],
226
+ })
227
+ );
228
+
229
+ // Use with Response (Edge/Workers)
230
+ return new Response(htmlStream, {
231
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
232
+ });
233
+ ```
234
+
235
+ **Flush Strategies:**
236
+
237
+ | Strategy | Description |
238
+ |----------|-------------|
239
+ | `immediate` | Flush each chunk as soon as it's ready |
240
+ | `batched` | Flush when buffer exceeds 1KB threshold |
241
+ | `manual` | Only flush at the end (for small pages) |
242
+
243
+ **StreamingRenderOptions:**
244
+
245
+ ```typescript
246
+ interface StreamingRenderOptions {
247
+ flushStrategy: 'immediate' | 'batched' | 'manual';
248
+ }
249
+ ```
250
+
251
+ ### Abort Signal Support
252
+
253
+ Cancel streaming when the client disconnects:
254
+
255
+ ```typescript
256
+ const controller = new AbortController();
257
+
258
+ const stream = renderToStream(program, { flushStrategy: 'batched' }, {
259
+ signal: controller.signal,
260
+ });
261
+
262
+ // Cancel on client disconnect
263
+ request.signal.addEventListener('abort', () => {
264
+ controller.abort();
265
+ });
266
+ ```
267
+
268
+ ## Suspense Boundaries
269
+
270
+ Server-side suspense for async content:
271
+
272
+ ```json
273
+ {
274
+ "view": {
275
+ "kind": "suspense",
276
+ "id": "user-data",
277
+ "fallback": {
278
+ "kind": "element",
279
+ "tag": "div",
280
+ "props": { "className": { "expr": "lit", "value": "skeleton" } },
281
+ "children": []
282
+ },
283
+ "content": {
284
+ "kind": "component",
285
+ "name": "UserProfile",
286
+ "props": { "user": { "expr": "data", "name": "user" } }
287
+ }
288
+ }
289
+ }
290
+ ```
291
+
292
+ Renders with markers for client-side hydration:
293
+
294
+ ```html
295
+ <div data-suspense-id="user-data">
296
+ <!-- Fallback content first -->
297
+ <div class="skeleton"></div>
298
+ </div>
299
+ <!-- Resolved content follows -->
300
+ ```
301
+
204
302
  ## Security
205
303
 
206
304
  - **HTML Escaping** - All text output is escaped
package/dist/index.js CHANGED
@@ -403,6 +403,8 @@ function evaluate(expr, ctx) {
403
403
  return expr.value;
404
404
  case "state":
405
405
  return ctx.state.get(expr.name);
406
+ case "local":
407
+ return ctx.locals[expr.name];
406
408
  case "var": {
407
409
  let varName = expr.name;
408
410
  let pathParts = [];
@@ -502,6 +504,9 @@ function evaluate(expr, ctx) {
502
504
  }
503
505
  case "call": {
504
506
  const callExpr = expr;
507
+ if (callExpr.target === null) {
508
+ return void 0;
509
+ }
505
510
  const target = evaluate(callExpr.target, ctx);
506
511
  if (target == null) return void 0;
507
512
  const args = callExpr.args?.map((arg) => {
@@ -1152,6 +1157,8 @@ function evaluate2(expr, ctx) {
1152
1157
  return expr.value;
1153
1158
  case "state":
1154
1159
  return ctx.state.get(expr.name);
1160
+ case "local":
1161
+ return ctx.locals[expr.name];
1155
1162
  case "var": {
1156
1163
  let varName = expr.name;
1157
1164
  let pathParts = [];
@@ -1253,6 +1260,9 @@ function evaluate2(expr, ctx) {
1253
1260
  }
1254
1261
  case "call": {
1255
1262
  const callExpr = expr;
1263
+ if (callExpr.target === null) {
1264
+ return void 0;
1265
+ }
1256
1266
  const target = evaluate2(callExpr.target, ctx);
1257
1267
  if (target == null) return void 0;
1258
1268
  const args = callExpr.args?.map((arg) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/server",
3
- "version": "13.0.0",
3
+ "version": "14.0.1",
4
4
  "description": "Server-side rendering for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,8 +15,8 @@
15
15
  "dist"
16
16
  ],
17
17
  "peerDependencies": {
18
- "@constela/compiler": "^0.15.0",
19
- "@constela/core": "^0.17.0"
18
+ "@constela/compiler": "^0.15.7",
19
+ "@constela/core": "^0.18.1"
20
20
  },
21
21
  "dependencies": {
22
22
  "isomorphic-dompurify": "^2.35.0",
@@ -29,8 +29,8 @@
29
29
  "tsup": "^8.0.0",
30
30
  "typescript": "^5.3.0",
31
31
  "vitest": "^2.0.0",
32
- "@constela/compiler": "0.15.0",
33
- "@constela/core": "0.17.0"
32
+ "@constela/core": "0.18.1",
33
+ "@constela/compiler": "0.15.7"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=20.0.0"