@constela/server 12.0.2 → 14.0.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/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.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CompiledProgram } from '@constela/compiler';
2
+ import { StreamingRenderOptions } from '@constela/core';
2
3
 
3
4
  /**
4
5
  * SSR Renderer
@@ -9,7 +10,7 @@ import { CompiledProgram } from '@constela/compiler';
9
10
  /**
10
11
  * Style preset definition for SSR
11
12
  */
12
- interface StylePreset {
13
+ interface StylePreset$1 {
13
14
  base: string;
14
15
  variants?: Record<string, Record<string, string>>;
15
16
  defaultVariants?: Record<string, string>;
@@ -27,7 +28,7 @@ interface RenderOptions {
27
28
  path?: string;
28
29
  };
29
30
  imports?: Record<string, unknown>;
30
- styles?: Record<string, StylePreset>;
31
+ styles?: Record<string, StylePreset$1>;
31
32
  stateOverrides?: Record<string, unknown>;
32
33
  cookies?: Record<string, string>;
33
34
  }
@@ -40,4 +41,70 @@ interface RenderOptions {
40
41
  */
41
42
  declare function renderToString(program: CompiledProgram, options?: RenderOptions): Promise<string>;
42
43
 
43
- export { type RenderOptions, renderToString };
44
+ /**
45
+ * Streaming SSR Renderer
46
+ *
47
+ * Renders CompiledProgram to a ReadableStream for Server-Side Rendering.
48
+ * Uses Web Streams API for Edge Runtime compatibility.
49
+ *
50
+ * Features:
51
+ * - Three flush strategies: immediate, batched, manual
52
+ * - Backpressure support via controller.desiredSize
53
+ * - AbortSignal support for cancellation
54
+ * - Suspense boundary support for async content
55
+ */
56
+
57
+ /**
58
+ * Style preset definition for SSR
59
+ */
60
+ interface StylePreset {
61
+ base: string;
62
+ variants?: Record<string, Record<string, string>>;
63
+ defaultVariants?: Record<string, string>;
64
+ compoundVariants?: Array<Record<string, string> & {
65
+ class: string;
66
+ }>;
67
+ }
68
+ /**
69
+ * Extended options for streaming render
70
+ */
71
+ interface StreamRenderOptions {
72
+ route?: {
73
+ params?: Record<string, string>;
74
+ query?: Record<string, string>;
75
+ path?: string;
76
+ };
77
+ imports?: Record<string, unknown>;
78
+ styles?: Record<string, StylePreset>;
79
+ stateOverrides?: Record<string, unknown>;
80
+ cookies?: Record<string, string>;
81
+ signal?: AbortSignal;
82
+ }
83
+ /**
84
+ * Options for HTML transform stream
85
+ */
86
+ interface HtmlTransformOptions {
87
+ title: string;
88
+ lang?: string;
89
+ meta?: Record<string, string>;
90
+ stylesheets?: string[];
91
+ scripts?: string[];
92
+ }
93
+ /**
94
+ * Renders a CompiledProgram to a ReadableStream.
95
+ *
96
+ * @param program - The compiled program to render
97
+ * @param streamOptions - Streaming options (flush strategy, etc.)
98
+ * @param options - Optional render options including route context
99
+ * @returns ReadableStream of HTML strings
100
+ */
101
+ declare function renderToStream(program: CompiledProgram, streamOptions: StreamingRenderOptions, options?: StreamRenderOptions): ReadableStream<string>;
102
+ /**
103
+ * Creates a TransformStream that wraps content with HTML document structure.
104
+ *
105
+ * @param options - HTML document options (title, meta, stylesheets, scripts)
106
+ * @returns TransformStream that wraps content with HTML structure
107
+ */
108
+ declare function createHtmlTransformStream(options: HtmlTransformOptions): TransformStream<string, string>;
109
+
110
+ export { type HtmlTransformOptions, type RenderOptions, type StreamRenderOptions, createHtmlTransformStream, renderToStream, renderToString };
package/dist/index.js CHANGED
@@ -502,6 +502,9 @@ function evaluate(expr, ctx) {
502
502
  }
503
503
  case "call": {
504
504
  const callExpr = expr;
505
+ if (callExpr.target === null) {
506
+ return void 0;
507
+ }
505
508
  const target = evaluate(callExpr.target, ctx);
506
509
  if (target == null) return void 0;
507
510
  const args = callExpr.args?.map((arg) => {
@@ -682,6 +685,12 @@ async function renderNode(node, ctx) {
682
685
  return await renderPortal(node, ctx);
683
686
  case "localState":
684
687
  return await renderLocalState(node, ctx);
688
+ case "island":
689
+ return await renderIsland(node, ctx);
690
+ case "suspense":
691
+ return await renderNode(node.content, ctx);
692
+ case "errorBoundary":
693
+ return await renderNode(node.content, ctx);
685
694
  default: {
686
695
  const _exhaustiveCheck = node;
687
696
  throw new Error(`Unknown node kind: ${JSON.stringify(_exhaustiveCheck)}`);
@@ -794,6 +803,34 @@ async function renderLocalState(node, ctx) {
794
803
  };
795
804
  return await renderNode(node.child, childCtx);
796
805
  }
806
+ async function renderIsland(node, ctx) {
807
+ const islandStateValues = {};
808
+ if (node.state) {
809
+ for (const [name, field] of Object.entries(node.state)) {
810
+ islandStateValues[name] = field.initial;
811
+ }
812
+ }
813
+ const islandState = new Map(ctx.state);
814
+ for (const [name, value] of Object.entries(islandStateValues)) {
815
+ islandState.set(name, value);
816
+ }
817
+ const islandCtx = {
818
+ ...ctx,
819
+ state: islandState
820
+ };
821
+ const content = await renderNode(node.content, islandCtx);
822
+ const dataAttrs = [
823
+ `data-island-id="${escapeHtml(node.id)}"`,
824
+ `data-island-strategy="${escapeHtml(node.strategy)}"`
825
+ ];
826
+ if (node.strategyOptions) {
827
+ dataAttrs.push(`data-island-options="${escapeHtml(JSON.stringify(node.strategyOptions))}"`);
828
+ }
829
+ if (node.state) {
830
+ dataAttrs.push(`data-island-state="${escapeHtml(JSON.stringify(islandStateValues))}"`);
831
+ }
832
+ return `<div ${dataAttrs.join(" ")}>${content}</div>`;
833
+ }
797
834
  async function renderToString(program, options) {
798
835
  const state = /* @__PURE__ */ new Map();
799
836
  for (const [name, field] of Object.entries(program.state)) {
@@ -820,6 +857,861 @@ async function renderToString(program, options) {
820
857
  };
821
858
  return await renderNode(program.view, ctx);
822
859
  }
860
+
861
+ // src/streaming.ts
862
+ import { isCookieInitialExpr as isCookieInitialExpr2 } from "@constela/core";
863
+ var VOID_ELEMENTS2 = /* @__PURE__ */ new Set([
864
+ "area",
865
+ "base",
866
+ "br",
867
+ "col",
868
+ "embed",
869
+ "hr",
870
+ "img",
871
+ "input",
872
+ "link",
873
+ "meta",
874
+ "param",
875
+ "source",
876
+ "track",
877
+ "wbr"
878
+ ]);
879
+ var SAFE_ARRAY_METHODS2 = /* @__PURE__ */ new Set([
880
+ "length",
881
+ "at",
882
+ "includes",
883
+ "slice",
884
+ "indexOf",
885
+ "join",
886
+ "filter",
887
+ "map",
888
+ "find",
889
+ "findIndex",
890
+ "some",
891
+ "every"
892
+ ]);
893
+ var SAFE_STRING_METHODS2 = /* @__PURE__ */ new Set([
894
+ "length",
895
+ "charAt",
896
+ "substring",
897
+ "slice",
898
+ "split",
899
+ "trim",
900
+ "toUpperCase",
901
+ "toLowerCase",
902
+ "replace",
903
+ "includes",
904
+ "startsWith",
905
+ "endsWith",
906
+ "indexOf"
907
+ ]);
908
+ var SAFE_MATH_METHODS2 = /* @__PURE__ */ new Set([
909
+ "min",
910
+ "max",
911
+ "round",
912
+ "floor",
913
+ "ceil",
914
+ "abs",
915
+ "sqrt",
916
+ "pow",
917
+ "random",
918
+ "sin",
919
+ "cos",
920
+ "tan"
921
+ ]);
922
+ var SAFE_DATE_STATIC_METHODS2 = /* @__PURE__ */ new Set(["now", "parse"]);
923
+ var SAFE_DATE_INSTANCE_METHODS2 = /* @__PURE__ */ new Set([
924
+ "toISOString",
925
+ "toDateString",
926
+ "toTimeString",
927
+ "getTime",
928
+ "getFullYear",
929
+ "getMonth",
930
+ "getDate",
931
+ "getHours",
932
+ "getMinutes",
933
+ "getSeconds",
934
+ "getMilliseconds"
935
+ ]);
936
+ var CHUNK_SIZE_THRESHOLD = 1024;
937
+ function isEventHandler2(value) {
938
+ return typeof value === "object" && value !== null && "event" in value && "action" in value;
939
+ }
940
+ function createLambdaFunction2(lambda, ctx) {
941
+ return (item, index) => {
942
+ const lambdaLocals = {
943
+ ...ctx.locals,
944
+ [lambda.param]: item
945
+ };
946
+ if (lambda.index !== void 0) {
947
+ lambdaLocals[lambda.index] = index;
948
+ }
949
+ return evaluate2(lambda.body, { ...ctx, locals: lambdaLocals });
950
+ };
951
+ }
952
+ function callArrayMethod2(target, method, args, ctx, rawArgs) {
953
+ if (!SAFE_ARRAY_METHODS2.has(method)) return void 0;
954
+ switch (method) {
955
+ case "length":
956
+ return target.length;
957
+ case "at": {
958
+ const index = typeof args[0] === "number" ? args[0] : 0;
959
+ return target.at(index);
960
+ }
961
+ case "includes": {
962
+ const searchElement = args[0];
963
+ const fromIndex = typeof args[1] === "number" ? args[1] : void 0;
964
+ return target.includes(searchElement, fromIndex);
965
+ }
966
+ case "slice": {
967
+ const start = typeof args[0] === "number" ? args[0] : void 0;
968
+ const end = typeof args[1] === "number" ? args[1] : void 0;
969
+ return target.slice(start, end);
970
+ }
971
+ case "indexOf": {
972
+ const searchElement = args[0];
973
+ const fromIndex = typeof args[1] === "number" ? args[1] : void 0;
974
+ return target.indexOf(searchElement, fromIndex);
975
+ }
976
+ case "join": {
977
+ const separator = typeof args[0] === "string" ? args[0] : ",";
978
+ return target.join(separator);
979
+ }
980
+ case "filter": {
981
+ const lambdaExpr = rawArgs?.[0];
982
+ if (!lambdaExpr || lambdaExpr.expr !== "lambda") return void 0;
983
+ const fn = createLambdaFunction2(lambdaExpr, ctx);
984
+ return target.filter((item, index) => !!fn(item, index));
985
+ }
986
+ case "map": {
987
+ const lambdaExpr = rawArgs?.[0];
988
+ if (!lambdaExpr || lambdaExpr.expr !== "lambda") return void 0;
989
+ const fn = createLambdaFunction2(lambdaExpr, ctx);
990
+ return target.map((item, index) => fn(item, index));
991
+ }
992
+ case "find": {
993
+ const lambdaExpr = rawArgs?.[0];
994
+ if (!lambdaExpr || lambdaExpr.expr !== "lambda") return void 0;
995
+ const fn = createLambdaFunction2(lambdaExpr, ctx);
996
+ return target.find((item, index) => !!fn(item, index));
997
+ }
998
+ case "findIndex": {
999
+ const lambdaExpr = rawArgs?.[0];
1000
+ if (!lambdaExpr || lambdaExpr.expr !== "lambda") return void 0;
1001
+ const fn = createLambdaFunction2(lambdaExpr, ctx);
1002
+ return target.findIndex((item, index) => !!fn(item, index));
1003
+ }
1004
+ case "some": {
1005
+ const lambdaExpr = rawArgs?.[0];
1006
+ if (!lambdaExpr || lambdaExpr.expr !== "lambda") return void 0;
1007
+ const fn = createLambdaFunction2(lambdaExpr, ctx);
1008
+ return target.some((item, index) => !!fn(item, index));
1009
+ }
1010
+ case "every": {
1011
+ const lambdaExpr = rawArgs?.[0];
1012
+ if (!lambdaExpr || lambdaExpr.expr !== "lambda") return void 0;
1013
+ const fn = createLambdaFunction2(lambdaExpr, ctx);
1014
+ return target.every((item, index) => !!fn(item, index));
1015
+ }
1016
+ default:
1017
+ return void 0;
1018
+ }
1019
+ }
1020
+ function callStringMethod2(target, method, args) {
1021
+ if (!SAFE_STRING_METHODS2.has(method)) return void 0;
1022
+ switch (method) {
1023
+ case "length":
1024
+ return target.length;
1025
+ case "charAt": {
1026
+ const index = typeof args[0] === "number" ? args[0] : 0;
1027
+ return target.charAt(index);
1028
+ }
1029
+ case "substring": {
1030
+ const start = typeof args[0] === "number" ? args[0] : 0;
1031
+ const end = typeof args[1] === "number" ? args[1] : void 0;
1032
+ return target.substring(start, end);
1033
+ }
1034
+ case "slice": {
1035
+ const start = typeof args[0] === "number" ? args[0] : void 0;
1036
+ const end = typeof args[1] === "number" ? args[1] : void 0;
1037
+ return target.slice(start, end);
1038
+ }
1039
+ case "split": {
1040
+ const separator = typeof args[0] === "string" ? args[0] : "";
1041
+ return target.split(separator);
1042
+ }
1043
+ case "trim":
1044
+ return target.trim();
1045
+ case "toUpperCase":
1046
+ return target.toUpperCase();
1047
+ case "toLowerCase":
1048
+ return target.toLowerCase();
1049
+ case "replace": {
1050
+ const search = typeof args[0] === "string" ? args[0] : "";
1051
+ const replace = typeof args[1] === "string" ? args[1] : "";
1052
+ return target.replace(search, replace);
1053
+ }
1054
+ case "includes": {
1055
+ const search = typeof args[0] === "string" ? args[0] : "";
1056
+ const position = typeof args[1] === "number" ? args[1] : void 0;
1057
+ return target.includes(search, position);
1058
+ }
1059
+ case "startsWith": {
1060
+ const search = typeof args[0] === "string" ? args[0] : "";
1061
+ const position = typeof args[1] === "number" ? args[1] : void 0;
1062
+ return target.startsWith(search, position);
1063
+ }
1064
+ case "endsWith": {
1065
+ const search = typeof args[0] === "string" ? args[0] : "";
1066
+ const length = typeof args[1] === "number" ? args[1] : void 0;
1067
+ return target.endsWith(search, length);
1068
+ }
1069
+ case "indexOf": {
1070
+ const search = typeof args[0] === "string" ? args[0] : "";
1071
+ const position = typeof args[1] === "number" ? args[1] : void 0;
1072
+ return target.indexOf(search, position);
1073
+ }
1074
+ default:
1075
+ return void 0;
1076
+ }
1077
+ }
1078
+ function callMathMethod2(method, args) {
1079
+ if (!SAFE_MATH_METHODS2.has(method)) return void 0;
1080
+ const numbers = args.filter((a) => typeof a === "number");
1081
+ switch (method) {
1082
+ case "min":
1083
+ return numbers.length > 0 ? Math.min(...numbers) : void 0;
1084
+ case "max":
1085
+ return numbers.length > 0 ? Math.max(...numbers) : void 0;
1086
+ case "round":
1087
+ return numbers[0] !== void 0 ? Math.round(numbers[0]) : void 0;
1088
+ case "floor":
1089
+ return numbers[0] !== void 0 ? Math.floor(numbers[0]) : void 0;
1090
+ case "ceil":
1091
+ return numbers[0] !== void 0 ? Math.ceil(numbers[0]) : void 0;
1092
+ case "abs":
1093
+ return numbers[0] !== void 0 ? Math.abs(numbers[0]) : void 0;
1094
+ case "sqrt":
1095
+ return numbers[0] !== void 0 ? Math.sqrt(numbers[0]) : void 0;
1096
+ case "pow":
1097
+ return numbers[0] !== void 0 && numbers[1] !== void 0 ? Math.pow(numbers[0], numbers[1]) : void 0;
1098
+ case "random":
1099
+ return Math.random();
1100
+ case "sin":
1101
+ return numbers[0] !== void 0 ? Math.sin(numbers[0]) : void 0;
1102
+ case "cos":
1103
+ return numbers[0] !== void 0 ? Math.cos(numbers[0]) : void 0;
1104
+ case "tan":
1105
+ return numbers[0] !== void 0 ? Math.tan(numbers[0]) : void 0;
1106
+ default:
1107
+ return void 0;
1108
+ }
1109
+ }
1110
+ function callDateStaticMethod2(method, args) {
1111
+ if (!SAFE_DATE_STATIC_METHODS2.has(method)) return void 0;
1112
+ switch (method) {
1113
+ case "now":
1114
+ return Date.now();
1115
+ case "parse": {
1116
+ const dateString = args[0];
1117
+ return typeof dateString === "string" ? Date.parse(dateString) : void 0;
1118
+ }
1119
+ default:
1120
+ return void 0;
1121
+ }
1122
+ }
1123
+ function callDateInstanceMethod2(target, method) {
1124
+ if (!SAFE_DATE_INSTANCE_METHODS2.has(method)) return void 0;
1125
+ switch (method) {
1126
+ case "toISOString":
1127
+ return target.toISOString();
1128
+ case "toDateString":
1129
+ return target.toDateString();
1130
+ case "toTimeString":
1131
+ return target.toTimeString();
1132
+ case "getTime":
1133
+ return target.getTime();
1134
+ case "getFullYear":
1135
+ return target.getFullYear();
1136
+ case "getMonth":
1137
+ return target.getMonth();
1138
+ case "getDate":
1139
+ return target.getDate();
1140
+ case "getHours":
1141
+ return target.getHours();
1142
+ case "getMinutes":
1143
+ return target.getMinutes();
1144
+ case "getSeconds":
1145
+ return target.getSeconds();
1146
+ case "getMilliseconds":
1147
+ return target.getMilliseconds();
1148
+ default:
1149
+ return void 0;
1150
+ }
1151
+ }
1152
+ function evaluate2(expr, ctx) {
1153
+ switch (expr.expr) {
1154
+ case "lit":
1155
+ return expr.value;
1156
+ case "state":
1157
+ return ctx.state.get(expr.name);
1158
+ case "var": {
1159
+ let varName = expr.name;
1160
+ let pathParts = [];
1161
+ if (varName.includes(".")) {
1162
+ const parts = varName.split(".");
1163
+ varName = parts[0];
1164
+ pathParts = parts.slice(1);
1165
+ }
1166
+ if (expr.path) {
1167
+ pathParts = pathParts.concat(expr.path.split("."));
1168
+ }
1169
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1170
+ for (const part of pathParts) {
1171
+ if (forbiddenKeys.has(part)) {
1172
+ return void 0;
1173
+ }
1174
+ }
1175
+ let value = ctx.locals[varName];
1176
+ for (const part of pathParts) {
1177
+ if (value == null) break;
1178
+ value = value[part];
1179
+ }
1180
+ return value;
1181
+ }
1182
+ case "bin":
1183
+ return evaluateBinary2(expr.op, expr.left, expr.right, ctx);
1184
+ case "not":
1185
+ return !evaluate2(expr.operand, ctx);
1186
+ case "cond":
1187
+ return evaluate2(expr.if, ctx) ? evaluate2(expr.then, ctx) : evaluate2(expr.else, ctx);
1188
+ case "get": {
1189
+ const baseValue = evaluate2(expr.base, ctx);
1190
+ if (baseValue == null) return void 0;
1191
+ const pathParts = expr.path.split(".");
1192
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1193
+ let value = baseValue;
1194
+ for (const part of pathParts) {
1195
+ if (forbiddenKeys.has(part)) return void 0;
1196
+ if (value == null) return void 0;
1197
+ value = value[part];
1198
+ }
1199
+ return value;
1200
+ }
1201
+ case "route": {
1202
+ const source = expr.source ?? "param";
1203
+ const routeCtx = ctx.route;
1204
+ if (!routeCtx) return "";
1205
+ switch (source) {
1206
+ case "param":
1207
+ return routeCtx.params[expr.name] ?? "";
1208
+ case "query":
1209
+ return routeCtx.query[expr.name] ?? "";
1210
+ case "path":
1211
+ return routeCtx.path;
1212
+ default:
1213
+ return "";
1214
+ }
1215
+ }
1216
+ case "import": {
1217
+ const importData = ctx.imports?.[expr.name];
1218
+ if (importData === void 0) return void 0;
1219
+ if (expr.path) {
1220
+ return getNestedValue2(importData, expr.path);
1221
+ }
1222
+ return importData;
1223
+ }
1224
+ case "data": {
1225
+ const dataValue = ctx.imports?.[expr.name];
1226
+ if (dataValue === void 0) return void 0;
1227
+ if (expr.path) {
1228
+ return getNestedValue2(dataValue, expr.path);
1229
+ }
1230
+ return dataValue;
1231
+ }
1232
+ case "ref":
1233
+ return null;
1234
+ case "index": {
1235
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1236
+ const base = evaluate2(expr.base, ctx);
1237
+ const key = evaluate2(expr.key, ctx);
1238
+ if (base == null || key == null) return void 0;
1239
+ if (typeof key === "string" && forbiddenKeys.has(key)) return void 0;
1240
+ return base[key];
1241
+ }
1242
+ case "param": {
1243
+ return void 0;
1244
+ }
1245
+ case "style": {
1246
+ return evaluateStyle2(expr, ctx);
1247
+ }
1248
+ case "concat": {
1249
+ return expr.items.map((item) => {
1250
+ const val = evaluate2(item, ctx);
1251
+ return val == null ? "" : String(val);
1252
+ }).join("");
1253
+ }
1254
+ case "validity": {
1255
+ return false;
1256
+ }
1257
+ case "call": {
1258
+ const callExpr = expr;
1259
+ if (callExpr.target === null) {
1260
+ return void 0;
1261
+ }
1262
+ const target = evaluate2(callExpr.target, ctx);
1263
+ if (target == null) return void 0;
1264
+ const args = callExpr.args?.map((arg) => {
1265
+ if (arg.expr === "lambda") return arg;
1266
+ return evaluate2(arg, ctx);
1267
+ }) ?? [];
1268
+ if (Array.isArray(target)) {
1269
+ return callArrayMethod2(target, callExpr.method, args, ctx, callExpr.args);
1270
+ }
1271
+ if (typeof target === "string") {
1272
+ return callStringMethod2(target, callExpr.method, args);
1273
+ }
1274
+ if (target === Math) {
1275
+ return callMathMethod2(callExpr.method, args);
1276
+ }
1277
+ if (target === Date) {
1278
+ return callDateStaticMethod2(callExpr.method, args);
1279
+ }
1280
+ if (target instanceof Date) {
1281
+ return callDateInstanceMethod2(target, callExpr.method);
1282
+ }
1283
+ return void 0;
1284
+ }
1285
+ case "lambda": {
1286
+ return void 0;
1287
+ }
1288
+ case "array": {
1289
+ const arrayExpr = expr;
1290
+ return arrayExpr.elements.map((elem) => evaluate2(elem, ctx));
1291
+ }
1292
+ default: {
1293
+ return void 0;
1294
+ }
1295
+ }
1296
+ }
1297
+ function getNestedValue2(obj, path) {
1298
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1299
+ const parts = path.split(".");
1300
+ let value = obj;
1301
+ for (const part of parts) {
1302
+ if (forbiddenKeys.has(part)) {
1303
+ return void 0;
1304
+ }
1305
+ if (value == null) {
1306
+ return void 0;
1307
+ }
1308
+ if (Array.isArray(value)) {
1309
+ const index = Number(part);
1310
+ if (Number.isInteger(index) && index >= 0) {
1311
+ value = value[index];
1312
+ } else {
1313
+ value = value[part];
1314
+ }
1315
+ } else if (typeof value === "object") {
1316
+ value = value[part];
1317
+ } else {
1318
+ return void 0;
1319
+ }
1320
+ }
1321
+ return value;
1322
+ }
1323
+ function evaluateBinary2(op, left, right, ctx) {
1324
+ if (op === "&&") {
1325
+ const leftVal2 = evaluate2(left, ctx);
1326
+ if (!leftVal2) return leftVal2;
1327
+ return evaluate2(right, ctx);
1328
+ }
1329
+ if (op === "||") {
1330
+ const leftVal2 = evaluate2(left, ctx);
1331
+ if (leftVal2) return leftVal2;
1332
+ return evaluate2(right, ctx);
1333
+ }
1334
+ const leftVal = evaluate2(left, ctx);
1335
+ const rightVal = evaluate2(right, ctx);
1336
+ switch (op) {
1337
+ case "+":
1338
+ if (typeof leftVal === "number" && typeof rightVal === "number") {
1339
+ return leftVal + rightVal;
1340
+ }
1341
+ return String(leftVal) + String(rightVal);
1342
+ case "-":
1343
+ return (typeof leftVal === "number" ? leftVal : 0) - (typeof rightVal === "number" ? rightVal : 0);
1344
+ case "*":
1345
+ return (typeof leftVal === "number" ? leftVal : 0) * (typeof rightVal === "number" ? rightVal : 0);
1346
+ case "/": {
1347
+ const dividend = typeof leftVal === "number" ? leftVal : 0;
1348
+ const divisor = typeof rightVal === "number" ? rightVal : 0;
1349
+ if (divisor === 0) {
1350
+ return dividend === 0 ? NaN : dividend > 0 ? Infinity : -Infinity;
1351
+ }
1352
+ return dividend / divisor;
1353
+ }
1354
+ case "==":
1355
+ return leftVal === rightVal;
1356
+ case "!=":
1357
+ return leftVal !== rightVal;
1358
+ case "<":
1359
+ if (typeof leftVal === "number" && typeof rightVal === "number") {
1360
+ return leftVal < rightVal;
1361
+ }
1362
+ return String(leftVal) < String(rightVal);
1363
+ case "<=":
1364
+ if (typeof leftVal === "number" && typeof rightVal === "number") {
1365
+ return leftVal <= rightVal;
1366
+ }
1367
+ return String(leftVal) <= String(rightVal);
1368
+ case ">":
1369
+ if (typeof leftVal === "number" && typeof rightVal === "number") {
1370
+ return leftVal > rightVal;
1371
+ }
1372
+ return String(leftVal) > String(rightVal);
1373
+ case ">=":
1374
+ if (typeof leftVal === "number" && typeof rightVal === "number") {
1375
+ return leftVal >= rightVal;
1376
+ }
1377
+ return String(leftVal) >= String(rightVal);
1378
+ default:
1379
+ throw new Error("Unknown binary operator: " + op);
1380
+ }
1381
+ }
1382
+ function evaluateStyle2(expr, ctx) {
1383
+ const preset = ctx.styles?.[expr.name];
1384
+ if (!preset) return "";
1385
+ let classes = preset.base;
1386
+ if (preset.variants) {
1387
+ for (const variantKey of Object.keys(preset.variants)) {
1388
+ let variantValueStr = null;
1389
+ if (expr.variants?.[variantKey]) {
1390
+ let variantValue;
1391
+ try {
1392
+ variantValue = evaluate2(expr.variants[variantKey], ctx);
1393
+ } catch {
1394
+ continue;
1395
+ }
1396
+ if (variantValue != null) {
1397
+ variantValueStr = String(variantValue);
1398
+ }
1399
+ } else if (preset.defaultVariants?.[variantKey] !== void 0) {
1400
+ variantValueStr = preset.defaultVariants[variantKey];
1401
+ }
1402
+ if (variantValueStr !== null) {
1403
+ const variantClasses = preset.variants[variantKey]?.[variantValueStr];
1404
+ if (variantClasses) {
1405
+ classes += " " + variantClasses;
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ return classes.trim();
1411
+ }
1412
+ function formatValue2(value) {
1413
+ if (value === null || value === void 0) {
1414
+ return "";
1415
+ }
1416
+ if (typeof value === "object") {
1417
+ return JSON.stringify(value);
1418
+ }
1419
+ return String(value);
1420
+ }
1421
+ function flush(ctx, force = false) {
1422
+ if (ctx.aborted) return;
1423
+ const { buffer, options, controller } = ctx;
1424
+ if (buffer.length === 0) return;
1425
+ switch (options.flushStrategy) {
1426
+ case "immediate":
1427
+ controller.enqueue(buffer);
1428
+ ctx.buffer = "";
1429
+ break;
1430
+ case "batched":
1431
+ if (force || buffer.length >= CHUNK_SIZE_THRESHOLD) {
1432
+ controller.enqueue(buffer);
1433
+ ctx.buffer = "";
1434
+ }
1435
+ break;
1436
+ case "manual":
1437
+ if (force) {
1438
+ controller.enqueue(buffer);
1439
+ ctx.buffer = "";
1440
+ }
1441
+ break;
1442
+ }
1443
+ }
1444
+ function write(ctx, content) {
1445
+ if (ctx.aborted) return;
1446
+ ctx.buffer += content;
1447
+ flush(ctx);
1448
+ }
1449
+ function checkAbort(ctx) {
1450
+ if (ctx.signal?.aborted) {
1451
+ ctx.aborted = true;
1452
+ return true;
1453
+ }
1454
+ return false;
1455
+ }
1456
+ async function renderNodeToStream(node, ctx) {
1457
+ if (checkAbort(ctx)) return;
1458
+ switch (node.kind) {
1459
+ case "element":
1460
+ await renderElementToStream(node, ctx);
1461
+ break;
1462
+ case "text":
1463
+ renderTextToStream(node, ctx);
1464
+ break;
1465
+ case "if":
1466
+ await renderIfToStream(node, ctx);
1467
+ break;
1468
+ case "each":
1469
+ await renderEachToStream(node, ctx);
1470
+ break;
1471
+ case "markdown":
1472
+ await renderMarkdownToStream(node, ctx);
1473
+ break;
1474
+ case "code":
1475
+ await renderCodeToStream(node, ctx);
1476
+ break;
1477
+ case "slot":
1478
+ break;
1479
+ case "portal":
1480
+ await renderPortalToStream(node, ctx);
1481
+ break;
1482
+ case "localState":
1483
+ await renderLocalStateToStream(node, ctx);
1484
+ break;
1485
+ default: {
1486
+ const unknownNode = node;
1487
+ if (unknownNode.kind === "suspense") {
1488
+ await renderSuspenseToStream(unknownNode, ctx);
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ async function renderSuspenseToStream(node, ctx) {
1494
+ write(ctx, '<div data-suspense-id="' + escapeHtml(node.id) + '">');
1495
+ await renderNodeToStream(node.fallback, ctx);
1496
+ write(ctx, "</div>");
1497
+ if (node.children && node.children.length > 0) {
1498
+ for (const child of node.children) {
1499
+ await renderNodeToStream(child, ctx);
1500
+ }
1501
+ }
1502
+ }
1503
+ async function renderElementToStream(node, ctx) {
1504
+ if (checkAbort(ctx)) return;
1505
+ const tag = node.tag;
1506
+ const isVoid = VOID_ELEMENTS2.has(tag);
1507
+ let attrs = "";
1508
+ if (node.props) {
1509
+ for (const [propName, propValue] of Object.entries(node.props)) {
1510
+ if (isEventHandler2(propValue)) {
1511
+ continue;
1512
+ }
1513
+ const value = evaluate2(propValue, ctx);
1514
+ if (value === false) {
1515
+ continue;
1516
+ }
1517
+ if (value === true) {
1518
+ attrs += " " + propName;
1519
+ continue;
1520
+ }
1521
+ if (value === null || value === void 0) {
1522
+ continue;
1523
+ }
1524
+ attrs += " " + propName + '="' + escapeHtml(String(value)) + '"';
1525
+ }
1526
+ }
1527
+ if (isVoid) {
1528
+ write(ctx, "<" + tag + attrs + " />");
1529
+ return;
1530
+ }
1531
+ write(ctx, "<" + tag + attrs + ">");
1532
+ if (node.children) {
1533
+ for (const child of node.children) {
1534
+ await renderNodeToStream(child, ctx);
1535
+ }
1536
+ }
1537
+ write(ctx, "</" + tag + ">");
1538
+ }
1539
+ function renderTextToStream(node, ctx) {
1540
+ const value = evaluate2(node.value, ctx);
1541
+ write(ctx, escapeHtml(formatValue2(value)));
1542
+ }
1543
+ async function renderIfToStream(node, ctx) {
1544
+ if (checkAbort(ctx)) return;
1545
+ const condition = evaluate2(node.condition, ctx);
1546
+ if (condition) {
1547
+ write(ctx, "<!--if:then-->");
1548
+ await renderNodeToStream(node.then, ctx);
1549
+ } else if (node.else) {
1550
+ write(ctx, "<!--if:else-->");
1551
+ await renderNodeToStream(node.else, ctx);
1552
+ } else {
1553
+ write(ctx, "<!--if:none-->");
1554
+ }
1555
+ }
1556
+ async function renderEachToStream(node, ctx) {
1557
+ if (checkAbort(ctx)) return;
1558
+ const items = evaluate2(node.items, ctx);
1559
+ if (!Array.isArray(items)) {
1560
+ return;
1561
+ }
1562
+ for (let index = 0; index < items.length; index++) {
1563
+ if (checkAbort(ctx)) return;
1564
+ const item = items[index];
1565
+ const itemLocals = {
1566
+ ...ctx.locals,
1567
+ [node.as]: item
1568
+ };
1569
+ if (node.index) {
1570
+ itemLocals[node.index] = index;
1571
+ }
1572
+ const itemCtx = {
1573
+ ...ctx,
1574
+ locals: itemLocals
1575
+ };
1576
+ await renderNodeToStream(node.body, itemCtx);
1577
+ if (index > 0 && index % 10 === 0) {
1578
+ flush(ctx);
1579
+ }
1580
+ }
1581
+ }
1582
+ async function renderMarkdownToStream(node, ctx) {
1583
+ const content = evaluate2(node.content, ctx);
1584
+ write(ctx, '<div class="constela-markdown">' + escapeHtml(formatValue2(content)) + "</div>");
1585
+ }
1586
+ async function renderCodeToStream(node, ctx) {
1587
+ const language = formatValue2(evaluate2(node.language, ctx));
1588
+ const content = formatValue2(evaluate2(node.content, ctx));
1589
+ const languageBadge = language ? '<div class="absolute right-12 top-3 z-10 rounded bg-muted-foreground/20 px-2 py-0.5 text-xs font-medium text-muted-foreground">' + escapeHtml(language) + "</div>" : "";
1590
+ const copyButton = '<button class="constela-copy-btn absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-md border border-border bg-background/80 opacity-0 transition-opacity hover:bg-muted group-hover:opacity-100" data-copy-target="code" aria-label="Copy code"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>';
1591
+ write(ctx, '<div class="constela-code" data-code-content="' + escapeHtml(content) + '"><div class="group relative">' + languageBadge + copyButton + "<pre><code>" + escapeHtml(content) + "</code></pre></div></div>");
1592
+ }
1593
+ async function renderPortalToStream(node, ctx) {
1594
+ write(ctx, "<!--portal:" + node.target + "-->");
1595
+ for (const child of node.children) {
1596
+ await renderNodeToStream(child, ctx);
1597
+ }
1598
+ write(ctx, "<!--/portal-->");
1599
+ }
1600
+ async function renderLocalStateToStream(node, ctx) {
1601
+ const localStateValues = {};
1602
+ for (const [name, field] of Object.entries(node.state)) {
1603
+ localStateValues[name] = field.initial;
1604
+ }
1605
+ const childCtx = {
1606
+ ...ctx,
1607
+ locals: {
1608
+ ...ctx.locals,
1609
+ ...localStateValues
1610
+ }
1611
+ };
1612
+ await renderNodeToStream(node.child, childCtx);
1613
+ }
1614
+ function renderToStream(program, streamOptions, options) {
1615
+ const state = /* @__PURE__ */ new Map();
1616
+ for (const [name, field] of Object.entries(program.state)) {
1617
+ const stateField = field;
1618
+ const overrideValue = options?.stateOverrides?.[name];
1619
+ if (overrideValue !== void 0) {
1620
+ state.set(name, overrideValue);
1621
+ } else if (isCookieInitialExpr2(stateField.initial)) {
1622
+ const cookieInitial = stateField.initial;
1623
+ const cookieValue = options?.cookies?.[cookieInitial.key];
1624
+ state.set(name, cookieValue !== void 0 ? cookieValue : cookieInitial.default);
1625
+ } else {
1626
+ state.set(name, stateField.initial);
1627
+ }
1628
+ }
1629
+ const signal = options?.signal;
1630
+ return new ReadableStream({
1631
+ async start(controller) {
1632
+ const baseCtx = {
1633
+ state,
1634
+ locals: {},
1635
+ route: options?.route ? {
1636
+ params: options.route.params ?? {},
1637
+ query: options.route.query ?? {},
1638
+ path: options.route.path ?? ""
1639
+ } : void 0,
1640
+ imports: options?.imports ?? program.importData,
1641
+ styles: options?.styles,
1642
+ controller,
1643
+ options: streamOptions,
1644
+ buffer: "",
1645
+ aborted: false
1646
+ };
1647
+ const ctx = signal ? { ...baseCtx, signal } : baseCtx;
1648
+ if (signal) {
1649
+ signal.addEventListener("abort", () => {
1650
+ ctx.aborted = true;
1651
+ try {
1652
+ controller.close();
1653
+ } catch {
1654
+ }
1655
+ });
1656
+ }
1657
+ try {
1658
+ await renderNodeToStream(program.view, ctx);
1659
+ flush(ctx, true);
1660
+ if (!ctx.aborted) {
1661
+ controller.close();
1662
+ }
1663
+ } catch (error) {
1664
+ if (!ctx.aborted) {
1665
+ controller.error(error);
1666
+ }
1667
+ }
1668
+ },
1669
+ cancel() {
1670
+ }
1671
+ });
1672
+ }
1673
+ function createHtmlTransformStream(options) {
1674
+ let isFirstChunk = true;
1675
+ return new TransformStream({
1676
+ transform(chunk, controller) {
1677
+ if (isFirstChunk) {
1678
+ const shell = buildDocumentShell(options);
1679
+ controller.enqueue(shell + chunk);
1680
+ isFirstChunk = false;
1681
+ } else {
1682
+ controller.enqueue(chunk);
1683
+ }
1684
+ },
1685
+ flush(controller) {
1686
+ const scripts = buildScriptTags(options.scripts);
1687
+ controller.enqueue(scripts + "</body></html>");
1688
+ }
1689
+ });
1690
+ }
1691
+ function buildDocumentShell(options) {
1692
+ const lang = options.lang ?? "en";
1693
+ let head = "<head>";
1694
+ head += '<meta charset="UTF-8">';
1695
+ head += "<title>" + escapeHtml(options.title) + "</title>";
1696
+ if (options.meta) {
1697
+ for (const [name, content] of Object.entries(options.meta)) {
1698
+ head += '<meta name="' + escapeHtml(name) + '" content="' + escapeHtml(content) + '">';
1699
+ }
1700
+ }
1701
+ if (options.stylesheets) {
1702
+ for (const href of options.stylesheets) {
1703
+ head += '<link rel="stylesheet" href="' + escapeHtml(href) + '">';
1704
+ }
1705
+ }
1706
+ head += "</head>";
1707
+ return '<!DOCTYPE html><html lang="' + escapeHtml(lang) + '">' + head + "<body>";
1708
+ }
1709
+ function buildScriptTags(scripts) {
1710
+ if (!scripts || scripts.length === 0) return "";
1711
+ return scripts.map((src) => '<script src="' + escapeHtml(src) + '"></script>').join("");
1712
+ }
823
1713
  export {
1714
+ createHtmlTransformStream,
1715
+ renderToStream,
824
1716
  renderToString
825
1717
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/server",
3
- "version": "12.0.2",
3
+ "version": "14.0.0",
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.14.7",
19
- "@constela/core": "^0.16.2"
18
+ "@constela/compiler": "^0.15.6",
19
+ "@constela/core": "^0.18.0"
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.14.7",
33
- "@constela/core": "0.16.2"
32
+ "@constela/core": "0.18.0",
33
+ "@constela/compiler": "0.15.6"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=20.0.0"