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